├── Makefile ├── examples ├── tutorials │ ├── 6_RPC │ │ ├── fib.tcl │ │ ├── rpc_server.tcl │ │ └── rpc_client.tcl │ ├── README.md │ ├── 1_Hello_World │ │ ├── send.tcl │ │ └── receive.tcl │ ├── 3_Publish_Subscribe │ │ ├── emit_log.tcl │ │ └── receive_logs.tcl │ ├── 2_Work_Queues │ │ ├── worker.tcl │ │ └── new_task.tcl │ ├── 5_Topics │ │ ├── emit_log_topic.tcl │ │ └── receive_log_topic.tcl │ └── 4_Routing │ │ ├── emit_log_direct.tcl │ │ └── receive_logs_direct.tcl ├── tlsConnect.tcl ├── blockedConnections.tcl ├── cancelNotifications.tcl ├── reflectedQueueRMQ.tcl ├── reflectedQueue.tcl └── publisherConfirms.tcl ├── package ├── pkgIndex.tcl ├── Makefile ├── Login.tcl ├── encoders.tcl ├── constants.tcl ├── decoders.tcl ├── Channel.tcl └── Connection.tcl ├── LICENSE ├── bumpVersion.tcl └── README.md /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for tclrmq package from repo's root directory 3 | # 4 | 5 | all: 6 | $(MAKE) -C package all 7 | 8 | install: 9 | $(MAKE) -C package install 10 | -------------------------------------------------------------------------------- /examples/tutorials/6_RPC/fib.tcl: -------------------------------------------------------------------------------- 1 | 2 | proc fib {n} { 3 | if {$n == 0} { 4 | return 0 5 | } elseif {$n == 1} { 6 | return 1 7 | } else { 8 | set arg1 [expr {$n - 1}] 9 | set arg2 [expr {$n - 2}] 10 | return [expr {[fib $arg1] + [fib $arg2]}] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/tutorials/README.md: -------------------------------------------------------------------------------- 1 | # Tutorial Examples 2 | 3 | This folder contains examples from the [RabbitMQ tutorials](https://www.rabbitmq.com/tutorials/tutorial-one-python.html). 4 | 5 | ## Defaults 6 | 7 | Each example connects to a RabbitMQ server on `localhost:5672` using `guest` as username and password. 8 | 9 | If this is not desirable, pass appropriate `-host` and `-port` arguments to the `Connection` constructor 10 | and/or create a `Login` object, specifying `-user` and `-pass` in its constructor. 11 | -------------------------------------------------------------------------------- /examples/tlsConnect.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc rmq_connected {conn} { 4 | puts " \[+\]: Connected using TLS!" 5 | $conn closeConnection 6 | set ::die 1 7 | } 8 | 9 | # using the guide in https://www.rabbitmq.com/ssl.html 10 | set conn [::rmq::Connection new -port 5671 -login [::rmq::Login new]] 11 | $conn onConnected rmq_connected 12 | $conn tlsOptions -cafile "$::env(HOME)/testca/cacert.pem" \ 13 | -certfile "$::env(HOME)/client/cert.pem" \ 14 | -keyfile "$::env(HOME)/client/key.pem" 15 | $conn connect 16 | 17 | vwait ::die 18 | -------------------------------------------------------------------------------- /examples/tutorials/1_Hello_World/send.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | $rChan queueDeclare "hello" 7 | # args: data exchangeName routingKey 8 | $rChan basicPublish "Hello World!" "" "hello" 9 | 10 | puts " \[x\] Sent 'Hello World!'" 11 | 12 | $conn closeConnection 13 | } 14 | 15 | proc finished {conn closeD} { 16 | exit 17 | } 18 | 19 | set conn [::rmq::Connection new] 20 | $conn onConnected create_channel 21 | $conn onClose finished 22 | 23 | $conn connect 24 | 25 | vwait die 26 | 27 | # vim: ts=4:sw=4:sts=4:noet 28 | -------------------------------------------------------------------------------- /examples/tutorials/1_Hello_World/receive.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | $rChan queueDeclare "hello" 7 | 8 | set consumeFlags [list $::rmq::CONSUME_NO_ACK] 9 | $rChan basicConsume callback "hello" $consumeFlags 10 | 11 | puts " \[*\] Waiting for messages. To exit press CTRL+C" 12 | } 13 | 14 | proc callback {rChan methodD frameD msg} { 15 | puts " \[x\] Received $msg" 16 | } 17 | 18 | set conn [::rmq::Connection new] 19 | $conn onConnected create_channel 20 | $conn connect 21 | 22 | vwait ::die 23 | 24 | # vim: ts=4:sw=4:sts=4:noet 25 | -------------------------------------------------------------------------------- /examples/blockedConnections.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc rmq_connected {conn} { 4 | # set this callback on the connection object 5 | $conn onBlocked blocked_connection 6 | $rChan onOpen setup_blocked_connection_cb 7 | } 8 | 9 | proc blocked_connection {conn blocked reason} { 10 | set connStatus [expr {$blocked ? "is" : "is not"}] 11 | puts " \[+\]: connection $connStatus blocked (reason: $reason)" 12 | } 13 | 14 | # blocked connection notifications are turned on by default 15 | # argument shown here for completeness 16 | # callback is set above on the Connection object 17 | set conn [::rmq::Connection new -blockedConnections 1] 18 | $conn onConnected rmq_connected 19 | $conn connect 20 | 21 | vwait ::die 22 | -------------------------------------------------------------------------------- /package/pkgIndex.tcl: -------------------------------------------------------------------------------- 1 | # Tcl package index file, version 1.1 2 | # This file is generated by the "pkg_mkIndex" command 3 | # and sourced either when an application starts up or 4 | # by a "package unknown" script. It invokes the 5 | # "package ifneeded" command to set up package-related 6 | # information so that packages will be loaded automatically 7 | # in response to "package require" commands. When this 8 | # script is sourced, the variable $dir must contain the 9 | # full path name of this file's directory. 10 | 11 | package ifneeded rmq 1.4.5 [list source [file join $dir Channel.tcl]]\n[list source [file join $dir Connection.tcl]]\n[list source [file join $dir Login.tcl]]\n[list source [file join $dir constants.tcl]]\n[list source [file join $dir decoders.tcl]]\n[list source [file join $dir encoders.tcl]] 12 | -------------------------------------------------------------------------------- /examples/tutorials/3_Publish_Subscribe/emit_log.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan exchangeDeclare "logs" "fanout" 8 | 9 | # send a message to the fanout 10 | global msg 11 | $rChan basicPublish $msg "logs" "" 12 | puts " \[x\] Sent $msg" 13 | 14 | [$rChan getConnection] connectionClose 15 | } 16 | 17 | proc quit {rChan closeD} { 18 | exit 19 | } 20 | 21 | global msg 22 | if {[llength $argv] > 0} { 23 | set msg $argv 24 | } else { 25 | set msg "info: Hello World!" 26 | } 27 | 28 | set conn [::rmq::Connection new] 29 | $conn onConnected create_channel 30 | $conn onClose quit 31 | $conn connect 32 | 33 | vwait ::die 34 | 35 | # vim: ts=4:sw=4:sts=4:noet 36 | -------------------------------------------------------------------------------- /package/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for tclrmq package 3 | # 4 | # basically we make a pkgIndex.tcl package file and install stuff 5 | # 6 | 7 | PACKAGE = tclrmq 8 | PREFIX ?= /usr/local 9 | TCLSH ?= tclsh 10 | PACKAGELIBDIR =$(PREFIX)/lib/$(PACKAGE) 11 | LIBSOURCES = Channel.tcl Connection.tcl Login.tcl encoders.tcl decoders.tcl constants.tcl 12 | 13 | all: pkgIndex.tcl 14 | @echo "'make install' to install" 15 | 16 | pkgIndex.tcl: $(LIBSOURCES) 17 | echo "pkg_mkIndex ." | $(TCLSH) 18 | 19 | install: install-package 20 | 21 | install-package: pkgIndex.tcl 22 | -mkdir -p $(PACKAGELIBDIR) 23 | cp *.tcl $(PACKAGELIBDIR) 24 | cd $(PACKAGELIBDIR); echo "package require rmq" | tclsh 25 | cd $(PACKAGELIBDIR); echo "pkg_mkIndex -verbose . *.tcl" | tclsh 26 | 27 | clean: 28 | rm -f pkgIndex.tcl 29 | 30 | -------------------------------------------------------------------------------- /examples/cancelNotifications.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc rmq_connected {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | $rChan onOpen setup_cancel_notifications 6 | } 7 | 8 | proc setup_cancel_notifications {rChan} { 9 | $rChan on basicCancel cancel_notification 10 | puts " \[+\]: will be alerted if consumer canceled..." 11 | 12 | # do some consuming 13 | } 14 | 15 | proc cancel_notification {rChan consumerTag} { 16 | puts " \[-\]: consumer $consumerTag was canceled by the server" 17 | } 18 | 19 | # cancel notifications are turned on by default when the connection 20 | # is created although the callback to receive them is on a channel 21 | # argument shown here for completeness 22 | set conn [::rmq::Connection new -cancelNotifications 1] 23 | $conn onConnected rmq_connected 24 | $conn connect 25 | 26 | vwait ::die 27 | -------------------------------------------------------------------------------- /examples/tutorials/2_Work_Queues/worker.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a durable queue for tasks 7 | set qFlags [list $::rmq::QUEUE_DURABLE] 8 | $rChan queueDeclare "task_queue" $qFlags 9 | 10 | $rChan basicQos 1 11 | $rChan basicConsume callback "task_queue" 12 | puts " \[*\] Waiting for messages. To exit press CTRL+C" 13 | } 14 | 15 | proc callback {rChan methodD frameD msg} { 16 | puts " \[x\] Received $msg" 17 | set sleepSecs [llength [lsearch -all [split $msg ""] "."]] 18 | puts "\tSleeping $sleepSecs secs" 19 | after [expr {$sleepSecs * 1000}] 20 | puts " \[x\] Done" 21 | $rChan basicAck [dict get $methodD deliveryTag] 22 | } 23 | 24 | set conn [::rmq::Connection new] 25 | $conn onConnected create_channel 26 | $conn connect 27 | 28 | vwait ::die 29 | 30 | # vim: ts=4:sw=4:sts=4:noet 31 | -------------------------------------------------------------------------------- /examples/tutorials/2_Work_Queues/new_task.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | set data "Hello World!" 4 | 5 | proc create_channel {conn} { 6 | global data 7 | set rChan [::rmq::Channel new $conn] 8 | 9 | # declare a durable queue for tasks 10 | set qFlags [list $::rmq::QUEUE_DURABLE] 11 | $rChan queueDeclare "task_queue" $qFlags 12 | 13 | # create a dict with the additional property needed 14 | # to make the message persistent 15 | set props [dict create delivery-mode 2] 16 | 17 | # args: data exchangeName routingKey flags properties 18 | $rChan basicPublish $data "" "task_queue" [list] $props 19 | 20 | puts " \[x\] Sent '$data'" 21 | 22 | $conn closeConnection 23 | } 24 | 25 | proc finished {conn closeD} { 26 | exit 27 | } 28 | 29 | if {[llength $argv] > 0} { 30 | set data $argv 31 | } 32 | 33 | set conn [::rmq::Connection new] 34 | $conn onConnected create_channel 35 | $conn onClose finished 36 | 37 | $conn connect 38 | 39 | vwait die 40 | 41 | # vim: ts=4:sw=4:sts=4:noet 42 | -------------------------------------------------------------------------------- /examples/tutorials/5_Topics/emit_log_topic.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan exchangeDeclare "topic_logs" "topic" 8 | 9 | # send a message to the direct exchange 10 | # using the severity as the routing key 11 | global msg routingKey 12 | $rChan basicPublish $msg "topic_logs" $routingKey 13 | puts " \[x\] Sent $routingKey:$msg" 14 | 15 | [$rChan getConnection] closeConnection 16 | } 17 | 18 | proc quit {args} { 19 | exit 20 | } 21 | 22 | global msg routingKey 23 | if {[llength $argv] > 0} { 24 | lassign $argv routingKey msg 25 | if {$msg eq ""} { 26 | set msg "Hello World!" 27 | } 28 | } else { 29 | set routingKey "anonymous.info" 30 | set msg "Hello World!" 31 | } 32 | 33 | set conn [::rmq::Connection new -autoReconnect 0] 34 | $conn onConnected create_channel 35 | $conn onClose quit 36 | $conn connect 37 | 38 | vwait ::die 39 | 40 | # vim: ts=4:sw=4:sts=4:noet 41 | -------------------------------------------------------------------------------- /examples/tutorials/4_Routing/emit_log_direct.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan on exchangeDeclareOk ready_to_send 8 | $rChan exchangeDeclare "direct_logs" "direct" 9 | vwait ::canSend 10 | 11 | # send a message to the direct exchange 12 | # using the severity as the routing key 13 | global msg severity 14 | $rChan basicPublish $msg "direct_logs" $severity 15 | puts " \[x\] Sent $severity:$msg" 16 | 17 | [$rChan getConnection] connectionClose 18 | set ::die 1 19 | } 20 | 21 | proc ready_to_send {rChan} { 22 | set ::canSend 1 23 | } 24 | 25 | proc quit {args} { 26 | exit 27 | } 28 | 29 | global msg severity 30 | if {[llength $argv] > 0} { 31 | lassign $argv severity msg 32 | if {$msg eq ""} { 33 | set msg "Hello World!" 34 | } 35 | } else { 36 | set severity "info" 37 | set msg "Hello World!" 38 | } 39 | 40 | set conn [::rmq::Connection new] 41 | $conn onConnected create_channel 42 | $conn onClose quit 43 | $conn connect 44 | 45 | vwait ::die 46 | 47 | # vim: ts=4:sw=4:sts=4:noet 48 | -------------------------------------------------------------------------------- /examples/tutorials/3_Publish_Subscribe/receive_logs.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan exchangeDeclare "logs" "fanout" 8 | 9 | # declare an exclusive queue and bind to the 10 | # fanout exchange 11 | $rChan on queueDeclareOk bind_to_fanout 12 | set qFlags [list $::rmq::QUEUE_EXCLUSIVE] 13 | $rChan queueDeclare "" $qFlags 14 | } 15 | 16 | proc bind_to_fanout {rChan qName msgCount consumers} { 17 | # bind the queue name to the fanout logs exchange 18 | # with the default empty string routing key 19 | $rChan queueBind $qName "logs" 20 | 21 | # basicConsume takes the callback proc name, queue name, consumer tag, flags 22 | set cFlags [list $::rmq::CONSUME_NO_ACK] 23 | $rChan basicConsume callback $qName "" $cFlags 24 | puts " \[*\] Waiting for logs. To exit press CTRL+C" 25 | } 26 | 27 | proc callback {rChan methodD frameD msg} { 28 | puts " \[x\] $msg" 29 | } 30 | 31 | set conn [::rmq::Connection new] 32 | $conn onConnected create_channel 33 | $conn connect 34 | 35 | vwait ::die 36 | 37 | # vim: ts=4:sw=4:sts=4:noet 38 | -------------------------------------------------------------------------------- /examples/tutorials/6_RPC/rpc_server.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | $rChan queueDeclare "rpc_queue" 7 | $rChan basicQos 1 8 | $rChan basicConsume on_request "rpc_queue" 9 | puts " \[x\] Awaiting RPC requests" 10 | } 11 | 12 | proc on_request {rChan methodD frameD n} { 13 | puts " \[.\] fib($n)" 14 | set response [fib $n] 15 | $rChan basicAck [dict get $methodD deliveryTag] 16 | 17 | set rpcProps [dict get $frameD properties] 18 | 19 | set props [dict create] 20 | dict set props correlation-id [dict get $rpcProps correlation-id] 21 | puts "Sending response $response to [dict get $rpcProps reply-to]" 22 | $rChan basicPublish $response "" [dict get $rpcProps reply-to] "" $props 23 | } 24 | 25 | proc fib {n} { 26 | if {$n == 0} { 27 | return 0 28 | } elseif {$n == 1} { 29 | return 1 30 | } else { 31 | set arg1 [expr {$n - 1}] 32 | set arg2 [expr {$n - 2}] 33 | return [expr {[fib $arg1] + [fib $arg2]}] 34 | } 35 | } 36 | 37 | set conn [::rmq::Connection new] 38 | $conn onConnected create_channel 39 | $conn connect 40 | 41 | vwait ::die 42 | 43 | # vim: ts=4:sw=4:sts=4:noet 44 | -------------------------------------------------------------------------------- /examples/reflectedQueueRMQ.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | source reflectedQueue.tcl 4 | 5 | proc rmq_connected {conn} { 6 | set rChan [::rmq::Channel new $conn] 7 | $rChan onOpen setup_queue_channel 8 | } 9 | 10 | proc setup_queue_channel {rChan} { 11 | # setup a reflected queue channel to put messages on 12 | # for processing 13 | global ch 14 | set ch [chan create "read write" qchan] 15 | 16 | chan configure $ch -buffering line 17 | chan event $ch readable [list read_queue $ch] 18 | 19 | # start consuming messages assuming all necessary 20 | # exchanges, queues and bindings have been established 21 | $rChan basicConsume consume_rmq "test" "reflected-queue-consumer" 22 | } 23 | 24 | proc consume_rmq {rmqChan methodD frameD msg} { 25 | global ch 26 | 27 | # when consuming, put the message onto the queue 28 | # channel so the consume callback finishes ASAP 29 | puts $ch $msg 30 | } 31 | 32 | proc read_queue {ch} { 33 | # Read a message from the queue and do some processing 34 | set msg [chan gets $ch] 35 | puts " \[+\]: Read from queue $msg" 36 | } 37 | 38 | set conn [::rmq::Connection new -login [::rmq::Login new]] 39 | $conn onConnected rmq_connected 40 | $conn connect 41 | 42 | vwait ::die 43 | 44 | 45 | -------------------------------------------------------------------------------- /package/Login.tcl: -------------------------------------------------------------------------------- 1 | package provide rmq 1.4.5 2 | 3 | package require TclOO 4 | 5 | namespace eval rmq { 6 | namespace export Login 7 | } 8 | 9 | oo::class create ::rmq::Login { 10 | variable user pass mechanism vhost 11 | 12 | constructor {args} { 13 | array set options {} 14 | set options(-user) $::rmq::DEFAULT_UN 15 | set options(-pass) $::rmq::DEFAULT_PW 16 | set options(-mechanism) $::rmq::DEFAULT_MECHANISM 17 | set options(-vhost) $::rmq::DEFAULT_VHOST 18 | 19 | foreach {opt val} $args { 20 | if {[info exists options($opt)]} { 21 | set options($opt) $val 22 | } 23 | } 24 | 25 | foreach opt [array names options] { 26 | set [string trimleft $opt -] $options($opt) 27 | } 28 | } 29 | 30 | method getVhost {} { 31 | return $vhost 32 | } 33 | 34 | method saslResponse {} { 35 | # can dispatch on method if more are supported in 36 | # the future but at this point, only send PLAIN format 37 | if {$mechanism eq "PLAIN"} { 38 | set unLen [string length $user] 39 | set pwLen [string length $pass] 40 | return "\x00[binary format a$unLen $user]\x00[binary format a$pwLen $pass]" 41 | } 42 | } 43 | } 44 | 45 | # vim: ts=4:sw=4:sts=4:noet 46 | -------------------------------------------------------------------------------- /examples/tutorials/6_RPC/rpc_client.tcl: -------------------------------------------------------------------------------- 1 | package require uuid 2 | 3 | package require rmq 4 | 5 | proc create_channel {conn} { 6 | set rChan [::rmq::Channel new $conn] 7 | 8 | # declare an exclusive queue and bind to the 9 | # fanout exchange 10 | $rChan on queueDeclareOk setup_rpc_client 11 | set qFlags [list $::rmq::QUEUE_EXCLUSIVE] 12 | $rChan queueDeclare "" $qFlags 13 | } 14 | 15 | proc setup_rpc_client {rChan qName msgCount consumers} { 16 | # basicConsume takes the callback proc name, queue name, consumer tag, flags 17 | set cFlags [list $::rmq::CONSUME_NO_ACK] 18 | $rChan basicConsume on_response $qName "" $cFlags 19 | 20 | # make call to request rpc response 21 | global correlationID 22 | set correlationID [::uuid::uuid generate] 23 | set props [dict create] 24 | dict set props correlation-id $correlationID 25 | dict set props reply-to $qName 26 | 27 | puts " \[x\] Request fib(30) $props" 28 | $rChan basicPublish 30 "" "rpc_queue" "" $props 29 | } 30 | 31 | proc on_response {rChan methodD frameD msg} { 32 | global correlationID 33 | set props [dict get $frameD properties] 34 | if {[dict get $props correlation-id] == $correlationID} { 35 | puts " \[.\] Got $msg" 36 | set ::die 1 37 | } 38 | } 39 | 40 | set conn [::rmq::Connection new] 41 | $conn onConnected create_channel 42 | $conn connect 43 | 44 | vwait ::die 45 | 46 | # vim: ts=4:sw=4:sts=4:noet 47 | -------------------------------------------------------------------------------- /examples/tutorials/5_Topics/receive_log_topic.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan exchangeDeclare "topic_logs" "topic" 8 | 9 | # declare an exclusive queue and bind to the 10 | # fanout exchange 11 | $rChan on queueDeclareOk bind_to_fanout 12 | set qFlags [list $::rmq::QUEUE_EXCLUSIVE] 13 | $rChan queueDeclare "" $qFlags 14 | } 15 | 16 | proc bind_to_fanout {rChan qName msgCount consumers} { 17 | global bindingKeys 18 | 19 | # bind to each severity we're interested in 20 | foreach bindingKey $bindingKeys { 21 | $rChan queueBind $qName "topic_logs" $bindingKey 22 | } 23 | 24 | # basicConsume takes the callback proc name, queue name, consumer tag, flags 25 | set cFlags [list $::rmq::CONSUME_NO_ACK] 26 | $rChan basicConsume callback $qName "" $cFlags 27 | puts " \[*\] Waiting for logs. To exit press CTRL+C" 28 | } 29 | 30 | proc callback {rChan methodD frameD msg} { 31 | puts " \[x\] [dict get $methodD routingKey]:$msg" 32 | } 33 | 34 | global bindingKeys 35 | if {[llength $argv] > 0} { 36 | set bindingKeys $argv 37 | } else { 38 | puts stderr "Usage: $::argv0 \[binding_key\]..." 39 | exit 1 40 | } 41 | 42 | set conn [::rmq::Connection new] 43 | $conn onConnected create_channel 44 | $conn connect 45 | 46 | vwait ::die 47 | 48 | # vim: ts=4:sw=4:sts=4:noet 49 | -------------------------------------------------------------------------------- /examples/tutorials/4_Routing/receive_logs_direct.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc create_channel {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | 6 | # declare a fanout exchange named logs 7 | $rChan exchangeDeclare "direct_logs" "direct" 8 | 9 | # declare an exclusive queue and bind to the 10 | # fanout exchange 11 | $rChan on queueDeclareOk bind_to_fanout 12 | set qFlags [list $::rmq::QUEUE_EXCLUSIVE] 13 | $rChan queueDeclare "" $qFlags 14 | } 15 | 16 | proc bind_to_fanout {rChan qName msgCount consumers} { 17 | global severities 18 | 19 | # bind to each severity we're interested in 20 | foreach severity $severities { 21 | $rChan queueBind $qName "direct_logs" $severity 22 | } 23 | 24 | # basicConsume takes the callback proc name, queue name, consumer tag, flags 25 | set cFlags [list $::rmq::CONSUME_NO_ACK] 26 | $rChan basicConsume callback $qName "" $cFlags 27 | puts " \[*\] Waiting for logs. To exit press CTRL+C" 28 | } 29 | 30 | proc callback {rChan methodD frameD msg} { 31 | puts " \[x\] [dict get $methodD routingKey]:$msg" 32 | } 33 | 34 | global severities 35 | if {[llength $argv] > 0} { 36 | set severities $argv 37 | } else { 38 | puts stderr "Usage: $::argv0 \[info\] \[warning\] \[error\]" 39 | exit 1 40 | } 41 | 42 | set conn [::rmq::Connection new] 43 | $conn onConnected create_channel 44 | $conn connect 45 | 46 | vwait ::die 47 | 48 | # vim: ts=4:sw=4:sts=4:noet 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, FlightAware LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | * Neither the name of the FlightAware LLC nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /bumpVersion.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh 2 | 3 | ## 4 | ## 5 | ## Used to bump the version of tclrmq 6 | ## 7 | ## Usage: ./bumpVersion.tcl 8 | ## 9 | ## Goes line by line through all files with a .tcl 10 | ## extension and copies each line into a temporary file 11 | ## with any lines containing the old version bumped to 12 | ## the new one. Once all lines in a file have been procesed, 13 | ## the temporary one replaces the original 14 | ## 15 | 16 | proc bump_line {line newv} { 17 | set start {(^\s*)} 18 | set prefix "(package provide rmq|package ifneeded rmq|set VERSION) " 19 | set version {([0-9]+\.[0-9]+\.[0-9]+)} 20 | set rest {(.*$)} 21 | if {![regexp "$start$prefix$version$rest" $line -> pSpace pReq pVer pRest]} { 22 | return $line 23 | } 24 | return "$pSpace$pReq $newv$pRest" 25 | } 26 | 27 | if {!$tcl_interactive} { 28 | if {$argc != 1} { 29 | puts stderr "Usage: $argv0 " 30 | exit 1 31 | } 32 | 33 | if {[catch {exec which gcut} result options] == 1} { 34 | set cutCmd cut 35 | } else { 36 | set cutCmd gcut 37 | } 38 | set curVer [exec git tag | tail -1 | $cutCmd --complement -c 1] 39 | set newVer [lindex $argv 0] 40 | puts stderr "Bumping rmq package from $curVer to $newVer" 41 | 42 | set fnames [glob -directory package *.tcl] 43 | foreach fname $fnames { 44 | set ofd [open $fname] 45 | set tfd [file tempfile tfname] 46 | while {[gets $ofd line] >= 0} { 47 | puts $tfd [bump_line $line $newVer] 48 | } 49 | 50 | close $tfd 51 | close $ofd 52 | 53 | file rename -force $tfname $fname 54 | puts stderr "Bumped $fname" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/reflectedQueue.tcl: -------------------------------------------------------------------------------- 1 | namespace eval qchan { 2 | package require struct 3 | variable chans 4 | array set chans {} 5 | 6 | proc initialize {chanid args} { 7 | variable chans 8 | 9 | set chans($chanid) [::struct::queue] 10 | foreach event {read write} { 11 | set chans($chanid,$event) 0 12 | } 13 | 14 | set map [dict create] 15 | foreach subCmd [list finalize watch read write] { 16 | dict set map $subCmd [list ::qchan::$subCmd $chanid] 17 | } 18 | 19 | namespace ensemble create -map $map -command ::$chanid 20 | return "initialize finalize watch read write" 21 | } 22 | 23 | proc finalize {chanid} { 24 | variable chans 25 | 26 | $chans($chanid) destroy 27 | unset chans($chanid) 28 | } 29 | 30 | proc watch {chanid events} { 31 | variable chans 32 | foreach event {read write} { 33 | set chans($chanid,$event) 0 34 | } 35 | foreach event $events { 36 | set chans($chanid,$event) 1 37 | } 38 | } 39 | 40 | proc read {chanid count} { 41 | variable chans 42 | 43 | if {[$chans($chanid) size] == 0} { 44 | return -code error EAGAIN 45 | } 46 | 47 | return [$chans($chanid) get] 48 | } 49 | 50 | proc write {chanid data} { 51 | variable chans 52 | 53 | $chans($chanid) put $data 54 | if {$chans($chanid,read)} { 55 | chan postevent $chanid read 56 | } 57 | 58 | return [string length $data] 59 | } 60 | 61 | namespace export -clear * 62 | namespace ensemble create -subcommands {} 63 | } 64 | 65 | # vim: set ts=8 sw=4 sts=4 noet : 66 | -------------------------------------------------------------------------------- /examples/publisherConfirms.tcl: -------------------------------------------------------------------------------- 1 | package require rmq 2 | 3 | proc rmq_connected {conn} { 4 | set rChan [::rmq::Channel new $conn] 5 | $rChan onOpen setup_publisher_confirms 6 | } 7 | 8 | proc confirm_selected {rChan} { 9 | set ::confirmsAccepted 1 10 | } 11 | 12 | proc setup_publisher_confirms {rChan} { 13 | # need to use the AMQP confirm.select method 14 | $rChan confirmSelect 15 | $rChan on confirmSelectOk confirm_selected 16 | puts " \[+\] Sent confirm.select: waiting for acceptance..." 17 | vwait ::confirmsAccepted 18 | 19 | # with confirm.select sent, need to setup a callback 20 | # for basic.ack 21 | $rChan on basicAck publisher_confirm 22 | 23 | # now need to publish some data 24 | # assumes the test queue has been setup already 25 | puts " \[+\] Publishing some messages..." 26 | set pFlags [list] 27 | set props [dict create delivery-mode 2] 28 | for {set i 0} {$i < 10} {incr i} { 29 | $rChan basicPublish [randomDelimString 1000] "" "test" $pFlags $props 30 | } 31 | puts " \[+\] Published messages..." 32 | } 33 | 34 | # Taken from the Tcl wiki: https://wiki.tcl.tk/3757 35 | binary scan A c A 36 | binary scan z c z 37 | proc randomDelimString [list length [list min $A] [list max $z]] { 38 | set range [expr {$max-$min}] 39 | 40 | set txt "" 41 | for {set i 0} {$i < $length} {incr i} { 42 | set ch [expr {$min+int(rand()*$range)}] 43 | append txt [binary format c $ch] 44 | } 45 | return $txt 46 | } 47 | 48 | proc publisher_confirm {rChan dTag multiple} { 49 | puts " \[+\]: Received confirm for delivery tag $dTag (multiple? $multiple)" 50 | 51 | # do some book-keeping to mark which message has been confirmed 52 | 53 | # in our case, will exit the event loop after getting all our confirms 54 | if {$dTag == 10} { 55 | set ::die 1 56 | } 57 | } 58 | 59 | set conn [::rmq::Connection new -login [::rmq::Login new]] 60 | $conn onConnected rmq_connected 61 | $conn connect 62 | 63 | vwait ::die 64 | -------------------------------------------------------------------------------- /package/encoders.tcl: -------------------------------------------------------------------------------- 1 | ## 2 | ## 3 | ## encoders.tcl - procs in the rmq namespace for encoding Tcl values 4 | ## into binary values, i.e., strings of raw bytes, suitable for 5 | ## sending to a RabbitMQ server 6 | ## 7 | ## before sending any data across the network, the Tcl representation 8 | ## will pass through a proc contained in this file 9 | ## 10 | ## 11 | package provide rmq 1.4.5 12 | 13 | namespace eval rmq { 14 | 15 | proc enc_frame {ftype channel content} { 16 | set ftype [::rmq::enc_byte $ftype] 17 | set channel [::rmq::enc_short $channel] 18 | set size [::rmq::enc_ulong [string length $content]] 19 | return ${ftype}${channel}${size}${content}$::rmq::FRAME_END 20 | } 21 | 22 | proc enc_method {mtype mid content} { 23 | set mtype [::rmq::enc_short $mtype] 24 | set mid [::rmq::enc_short $mid] 25 | return "${mtype}${mid}${content}" 26 | } 27 | 28 | proc enc_protocol_header {} { 29 | return "AMQP[binary format c4 {0 0 9 1}]" 30 | } 31 | 32 | # 33 | # enc_field_table - given a dict, convert it into a field 34 | # table binary string 35 | # this proc attempts to convert any values passed to it 36 | # using the string is command 37 | # integers below 2**8 - 1 will be encoded as a short short 38 | # integers below 2**16 - 1 will be encoded as a short 39 | # integers larger than that will be encoded as longs 40 | # all double values will be converted to a float 41 | # booleans are checked after ints and doubles, so a textual 42 | # true or false value must be used to get a boolean conversion 43 | # string values are encoded as long strings 44 | # 45 | # takes an optional second argument to ignore certain fields 46 | # if the type recognition offered by the proc is insufficient 47 | # 48 | proc enc_field_table {fieldD {skipKeys ""}} { 49 | set fieldStr "" 50 | dict for {k v} $fieldD { 51 | if {$k in $skipKeys} { 52 | set v $v 53 | } elseif {[string is integer -strict $v]} { 54 | if {$v <= 2**8 - 1} { 55 | set v "[::rmq::enc_field_value short-short-int][::rmq::enc_short_short $v]" 56 | } elseif {$v <= 2**16 - 1} { 57 | set v "[::rmq::enc_field_value short-int][::rmq::enc_short $v]" 58 | } else { 59 | set v "[::rmq::enc_field_value long-int][::rmq::enc_long $v]" 60 | } 61 | } elseif {[string is double -strict $v]} { 62 | set v "[::rmq::enc_field_value float][::rmq::enc_float $v]" 63 | } elseif {[string is boolean -strict $v]} { 64 | set v "[::rmq::enc_field_value boolean][::rmq::enc_boolean $v]" 65 | } elseif {[string is alnum $v]} { 66 | set v "[::rmq::enc_field_value long-string][::rmq::enc_long_string $v]" 67 | } 68 | 69 | append fieldStr "[::rmq::enc_short_string $k]$v" 70 | } 71 | 72 | set fieldStrLen [::rmq::enc_ulong [string length $fieldStr]] 73 | 74 | return "${fieldStrLen}${fieldStr}" 75 | } 76 | 77 | # 78 | # enc_field_value - given a textual description of a field table 79 | # data value type, return the binary equivalent 80 | # 81 | # when encoding a field table all field table values must be 82 | # prefaced by a byte encoding their data type and this proc helps 83 | # while building up a Tcl dict for encoding 84 | # 85 | proc enc_field_value {typeDesc} { 86 | return [binary format a1 [lookup_field_value $typeDesc]] 87 | } 88 | 89 | proc lookup_field_value {typeDesc} { 90 | switch $typeDesc { 91 | boolean {return t} 92 | short-short-int {return b} 93 | short-short-uint {return B} 94 | short-int {return U} 95 | short-uint {return u} 96 | long-int {return I} 97 | long-uint {return i} 98 | long-long-int {return L} 99 | long-long-uint {return l} 100 | float {return f} 101 | double {return d} 102 | decimal-value {return D} 103 | short-string {return s} 104 | long-string {return S} 105 | field-array {return A} 106 | timestamp {return T} 107 | field-table {return F} 108 | no-field {return V} 109 | } 110 | } 111 | 112 | proc enc_byte {value} { 113 | return [binary format cu $value] 114 | } 115 | 116 | proc enc_boolean {boolean} { 117 | return [binary format c [string is true -strict $boolean]] 118 | } 119 | 120 | proc enc_short_string {str} { 121 | # Short string consists of OCTET *string-char 122 | set sLen [string length $str] 123 | 124 | return "[::rmq::enc_byte $sLen][binary format a$sLen $str]" 125 | } 126 | 127 | proc enc_long_string {str} { 128 | # Long string consists of long-uint *OCTET 129 | set sLen [string length $str] 130 | 131 | return "[::rmq::enc_ulong $sLen][binary format a$sLen $str]" 132 | } 133 | 134 | # 135 | # enc_field_array - given the name of an array to be upvar'ed in 136 | # the proc, convert it to a field array binary string 137 | # 138 | proc enc_field_array {_fieldA} { 139 | error "Need to implement field array encoding" 140 | } 141 | 142 | proc enc_short_short {int} { 143 | return [binary format c $int] 144 | } 145 | 146 | proc enc_short_short_uint {int} { 147 | return [binary format cu $int] 148 | } 149 | 150 | proc enc_short {int} { 151 | return [binary format S $int] 152 | } 153 | 154 | proc enc_ushort {int} { 155 | return [binary format Su $int] 156 | } 157 | 158 | proc enc_long {int} { 159 | return [binary format I $int] 160 | } 161 | 162 | proc enc_ulong {int} { 163 | return [binary format Iu $int] 164 | } 165 | 166 | proc enc_ulong_long {int} { 167 | return [binary format Wu $int] 168 | } 169 | 170 | proc enc_float {float} { 171 | return [binary format R $float] 172 | } 173 | 174 | proc enc_double {double} { 175 | return [binary format Q $double] 176 | } 177 | 178 | proc enc_timestamp {timestamp} { 179 | return [binary format Wu $timestamp] 180 | } 181 | 182 | # takes a class ID and a dictionary of property values 183 | # this proc handles encoding the props 184 | proc enc_content_header {classID bodySize propsD} { 185 | set classID [::rmq::enc_short $classID] 186 | set weight [::rmq::enc_short 0] 187 | set bodySize [::rmq::enc_ulong_long $bodySize] 188 | set props [::rmq::enc_properties $propsD] 189 | 190 | return ${classID}${weight}${bodySize}${props} 191 | } 192 | 193 | # for encoding message properties when publishing a message 194 | proc enc_properties {propsD} { 195 | # first need the flags field and then a property list 196 | set propFlags 0 197 | set props "" 198 | dict for {k v} $propsD { 199 | if {![dict exists $::rmq::PROPERTY_FLAGS $k]} { 200 | continue 201 | } 202 | 203 | set propFlag [dict get $::rmq::PROPERTY_FLAGS $k] 204 | set propFlags [expr {$propFlags | $propFlag}] 205 | 206 | set propEncoder "::rmq::enc_[dict get $::rmq::PROPERTY_TYPES $k]" 207 | append props [$propEncoder $v] 208 | } 209 | set propFlags [::rmq::enc_short $propFlags] 210 | 211 | return ${propFlags}${props} 212 | } 213 | } 214 | 215 | 216 | # vim: ts=4:sw=4:sts=4:noet 217 | -------------------------------------------------------------------------------- /package/constants.tcl: -------------------------------------------------------------------------------- 1 | package provide rmq 1.4.5 2 | 3 | namespace eval rmq { 4 | # Frame types 5 | set FRAME_METHOD 1 6 | set FRAME_HEADER 2 7 | set FRAME_BODY 3 8 | set FRAME_HEARTBEAT 8 9 | set FRAME_MIN_SIZE 4096 10 | set FRAME_END "\xCE" 11 | 12 | # Class names 13 | set CONNECTION_CLASS 10 14 | set CHANNEL_CLASS 20 15 | set EXCHANGE_CLASS 40 16 | set QUEUE_CLASS 50 17 | set BASIC_CLASS 60 18 | set CONFIRM_CLASS 85 19 | set TX_CLASS 90 20 | 21 | set CHANNEL_CLASSES [list $CHANNEL_CLASS $EXCHANGE_CLASS \ 22 | $QUEUE_CLASS $BASIC_CLASS \ 23 | $CONFIRM_CLASS $TX_CLASS] 24 | 25 | ## 26 | ## Methods 27 | ## 28 | set CONNECTION_START 10 29 | set CONNECTION_START_OK 11 30 | set CONNECTION_SECURE 20 31 | set CONNECTION_SECURE_OK 21 32 | set CONNECTION_TUNE 30 33 | set CONNECTION_TUNE_OK 31 34 | set CONNECTION_OPEN 40 35 | set CONNECTION_OPEN_OK 41 36 | set CONNECTION_CLOSE 50 37 | set CONNECTION_CLOSE_OK 51 38 | set CONNECTION_BLOCKED 60 39 | set CONNECTION_UNBLOCKED 61 40 | 41 | set CHANNEL_OPEN 10 42 | set CHANNEL_OPEN_OK 11 43 | set CHANNEL_FLOW 20 44 | set CHANNEL_FLOW_OK 21 45 | set CHANNEL_CLOSE 40 46 | set CHANNEL_CLOSE_OK 41 47 | 48 | set EXCHANGE_DECLARE 10 49 | set EXCHANGE_DECLARE_OK 11 50 | set EXCHANGE_DELETE 20 51 | set EXCHANGE_DELETE_OK 21 52 | set EXCHANGE_BIND 30 53 | set EXCHANGE_BIND_OK 31 54 | set EXCHANGE_UNBIND 40 55 | set EXCHANGE_UNBIND_OK 41 56 | 57 | set QUEUE_DECLARE 10 58 | set QUEUE_DECLARE_OK 11 59 | set QUEUE_BIND 20 60 | set QUEUE_BIND_OK 21 61 | set QUEUE_UNBIND 50 62 | set QUEUE_UNBIND_OK 51 63 | set QUEUE_PURGE 30 64 | set QUEUE_PURGE_OK 31 65 | set QUEUE_DELETE 40 66 | set QUEUE_DELETE_OK 41 67 | 68 | set BASIC_QOS 10 69 | set BASIC_QOS_OK 11 70 | set BASIC_CONSUME 20 71 | set BASIC_CONSUME_OK 21 72 | set BASIC_CANCEL 30 73 | set BASIC_CANCEL_OK 31 74 | set BASIC_PUBLISH 40 75 | set BASIC_RETURN 50 76 | set BASIC_DELIVER 60 77 | set BASIC_GET 70 78 | set BASIC_GET_OK 71 79 | set BASIC_GET_EMPTY 72 80 | set BASIC_ACK 80 81 | set BASIC_REJECT 90 82 | set BASIC_RECOVER_ASYNC 100 83 | set BASIC_RECOVER 110 84 | set BASIC_RECOVER_OK 111 85 | set BASIC_NACK 120 86 | 87 | set CONFIRM_SELECT 10 88 | set CONFIRM_SELECT_OK 11 89 | 90 | set TX_SELECT 10 91 | set TX_SELECT_OK 11 92 | set TX_COMMIT 20 93 | set TX_COMMIT_OK 21 94 | set TX_ROLLBACK 30 95 | set TX_ROLLBACK_OK 31 96 | 97 | ## 98 | ## 99 | ## Bitmasks 100 | ## 101 | ## 102 | set EXCHANGE_PASSIVE 1 103 | set EXCHANGE_DURABLE 2 104 | # auto-delete and internal should be set to 0 105 | set EXCHANGE_AUTO_DELETE 4 106 | set EXCHANGE_INTERNAL 8 107 | set EXCHANGE_NO_WAIT 16 108 | 109 | set QUEUE_PASSIVE 1 110 | set QUEUE_DURABLE 2 111 | set QUEUE_EXCLUSIVE 4 112 | set QUEUE_AUTO_DELETE 8 113 | set QUEUE_DECLARE_NO_WAIT 16 114 | 115 | set QUEUE_IF_UNUSED 1 116 | set QUEUE_IF_EMPTY 2 117 | set QUEUE_DELETE_NO_WAIT 4 118 | 119 | # no local means the server will not send messages 120 | # to the connection that published them 121 | set CONSUME_NO_LOCAL 1 122 | set CONSUME_NO_ACK 2 123 | set CONSUME_EXCLUSIVE 4 124 | set CONSUME_NO_WAIT 8 125 | 126 | set PUBLISH_MANDATORY 1 127 | set PUBLISH_IMMEDIATE 2 128 | 129 | set NACK_MULTIPLE 1 130 | set NACK_REQUEUE 2 131 | 132 | # content header flags ordered from high-to-low 133 | # so the first property is bit 15 134 | set PROPERTY_CONTENT_TYPE [expr {2**15}] 135 | set PROPERTY_CONTENT_ENCODING [expr {2**14}] 136 | set PROPERTY_HEADERS [expr {2**13}] 137 | set PROPERTY_DELIVERY_MODE [expr {2**12}] 138 | set PROPERTY_PRIORITY [expr {2**11}] 139 | set PROPERTY_CORRELATION_ID 1024 140 | set PROPERTY_REPLY_TO 512 141 | set PROPERTY_EXPIRATION 256 142 | set PROPERTY_MESSAGE_ID 128 143 | set PROPERTY_TIMESTAMP 64 144 | set PROPERTY_TYPE 32 145 | set PROPERTY_USER_ID 16 146 | set PROPERTY_APP_ID 8 147 | set PROPERTY_RESERVED 4 148 | 149 | ## 150 | ## Message Property Types 151 | ## 152 | # map property name to AMQP type 153 | set PROPERTY_TYPES [dict create {*}{ 154 | content-type short_string 155 | content-encoding short_string 156 | headers field_table 157 | delivery-mode byte 158 | priority byte 159 | correlation-id short_string 160 | reply-to short_string 161 | expiration short_string 162 | message-id short_string 163 | timestamp timestamp 164 | type short_string 165 | user-id short_string 166 | app-id short_string 167 | reserved short_string 168 | }] 169 | 170 | # map property name to bit flag 171 | set PROPERTY_FLAGS [dict create {*}[subst { 172 | content-type $::rmq::PROPERTY_CONTENT_TYPE 173 | content-encoding $::rmq::PROPERTY_CONTENT_ENCODING 174 | headers $::rmq::PROPERTY_HEADERS 175 | delivery-mode $::rmq::PROPERTY_DELIVERY_MODE 176 | priority $::rmq::PROPERTY_PRIORITY 177 | correlation-id $::rmq::PROPERTY_CORRELATION_ID 178 | reply-to $::rmq::PROPERTY_REPLY_TO 179 | expiration $::rmq::PROPERTY_EXPIRATION 180 | message-id $::rmq::PROPERTY_MESSAGE_ID 181 | timestamp $::rmq::PROPERTY_TIMESTAMP 182 | type $::rmq::PROPERTY_TYPE 183 | user-id $::rmq::PROPERTY_USER_ID 184 | app-id $::rmq::PROPERTY_APP_ID 185 | reserved $::rmq::PROPERTY_RESERVED 186 | }]] 187 | 188 | # map property bit number to property name 189 | set PROPERTY_BITS [dict create {*}{ 190 | 15 content-type 191 | 14 content-encoding 192 | 13 headers 193 | 12 delivery-mode 194 | 11 priority 195 | 10 correlation-id 196 | 9 reply-to 197 | 8 expiration 198 | 7 message-id 199 | 6 timestamp 200 | 5 type 201 | 4 user-id 202 | 3 app-id 203 | 2 reserved 204 | }] 205 | 206 | ## 207 | ## Error handling 208 | ## 209 | 210 | # ERROR_CODES dict maps numeric error codes 211 | # found in frame type fields to their 212 | # name 213 | set ERROR_CODES [dict create {*}{ 214 | 200 reply-success 215 | 311 content-too-large 216 | 313 no-consumers 217 | 320 connection-forced 218 | 402 invalid-path 219 | 403 access-refused 220 | 404 not-found 221 | 405 resource-locked 222 | 406 precondition-failed 223 | 501 frame-error 224 | 502 syntax-error 225 | 503 command-invalid 226 | 504 channel-error 227 | 505 unexpected-frame 228 | 506 resource-error 229 | 530 not-allowed 230 | 540 not-implemented 231 | 541 internal-error 232 | }] 233 | 234 | # CONN_ERRORS is a sorted list of 235 | # numeric error codes which are connection 236 | # errors and require closing the socket 237 | set CONN_ERRORS { 238 | 320 239 | 402 240 | 501 241 | 502 242 | 503 243 | 504 244 | 505 245 | 530 246 | 540 247 | 541 248 | } 249 | 250 | ## 251 | ## Defaults and Misc 252 | ## 253 | set DEFAULT_HOST "localhost" 254 | set DEFAULT_PORT 5672 255 | set DEFAULT_UN "guest" 256 | set DEFAULT_PW "guest" 257 | set DEFAULT_VHOST "/" 258 | set DEFAULT_MECHANISM "PLAIN" 259 | set DEFAULT_LOCALE "en_US" 260 | 261 | # Max frame size for connection negotiation 262 | set MAX_FRAME_SIZE 131072 263 | 264 | # A value of 0 means the client does not want heartbeats 265 | set HEARTBEAT_SECS 60 266 | 267 | # Whether receiving consumer cancels from the server is supported 268 | set CANCEL_NOTIFICATIONS 1 269 | 270 | # Whether receiving blocked connection notifications is supported 271 | set BLOCKED_CONNECTIONS 1 272 | 273 | # Channel defaults where a channel max of 0 means no limit imposed 274 | set MAX_CHANNELS [expr {2**16} - 1] 275 | set DEFAULT_CHANNEL_MAX 0 276 | 277 | # Auto-reconnect with exponential backoff constants 278 | set DEFAULT_AUTO_RECONNECT 1 279 | set DEFAULT_MAX_BACKOFF 64 280 | set DEFAULT_MAX_RECONNECT_ATTEMPTS 5 281 | 282 | # Timeout threshold in seconds for trying to connect 283 | set DEFAULT_MAX_TIMEOUT 3 284 | 285 | # How many msecs to check for active connection 286 | set CHECK_CONNECTION 500 287 | 288 | # Version supported by the library 289 | set AMQP_VMAJOR 0 290 | set AMQP_VMINOR 9 291 | 292 | # client properties 293 | set PRODUCT tclrmq 294 | set VERSION 1.4.5 295 | } 296 | 297 | # vim: ts=4:sw=4:sts=4:noet 298 | -------------------------------------------------------------------------------- /package/decoders.tcl: -------------------------------------------------------------------------------- 1 | package provide rmq 1.4.5 2 | 3 | namespace eval rmq { 4 | 5 | proc dec_field_table {data _bytes} { 6 | upvar $_bytes bytes 7 | 8 | # will build up a dict of key-value pairs 9 | set fieldD [dict create] 10 | 11 | # field-table: long-uint *field-value-pair 12 | # field-value-pair: field-name field-value 13 | # field-name: short-string 14 | binary scan $data Iu len 15 | ::rmq::debug "Field table is $len bytes" 16 | set data [string range $data 4 end] 17 | 18 | set bytesProcessed 4 19 | while {$bytesProcessed < $len + 4} { 20 | # first, get the short string for the key 21 | set key [::rmq::dec_short_string $data bytes] 22 | set data [string range $data $bytes end] 23 | incr bytesProcessed $bytes 24 | 25 | # next, get the field value type and field value 26 | binary scan $data a1 valueType 27 | set data [string range $data 1 end] 28 | 29 | set decoder [::rmq::dec_field_value_type $valueType] 30 | if {[llength $decoder] > 1} { 31 | set value [{*}$decoder $data bytes] 32 | } else { 33 | set value [$decoder $data bytes] 34 | } 35 | incr bytesProcessed [expr {1 + $bytes}] 36 | set data [string range $data $bytes end] 37 | 38 | # put the key-value pair into the dict 39 | dict set fieldD $key $value 40 | } 41 | 42 | set bytes $bytesProcessed 43 | return $fieldD 44 | } 45 | 46 | proc dec_generic {spec data _bytes} { 47 | upvar 2 $_bytes bytes 48 | 49 | binary scan $data $spec value 50 | 51 | switch $spec { 52 | c - 53 | cu { 54 | set bytes 1 55 | } 56 | 57 | S - 58 | Su { 59 | set bytes 2 60 | } 61 | 62 | I - 63 | Iu - 64 | f { 65 | set bytes 4 66 | } 67 | 68 | W - 69 | Wu - 70 | d { 71 | set bytes 8 72 | } 73 | } 74 | 75 | return $value 76 | } 77 | 78 | proc dec_field_array {data _bytes} { 79 | error "Field array decoder not implemented" 80 | } 81 | 82 | proc dec_decimal_value {data _bytes} { 83 | error "Decimal value decoder not implemeneted" 84 | } 85 | 86 | proc dec_byte {data _bytes} { 87 | return [::rmq::dec_generic c $data $_bytes] 88 | } 89 | 90 | proc dec_short {data _bytes} { 91 | return [::rmq::dec_generic S $data $_bytes] 92 | } 93 | 94 | proc dec_ushort {data _bytes} { 95 | return [::rmq::dec_generic Su $data $_bytes] 96 | } 97 | 98 | proc dec_long {data _bytes} { 99 | return [::rmq::dec_generic I $data $_bytes] 100 | } 101 | 102 | proc dec_ulong {data _bytes} { 103 | return [::rmq::dec_generic Iu $data $_bytes] 104 | } 105 | 106 | proc dec_long_long {data _bytes} { 107 | return [::rmq::dec_generic W $data $_bytes] 108 | } 109 | 110 | proc dec_ulong_long {data _bytes} { 111 | return [::rmq::dec_generic Wu $data $_bytes] 112 | } 113 | 114 | proc dec_float {data _bytes} { 115 | return [::rmq::dec_generic f $data $_bytes] 116 | } 117 | 118 | proc dec_double {data _bytes} { 119 | return [::rmq::dec_generic d $data $_bytes] 120 | } 121 | 122 | proc dec_field_value_type {valueType} { 123 | # need to dispatch based on value type 124 | switch $valueType { 125 | "t" - 126 | "b" { 127 | return "::rmq::dec_generic c" 128 | } 129 | 130 | "B" { 131 | return "::rmq::dec_generic cu" 132 | } 133 | 134 | "U" { 135 | return "::rmq::dec_generic S" 136 | } 137 | 138 | "u" { 139 | return "::rmq::dec_generic Su" 140 | } 141 | 142 | "I" { 143 | return "::rmq::dec_generic I" 144 | } 145 | 146 | "i" { 147 | return "::rmq::dec_generic Iu" 148 | } 149 | 150 | "L" { 151 | return "::rmq::dec_generic W" 152 | } 153 | 154 | "l" { 155 | return "::rmq::dec_generic Wu" 156 | } 157 | 158 | "f" { 159 | return "::rmq::dec_generic f" 160 | } 161 | 162 | "d" { 163 | return "::rmq::dec_generic d" 164 | } 165 | 166 | "D" { 167 | return "::rmq::dec_decimal_value" 168 | } 169 | 170 | "s" { 171 | return ::rmq::dec_short_string 172 | } 173 | 174 | "S" { 175 | return ::rmq::dec_long_string 176 | } 177 | 178 | "A" { 179 | return ::rmq::dec_field_array 180 | } 181 | 182 | "T" { 183 | return "::rmq::dec_generic Wu" 184 | } 185 | 186 | "F" { 187 | return ::rmq::dec_field_table 188 | } 189 | 190 | "V" { 191 | return ::rmq::dec_no_field 192 | } 193 | } 194 | } 195 | 196 | proc dec_short_string {data _bytes} { 197 | upvar $_bytes bytes 198 | 199 | # starts with a byte for the length 200 | binary scan $data cu len 201 | 202 | # then need to read the actual string 203 | set str [string range $data 1 $len] 204 | 205 | # book-keeping the number of bytes processed 206 | set bytes [expr {1 + $len}] 207 | 208 | return $str 209 | } 210 | 211 | proc dec_long_string {data _bytes} { 212 | upvar $_bytes bytes 213 | 214 | # starts with 4 bytes for the length 215 | binary scan $data Iu len 216 | 217 | # then need to read the full string 218 | set str [string range $data 4 [expr {4 + $len}]] 219 | 220 | # book-keeping the number of bytes processed 221 | set bytes [expr {4 + $len}] 222 | 223 | return $str 224 | } 225 | 226 | proc dec_decimal_value {data _bytes} { 227 | upvar $_bytes bytes 228 | 229 | # starts with a byte specifying the number of 230 | # decimal digits to come 231 | binary scan $data cu scale 232 | 233 | # now can read the rest of the data 234 | binary scan $data @1Iu value 235 | 236 | # always read a byte + a long-unint 237 | set bytes 5 238 | 239 | # need to turn the value into a decimal 240 | # with scale digits after the decimal point 241 | if {$scale == 0} { 242 | return $value 243 | } else { 244 | set numDigits [string length $value] 245 | set beforeDec [expr {$numDigits - $scale}] 246 | 247 | set beforeDecStr [string range $value 0 [expr {$beforeDec - 1}]] 248 | set afterDecStr [string range $value $beforeDec end] 249 | 250 | if {$afterDecStr eq ""} { 251 | return $beforeDecStr 252 | } else { 253 | return "${beforeDecStr}.${afterDecStr}" 254 | } 255 | } 256 | } 257 | 258 | proc dec_method {mClass mID} { 259 | if {$mClass eq $::rmq::CONNECTION_CLASS} { 260 | return "connection[::rmq::dec_connection_method $mID]" 261 | } elseif {$mClass eq $::rmq::CHANNEL_CLASS} { 262 | return "channel[::rmq::dec_channel_method $mID]" 263 | } elseif {$mClass eq $::rmq::EXCHANGE_CLASS} { 264 | return "exchange[::rmq::dec_exchange_method $mID]" 265 | } elseif {$mClass eq $::rmq::QUEUE_CLASS} { 266 | return "queue[::rmq::dec_queue_method $mID]" 267 | } elseif {$mClass eq $::rmq::BASIC_CLASS} { 268 | return "basic[::rmq::dec_basic_method $mID]" 269 | } elseif {$mClass eq $::rmq::CONFIRM_CLASS} { 270 | return "confirm[::rmq::dec_confirm_method $mID]" 271 | } elseif {$mClass eq $::rmq::TX_CLASS} { 272 | return "tx[::rmq::dec_tx_method $mID]" 273 | } 274 | } 275 | 276 | proc dec_connection_method {mID} { 277 | if {$mID eq $::rmq::CONNECTION_START} {return Start} 278 | if {$mID eq $::rmq::CONNECTION_START_OK} {return StartOk} 279 | if {$mID eq $::rmq::CONNECTION_SECURE} {return Secure} 280 | if {$mID eq $::rmq::CONNECTION_SECURE_OK} {return SecureOk} 281 | if {$mID eq $::rmq::CONNECTION_TUNE} {return Tune} 282 | if {$mID eq $::rmq::CONNECTION_TUNE_OK} {return TuneOk} 283 | if {$mID eq $::rmq::CONNECTION_OPEN} {return Open} 284 | if {$mID eq $::rmq::CONNECTION_OPEN_OK} {return OpenOk} 285 | if {$mID eq $::rmq::CONNECTION_CLOSE} {return CloseRecv} 286 | if {$mID eq $::rmq::CONNECTION_CLOSE_OK} {return CloseOk} 287 | } 288 | 289 | proc dec_channel_method {mID} { 290 | if {$mID eq $::rmq::CHANNEL_OPEN} {return Open} 291 | if {$mID eq $::rmq::CHANNEL_OPEN_OK} {return OpenOk} 292 | if {$mID eq $::rmq::CHANNEL_FLOW} {return Flow} 293 | if {$mID eq $::rmq::CHANNEL_FLOW_OK} {return FlowOk} 294 | if {$mID eq $::rmq::CHANNEL_CLOSE} {return CloseRecv} 295 | if {$mID eq $::rmq::CHANNEL_CLOSE_OK} {return CloseOk} 296 | } 297 | 298 | proc dec_exchange_method {mID} { 299 | if {$mID eq $::rmq::EXCHANGE_DECLARE} {return Declare} 300 | if {$mID eq $::rmq::EXCHANGE_DECLARE_OK} {return DeclareOk} 301 | if {$mID eq $::rmq::EXCHANGE_DELETE} {return Delete} 302 | if {$mID eq $::rmq::EXCHANGE_DELETE_OK} {return DeleteOk} 303 | if {$mID eq $::rmq::EXCHANGE_BIND_OK} {return BindOk} 304 | if {$mID eq $::rmq::EXCHANGE_UNBIND_OK} {return UnbindOk} 305 | } 306 | 307 | proc dec_queue_method {mID} { 308 | if {$mID eq $::rmq::QUEUE_DECLARE} {return Declare} 309 | if {$mID eq $::rmq::QUEUE_DECLARE_OK} {return DeclareOk} 310 | if {$mID eq $::rmq::QUEUE_BIND} {return Bind} 311 | if {$mID eq $::rmq::QUEUE_BIND_OK} {return BindOk} 312 | if {$mID eq $::rmq::QUEUE_UNBIND} {return Unbind} 313 | if {$mID eq $::rmq::QUEUE_UNBIND_OK} {return UnbindOk} 314 | if {$mID eq $::rmq::QUEUE_PURGE} {return Purge} 315 | if {$mID eq $::rmq::QUEUE_PURGE_OK} {return PurgeOk} 316 | if {$mID eq $::rmq::QUEUE_DELETE} {return Delete} 317 | if {$mID eq $::rmq::QUEUE_DELETE_OK} {return DeleteOk} 318 | } 319 | 320 | proc dec_basic_method {mID} { 321 | if {$mID eq $::rmq::BASIC_QOS} {return Qos} 322 | if {$mID eq $::rmq::BASIC_QOS_OK} {return QosOk} 323 | if {$mID eq $::rmq::BASIC_CONSUME} {return Consume} 324 | if {$mID eq $::rmq::BASIC_CONSUME_OK} {return ConsumeOk} 325 | if {$mID eq $::rmq::BASIC_CANCEL} {return CancelRecv} 326 | if {$mID eq $::rmq::BASIC_CANCEL_OK} {return CancelOk} 327 | if {$mID eq $::rmq::BASIC_PUBLISH} {return Publish} 328 | if {$mID eq $::rmq::BASIC_RETURN} {return Return} 329 | if {$mID eq $::rmq::BASIC_DELIVER} {return Deliver} 330 | if {$mID eq $::rmq::BASIC_GET} {return Get} 331 | if {$mID eq $::rmq::BASIC_GET_OK} {return GetOk} 332 | if {$mID eq $::rmq::BASIC_GET_EMPTY} {return GetEmpty} 333 | if {$mID eq $::rmq::BASIC_ACK} {return AckReceived} 334 | if {$mID eq $::rmq::BASIC_REJECT} {return Reject} 335 | if {$mID eq $::rmq::BASIC_RECOVER_ASYNC} {return RecoverAsync} 336 | if {$mID eq $::rmq::BASIC_RECOVER} {return Recover} 337 | if {$mID eq $::rmq::BASIC_RECOVER_OK} {return RecoverOk} 338 | if {$mID eq $::rmq::BASIC_NACK} {return NackReceived} 339 | } 340 | 341 | proc dec_confirm_method {mID} { 342 | if {$mID eq $::rmq::CONFIRM_SELECT} {return Select} 343 | if {$mID eq $::rmq::CONFIRM_SELECT_OK} {return SelectOk} 344 | } 345 | 346 | proc dec_tx_method {mID} { 347 | if {$mID eq $::rmq::TX_SELECT} {return Select} 348 | if {$mID eq $::rmq::TX_SELECT_OK} {return SelectOk} 349 | if {$mID eq $::rmq::TX_COMMIT} {return Commit} 350 | if {$mID eq $::rmq::TX_COMMIT_OK} {return CommitOk} 351 | if {$mID eq $::rmq::TX_ROLLBACK} {return Rollback} 352 | if {$mID eq $::rmq::TX_ROLLBACK_OK} {return RollbackOk} 353 | } 354 | 355 | proc dec_content_header {data} { 356 | ::rmq::debug "Decode content header" 357 | 358 | # the class ID field 359 | set cID [::rmq::dec_short $data _] 360 | 361 | # TODO: validate that the class ID matches the 362 | # frame class ID and respond with a 501 if not 363 | 364 | # unused weight field 365 | set data [string range $data 2 end] 366 | set weight [::rmq::dec_short $data _] 367 | 368 | # TODO: validate that the weight field is 0 369 | 370 | # body size for the content body frames to come 371 | set data [string range $data 2 end] 372 | set bodySize [::rmq::dec_ulong_long $data bytes] 373 | ::rmq::debug "Content header class ID $cID with a body size of $bodySize" 374 | 375 | # lastly, parse the property flags field 376 | set data [string range $data $bytes end] 377 | set flags [::rmq::dec_ushort $data _] 378 | 379 | # if the last bit (0) is set, more property flags remain 380 | # so combine them all together 381 | set propFlags $flags 382 | while {$flags & (1 << 0)} { 383 | ::rmq::debug "More than the typical number of property flags!" 384 | set data [string range $data 2 end] 385 | set flags [::rmq::dec_ushort $data _] 386 | #lappend propFlags $flags 387 | } 388 | 389 | set data [string range $data 2 end] 390 | set propList $data 391 | 392 | # the property flags indicate which fields are included 393 | # in the property list, so need to find that out now 394 | # so we know which decoders to apply to the propList blob 395 | set propsSet [list] 396 | dict for {bitNum propName} $::rmq::PROPERTY_BITS { 397 | if {$propFlags & (1 << $bitNum)} { 398 | lappend propsSet $propName 399 | } 400 | } 401 | 402 | set propsD [dict create] 403 | foreach propSet $propsSet { 404 | set decoder "::rmq::dec_[dict get $::rmq::PROPERTY_TYPES $propSet]" 405 | dict set propsD $propSet [$decoder $propList bytes] 406 | set propList [string range $propList $bytes end] 407 | } 408 | 409 | # return a dict of all the data 410 | return [dict create classID $cID bodySize $bodySize properties $propsD] 411 | } 412 | } 413 | 414 | # vim: ts=4:sw=4:sts=4:noet 415 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tclrmq 2 | Pure TCL RabbitMQ Library implementing AMQP 0.9.1 3 | 4 | This library is completely asynchronous and makes no blocking calls. 5 | It relies on TclOO and requires Tcl 8.6, but has no other dependencies 6 | (other than a RabbitMQ server). 7 | 8 | # About 9 | Developed for use within FlightAware (https://flightaware.com). 10 | 11 | The package directory contains a Makefile for installing globally. By 12 | default the Makefile installs to `/usr/local/lib`, so this will need editing 13 | if an alternative directory is required. 14 | 15 | # Basic Usage 16 | There are two primary classes required for using the library. 17 | 18 | 1) Connection 19 | 20 | The _Connection_ class is used for initiating initial communication with the 21 | RabbitMQ server. It also relies on a subsidiary _Login_ class, which is used 22 | for specifying username, password, vhost and authentication mechanism. Out of 23 | the box, this library only supports the PLAIN SASL mechanism. It can be easily 24 | extended to support an additional mechanism if required. 25 | 26 | ```tcl 27 | package require rmq 28 | 29 | # Arguments: -user -pass -vhost 30 | # All optional and shown with their defaults 31 | set login [Login new -user "guest" -pass "guest" -vhost "/"] 32 | 33 | # Pass the login object created above to the Connection 34 | # constructor 35 | # -host and -port are shown with their default values 36 | set conn [Connection new -host localhost -port 5672 -login $login] 37 | 38 | # Set a callback for when the connection is ready to use 39 | # which will be passed the connection object 40 | $conn onConnected rmq_conn_ready 41 | proc rmq_conn_ready {conn} { 42 | puts "Connection ready!" 43 | $conn connectionClose 44 | } 45 | 46 | # Set a callback for when the connection is closed 47 | $conn onClosed rmq_conn_closed 48 | proc rmq_conn_closed {conn} { 49 | puts "Connection closed!" 50 | } 51 | 52 | # Initiate the connection handshake and enter the event loop 53 | $conn connect 54 | vwait die 55 | ``` 56 | 57 | 2) Channel 58 | 59 | The _Channel_ class is where most of the action happens. The vast majority of AMQP 60 | methods refer to a specific channel. After the Connection object has gone through 61 | the opening handshake and calls its _onOpen_ callback, a _Channel_ object can be 62 | created by passing the _Connection_ object to the _Channel_ class' constructor. 63 | 64 | ```tcl 65 | # Assume the following proc has been set as the Connection object's 66 | # onOpen callback 67 | proc rmq_conn_ready {conn} { 68 | # Create a channel object 69 | # If no channel number is specified, the 70 | # next available will be chosen 71 | set chan [Channel new $conn] 72 | 73 | # Do something with the channel, like 74 | # declare an exchange 75 | set flags [list $::rmq::EXCHANGE_DURABLE] 76 | $chan exchangeDeclare "test" "direct" $flags 77 | } 78 | ``` 79 | 80 | # Callbacks 81 | Using this library for anything useful requires setting callbacks for the 82 | AMQP methods needed in the client application. Most callbacks will be set on 83 | _Channel_ objects, but the _Connection_ object supports a few as well. 84 | 85 | All callbacks are passed the object they were set on as the first parameter. 86 | Depending on the AMQP method or object event, additional parameters are provided 87 | as appropriate. 88 | 89 | ## Connection Callbacks 90 | 91 | Connection objects allow for the setting of the following callbacks: 92 | 93 | 1) _onConnected_: called when the AMQP connection handshake finishes and is passed the 94 | _Connection_ object 95 | 96 | 2) _onBlocked_: called when the RabbitMQ server has blocked connections due to 97 | [resource limitations](https://www.rabbitmq.com/connection-blocked.html). 98 | Callback is passed the _Connection_ object, a boolean for whether the connection 99 | is blocked or not and a textual reason 100 | 101 | 3) _onClosed_: called when the connection is closed and is passed the _Connection_ object 102 | and a dict containing the reply code, any reply text, the class ID and the method ID. 103 | The corresponding dictionary keys are `replyCode`, `replyText`, `classID` and 104 | `methodID` respectively. An alias method _onClose_ is also provided. 105 | 106 | 3) _onError_: called when an error code has been sent to the _Connection_ and is passed 107 | the error code and any accompanying data in the frame 108 | 109 | 4) _onFailedReconnect_: called when all reconnection attempts have been exhausted 110 | 111 | ```tcl 112 | package require rmq 113 | 114 | # Arguments: username password vhost 115 | set login [Login new -user "guest" -pass "guest" -vhost "/"] 116 | 117 | # Pass the login object created above to the Connection 118 | # constructor 119 | set conn [Connection new -host localhost -port 5672 -login $login] 120 | 121 | $conn onConnected rmq_connected 122 | $conn onClosed rmq_closed 123 | $conn onError rmq_connection_error 124 | 125 | proc rmq_connected {rmqConn} { 126 | # do useful things 127 | } 128 | 129 | proc rmq_closed {rmqConn closeD} { 130 | # do other useful things 131 | } 132 | 133 | proc rmq_error {rmqConn frameType frameData} { 134 | # do even more useful things 135 | } 136 | ``` 137 | 138 | ## Channel Callbacks 139 | 140 | _Channel_ objects have a few specific callbacks that can be set along with a more general 141 | callback mechanism for the majority of AMQP method calls. 142 | 143 | ### Specific Callbacks 144 | 145 | The specific callbacks provided for _Channel_ objects mirror those available for _Connection_ 146 | objects. They are: 147 | 148 | 1) _onOpen_: called when the channel is open and ready to use, i.e., when the Channel.Open-Ok 149 | method is received from the RabbitMQ server and is passed the same arguments as 150 | the _onConnected_ callback for Connection objects 151 | 152 | 2) _onClose_: called when the channel has been fully closed, i.e., when the Channel.Close-Ok 153 | method is received from the RabbitMQ server and is passed the _Channel_ object 154 | and the same dictionary passed to the _onClosed_ callback for _Connection_ objects 155 | 156 | 3) _onError_: called when the channel receives an error, i.e., a frame is received for the 157 | given channel but contains an AMQP error code and is passed the same arguments as 158 | the _onError_ callback for Connection objects 159 | 160 | ### General Callback Mechanism 161 | 162 | Other than the above callbacks, a Channel object can be supplied a callback for every method that 163 | can be sent in response to an AMQP method by using the _on_ method of Channel objects. 164 | 165 | These callbacks are passed the Channel object they were set on unless otherwise specified in the 166 | full method documentation found below. 167 | 168 | When specifying the name of the AMQP method the callback will be invoked on, start with a lowercase 169 | letter and use camel case. All AMQP methods documented in the 170 | [RabbitMQ 0-9-1 extended specification](https://www.rabbitmq.com/resources/specs/amqp0-9-1.extended.xml) 171 | are available. 172 | 173 | ```tcl 174 | # Asumming a channel object by name rmqChan exists 175 | $rmqChan on exchangeDeclareOk exchange_declared 176 | $rmqChan on queueDeclareOk queue_declared 177 | $rmqChan on queueBindOk queue_bound 178 | 179 | $rmqChan exchangeDeclare "the_best_exchange" "fanout" 180 | vwait exchangeDelcared 181 | 182 | $rmqChan queueDeclare "the_best_queue" 183 | vwait queueDeclared 184 | 185 | $rmqChan queueBind "the_best_queue" "the_best_exchange" "the_best_routing_key" 186 | 187 | proc exchange_delcared {rmqChan} { 188 | set ::exchangeDeclared 1 189 | } 190 | 191 | proc queue_declared {rmqChan} { 192 | set ::queueDeclared 1 193 | } 194 | 195 | proc queue_bound {rmqChan} { 196 | set ::queueBound 1 197 | } 198 | ``` 199 | 200 | ### The Exception of Consuming 201 | 202 | When consuming messages from a queue using either _Basic.Consume_ or _Basic.Get_, the process of 203 | setting a callback and the data passed into the callback differs from every other case. 204 | 205 | For consuming, the Channel object methods _basicConsume_ and _basicGet_ take the name of the callback 206 | invoked for each message delivered and then their arguments. The callbacks get passed in the 207 | Channel object, a dictionary of method data, a dictionary of frame data, and the data from the queue. 208 | 209 | ```tcl 210 | # Assuming a channel object by name rmqChan exists 211 | $rmqChan basicConsume consume_callback "the_best_queue" 212 | 213 | proc consume_callback {rmqChan methodD frameD data} { 214 | # Can inspect the consumer tag and dispatch on it 215 | switch [dict get $methodD consumerTag] { 216 | # useful things 217 | } 218 | 219 | # Can get the delivery tag to ack the message 220 | $rmqChan basicAck [dict get $methodD deliveryTag] 221 | 222 | # Frame data includes things like the data body size 223 | # and is likely less immediately useful but it is 224 | # passed in because it might be necessary for a given 225 | # application 226 | } 227 | ``` 228 | 229 | #### Consuming From Multiple Queues 230 | 231 | For a given channel, multiple queues can be consumed from and each queue can be given its own callback proc by passing in (or allowing the server to generate) a distinct _consumerTag_ for each invocation of _basicConsume_. Otherwise, dispatching based on the method or frame metadata allows a single callback proc to customize the handling of messages from different queues. When the client application is not constrained in its use of channels, instantiating multiple _Channel_ objects is a straight-forward way for one consumer to concurrently pull data from more than one queue. 232 | 233 | ### Method Data 234 | 235 | The dictionary of method data passed as the second argument to consumer callbacks contains the following items: 236 | 237 | * __consumerTag__ 238 | 239 | The string consumer tag, either specified at the time _basicConsume_ is called, or auto-generated by the server. 240 | 241 | * __deliveryTag__ 242 | 243 | Integer numbering for the message being consumed. This is used for the _basicAck_ or _basicNack_ methods. 244 | 245 | * __redelivered__ 246 | 247 | Boolean integer. 248 | 249 | * __exchange__ 250 | 251 | Name of the exchange the message came from. 252 | 253 | * __routingKey__ 254 | 255 | Routing key used for delivery of the message. 256 | 257 | ### Frame Data 258 | 259 | The dictionary of frame data passed as the third argument to consumer callbacks contains the following items: 260 | 261 | * __classID__ 262 | 263 | AMQP defined integer for the class used for delivering the message. 264 | 265 | * __bodySize__ 266 | 267 | Size in bytes for the data consumed from the queue. 268 | 269 | * __properties__ 270 | 271 | Dictionary of AMQP [Basic method properties](https://www.rabbitmq.com/amqp-0-9-1-reference.html), e.g., 272 | _correlation-id_, _timestamp_ or _content-type_. 273 | 274 | # Special Arguments 275 | 276 | ## Flags 277 | 278 | For AMQP methods like _queueDeclare_ or _exchangeDeclare_ which take flags, these are passed in as a list of 279 | constants. All supported flags are mentioned in the documentation below detailing each _Channel_ method. 280 | Within the source, supported flag constants are found in [constants.tcl](package/constants.tcl#L102-L130). 281 | 282 | ## Properties / Headers 283 | 284 | For AMQP class methods which take properties and/or headers, e.g., _basicConsume_, _basicPublish_, or _exchangeDeclare_, the 285 | properties and headers are passed in as a Tcl dict. The library takes care of encoding them properly. 286 | 287 | # Library Documentation 288 | 289 | All methods defined for _Connection_, _Login_, and _Channel_ classes are detailed below. Only includes methods that are 290 | part of the public interface for each object. Any additional methods found in the source are meant to be called internally. 291 | 292 | ## _Connection_ Class 293 | 294 | Class for connecting to a RabbitMQ server. 295 | 296 | ### constructor 297 | 298 | The constructor takes the following arguments (all optional): 299 | 300 | * __-host__ 301 | 302 | Defaults to localhost 303 | 304 | * __-port__ 305 | 306 | Defaults to 5672 307 | 308 | * __-tls__ 309 | 310 | Either 0 or 1, but defaults to 0. Controls whether to connect to the RabbitMQ server using TLS. To set 311 | TLS options, e.g., if using a client cert, call the _tlsOptions_ method before invoking _connect_. 312 | 313 | * __-login__ 314 | 315 | _Login_ object. Defaults to calling the Login constructor with no arguments. 316 | 317 | * __-frameMax__ 318 | 319 | Maximum frame size in bytes. Defaults to the value offered by the RabbitMQ server in _Connection.Tune_. 320 | 321 | * __-maxChannels__ 322 | 323 | Maximum number of channels available for this connection. Defaults to no imposed limit, which is essentially 65,535. 324 | 325 | * __-locale__ 326 | 327 | Defaults to en_US. 328 | 329 | * __-heartbeatSecs__ 330 | 331 | Interval in seconds for sending out heartbeat frames. Defaults to 60 seconds. A value of 0 means no heartbeats will be sent. 332 | 333 | * __-blockedConnections__ 334 | 335 | Either 0 or 1, but defaults to 1. Controls whether to use this [RabbitMQ extension](https://www.rabbitmq.com/connection-blocked.html). 336 | 337 | * __-cancelNotifications__ 338 | 339 | Either 0 or 1, but deafults to 1. Controls whether to use this [RabbitMQ extension](https://www.rabbitmq.com/specification.html). 340 | 341 | * __-maxTimeout__ 342 | 343 | Integer seconds to wait before timing out the connection attempt to the server. Defaults to 3. 344 | 345 | * __-autoReconnect__ 346 | 347 | Either 0 or 1, but defaults to 1. Controls whether the library attempts to reconnect to the RabbitMQ server when the initial call to _Connection.connect_ fails or an established socket connection is closed by the server or by network conditions. 348 | 349 | * __-maxBackoff__ 350 | 351 | Integer number of seconds past which [exponential backoff](https://cloud.google.com/storage/docs/exponential-backoff), which is the reconnection strategy employed, will not go. Defaults to 64 seconds. 352 | 353 | * __-maxReconnects__ 354 | 355 | Integer number of reconnects to attempt before giving up. Defaults to 5. A value of 0 means infinite reconnects. To disable retries, pass _-autoReconnect_ as 0. 356 | 357 | * __-debug__ 358 | 359 | Either 0 or 1, but defaults to 0. Controls whether or not debug statements are passed to `-logCommand` detailing the operations of the library. 360 | 361 | * __-logCommand__ 362 | 363 | If the `-debug` option is true, the value of this argument will be passed debugging statements detailing the operations of the library. The specified `-logCommand` must take a string argument containing a single debugging statement. Defaults to `puts stderr`. 364 | 365 | ### attemptReconnect 366 | 367 | Takes no arguments. Using the _-maxBackoff_ and _-maxReconnects_ constructor arguments, attempts to reconnect to the server. If this cannot be done, and an _onFailedReconnect_ callback has been set, it is invoked. 368 | 369 | ### closeConnection 370 | 371 | Takes an optional boolean argument controlling whether the _onClose_ callback is invoked (defaults to true). Closes the connection and, if specified, calls any callback set with _onClose_. This is not meant to 372 | be called externally as it does not uses the AMQP protocol for closing the channel. Instead, _connectionClose_ should be used in client applications. 373 | 374 | ### connect 375 | 376 | Takes no arguments. Actually initiates a socket connection with the RabbitMQ server. If the connection fails the _onClose_ callback is invoked. 377 | Two timeouts can potentially occur in this method: one during the TCP handshake and one during the AMQP handshake. In both cases, the _-maxTimeout_ variable is used. Returns 1 if a connection is fully established, or 0 otherwise. 378 | 379 | ### connected? 380 | 381 | Takes no arguments. Returns 0 or 1 depending on whether the socket connection to the server has been established and an AMQP handshake completed. It is only true once both those conditions have been satisfied. In the event that a connection fails, the `getSocket` method can be used to obtain and query the socket channel and determine whether the problem is network or protocol based. 382 | 383 | ### getSocket 384 | 385 | Takes no arguments. Returns the socket object for communicating with the server. This allows for more fine-grained inspection and tuning if 386 | so desired. 387 | 388 | ### onBlocked 389 | 390 | Takes the name of a callback proc which will be used for [blocked connection notifications](https://www.rabbitmq.com/connection-blocked.html). 391 | Blocked connection notifications are always requested by this library, but the setting of a callback is optional. The callback takes 392 | the _Connection_ object, a boolean for whether the connection is blocked (this callback is also used when the connection is no longer 393 | blocked), and a textual reason why. 394 | 395 | ### onClose 396 | 397 | Takes the name of a callback proc which will be called when the connection is closed. This includes a failed connection to the RabbitMQ server when 398 | first calling _connect_ and a disconnection after establishing communication with the RabbitMQ server. The callback takes the _Connection_ 399 | object and a dictionary with the keys specified in the documentation to the _onClosed_ callback. 400 | 401 | ### onClosed 402 | 403 | Alias for _onClose_ method. 404 | 405 | ### onConnected 406 | 407 | Takes the name of a callback proc which will be used when the AMQP handshake is finished. When this callback is invoked, the _Connection_ 408 | object is ready to create channels and perform useful work. 409 | 410 | ### onError 411 | 412 | Takes the name of a callback proc used when an error is reported by the RabbitMQ server on the connection level. The callback proc takes 413 | the _Connection_ object, a frame type and any extra data included in the frame. 414 | 415 | ### onFailedReconnect 416 | 417 | Takes the name of a callback proc used when the maximum number of connection attempts have been made without sucess. The callback proc takes the __Connection__ object. 418 | 419 | ### removeCallbacks 420 | 421 | Takes an optional boolean _channelsToo_, which defaults to 0. Unsets all callbacks for the _Connection_ object. If _channelsToo_ is 1, also unsets callbacks on all of its channels. 422 | 423 | ### reconnecting? 424 | 425 | Takes no argument. Returns 0 or 1 depending on whether the _Connection_ is in the process of attempting a reconnect. 426 | 427 | ### resetRetries 428 | 429 | Takes no arguments. Sets the count of connection retries back to 0. Useful in cases where _-autoReconnect_ is true and more fine-grained control of the retry loop is desired. Internally the retry count is reset to 0 when the AMQP handshake completes. 430 | 431 | ### tlsOptions 432 | 433 | Used to setup the parameters for an SSL / TLS connection to the RabbitMQ server. 434 | Supports all arguments supported by the Tcl tls package's `::tls::import::` command 435 | as specified in the [Tcl TLS documentation](https://core.tcl.tk/tcltls/wiki/Documentation). 436 | 437 | If a TLS connection is desired, this method needs to be called before _connect_. 438 | 439 | ## _Login_ Class 440 | 441 | ### constructor 442 | 443 | The constructor takes the following arguments (all optional): 444 | 445 | * __-user__ 446 | 447 | Username to login with. Defaults to guest 448 | 449 | * __-pass__ 450 | 451 | Password to login with. Defaults to guest 452 | 453 | * __-mechanism__ 454 | 455 | Authentication mechanism to use. Defaults to PLAIN 456 | 457 | * __-vhost__ 458 | 459 | Virtual host to login to. Defaults to / 460 | 461 | ### saslResponse 462 | 463 | Takes no arguments. This method needs to overridden if an alternative mechanism is desired. 464 | 465 | ## _Channel_ Class 466 | 467 | Most of the methods made available by this library come from the _Channel_ class. It implements the majority of the AMQP methods. 468 | 469 | ### constructor 470 | 471 | Takes the following arguments: 472 | 473 | * __connectionObj__ 474 | 475 | The _Connection_ object to open a channel for. This is the only required argument. 476 | 477 | * __channelNum__ 478 | 479 | The channel number to open. Optional. If not specified, the next available number starting from 1 will be used. Passing in an 480 | empty string or 0 is equivalent to not providing this argument, i.e., the class will pick the next available channel number for the 481 | _Connection_ object provided. 482 | 483 | * __shouldOpen__ 484 | 485 | A boolean argument that defaults to 1. If set to 1 the channel will open after it is created. If not, the _channelOpen_ method must be 486 | called manually before anything can be done with the _Channel_ object. 487 | 488 | ### active? 489 | 490 | Takes no arguments and returns 1 if the channel is active, i.e., it has been opened successfully, and 0 otherwise. 491 | 492 | ### closeChannel 493 | 494 | Not meant to be called externally. Instead, this method is used internally by the library to properly set the _Channel_ object's state before 495 | and after calling the _onClose_ callback. 496 | 497 | ### closeConnection 498 | 499 | Takes an optional boolean argument, _callCloseCB_, which defaults to 1. Closes the associated _Connection_ object and if _callCloseCB_ is true, any callback set with the _Connection_ object's _onClose_ method is invoked, otherwise it is ignored. 500 | 501 | ### closing? 502 | 503 | Takes no arguments and returns 1 if the _Channel_ is in the process of closing and 0 otherwise. 504 | 505 | ### getChannelNum 506 | 507 | Takes no arguments, and returns the channel number. 508 | 509 | ### getConnection 510 | 511 | Takes no arguments, and returns the _Connection_ object passed into the constructor. 512 | 513 | ### open? 514 | 515 | Alias for _active?_. 516 | 517 | ### on 518 | 519 | Takes an AMQP method name in camel case, starting with a lower case letter and the name of a callback proc for the method. To unset a callback, set its callback proc to the empty string or use _removeCallback_. 520 | 521 | ### onClose 522 | 523 | Takes the name of a callback proc to be called when the channel is closed. The callback takes the _Channel_ object and a dictionary 524 | of data, which is specified in the section about _onClose_ callbacks. 525 | 526 | ### onClosed 527 | 528 | Alias for _onClose_. 529 | 530 | ### onError 531 | 532 | Takes the name of a callback proc invoked when an error occurs on this particular _Channel_ object. The error callback is passed 533 | the _Channel_ object, a numeric error code as returned from the server, and any additional data passed back. Errors occur on a channel 534 | when the server returns an unexpected response but not when a disconnection occurs or the channel is closed forcefully by the server. 535 | 536 | ### onOpen 537 | 538 | Takes the name of a callback proc to be called when the channel successfully opens. Once it is open, AMQP methods can be called. 539 | The callback takes the _Channel_ object. 540 | 541 | ### onOpened 542 | 543 | Alias for _onOpen_. 544 | 545 | ### reconnecting? 546 | 547 | Takes no arguments. Returns 1 if _Connection_ is in the process of attempting a reconnect and 0 otherwise. 548 | 549 | ### removeCallback 550 | 551 | Takes the name of an AMQP method as defined on a _Channel_ object. 552 | 553 | ### removeCallbacks 554 | 555 | Takes no arguments. Sets all callbacks to the empty string, effectively removing them. 556 | 557 | ### setCallback 558 | 559 | Takes the name of an AMQP method as defined on a _Channel_ object (or for the _on_ Channel method). The preferred method to use is _on_, but this is alternative method for setting a callback. To unset a callback, set its callback proc to the empty string or use _removeCallback_. 560 | 561 | ## _Channel_ AMQP Methods 562 | 563 | The following methods are defined on _Channel_ objects and implement the methods and classes detailed in the 564 | [AMQP specification](https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf). 565 | 566 | ### Channel Methods 567 | 568 | #### channelClose 569 | 570 | Takes the following arguments: 571 | 572 | * __replyCode__ 573 | 574 | Numeric reply code for closing the channel as specified in the AMQP specification. 575 | 576 | * __replyText__ 577 | 578 | Textual description of the reply code. 579 | 580 | * __classID__ 581 | 582 | AMQP class ID number. 583 | 584 | * __methodID__ 585 | 586 | AMQP method ID number. 587 | 588 | To place a callback for the closing of a channel, use the _onClose_ or _onClosed_ method. The callback takes the _Channel_ object 589 | and a dictionary of data with key names matching the arguments listed above. 590 | 591 | #### channelOpen 592 | 593 | Takes no arguments. 594 | 595 | To place a callback for the opening of a channel use the _onOpen_ method. The callback takes only the _Channel_ object. 596 | 597 | ### Exchange Methods 598 | 599 | #### exchangeBind 600 | 601 | Takes the following arguments: 602 | 603 | * __dst__ 604 | 605 | Destination exchange name. 606 | 607 | * __src__ 608 | 609 | Source exchange name. 610 | 611 | * __rKey__ 612 | 613 | Routing key for the exchange binding. 614 | 615 | * __noWait__ 616 | 617 | Boolean integer, which defaults to 0. 618 | 619 | * __eArgs__ 620 | 621 | Exchange binding arguments (optional). Passed in as a dict. Defaults to an empty dict. 622 | 623 | To set a callback for exchange to exchange bindings use the _on_ method with _exchangeBindOk_ as the first argument. 624 | Callback only takes the _Channel_ object. 625 | 626 | #### exchangeDeclare 627 | 628 | Takes the following arguments: 629 | 630 | * __eName__ 631 | 632 | Exchange name. 633 | 634 | * __eType__ 635 | 636 | Exchange type: direct, fanout, header, topic 637 | 638 | * __eFlags__ 639 | 640 | Optional flags. Flags supported (all in the ::rmq namespace): 641 | 642 | - EXCHANGE_PASSIVE 643 | 644 | - EXCHANGE_DURABLE 645 | 646 | - EXCHANGE_AUTO_DELETE 647 | 648 | - EXCHANGE_INTERNAL 649 | 650 | - EXCHANGE_NO_WAIT 651 | 652 | * __eArgs__ 653 | 654 | Optional dict of exchange declare arguments. 655 | 656 | To set a callback on an exchange declaration, use the _on_ method with _exchangeDeclareOk_ as the first argument. 657 | Callback only takes the _Channel_ object. 658 | 659 | #### exchangeDelete 660 | 661 | Takes the following arguments: 662 | 663 | * __eName__ 664 | 665 | Exchange name to delete. 666 | 667 | * __inUse__ 668 | 669 | Optional boolean argument defaults to 0. If set to 1, will not delete an exchange with bindings on it. 670 | 671 | * __noWait__ 672 | 673 | Optional boolean argument defaults to 0. 674 | 675 | To set a callback on the exchange deletion, use the _on_ method with _exchangeDeleteOk_ as the first argument. 676 | Callback only takes the _Channel_ object. 677 | 678 | #### exchangeUnbind 679 | 680 | Takes the same arguments as _exchangeBind_, with the same callback data. 681 | 682 | ### Queue Methods 683 | 684 | #### queueBind 685 | 686 | Takes the following arguments: 687 | 688 | * __qName__ 689 | 690 | Queue name. 691 | 692 | * __eName__ 693 | 694 | Exchange name. 695 | 696 | * __rKey__ 697 | 698 | Routing key (optional). Defaults to the empty string. 699 | 700 | * __noWait__ 701 | 702 | Boolean integer (optional). Defaults to 0. 703 | 704 | * __qArgs__ 705 | 706 | Queue binding arguments (optional). Needs to be passed in as a dict. Defaults to an empty dict. 707 | 708 | To set a callback on a queue binding, use the _on_ method with _queueBindOk_ as the first argument. 709 | Callback only takes the _Channel_ object. 710 | 711 | #### queueDeclare 712 | 713 | Takes the following arguments: 714 | 715 | * __qName__ 716 | 717 | Queue name. 718 | 719 | * __qFlags__ 720 | 721 | Optional list of queue declare flags. Supports the following flag constants (in the ::rmq namespace): 722 | 723 | - QUEUE_PASSIVE 724 | 725 | - QUEUE_DURABLE 726 | 727 | - QUEUE_EXCLUSIVE 728 | 729 | - QUEUE_AUTO_DELETE 730 | 731 | - QUEUE_DECLARE_NO_WAIT 732 | 733 | * __qArgs__ 734 | 735 | Optional dictionary of queue declare arguments. Allows for setting features like TTLs, max lengths or a single consumer policy. 736 | 737 | To set a callback on a queue declare, use the _on_ method with _queueDeclareOk_ as the first argument. 738 | Callback takes the _Channel_ object, the queue name (especially important for exclusive queues), message count, 739 | number of consumers on the queue. 740 | 741 | #### queueDelete 742 | 743 | Takes the following arguments: 744 | 745 | * __qName__ 746 | 747 | Queue name. 748 | 749 | * __flags__ 750 | 751 | Optional list of flags. Supported flags (in the ::rmq namespace): 752 | 753 | 754 | - QUEUE_IF_UNUSED 755 | 756 | - QUEUE_IF_EMPTY 757 | 758 | - QUEUE_DELETE_NO_WAIT 759 | 760 | To set a callback on a queue delete, use the _on_ method with _queueDeleteOk_ as the first argument. 761 | Callback takes the _Channel_ object and a message count from the delete queue. 762 | 763 | #### queuePurge 764 | 765 | Takes the following arguments: 766 | 767 | * __qName__ 768 | 769 | Queue name. 770 | 771 | * __noWait__ 772 | 773 | Optional boolean argument. Defaults to 0. 774 | 775 | To set a callback on a queue purge, use the _on_ method with _queuePurgeOk_ as the first argument. 776 | Callback takes the _Channel_ object and a message count from the purged queue. 777 | 778 | #### queueUnbind 779 | 780 | Takes the following arguments: 781 | 782 | * __qName__ 783 | 784 | Queue name. 785 | 786 | * __eName__ 787 | 788 | Exchange name. 789 | 790 | * __rKey__ 791 | 792 | Routing key. 793 | 794 | * __qArgs__ 795 | 796 | Optional queue arguments. Passed in as a dict. 797 | 798 | To set a callback on a queue unbinding, use the _on_ method with _queueUnbindOk_ as the first argument. 799 | Callback takes only the _Channel_ object. 800 | 801 | ### Basic Methods 802 | 803 | #### basicAck 804 | 805 | Takes the following arguments: 806 | 807 | * __deliveryTag__ 808 | 809 | Delivery tag being acknowledged. 810 | 811 | * __multiple__ 812 | 813 | Optional boolean, defaults to 0. If set to 1, all messages up to and including the _deliveryTag_ argument's value. 814 | 815 | Setting a callback on this method using the _on_ method is for publisher confirms. The callback takes the _Channel_ 816 | object, a delivery tag and a multiple boolean. 817 | 818 | #### basicCancel 819 | 820 | Takes the following arguments: 821 | 822 | * __cTag__ 823 | 824 | Consumer tag. 825 | 826 | * __noWait__ 827 | 828 | Optional boolean argument. Defaults to 0. 829 | 830 | To set a callback on a basic cancel, use the _on_ method with _basicCancelOk_ as the first argument. 831 | Callback takes the _Channel_ object and the consumer tag that was canceled. 832 | 833 | #### basicConsume 834 | 835 | Takes the following arguments: 836 | 837 | * __callback__ 838 | 839 | Name of a callback to use for consuming messages. The callback takes the _Channel_ object, a dict of method data, a dict of 840 | frame data and the data from the queue. 841 | 842 | * __qName__ 843 | 844 | Queue name to consume from. 845 | 846 | * __cTag__ 847 | 848 | Optional consumer tag. 849 | 850 | * __cFlags__ 851 | 852 | Optional list of flags. Supported flags (all in the ::rmq namespace): 853 | 854 | - CONSUME_NO_LOCAL 855 | 856 | - CONSUME_NO_ACK 857 | 858 | - CONSUME_EXCLUSIVE 859 | 860 | - CONSUME_NO_WAIT 861 | 862 | * __cArgs__ 863 | 864 | Optional arguments to control consuming. Passed in as a dict. Supports all arguments specified for the 865 | [basic class](https://www.rabbitmq.com/amqp-0-9-1-reference.html). 866 | 867 | Callback is set directly from this method. 868 | 869 | #### basicGet 870 | 871 | Takes the following arguments: 872 | 873 | * __callback__ 874 | 875 | Name of a callback proc using the same arguments as that for _basicConsume_. 876 | 877 | * __qName__ 878 | 879 | Queue name to get a message from 880 | 881 | * __noWait__ 882 | 883 | Optional boolean. Defaults to 0. 884 | 885 | Like with _basicConsume_ the callback for this method is set directly from the method call. 886 | 887 | #### basicNack 888 | 889 | Takes the following arguments: 890 | 891 | * __deliveryTag__ 892 | 893 | Delivery tag for message being nack'ed. 894 | 895 | * __nackFlags__ 896 | 897 | Optional list of flags. Supports the following (in the ::rmq namespace): 898 | 899 | - NACK_MULTIPLE 900 | 901 | - NACK_REQUEUE 902 | 903 | Setting a callback on this method using the _on_ method is for publisher confirms. The callback takes the _Channel_ 904 | object, a delivery tag and a multiple boolean. 905 | 906 | #### basicQos 907 | 908 | Takes the following arguments: 909 | 910 | * __prefetchCount__ 911 | 912 | Integer prefetch count, i.e., the number of unacknowledged messages that can be delivered to a consumer at one time. 913 | 914 | * __globalQos__ 915 | 916 | Optional boolean which defaults to 0. If set to 1, the prefecth count is set globally [for all consumers on the channel](https://www.rabbitmq.com/consumer-prefetch.html). 917 | 918 | To set a callback on a basic QOS call, use the _on_ method with _basicQosOk_ as the first argument. 919 | Callback takes only the _Channel_ object. 920 | 921 | #### basicPublish 922 | 923 | Takes the following arguments: 924 | 925 | * __data__ 926 | 927 | The data to publish to the queue. 928 | 929 | * __eName__ 930 | 931 | Exchange name. 932 | 933 | * __rKey__ 934 | 935 | Routing key. 936 | 937 | * __pFlags__ 938 | 939 | Optional list of flags. Supports the following flags (in the ::rmq namespace): 940 | 941 | - PUBLISH_MANDATORY 942 | 943 | - PUBLISH_IMMEDIATE 944 | 945 | No callback can be set on this directly. For [publisher confirms](https://www.rabbitmq.com/confirms.html) 946 | use the _on_ method with _basicAck_ as the first argument. That callback takes the _Channel_ object, 947 | the delivery tag and a boolean for whether the ack is for multiple messages. 948 | 949 | #### basicRecover 950 | 951 | Same as _basicRecoverAsync_. 952 | 953 | ### Confirm Methods 954 | 955 | #### confirmSelect 956 | 957 | Takes the following arguments: 958 | 959 | * __noWait__ 960 | 961 | Optional boolean argument, defaults to 0. 962 | 963 | To set a callback on a confirm select call, use the _on_ method with _confirmSelectOk_ as the first argument. 964 | Callback takes the _Channel_ object. 965 | 966 | #### basicRecoverAsync 967 | 968 | Takes the following arguments: 969 | 970 | * __reQueue__ 971 | 972 | Boolean argument. If 0, the message will be redelivered to the original recipient. If 1, an 973 | alternate recipient can get the redelivery. 974 | 975 | To set a callback on a basic recover, use the _on_ method with _basicRecoverOk_ as the first argument. 976 | Callback takes the _Channel_ object. 977 | 978 | #### basicReject 979 | 980 | Takes the following arguments: 981 | 982 | * __deliveryTag__ 983 | 984 | Delivery tag of message being rejected by the client. 985 | 986 | * __reQueue__ 987 | 988 | Optional boolean argument, defaults to 0. If set to 1, the rejected message will be requeued. 989 | 990 | #### basicReturn 991 | 992 | This method is not to be called directly, but to use a callback to handle returned messages, use the _on_ method 993 | with _basicReturn_ as the first argument. The callback takes the same arguments as the _basicConsume_ callback. 994 | 995 | ### TX Methods 996 | 997 | #### txSelect 998 | 999 | Takes no arguments. 1000 | 1001 | To set a callback on a transaction select call, use the _on_ method with _txSelectOk_ as the first argument. 1002 | Callback takes the _Channel_ object. 1003 | 1004 | #### txCommit 1005 | 1006 | Takes no arguments. 1007 | 1008 | To set a callback on a transaction commit call, use the _on_ method with _txCommitOk_ as the first argument. 1009 | Callback takes the _Channel_ object. 1010 | 1011 | #### txRollback 1012 | 1013 | Takes no arguments. 1014 | 1015 | To set a callback on a transaction commit call, use the _on_ method with _txRollbackOk_ as the first argument. 1016 | Callback takes the _Channel_ object. 1017 | -------------------------------------------------------------------------------- /package/Channel.tcl: -------------------------------------------------------------------------------- 1 | package provide rmq 1.4.5 2 | 3 | package require TclOO 4 | 5 | namespace eval rmq { 6 | namespace export Channel 7 | } 8 | 9 | oo::class create ::rmq::Channel { 10 | # channel is always attached to a connection object 11 | # and always has an unsigned int for referring to it 12 | variable connection 13 | variable num 14 | 15 | # track whether the channel is open or not 16 | variable opened 17 | 18 | # track whether the channel is active or not 19 | # as controlled by Channel flow methods 20 | variable active 21 | 22 | # track whether the channel is closing or not 23 | variable closing 24 | 25 | # metadata from Connection.Close 26 | variable closeD 27 | 28 | # whether the channel is in confirm mode 29 | # and, if so, the publish number of the next message 30 | variable confirmMode 31 | 32 | # can set a callback for when the channel is open or closed 33 | # or when the channel receives an error code 34 | variable openCB 35 | variable closedCB 36 | variable errorCB 37 | 38 | # more general way of setting a callback on any other method 39 | variable callbacksD 40 | 41 | constructor {connectionObj {channelNum ""} {shouldOpen 1}} { 42 | set connection $connectionObj 43 | if {$channelNum eq "" || $channelNum <= 0} { 44 | set num [$connection getNextChannel] 45 | } else { 46 | set num $channelNum 47 | } 48 | 49 | # store the channel number in the connection 50 | # so that responses for this channel can be 51 | # received accordingly 52 | $connection saveChannel $num [self] 53 | 54 | # not opened yet 55 | set opened 0 56 | 57 | # not yet active 58 | set active 0 59 | 60 | # not closing 61 | set closing 0 62 | 63 | # no closing metadata yet 64 | set closeD [dict create] 65 | 66 | # not in cofirm mode 67 | set confirmMode 0 68 | 69 | # callbacks for major channel events 70 | set openCB "" 71 | set closedCB "" 72 | set errorCB "" 73 | 74 | # callbacks for all other methods 75 | # maps a string of the method to its callback 76 | set callbacksD [dict create] 77 | 78 | # handling for Basic method callbacks 79 | set methodData [dict create] 80 | set frameData [dict create] 81 | set receivedData "" 82 | array set consumerCBs {} 83 | set consumerCBArgs [list] 84 | set lastBasicMethod "" 85 | 86 | # send the frame to open the channel if specified 87 | if {$shouldOpen} { 88 | my channelOpen 89 | } 90 | } 91 | 92 | method active? {} { 93 | return $active 94 | } 95 | 96 | method closeChannel {} { 97 | set opened 0 98 | set active 0 99 | set closing 0 100 | 101 | if {$closedCB ne ""} { 102 | {*}$closedCB [self] $closeD 103 | } 104 | set closeD [dict create] 105 | } 106 | 107 | method closeConnection {{callCloseCB 1}} { 108 | $connection closeConnection $callCloseCB 109 | } 110 | 111 | method closing? {} { 112 | return $closing 113 | } 114 | 115 | method callback {methodName args} { 116 | if {[dict exists $callbacksD $methodName]} { 117 | {*}[dict get $callbacksD $methodName] [self] {*}$args 118 | } 119 | } 120 | 121 | method contentBody {data} { 122 | ::rmq::debug "Channel $num processing a content body frame" 123 | 124 | # add the current blob of data to the receivedData variable 125 | append receivedData $data 126 | 127 | # validate that the frameData variable actually contains 128 | # the key we are looking for and, if not, respond with an 129 | # appropriate error code 130 | if {![dict exists $frameData bodySize]} { 131 | # Error code 406 means a precondition failed 132 | ::rmq::debug "Frame data dict does not contain expected bodySize variable" 133 | return [$connection send [::rmq::enc_frame 406 0 ""]] 134 | } else { 135 | ::rmq::debug "Expecting [dict get $frameData bodySize] bytes of data" 136 | } 137 | 138 | if {[string length $receivedData] == [dict get $frameData bodySize]} { 139 | ::rmq::debug "Received all the data for the content, invoking necessary callback" 140 | 141 | # reset all variables used to keep track of consumed data 142 | set consumerCBArgs [list $methodData $frameData $receivedData] 143 | 144 | set methodData [dict create] 145 | set frameData [dict create] 146 | set receivedData "" 147 | 148 | # invoke the callback 149 | if {$lastBasicMethod eq "deliver"} { 150 | set cTag [dict get [lindex $consumerCBArgs 0] consumerTag] 151 | if {[info exists consumerCBs($cTag)]} { 152 | {*}$consumerCBs($cTag) [self] {*}$consumerCBArgs 153 | } else { 154 | ::rmq::debug "Delivered message for consumer tag $cTag, but no callback set" 155 | } 156 | } elseif {$lastBasicMethod eq "get"} { 157 | my callback basicDeliver {*}$consumerCBArgs 158 | } elseif {$lastBasicMethod eq "return"} { 159 | my callback basicReturn {*}$consumerCBArgs 160 | } else { 161 | ::rmq::debug "Received enqueued data ($consumerCBArgs) but no callback set" 162 | } 163 | } else { 164 | ::rmq::debug "Received [string length $receivedData] bytes so far" 165 | } 166 | } 167 | 168 | method contentHeader {headerD} { 169 | ::rmq::debug "Channel $num processing a content header frame: $headerD" 170 | set frameData $headerD 171 | } 172 | 173 | method errorHandler {errorCode data} { 174 | ::rmq::debug "Channel error handler with code $errorCode and data $data" 175 | 176 | if {$errorCB ne ""} { 177 | {*}$errorCB [self] $errorCode $data 178 | } 179 | } 180 | 181 | method getCallback {amqpMethod} { 182 | if {[dict exists $callbacksD $amqpMethod]} { 183 | return [dict get $callbacksD $amqpMethod] 184 | } 185 | } 186 | 187 | method getChannelNum {} { 188 | return $num 189 | } 190 | 191 | method getConnection {} { 192 | return $connection 193 | } 194 | 195 | method getConsumerCallbackArgs {} { 196 | return $consumerCBArgs 197 | } 198 | 199 | method getFrameData {} { 200 | return $frameData 201 | } 202 | 203 | method getMethodData {} { 204 | return $methodData 205 | } 206 | 207 | method getReceivedData {} { 208 | return $receivedData 209 | } 210 | 211 | method open? {} { 212 | return $opened 213 | } 214 | 215 | # 216 | # alias for setCallback method 217 | # 218 | method on {amqpMethod cb} { 219 | dict set callbacksD $amqpMethod $cb 220 | } 221 | 222 | method onConnect {cb} { 223 | set openCB $cb 224 | } 225 | 226 | method onConnected {cb} { 227 | set openCB $cb 228 | } 229 | 230 | method onOpen {cb} { 231 | set openCB $cb 232 | } 233 | 234 | method onOpened {cb} { 235 | set openCB $cb 236 | } 237 | 238 | method onClose {cb} { 239 | set closedCB $cb 240 | } 241 | 242 | method onClosed {cb} { 243 | set closedCB $cb 244 | } 245 | 246 | method onError {cb} { 247 | set errorCB $cb 248 | } 249 | 250 | method removeCallback {amqpMethod} { 251 | dict unset callbacksD $amqpMethod 252 | } 253 | 254 | method removeCallbacks {} { 255 | set callbacksD [dict create] 256 | array unset consumerCBs 257 | array set consumerCBs {} 258 | } 259 | 260 | method setCallback {amqpMethod cb} { 261 | dict set callbacksD $amqpMethod $cb 262 | } 263 | } 264 | 265 | ## 266 | ## 267 | ## AMQP Channel Methods 268 | ## 269 | ## 270 | oo::define ::rmq::Channel { 271 | method channelClose {{data ""} {replyCode 200} {replyText "Normal"} {cID 0} {mID 0}} { 272 | ::rmq::debug "Channel.Close" 273 | set closing 1 274 | 275 | # need to send a Channel.Close frame 276 | set replyCode [::rmq::enc_short $replyCode] 277 | set replyText [::rmq::enc_short_string $replyText] 278 | set cID [::rmq::enc_short $cID] 279 | set mID [::rmq::enc_short $mID] 280 | 281 | set methodData "${replyCode}${replyText}${cID}${mID}" 282 | set methodData [::rmq::enc_method $::rmq::CHANNEL_CLASS $::rmq::CHANNEL_CLOSE $methodData] 283 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodData] 284 | } 285 | 286 | method channelCloseRecv {data} { 287 | dict set closeD replyCode [::rmq::dec_short $data _] 288 | dict set closeD replyText [::rmq::dec_short_string [string range $data 2 end] bytes] 289 | set data [string range $data [expr {2 + $bytes}] end] 290 | dict set closeD classID [::rmq::dec_short $data _] 291 | dict set closeD methodID [::rmq::dec_short [string range $data 2 end] _] 292 | 293 | ::rmq::debug "Channel.CloseOk \[$num\] ($closeD)" 294 | 295 | # send Connection.Close-Ok 296 | my sendChannelCloseOk 297 | } 298 | 299 | method channelCloseOk {data} { 300 | ::rmq::debug "Channel.CloseOk" 301 | my closeChannel 302 | } 303 | 304 | method channelFlow {data} { 305 | ::rmq::debug "Channel.Flow" 306 | set active [::rmq::dec_byte $data _] 307 | my sendChannelFlowOk 308 | } 309 | 310 | method channelFlowOk {data} { 311 | set active [::rmq::dec_byte $data _] 312 | ::rmq::debug "Channel.FlowOk (active $active)" 313 | } 314 | 315 | method channelOpen {} { 316 | ::rmq::debug "Channel.Open channel $num" 317 | 318 | set payload [::rmq::enc_short_string ""] 319 | set payload [::rmq::enc_method $::rmq::CHANNEL_CLASS $::rmq::CHANNEL_OPEN $payload] 320 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $payload] 321 | } 322 | 323 | method channelOpenOk {data} { 324 | ::rmq::debug "Channel.OpenOk channel $num" 325 | set opened 1 326 | set active 1 327 | 328 | if {$openCB ne ""} { 329 | {*}$openCB [self] 330 | } 331 | } 332 | 333 | method sendChannelCloseOk {} { 334 | set methodData [::rmq::enc_method $::rmq::CHANNEL_CLASS $::rmq::CHANNEL_CLOSE_OK ""] 335 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodData] 336 | my closeChannel 337 | } 338 | 339 | method sendChannelFlow {flowActive} { 340 | ::rmq::debug "Sending Channel.Flow (active $flowActive)" 341 | 342 | set payload [::rmq::enc_byte $flowActive] 343 | set payload [::rmq::enc_method $::rmq::CHANNEL_CLASS $::rmq::CHANNEL_FLOW $payload] 344 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $payload] 345 | } 346 | 347 | method sendChannelFlowOk {} { 348 | ::rmq::debug "Sending Channel.FlowOk (active $active)" 349 | 350 | set payload [::rmq::enc_byte $active] 351 | set payload [::rmq::enc_method $::rmq::CHANNEL_CLASS $::rmq::CHANNEL_FLOW $payload] 352 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $payload] 353 | } 354 | } 355 | 356 | ## 357 | ## 358 | ## AMQP Exchange Methods 359 | ## 360 | ## 361 | oo::define ::rmq::Channel { 362 | method exchangeBind {dst src rKey {noWait 0} {eArgs ""}} { 363 | ::rmq::debug "Channel $num Exchange.Bind" 364 | 365 | # there's a reserved short field 366 | set reserved [::rmq::enc_short 0] 367 | 368 | # name of the destination exchange to bind 369 | set dst [::rmq::enc_short_string $dst] 370 | 371 | # name of the source exchange to bind 372 | set src [::rmq::enc_short_string $src] 373 | 374 | # routing key for the binding 375 | set rKey [::rmq::enc_short_string $rKey] 376 | 377 | # whether or not to wait on a response 378 | set noWait [::rmq::enc_byte $noWait] 379 | 380 | # additional args as a field table 381 | set eArgs [::rmq::enc_field_table $eArgs] 382 | 383 | # build up the payload to send 384 | set payload "${reserved}${dst}${src}${rKey}${noWait}${eArgs}" 385 | set methodLayer [::rmq::enc_method $::rmq::EXCHANGE_CLASS \ 386 | $::rmq::EXCHANGE_BIND $payload] 387 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 388 | } 389 | 390 | method exchangeBindOk {data} { 391 | ::rmq::debug "Channel $num Exchange.BindOk" 392 | 393 | # nothing really passed into this method 394 | my callback exchangeBindOk 395 | } 396 | 397 | method exchangeDeclare {eName eType {eFlags ""} {eArgs ""}} { 398 | ::rmq::debug "Channel $num Exchange.Declare" 399 | 400 | # there's a reserved short field 401 | set reserved [::rmq::enc_short 0] 402 | 403 | # need the exchange name and type 404 | set eName [::rmq::enc_short_string $eName] 405 | set eType [::rmq::enc_short_string $eType] 406 | 407 | # set the series of flags (from low-to-high) 408 | # passive, durable, auto-delete, internal, no-wait 409 | set flags 0 410 | foreach eFlag $eFlags { 411 | set flags [expr {$flags | $eFlag}] 412 | } 413 | set flags [::rmq::enc_byte $flags] 414 | 415 | # possible to have a field table of arguments 416 | set eArgs [::rmq::enc_field_table $eArgs] 417 | 418 | # build up the payload to send 419 | set payload "${reserved}${eName}${eType}${flags}${eArgs}" 420 | set methodLayer [::rmq::enc_method $::rmq::EXCHANGE_CLASS \ 421 | $::rmq::EXCHANGE_DECLARE $payload] 422 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 423 | } 424 | 425 | # 426 | # set a callback with the name exchangeDeclareOk 427 | # 428 | method exchangeDeclareOk {data} { 429 | ::rmq::debug "Channel $num Exchange.DeclareOk" 430 | 431 | my callback exchangeDeclareOk 432 | } 433 | 434 | method exchangeDelete {eName {inUse 0} {noWait 0}} { 435 | ::rmq::debug "Channel $num Exchange.Delete" 436 | 437 | # reserved short field 438 | set reserved [::rmq::enc_short 0] 439 | 440 | # set the exchange name 441 | set eName [::rmq::enc_short_string $eName] 442 | 443 | # set the bit fields for in-use and no-wait 444 | set flags 0 445 | if {$inUse} { 446 | set flags [expr {$flags | 1}] 447 | } 448 | 449 | if {$noWait} { 450 | set flags [expr {$flags | 2}] 451 | } 452 | set flags [::rmq::enc_byte $flags] 453 | 454 | # now can package this up and send 455 | set payload "${reserved}${eName}${flags}" 456 | set methodLayer [::rmq::enc_method $::rmq::EXCHANGE_CLASS \ 457 | $::rmq::EXCHANGE_DELETE $payload] 458 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 459 | } 460 | 461 | # 462 | # set a callback with the name exchangeDeleteOk 463 | # 464 | method exchangeDeleteOk {data} { 465 | ::rmq::debug "Channel $num Exchange.DeleteOk" 466 | 467 | my callback exchangeDeleteOk 468 | } 469 | 470 | method exchangeUnbind {dst src rKey {noWait 0} {eArgs ""}} { 471 | ::rmq::debug "Channel $num Exchange.Unbind" 472 | # there's a reserved short field 473 | set reserved [::rmq::enc_short 0] 474 | 475 | # name of the destination exchange to bind 476 | set dst [::rmq::enc_short_string $dst] 477 | 478 | # name of the source exchange to bind 479 | set src [::rmq::enc_short_string $src] 480 | 481 | # routing key for the binding 482 | set rKey [::rmq::enc_short_string $rKey] 483 | 484 | # whether or not to wait on a response 485 | set noWait [::rmq::enc_byte $noWait] 486 | 487 | # additional args as a field table 488 | set eArgs [::rmq::enc_field_table $eArgs] 489 | 490 | # build up the payload to send 491 | set payload "${reserved}${dst}${src}${rKey}${noWait}${eArgs}" 492 | set methodLayer [::rmq::enc_method $::rmq::EXCHANGE_CLASS \ 493 | $::rmq::EXCHANGE_UNBIND $payload] 494 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 495 | } 496 | 497 | method exchangeUnbindOk {data} { 498 | ::rmq::debug "Channel $num Exchange.UnbindOk" 499 | 500 | # nothing really passed into this method 501 | my callback exchangeUnbindOk "" 502 | } 503 | } 504 | 505 | ## 506 | ## 507 | ## AMQP Queue Methods 508 | ## 509 | ## 510 | oo::define ::rmq::Channel { 511 | method queueBind {qName eName {rKey ""} {noWait 0} {qArgs ""}} { 512 | ::rmq::debug "Queue.Bind" 513 | 514 | # deprecated short ticket field set to 0 515 | set ticket [::rmq::enc_short 0] 516 | 517 | # queue name 518 | set qName [::rmq::enc_short_string $qName] 519 | 520 | # exchange name 521 | set eName [::rmq::enc_short_string $eName] 522 | 523 | # routing key 524 | set rKey [::rmq::enc_short_string $rKey] 525 | 526 | # no wait bit 527 | set noWait [::rmq::enc_byte $noWait] 528 | 529 | # additional args as a field table 530 | set qArgs [::rmq::enc_field_table $qArgs] 531 | 532 | # now ready to send the payload 533 | set methodLayer [::rmq::enc_method $::rmq::QUEUE_CLASS \ 534 | $::rmq::QUEUE_BIND \ 535 | ${ticket}${qName}${eName}${rKey}${noWait}${qArgs}] 536 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 537 | } 538 | 539 | # 540 | # set a callback with the name queueBindOk 541 | # 542 | method queueBindOk {data} { 543 | ::rmq::debug "Queue.BindOk" 544 | 545 | # No parameters included 546 | my callback queueBindOk 547 | } 548 | 549 | method queueDeclare {qName {qFlags ""} {qArgs ""}} { 550 | ::rmq::debug "Queue.Declare" 551 | 552 | # a short reserved field 553 | set reserved [::rmq::enc_short 0] 554 | 555 | # queue name 556 | set qName [::rmq::enc_short_string $qName] 557 | 558 | # passive, durable, exclusive, auto-delete, no-wait 559 | set flags 0 560 | foreach qFlag $qFlags { 561 | set flags [expr {$flags | $qFlag}] 562 | } 563 | set flags [::rmq::enc_byte $flags] 564 | 565 | # arguments for declaration 566 | set qArgs [::rmq::enc_field_table $qArgs] 567 | 568 | # create the method frame with its payload 569 | set methodLayer [::rmq::enc_method $::rmq::QUEUE_CLASS \ 570 | $::rmq::QUEUE_DECLARE \ 571 | ${reserved}${qName}${flags}${qArgs}] 572 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 573 | } 574 | 575 | # 576 | # set a callback with the name queueDeclareOk 577 | # 578 | method queueDeclareOk {data} { 579 | # Grab the data from the response 580 | set qName [::rmq::dec_short_string $data bytes] 581 | set data [string range $data $bytes end] 582 | 583 | set msgCount [::rmq::dec_ulong $data bytes] 584 | set data [string range $data $bytes end] 585 | 586 | set consumers [::rmq::dec_ulong $data bytes] 587 | 588 | ::rmq::debug "Queue.DeclareOk (name $qName) (msgs $msgCount) (consumers $consumers)" 589 | 590 | my callback queueDeclareOk $qName $msgCount $consumers 591 | } 592 | 593 | method queueDelete {qName {flags ""}} { 594 | ::rmq::debug "Queue.Delete" 595 | 596 | set reserved [::rmq::enc_short 0] 597 | 598 | set qName [::rmq::enc_short_string $qName] 599 | 600 | set dFlags 0 601 | foreach flag $flags { 602 | set dFlags [expr {$dFlags | $flag}] 603 | } 604 | set dFlags [::rmq::enc_byte $dFlags] 605 | 606 | set methodLayer [::rmq::enc_method $::rmq::QUEUE_CLASS \ 607 | $::rmq::QUEUE_DELETE \ 608 | ${reserved}${qName}${dFlags}] 609 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 610 | } 611 | 612 | # 613 | # set a callback with the name queueDeleteOk 614 | # 615 | method queueDeleteOk {data} { 616 | ::rmq::debug "Queue.DeleteOk" 617 | 618 | set msgCount [::rmq::dec_ulong $data _] 619 | 620 | my callback queueDeleteOk $msgCount 621 | } 622 | 623 | method queuePurge {qName {noWait 0}} { 624 | ::rmq::debug "Queue.Purge" 625 | 626 | # reserved short field 627 | set reserved [::rmq::enc_short 0] 628 | 629 | # queue name 630 | set qName [::rmq::enc_short_string $qName] 631 | 632 | # no wait bit 633 | set noWait [::rmq::enc_byte $noWait] 634 | 635 | set methodLayer [::rmq::enc_method $::rmq::QUEUE_CLASS \ 636 | $::rmq::QUEUE_PURGE \ 637 | ${reserved}${qName}${noWait}] 638 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 639 | } 640 | 641 | # 642 | # set a callback with the name queuePurgeOk 643 | # 644 | method queuePurgeOk {data} { 645 | ::rmq::debug "Queue.PurgeOk" 646 | 647 | set msgCount [::rmq::dec_ulong $data _] 648 | 649 | my callback queuePurgeOk $msgCount 650 | } 651 | 652 | method queueUnbind {qName eName rKey {qArgs ""}} { 653 | ::rmq::debug "Queue.Unbind" 654 | 655 | # reserved short field 656 | set reserved [::rmq::enc_short 0] 657 | 658 | # queue name 659 | set qName [::rmq::enc_short_string $qName] 660 | 661 | # exchange name 662 | set eName [::rmq::enc_short_string $eName] 663 | 664 | # routing key 665 | set rKey [::rmq::enc_short_string $rKey] 666 | 667 | # additional arguments 668 | set qArgs [::rmq::enc_field_table $qArgs] 669 | 670 | # bundle it up and send it off 671 | set methodLayer [::rmq::enc_method $::rmq::QUEUE_CLASS \ 672 | $::rmq::QUEUE_UNBIND \ 673 | ${reserved}${qName}${eName}${rKey}${qArgs}] 674 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 675 | } 676 | 677 | # 678 | # set a callback with the name queueUnbindOk 679 | # 680 | method queueUnbindOk {data} { 681 | ::rmq::debug "Queue.UnbindOk" 682 | 683 | # No parameters included 684 | my callback queueUnbindOk 685 | } 686 | } 687 | 688 | ## 689 | ## 690 | ## AMQP Basic Methods 691 | ## 692 | ## 693 | oo::define ::rmq::Channel { 694 | # housekeeping for the receiving of data 695 | variable lastBasicMethod 696 | variable methodData 697 | variable frameData 698 | variable receivedData 699 | variable consumerCBs 700 | variable consumerCBArgs 701 | 702 | method basicAck {deliveryTag {multiple 0}} { 703 | ::rmq::debug "Basic.Ack" 704 | 705 | set deliveryTag [::rmq::enc_ulong_long $deliveryTag] 706 | set multiple [::rmq::enc_byte $multiple] 707 | 708 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 709 | $::rmq::BASIC_ACK ${deliveryTag}${multiple}] 710 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 711 | } 712 | 713 | method basicAckReceived {data} { 714 | set deliveryTag [::rmq::dec_ulong_long $data bytes] 715 | set multiple [::rmq::dec_byte [string range $data $bytes end] _] 716 | 717 | ::rmq::debug "Basic.Ack received for $deliveryTag with multiple ($multiple)" 718 | my callback basicAck $deliveryTag $multiple 719 | } 720 | 721 | method basicConsume {callback qName {cTag ""} {cFlags ""} {cArgs ""}} { 722 | ::rmq::debug "Basic.Consume" 723 | 724 | # setup for the callback 725 | my setCallback basicDeliver $callback 726 | set consumerCBs($cTag) $callback 727 | 728 | set reserved [::rmq::enc_short 0] 729 | set qName [::rmq::enc_short_string $qName] 730 | set cTag [::rmq::enc_short_string $cTag] 731 | 732 | # no-local, no-ack, exclusive, no-wait 733 | set flags 0 734 | foreach cFlag $cFlags { 735 | set flags [expr {$flags | $cFlag}] 736 | } 737 | set flags [::rmq::enc_byte $flags] 738 | 739 | set cArgs [::rmq::enc_field_table $cArgs] 740 | 741 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 742 | $::rmq::BASIC_CONSUME \ 743 | ${reserved}${qName}${cTag}${flags}${cArgs}] 744 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 745 | } 746 | 747 | method basicConsumeOk {data} { 748 | set cTag [::rmq::dec_short_string $data _] 749 | ::rmq::debug "Basic.ConsumeOk (consumer tag: $cTag)" 750 | 751 | # if just learned the consumer tag, update callbacks array 752 | if {[info exists consumerCBs()]} { 753 | ::rmq::debug "Have server generated consumer tag, unsetting empty callback key" 754 | set consumerCBs($cTag) $consumerCBs() 755 | unset consumerCBs() 756 | } 757 | 758 | my callback basicConsumeOk $cTag 759 | } 760 | 761 | method basicCancel {cTag {noWait 0}} { 762 | ::rmq::debug "Basic.Cancel" 763 | 764 | set cTag [::rmq::enc_short_string $cTag] 765 | set noWait [::rmq::enc_byte $noWait] 766 | 767 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 768 | $::rmq::BASIC_CANCEL \ 769 | ${cTag}${noWait}] 770 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 771 | } 772 | 773 | method basicCancelOk {data} { 774 | ::rmq::debug "Basic.CancelOk" 775 | 776 | set cTag [::rmq::dec_short_string $data _] 777 | 778 | my callback basicCancelOk $cTag 779 | } 780 | 781 | method basicCancelRecv {data} { 782 | set cTag [::rmq::dec_short_string $data _] 783 | 784 | ::rmq::debug "Basic.Cancel received for consumer tag $cTag" 785 | 786 | array unset consumerCBs $cTag 787 | 788 | my callback basicCancel $cTag 789 | } 790 | 791 | method basicDeliver {data} { 792 | ::rmq::debug "Basic.Deliver" 793 | 794 | set cTag [::rmq::dec_short_string $data bytes] 795 | set data [string range $data $bytes end] 796 | 797 | set dTag [::rmq::dec_ulong_long $data bytes] 798 | set data [string range $data $bytes end] 799 | 800 | set redelivered [::rmq::dec_byte $data bytes] 801 | set data [string range $data $bytes end] 802 | 803 | set eName [::rmq::dec_short_string $data bytes] 804 | set data [string range $data $bytes end] 805 | 806 | set rKey [::rmq::dec_short_string $data bytes] 807 | 808 | # save the basic deliver data for passing to the user 809 | # callback after the body frame is received 810 | set methodData [dict create \ 811 | consumerTag $cTag \ 812 | deliveryTag $dTag \ 813 | redelivered $redelivered \ 814 | exchange $eName \ 815 | routingKey $rKey] 816 | set lastBasicMethod deliver 817 | } 818 | 819 | method basicGet {callback qName {noAck 1}} { 820 | ::rmq::debug "Basic.Get" 821 | 822 | my setCallback basicDeliver $callback 823 | set lastBasicMethod get 824 | 825 | set reserved [::rmq::enc_short 0] 826 | set qName [::rmq::enc_short_string $qName] 827 | set noAck [::rmq::enc_byte $noAck] 828 | 829 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 830 | $::rmq::BASIC_GET \ 831 | ${reserved}${qName}${noAck}] 832 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 833 | } 834 | 835 | method basicGetEmpty {data} { 836 | ::rmq::debug "Basic.GetEmpty" 837 | 838 | # only have a single reserved parameter 839 | 840 | # save an empty method data dict for this case 841 | set methodData [dict create] 842 | set lastBasicMethod get 843 | } 844 | 845 | method basicGetOk {data} { 846 | ::rmq::debug "Basic.GetOk" 847 | 848 | set dTag [::rmq::dec_ulong_long $data bytes] 849 | set data [string range $data $bytes end] 850 | 851 | set redelivered [::rmq::dec_byte $data bytes] 852 | set data [string range $data $bytes end] 853 | 854 | set eName [::rmq::dec_short_string $data bytes] 855 | set data [string range $data $bytes end] 856 | 857 | set rKey [::rmq::dec_short_string $data bytes] 858 | set data [string range $data $bytes end] 859 | 860 | set msgCount [::rmq::dec_ulong $data bytes] 861 | 862 | # save the basic get data for passing to the user 863 | # callback after the body frame is received 864 | set methodData [dict create \ 865 | deliveryTag $dTag \ 866 | redelivered $redelivered \ 867 | exchange $eName \ 868 | routingKey $rKey \ 869 | messageCount $msgCount] 870 | set lastBasicMethod get 871 | } 872 | 873 | method basicQos {prefetchCount {globalQos 0}} { 874 | ::rmq::debug "Basic.Qos" 875 | 876 | # prefetchSize is always 0 for RabbitMQ as any other 877 | # value is unsupported and will lead to a channel error 878 | set prefetchSize [::rmq::enc_ulong 0] 879 | set prefetchCount [::rmq::enc_ushort $prefetchCount] 880 | set globalQos [::rmq::enc_byte $globalQos] 881 | 882 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 883 | $::rmq::BASIC_QOS \ 884 | ${prefetchSize}${prefetchCount}${globalQos}] 885 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 886 | } 887 | 888 | method basicNack {deliveryTag {nackFlags ""}} { 889 | ::rmq::debug "Basic.Nack" 890 | 891 | set deliveryTag [::rmq::enc_ulong_long $deliveryTag] 892 | 893 | # multiple, requeue 894 | set flags 0 895 | foreach nackFlag $nackFlags { 896 | set flags [expr {$flags | $nackFlag}] 897 | } 898 | set flags [::rmq::enc_byte $flags] 899 | 900 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 901 | $::rmq::BASIC_NACK ${deliveryTag}${flags}] 902 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 903 | } 904 | 905 | method basicNackReceived {data} { 906 | set deliveryTag [::rmq::dec_ulong_long $data bytes] 907 | 908 | # If the multiple field is 1, and the delivery tag is zero, 909 | # this indicates rejection of all outstanding messages. 910 | set multiple [::rmq::dec_byte [string range $data $bytes end] _] 911 | ::rmq::debug "Basic.Nack received for $deliveryTag with multiple ($multiple)" 912 | 913 | # there is also a requeue bit in the data but the spec says 914 | # "Clients receiving the Nack methods should ignore this flag." 915 | 916 | my callback basicNack $deliveryTag $multiple 917 | } 918 | 919 | method basicQosOk {data} { 920 | ::rmq::debug "Basic.QosOk" 921 | 922 | # no parameters included 923 | my callback basicQosOk 924 | } 925 | 926 | method basicPublish {data eName rKey {pFlags ""} {props ""}} { 927 | ::rmq::debug "Basic.Publish to exchange $eName w/ routing key $rKey" 928 | 929 | set reserved [::rmq::enc_short 0] 930 | set eName [::rmq::enc_short_string $eName] 931 | set rKey [::rmq::enc_short_string $rKey] 932 | 933 | # mandatory, immediate 934 | set flags 0 935 | foreach pFlag $pFlags { 936 | set flags [expr {$flags | $pFlag}] 937 | } 938 | set flags [::rmq::enc_byte $flags] 939 | 940 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 941 | $::rmq::BASIC_PUBLISH \ 942 | ${reserved}${eName}${rKey}${flags}] 943 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 944 | 945 | # after sending the basic publish method, now need to send the data 946 | # this requires sending a content header and then a content body 947 | set bodyLen [string length $data] 948 | set header [::rmq::enc_content_header $::rmq::BASIC_CLASS $bodyLen $props] 949 | $connection send [::rmq::enc_frame $::rmq::FRAME_HEADER $num $header] 950 | 951 | # might need to break content up into several frames 952 | set maxPayload [expr {[$connection getFrameMax] - 8}] 953 | set bytesSent 0 954 | while {$bytesSent < $bodyLen} { 955 | set payload [string range $data $bytesSent [expr {$bytesSent + $maxPayload - 1}]] 956 | $connection send [::rmq::enc_frame $::rmq::FRAME_BODY $num $payload] 957 | incr bytesSent $maxPayload 958 | } 959 | } 960 | 961 | method basicRecover {reQueue} { 962 | ::rmq::debug "Basic.Recover" 963 | set reQueue [::rmq::enc_byte $reQueue] 964 | 965 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 966 | $::rmq::BASIC_RECOVER ${reQueue}] 967 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 968 | } 969 | 970 | method basicRecoverAsync {reQueue} { 971 | ::rmq::debug "Basic.RecoverAsync (deprecated by Reject/Reject-Ok)" 972 | 973 | set reQueue [::rmq::enc_byte $reQueue] 974 | 975 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 976 | $::rmq::BASIC_RECOVER_ASYNC ${reQueue}] 977 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 978 | } 979 | 980 | method basicRecoverOk {data} { 981 | ::rmq::debug "Basic.RecoverOk" 982 | 983 | # no parameters 984 | my callback basicRecoverOk 985 | } 986 | 987 | method basicReject {deliveryTag {reQueue 0}} { 988 | ::rmq::debug "Basic.Reject" 989 | 990 | set deliveryTag [::rmq::enc_ulong_long $deliveryTag] 991 | set reQueue [::rmq::enc_byte $reQueue] 992 | 993 | set methodLayer [::rmq::enc_method $::rmq::BASIC_CLASS \ 994 | $::rmq::BASIC_REJECT ${deliveryTag}${reQueue}] 995 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 996 | } 997 | 998 | method basicReturn {data} { 999 | ::rmq::debug "Basic.Return" 1000 | 1001 | set replyCode [::rmq::dec_short $data bytes] 1002 | set data [string range $data $bytes end] 1003 | 1004 | set replyText [::rmq::dec_short_string $data bytes] 1005 | set data [string range $data $bytes end] 1006 | 1007 | set exchange [::rmq::dec_short_string $data bytes] 1008 | set data [string range $data $bytes end] 1009 | 1010 | set routingKey [::rmq::dec_short_string $data bytes] 1011 | 1012 | set methodData [dict create \ 1013 | replyCode $replyCode \ 1014 | replyText $replyText \ 1015 | exchange $exchange \ 1016 | routingKey $routingKey] 1017 | set lastBasicMethod return 1018 | } 1019 | } 1020 | 1021 | ## 1022 | ## 1023 | ## AMQP Confirm Methods 1024 | ## 1025 | ## 1026 | oo::define ::rmq::Channel { 1027 | method confirmSelect {{noWait 0}} { 1028 | ::rmq::debug "Confirm.Select" 1029 | 1030 | set noWait [::rmq::enc_byte $noWait] 1031 | set methodLayer [::rmq::enc_method $::rmq::CONFIRM_CLASS \ 1032 | $::rmq::CONFIRM_SELECT $noWait] 1033 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD $num $methodLayer] 1034 | } 1035 | 1036 | method confirmSelectOk {data} { 1037 | ::rmq::debug "Confirm.SelectOk (now in confirm mode)" 1038 | set confirmMode 1 1039 | 1040 | my callback confirmSelectOk 1041 | } 1042 | } 1043 | 1044 | ## 1045 | ## 1046 | ## AMQP TX Methods 1047 | ## 1048 | ## 1049 | oo::define ::rmq::Channel { 1050 | method txSelect {} { 1051 | ::rmq::debug "Tx.Select" 1052 | 1053 | # no parameters for this method 1054 | set methodLayer [::rmq::enc_method $::rmq::TX_CLASS \ 1055 | $::rmq::TX_SELECT ""] 1056 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD \ 1057 | $num $methodLayer] 1058 | } 1059 | 1060 | method txSelectOk {data} { 1061 | ::rmq::debug "Tx.SelectOk" 1062 | 1063 | # no parameters 1064 | my callback txSelectOk 1065 | } 1066 | 1067 | method txCommit {} { 1068 | ::rmq::debug "Tx.Commit" 1069 | 1070 | # no parameters 1071 | set methodLayer [::rmq::enc_method $::rmq::TX_CLASS \ 1072 | $::rmq::TX_COMMIT ""] 1073 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD \ 1074 | $num $methodLayer] 1075 | } 1076 | 1077 | method txCommitOk {data} { 1078 | ::rmq::debug "Tx.CommitOk" 1079 | 1080 | # no parameters 1081 | my callback txCommitOk 1082 | } 1083 | 1084 | method txRollback {} { 1085 | ::rmq::debug "Tx.Rollback" 1086 | 1087 | # no parameters 1088 | set methodLayer [::rmq::enc_method $::rmq::TX_CLASS \ 1089 | $::rmq::TX_ROLLBACK ""] 1090 | $connection send [::rmq::enc_frame $::rmq::FRAME_METHOD \ 1091 | $num $methodLayer] 1092 | } 1093 | 1094 | method txRollbackOk {data} { 1095 | ::rmq::debug "Tx.RollbackOk" 1096 | 1097 | # no parameters 1098 | my callback txRollbackOk 1099 | } 1100 | } 1101 | 1102 | # vim: ts=4:sw=4:sts=4:noet 1103 | -------------------------------------------------------------------------------- /package/Connection.tcl: -------------------------------------------------------------------------------- 1 | package provide rmq 1.4.5 2 | 3 | package require TclOO 4 | package require tls 5 | 6 | namespace eval rmq { 7 | namespace export Connection 8 | 9 | proc debug {msg} { 10 | if {$::rmq::debug} { 11 | set ts [clock format [clock seconds] -format "%D %T" -gmt 1] 12 | {*}$::rmq::logCommand "\[DEBUG\] ($ts): $msg" 13 | } 14 | } 15 | 16 | # return random integer in [start, end] 17 | proc rand_int {start end} { 18 | set range [expr {$end - $start + 1}] 19 | return [expr {$start + int(rand() * $range)}] 20 | } 21 | } 22 | 23 | oo::class create ::rmq::Connection { 24 | # TCP socket used for communication with the 25 | # RabbitMQ server 26 | variable sock 27 | 28 | # Hostname of the server 29 | variable host 30 | 31 | # Port of the server 32 | variable port 33 | 34 | # ::rmq::Login object containing needed credentials 35 | # for logging into the RabbitMQ server 36 | variable login 37 | 38 | # boolean tracking whether we are connected 39 | # this is set to true after performing the handshake 40 | variable connected 41 | 42 | # max timeout in secs for obtaining socket connection 43 | variable maxTimeout 44 | 45 | # whether the connection is blocked 46 | variable blocked 47 | 48 | # whether to allow for blocked connection notifications 49 | variable blockedConnections 50 | 51 | # Locale for content 52 | variable locale 53 | 54 | # whether to allow cancel notifications 55 | variable cancelNotifications 56 | 57 | # Track the next channel number available 58 | # to be used for auto generating channels without the 59 | # client having to specify a number 60 | variable nextChannel 61 | variable maxChannels 62 | 63 | # Track the channels by mapping each channel number to 64 | # the channel object it represents 65 | variable channelsD 66 | 67 | # Save connection closed meta data when received 68 | variable closeD 69 | 70 | # Last frame read from the socket 71 | variable frame 72 | 73 | # Max frame size 74 | variable frameMax 75 | 76 | # Partial frames can occur, so we buffer them here 77 | variable partialFrame 78 | 79 | # Timestamp of the last socket activity for heartbeat 80 | # purposes 81 | variable lastRead 82 | variable lastSend 83 | 84 | # Variables for dealing with heartbeats 85 | # after event loop ID for heartbeat frames 86 | variable heartbeatSecs 87 | variable heartbeatID 88 | 89 | # If we periodically poll the socket for EOF to 90 | # to try and detect inactivity when hearthbeats 91 | # are disabled save the after event ID 92 | variable sockHealthPollingID 93 | 94 | # Whether to use TLS for the socket connection 95 | variable tls 96 | variable tlsOpts 97 | 98 | # Whether to try and auto-reconnect when connection forcibly 99 | # closed by the remote side or the network 100 | # Maximum number of exponential backoff attempts to try 101 | # Maximum retry attempts 102 | # Are we trying to actively reconnect 103 | # How many retries have we attempted 104 | variable autoReconnect 105 | variable maxBackoff 106 | variable maxReconnects 107 | variable reconnecting 108 | variable retries 109 | 110 | ## 111 | ## 112 | ## callbacks 113 | ## 114 | ## 115 | variable blockedCB 116 | variable connectedCB 117 | variable closedCB 118 | variable errorCB 119 | variable failedReconnectCB 120 | 121 | constructor {args} { 122 | array set options {} 123 | set options(-host) $::rmq::DEFAULT_HOST 124 | set options(-port) $::rmq::DEFAULT_PORT 125 | set options(-tls) 0 126 | set options(-login) [::rmq::Login new] 127 | set options(-frameMax) $::rmq::MAX_FRAME_SIZE 128 | set options(-maxChannels) $::rmq::DEFAULT_CHANNEL_MAX 129 | set options(-locale) $::rmq::DEFAULT_LOCALE 130 | set options(-heartbeatSecs) $::rmq::HEARTBEAT_SECS 131 | set options(-blockedConnections) $::rmq::BLOCKED_CONNECTIONS 132 | set options(-cancelNotifications) $::rmq::CANCEL_NOTIFICATIONS 133 | set options(-maxTimeout) $::rmq::DEFAULT_MAX_TIMEOUT 134 | set options(-autoReconnect) $::rmq::DEFAULT_AUTO_RECONNECT 135 | set options(-maxBackoff) $::rmq::DEFAULT_MAX_BACKOFF 136 | set options(-maxReconnects) $::rmq::DEFAULT_MAX_RECONNECT_ATTEMPTS 137 | set options(-debug) 0 138 | set options(-logCommand) "puts stderr" 139 | 140 | foreach {opt val} $args { 141 | if {[info exists options($opt)]} { 142 | set options($opt) $val 143 | } 144 | } 145 | 146 | foreach opt [array names options] { 147 | set [string trimleft $opt -] $options($opt) 148 | } 149 | 150 | # whether we are in debug mode and the 151 | # log command to use if we are 152 | set ::rmq::debug $debug 153 | set ::rmq::logCommand $logCommand 154 | 155 | # socket variable 156 | set sock "" 157 | 158 | # no TLS options yet 159 | array set tlsOpts {} 160 | 161 | # not currently connected 162 | set connected 0 163 | 164 | # not currently blocked 165 | set blocked 0 166 | 167 | # no connection closed metadata yet 168 | set closeD [dict create] 169 | 170 | # map channel number to channel object 171 | set channelsD [dict create] 172 | 173 | # book-keeping for frame processing 174 | set partialFrame "" 175 | 176 | # if doing periodic socket polling 177 | set sockHealthPollingID "" 178 | 179 | # whether we are attempting to reconnect 180 | set reconnecting 0 181 | 182 | # how many retries we have attempted 183 | set retries 0 184 | 185 | # heartbeat setup 186 | set lastRead 0 187 | set lastSend 0 188 | set heartbeatID "" 189 | 190 | # callbacks 191 | set blockedCB "" 192 | set connectedCB "" 193 | set closedCB "" 194 | set errorCB "" 195 | set failedReconnectCB "" 196 | } 197 | 198 | destructor { 199 | catch {close $sock} 200 | after cancel $heartbeatID 201 | after cancel $sockHealthPollingID 202 | } 203 | 204 | # attempt to reconnect to the server using 205 | # exponential backoff 206 | # 207 | # this is triggered when the check connection method 208 | # detects that the socket has gone down 209 | # 210 | # if auto reconnection is not desired this method can 211 | # be called in the closed callback or equivalent place 212 | method attemptReconnect {} { 213 | ::rmq::debug "Attempting auto-reconnect with exponential backoff" 214 | set reconnecting 1 215 | if {$maxReconnects == 0 || $retries < $maxReconnects} { 216 | if {[my connect]} { 217 | set reconnecting 0 218 | return 219 | } 220 | 221 | set waitTime [expr {(2**$retries) * 1000 + [::rmq::rand_int 1 1000]}] 222 | set waitTime [expr {min($waitTime, $maxBackoff * 1000)}] 223 | 224 | incr retries 225 | ::rmq::debug "After $waitTime msecs, attempting reconnect ($retries time(s))..." 226 | 227 | after $waitTime [list [self] attemptReconnect] 228 | } else { 229 | ::rmq::debug "Unable to successfully reconnect, not attempting any more..." 230 | set reconnecting 0 231 | if {$failedReconnectCB ne ""} { 232 | {*}$failedReconnectCB [self] 233 | } 234 | } 235 | } 236 | 237 | # 238 | # method run periodically to check whether the socket is 239 | # still connected by catching any error on the eof proc 240 | # 241 | method checkConnection {} { 242 | try { 243 | chan eof $sock 244 | set sockHealthPollingID \ 245 | [after $::rmq::CHECK_CONNECTION [list [self] checkConnection]] 246 | } on error {result options} { 247 | ::rmq::debug "When testing EOF on socket: '$result'" 248 | my closeConnection 249 | } 250 | } 251 | 252 | # 253 | # perform all necessary book-keeping to close the Connection 254 | # does not send any AMQP method: cleans up object state 255 | # 256 | method closeConnection {{callCloseCB 1}} { 257 | ::rmq::debug "Closing connection" 258 | try { 259 | chan event $sock readable "" 260 | close $sock 261 | } on error {result options} { 262 | ::rmq::debug "Close connection error: '$result'" 263 | } 264 | 265 | # reset all necessary variables 266 | set connected 0 267 | set lastRead 0 268 | set lastSend 0 269 | set channelsD [dict create] 270 | after cancel $heartbeatID 271 | after cancel $sockHealthPollingID 272 | 273 | # if we are reconnecting, do not want to call this 274 | # too many times so we check the reconnecting flag 275 | # this way, the first disconnection will invoke this 276 | # but not every subsequent one 277 | # then, if reconnects fail, a separate callback is used 278 | if {$closedCB ne "" && $callCloseCB && !$reconnecting} { 279 | {*}$closedCB [self] $closeD 280 | } 281 | set closeD [dict create] 282 | 283 | # time to try an auto reconnect is configured to do so 284 | if {$autoReconnect && !$reconnecting} { 285 | my attemptReconnect 286 | } 287 | } 288 | 289 | # 290 | # connect - method to perform AMQP handshake 291 | # this method performs asynchronous socket IO 292 | # to establish a connection with the RabbitMQ 293 | # server 294 | # 295 | # 296 | method connect {} { 297 | # create the socket and configure accordingly 298 | try { 299 | set connected 0 300 | if {!$tls} { 301 | set sock [socket -async $host $port] 302 | } else { 303 | ::rmq::debug "Making TLS connection with options: [array get tlsOpts]" 304 | set sock [tls::socket {*}[concat [array get tlsOpts] [list -async $host $port]]] 305 | } 306 | 307 | # configure the socket 308 | ::rmq::debug "Opening connection to ${host}:${port}" 309 | chan configure $sock \ 310 | -blocking 0 \ 311 | -buffering none \ 312 | -encoding binary \ 313 | -translation binary 314 | 315 | # since we connected using -async, need to wait for a possible timeout 316 | # once the socket is connected the writable event will fire and we move on 317 | # otherwise we use after to trigger a forceful cancel of the connection 318 | chan event $sock writable [list set ::rmq::connectTimeout 0] 319 | set timeoutID [after [expr {$maxTimeout * 1000}] \ 320 | [list set ::rmq::connectTimeout 1]] 321 | vwait ::rmq::connectTimeout 322 | 323 | # get rid of connection timeout state 324 | chan event $sock writable "" 325 | after cancel $timeoutID 326 | 327 | # potentially reconnect after a timeout 328 | # otherwise, unset the writable handler, cancel the timeout check 329 | # and move on with something useful 330 | if {$::rmq::connectTimeout && [chan configure $sock -connecting]} { 331 | ::rmq::debug "Connection attempt timed out to $host:$port" 332 | 333 | my closeConnection 334 | if {$autoReconnect && !$reconnecting} { 335 | after idle [after 0 [list [self] attemptReconnect]] 336 | } 337 | return 0 338 | } elseif {[set sockErr [chan configure $sock -error]] ne ""} { 339 | ::rmq::debug "Socket error during connection attempt: $sockErr" 340 | my closeConnection 341 | if {$autoReconnect && !$reconnecting} { 342 | after idle [after 0 [list [self] attemptReconnect]] 343 | } 344 | return 0 345 | } 346 | 347 | # setup a readable callback for parsing rmq data 348 | chan event $sock readable [list [self] readFrame] 349 | 350 | # periodically monitor for connection status if heartbeats 351 | # disabled 352 | if {$heartbeatSecs == 0} { 353 | set sockHealthPollingID \ 354 | [after $::rmq::CHECK_CONNECTION [list [self] checkConnection]] 355 | } 356 | 357 | # send the protocol header 358 | my send [::rmq::enc_protocol_header] 359 | ::rmq::debug "Sent protocol header" 360 | 361 | # return success once we receive connection.open-ok AMQP method 362 | set timeoutID [after [expr {$maxTimeout * 1000}] \ 363 | [list set [namespace current]::connected 0]] 364 | vwait [namespace current]::connected 365 | after cancel $timeoutID 366 | return $connected 367 | } on error {result options} { 368 | # when using -async this is reached when a DNS lookup fails 369 | ::rmq::debug "Error connecting to $host:$port '$result'" 370 | my closeConnection 371 | return 0 372 | } 373 | } 374 | 375 | method connected? {} { 376 | return $connected 377 | } 378 | 379 | method getFrameMax {} { 380 | return $frameMax 381 | } 382 | 383 | method getNextChannel {} { 384 | for {set chanNum 1} {$chanNum <= $maxChannels} {incr chanNum} { 385 | if {![dict exists $channelsD $chanNum]} { 386 | return $chanNum 387 | } 388 | } 389 | 390 | return -1 391 | } 392 | 393 | method getSocket {} { 394 | return $sock 395 | } 396 | 397 | # 398 | # set a callback for when the Connection object is blocked 399 | # by the RabbitMQ server 400 | # 401 | method onBlocked {cb} { 402 | set blockedCB $cb 403 | } 404 | 405 | # 406 | # set a callback for when the Connection object is closed 407 | # by the RabbitMQ server 408 | # 409 | method onClose {cb} { 410 | set closedCB $cb 411 | } 412 | 413 | # 414 | # set a callback for when the Connection object is closed 415 | # by the RabbitMQ server 416 | # 417 | method onClosed {cb} { 418 | set closedCB $cb 419 | } 420 | 421 | # 422 | # set a callback for when the AMQP handshake has completed 423 | # and the Connection object is ready to use, e.g., to create 424 | # Channel objects 425 | # 426 | # this is called when Connection.OpenOk is received 427 | # 428 | method onConnected {cb} { 429 | set connectedCB $cb 430 | } 431 | 432 | # 433 | # set a callback for when the Connection object processes a frame 434 | # with an error code 435 | # 436 | method onError {cb} { 437 | set errorCB $cb 438 | } 439 | 440 | # 441 | # set a callback for when an attempt to reconnect to the server 442 | # fails after the maximum number of retries 443 | # 444 | method onFailedReconnect {cb} { 445 | set failedReconnectCB $cb 446 | } 447 | 448 | # 449 | # a core method, called on each frame received 450 | # 451 | # if a partial frame is being handled, this is saved in the object 452 | # otherwise, the frame is deciphered and the appropriate method is 453 | # dispatched depending on what type of frame is handled 454 | # 455 | # the frame header and end byte is removed before the frame data is 456 | # passed downstream for further processing 457 | # 458 | method processFrame {data} { 459 | if {[string length $data] < 7} { 460 | ::rmq::debug "Frame not even 7 bytes: saving partial frame" 461 | append partialFrame $data 462 | return 0 463 | } 464 | 465 | # read the header to figure out what we're dealing with 466 | binary scan $data cuSuIu ftype fchannel fsize 467 | ::rmq::debug "Frame type: $ftype Channel: $fchannel Size: $fsize" 468 | 469 | # verify that the channel number makes sense 470 | if {$fchannel == 0} { 471 | set channelObj "" 472 | } elseif {[dict exists $channelsD $fchannel]} { 473 | # grab the channel object this corresponds to 474 | set channelObj [dict get $channelsD $fchannel] 475 | } else { 476 | # Error code 505 is an unexpected frame error 477 | ::rmq::debug "Channel number $fchannel does not exist" 478 | my send [::rmq::enc_frame 505 0 ""] 479 | return 0 480 | } 481 | 482 | # dispatch based on the frame type 483 | if {$ftype != $::rmq::FRAME_HEARTBEAT && $fsize == 0} { 484 | ::rmq::debug "Non-heartbeat frame with a payload size of 0" 485 | return 1 486 | } 487 | 488 | # make sure the frame ends with the right character 489 | if {[string range $data end end] != $::rmq::FRAME_END} { 490 | ::rmq::debug "Frame does not end with correct byte value: saving partial frame" 491 | ::rmq::debug "Frame was [string length $data] bytes with claimed $fsize size on channel $fchannel" 492 | 493 | # partial frames are buffered here 494 | append partialFrame $data 495 | return 0 496 | } 497 | 498 | # dispatch on type of payload 499 | set data [string range $data 7 end-1] 500 | if {$ftype eq $::rmq::FRAME_METHOD} { 501 | my processMethodFrame $channelObj $data 502 | } elseif {$ftype eq $::rmq::FRAME_HEADER} { 503 | my processHeaderFrame $channelObj $data 504 | } elseif {$ftype eq $::rmq::FRAME_BODY} { 505 | $channelObj contentBody $data 506 | } elseif {$ftype eq $::rmq::FRAME_HEARTBEAT} { 507 | my processHeartbeatFrame $channelObj $data 508 | } else { 509 | my processUnknownFrame $ftype $fchannel $data 510 | } 511 | 512 | return 1 513 | } 514 | 515 | method processFrameSafe {data} { 516 | try { 517 | my processFrame $data 518 | return 1 519 | } on error {result options} { 520 | ::rmq::debug "processFrame error: $result $options" 521 | return 0 522 | } 523 | } 524 | 525 | method processHeaderFrame {channelObj data} { 526 | # Reply code 504 is a channel error 527 | if {$channelObj eq ""} { 528 | ::rmq::debug "\[ERROR\]: Channel is 0 for header frame" 529 | return [my send [::rmq::enc_frame 504 0 ""]] 530 | } 531 | 532 | set headerD [::rmq::dec_content_header $data] 533 | $channelObj contentHeader $headerD 534 | } 535 | 536 | method processHeartbeatFrame {channelObj data} { 537 | # Reply code 501 is a frame error when the channel is not 0 538 | if {$channelObj ne ""} { 539 | ::rmq::debug "\[ERROR\]: Channel is not 0 for heartbeat" 540 | return [my send [::rmq::enc_frame 501 0 ""]] 541 | } 542 | 543 | # otherwise send a heartbeat back 544 | my sendHeartbeat 545 | } 546 | 547 | method processMethodFrame {channelObj data} { 548 | # Need to know the class ID and method ID 549 | binary scan $data SuSu classID methodID 550 | set data [string range $data 4 end] 551 | ::rmq::debug "class ID $classID method ID $methodID" 552 | 553 | # dispatch based on class ID 554 | if {$classID == $::rmq::CONNECTION_CLASS} { 555 | my [::rmq::dec_method $classID $methodID] $data 556 | } elseif {$classID in $::rmq::CHANNEL_CLASSES} { 557 | # all classes other than Connection are handled by a channel 558 | # object since they all execute in the context of one 559 | $channelObj [::rmq::dec_method $classID $methodID] $data 560 | } else { 561 | my processUnknownMethod $classID $methodID $data 562 | } 563 | } 564 | 565 | method processUnknownFrame {ftype channelObj data} { 566 | ::rmq::debug "Processing an unknown frame type $ftype" 567 | 568 | # check if the ftype is a known error code 569 | if {[dict exists $::rmq::ERROR_CODES $ftype]} { 570 | # if so, is the error on the connection or the channel 571 | # if connection, close it down and move on 572 | # if channel, pass it on to a channel error handler to 573 | # decide whether the channel needs to be closed or not 574 | if {$channelObj eq ""} { 575 | if {$errorCB ne ""} { 576 | {*}$errorCB [self] $ftype $data 577 | } 578 | } else { 579 | $channelObj errorHandler $ftype $data 580 | } 581 | } 582 | } 583 | 584 | method processUnknownMethod {classID methodID data} { 585 | # Received an unsupported method 586 | ::rmq::debug "Unsupported class ID $classID and method ID $methodID" 587 | } 588 | 589 | # 590 | # readable handler for the socket connection to the 591 | # RabbitMQ server 592 | # 593 | method readFrame {} { 594 | try { 595 | set data [chan read $sock $frameMax] 596 | if {$data eq "" && [chan eof $sock]} { 597 | ::rmq::debug "Reached EOF reading from socket" 598 | return [my closeConnection] 599 | } 600 | } on error {result options} { 601 | ::rmq::debug "Error reading from socket '$result'" 602 | return [my closeConnection] 603 | } 604 | 605 | if {$data ne ""} { 606 | # mark time for socket read activity 607 | set lastRead [clock seconds] 608 | 609 | # process frames 610 | foreach frame [my splitFrames $data] { 611 | if {![my processFrameSafe $frame]} { 612 | break 613 | } 614 | } 615 | } 616 | } 617 | 618 | method reconnecting? {} { 619 | return $reconnecting 620 | } 621 | 622 | method removeCallbacks {{channelsToo 0}} { 623 | # reset all specific callbacks for Connection 624 | set blockedCB "" 625 | set connectedCB "" 626 | set closedCB "" 627 | set errorCB "" 628 | set failedReconnectCB "" 629 | 630 | if {!$channelsToo} { 631 | return 632 | } 633 | 634 | foreach chanObj [dict values $channelsD] { 635 | $chanObj removeCallbacks 636 | } 637 | } 638 | 639 | # 640 | # remove a Channel object from the Connection 641 | # 642 | method removeChannel {num} { 643 | dict unset channelsD $num 644 | } 645 | 646 | # 647 | # manually reset the number of retries counted 648 | # for connection attempts 649 | # 650 | method resetRetries {} { 651 | set retries 0 652 | } 653 | 654 | # 655 | # add a Channel object to this Connection 656 | # 657 | method saveChannel {num channelObj} { 658 | if {$num > 0} { 659 | dict set channelsD $num $channelObj 660 | } 661 | } 662 | 663 | method ScheduleHeartbeatCheck {} { 664 | after cancel $heartbeatID 665 | set heartbeatID [after [expr {1000 * $heartbeatSecs / 2}] [list [self] sendHeartbeat]] 666 | } 667 | 668 | # 669 | # send binary data to the RabbitMQ server 670 | # 671 | method send {data} { 672 | try { 673 | puts -nonewline $sock $data 674 | set lastSend [clock seconds] 675 | } on error {result options} { 676 | ::rmq::debug "Error sending to socket" 677 | my closeConnection 678 | } 679 | } 680 | 681 | # 682 | # send a heartbeat to the RabbitMQ server 683 | # 684 | method sendHeartbeat {} { 685 | # schedule another check before proceeding 686 | my ScheduleHeartbeatCheck 687 | 688 | # figure out how long it's been since we've seen any activity 689 | set now [clock seconds] 690 | set sinceLastRead [expr {$now - $lastRead}] 691 | set sinceLastSend [expr {$now - $lastSend}] 692 | ::rmq::debug "Heartbeat: $sinceLastRead secs since last read and $sinceLastSend secs since last send" 693 | 694 | # despite being able to send data on the socket, if we haven't heard anything 695 | # from the server for > 2 heartbeat intervals, we need to disconnect 696 | # this behavior comes directly from the spec's section about heartbeats: 697 | # https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf 698 | if {$sinceLastRead > $heartbeatSecs * 2} { 699 | ::rmq::debug "Been more than 2 heartbeat intervals without socket read activity, shutting down connection" 700 | return [my closeConnection] 701 | } 702 | 703 | # since we check whether to send a heartbeat every heartbeatSecs / 2 secs 704 | # need to check if we'd be over the heartbeat interval at the end of the 705 | # after wait if we did not send a heartbeat right now 706 | if {max($sinceLastRead, $sinceLastSend) * 2 >= $heartbeatSecs} { 707 | ::rmq::debug "Sending heartbeat frame: long enough since last send or last read" 708 | return [my send [::rmq::enc_frame $::rmq::FRAME_HEARTBEAT 0 ""]] 709 | } 710 | } 711 | 712 | # 713 | # break the binary data received from the RabbitMQ server 714 | # into a list of frames 715 | # 716 | # 717 | method splitFrames {data} { 718 | set frames [list] 719 | 720 | if {$partialFrame ne ""} { 721 | set data ${partialFrame}${data} 722 | set partialFrame "" 723 | } 724 | 725 | # requires at least 8 bytes for a frame 726 | while {[string length $data] >= 7} { 727 | # need to get the frame size 728 | binary scan $data @3Iu fsize 729 | ::rmq::debug "Split frames size $fsize" 730 | 731 | # 8 bytes of the frame is non-payload data 732 | # 1 byte for type, 2 bytes for channel, 4 bytes for size, 1 byte for frame end 733 | lappend frames [string range $data 0 [expr {$fsize + 7}]] 734 | set data [string range $data [expr {$fsize + 8}] end] 735 | } 736 | 737 | if {$data ne ""} { 738 | ::rmq::debug "Left over data in split frames, adding to the end of the frames" 739 | lappend frames $data 740 | } 741 | 742 | ::rmq::debug "Received [llength $frames] frames" 743 | return $frames 744 | } 745 | 746 | # 747 | # set TLS options for connecting to RabbitMQ 748 | # supports all arguments supported by tls::import 749 | # as detailed at: 750 | # http://tls.sourceforge.net/tls.htm 751 | # 752 | method tlsOptions {args} { 753 | if {[llength $args] & 1} { 754 | return -code error "Require an even number of args" 755 | } 756 | 757 | # if we reach here, tls is set for when the socket is created 758 | array set tlsOpts $args 759 | return [set tls 1] 760 | } 761 | } 762 | 763 | oo::define ::rmq::Connection { 764 | ## 765 | ## 766 | ## AMQP Connection Class Method Handlers 767 | ## 768 | ## 769 | 770 | method connectionBlocked {data} { 771 | ::rmq::debug "Connection.Blocked" 772 | 773 | set blocked 1 774 | set reason [::rmq::dec_short_string $data _] 775 | if {$BlockedCB ne ""} { 776 | {*}$blockedCB [self] $blocked $reason 777 | } 778 | } 779 | 780 | method connectionClose {{replyCode 200} {replyText "Normal"} {cID 0} {mID 0}} { 781 | ::rmq::debug "Connection.Close" 782 | set replyCode [::rmq::enc_short $replyCode] 783 | set replyText [::rmq::enc_short_string $replyText] 784 | set classID [::rmq::enc_short $cID] 785 | set methodID [::rmq::enc_short $mID] 786 | 787 | set methodData "${replyCode}${replyText}${classID}${methodID}" 788 | set methodData [::rmq::enc_method $::rmq::CONNECTION_CLASS \ 789 | $::rmq::CONNECTION_CLOSE $methodData] 790 | my send [::rmq::enc_frame $::rmq::FRAME_METHOD 0 $methodData] 791 | } 792 | 793 | method connectionCloseRecv {data} { 794 | dict set closeD replyCode [::rmq::dec_short $data _] 795 | dict set closeD replyText [::rmq::dec_short_string [string range $data 2 end] bytes] 796 | set data [string range $data [expr {2 + $bytes}] end] 797 | dict set closeD classID [::rmq::dec_short $data _] 798 | dict set closeD methodID [::rmq::dec_short [string range $data 2 end] _] 799 | 800 | ::rmq::debug "Connection.Close ($closeD)" 801 | 802 | # send Connection.Close-Ok 803 | my sendConnectionCloseOk 804 | } 805 | 806 | method connectionCloseOk {data} { 807 | ::rmq::debug "Connection.CloseOk" 808 | my closeConnection 809 | } 810 | 811 | method connectionOpen {} { 812 | ::rmq::debug "Connection.Open vhost [$login getVhost]" 813 | 814 | set vhostVal [::rmq::enc_short_string [$login getVhost]] 815 | set reserve1 [::rmq::enc_short_string ""] 816 | set reserve2 [::rmq::enc_byte 1] 817 | set payload "${vhostVal}${reserve1}${reserve2}" 818 | 819 | set methodData [::rmq::enc_method 10 40 $payload] 820 | my send [::rmq::enc_frame 1 0 $methodData] 821 | } 822 | 823 | method connectionOpenOk {data} { 824 | # this method signals the connection is ready 825 | # and that we are no longer in a retry loop 826 | set retries 0 827 | set connected 1 828 | 829 | ::rmq::debug "Connection.OpenOk: connection now established" 830 | 831 | # call user supplied callback for when Connection is ready for use 832 | if {$connectedCB ne ""} { 833 | {*}$connectedCB [self] 834 | } 835 | } 836 | 837 | method connectionSecure {data} { 838 | ::rmq::debug "Connection.Secure" 839 | 840 | set challenge [::rmq::dec_long_string $data _] 841 | my connectionSecureOk $challenge 842 | } 843 | 844 | method connectionSecureOk {challenge} { 845 | ::rmq::debug "Connection.SecureOk" 846 | 847 | set resp [::rmq::enc_long_string [$login saslResponse]] 848 | set payload [::rmq::enc_method 10 21 $resp] 849 | my send [::rmq::enc_frame 1 0 $payload] 850 | } 851 | 852 | # 853 | # Connection.sendConnectionCloseOk - used to differentiate the 854 | # sending of AMQP Connection.CloseOk method from receiving it 855 | # 856 | method sendConnectionCloseOk {} { 857 | ::rmq::debug "Sending Connection.CloseOk" 858 | set methodData [::rmq::enc_method $::rmq::CONNECTION_CLASS \ 859 | $::rmq::CONNECTION_CLOSE_OK ""] 860 | my send [::rmq::enc_frame $::rmq::FRAME_METHOD 0 $methodData] 861 | my closeConnection 862 | } 863 | 864 | # 865 | # Connection.Start - given a frame containing 866 | # containing the Connection.Start method, return 867 | # a dict of the connection parameters contained 868 | # in the server's data 869 | # 870 | method connectionStart {data} { 871 | ::rmq::debug "Connection.Start" 872 | 873 | # starts with a protocol major and minor version 874 | binary scan $data cc versionMajor versionMinor 875 | if {$versionMajor ne $::rmq::AMQP_VMAJOR || \ 876 | $versionMinor ne $::rmq::AMQP_VMINOR} { 877 | error "AMQP version $versionMajor.$versionMinor specified by server not supported" 878 | } 879 | set data [string range $data 2 end] 880 | 881 | # consists of a field table of parameters 882 | set connectionParams [::rmq::dec_field_table $data bytesProcessed] 883 | 884 | # and two more strings 885 | set data [string range $data $bytesProcessed end] 886 | 887 | # then have two longstr to parse 888 | set mechanisms [::rmq::dec_long_string $data bytesProcessed] 889 | dict set connectionParams mechanisms $mechanisms 890 | set data [string range $data $bytesProcessed end] 891 | 892 | set locale [::rmq::dec_long_string $data bytesProcessed] 893 | dict set connectionParams locale $locale 894 | set data [string range $data $bytesProcessed end] 895 | 896 | my connectionStartOk $connectionParams 897 | } 898 | 899 | # 900 | # Connection.StartOk - given a dict of Connection.Start 901 | # parameters, send back a Connection.Ok response 902 | # or disconnect and signal an error if anything 903 | # goes wrong 904 | # 905 | method connectionStartOk {params} { 906 | ::rmq::debug "Connection.StartOk" 907 | 908 | # first need to include the client properties 909 | set clientProps [dict create] 910 | 911 | # platform 912 | set valueType [::rmq::enc_field_value long-string] 913 | set value [::rmq::enc_long_string "Tcl [info tclversion]"] 914 | dict set clientProps platform "${valueType}${value}" 915 | 916 | # product 917 | set valueType [::rmq::enc_field_value long-string] 918 | set value [::rmq::enc_long_string $::rmq::PRODUCT] 919 | dict set clientProps product "${valueType}${value}" 920 | 921 | # version 922 | set valueType [::rmq::enc_field_value long-string] 923 | set value [::rmq::enc_long_string $::rmq::VERSION] 924 | dict set clientProps version "${valueType}${value}" 925 | 926 | # capabilities dict is not inspected by the broker 927 | # see https://www.rabbitmq.com/consumer-cancel.html#capabilities 928 | set capabilities [dict create] 929 | 930 | set valueType [::rmq::enc_field_value boolean] 931 | set value [::rmq::enc_byte 1] 932 | 933 | # connection.blocked (indicates we support connection.blocked methods) 934 | if {$blockedConnections} { 935 | dict set capabilities connection.blocked "${valueType}${value}" 936 | } 937 | 938 | # authetication_failure_closed 939 | # reuses the boolean valueType 940 | dict set capabilities authentication_failure_close "${valueType}${value}" 941 | 942 | # basic.nack 943 | # reuses the boolean valueType 944 | dict set capabilities basic.nack "${valueType}${value}" 945 | 946 | # whether consumer cancel notifications are supported 947 | if {$cancelNotifications} { 948 | dict set capabilities consumer_cancel_notify "${valueType}${value}" 949 | } 950 | 951 | # add the entire capabilities dict 952 | set valueType [::rmq::enc_field_value field-table] 953 | set value [::rmq::enc_field_table $capabilities] 954 | dict set clientProps capabilities "${valueType}${value}" 955 | 956 | # verify that the mechanism provided by this library supported by server 957 | if {[string first $::rmq::DEFAULT_MECHANISM [dict get $params mechanisms]] == -1} { 958 | ::rmq::debug "tclrmq mechanism of $::rmq::DEFAULT_MECHANISM not supported by server [dict get $params mechanisms]" 959 | return [my closeConnection] 960 | } 961 | set mechanismVal [::rmq::enc_short_string $::rmq::DEFAULT_MECHANISM] 962 | 963 | # response 964 | set responseVal [::rmq::enc_long_string [$login saslResponse]] 965 | 966 | # Verify that the locale matches what we support 967 | if {$locale ne [dict get $params locale]} { 968 | ::rmq::debug "Our locale of $locale not supported by server [dict get $params locale]" 969 | return [my closeConnection] 970 | } 971 | set localeVal [::rmq::enc_short_string $locale] 972 | 973 | # full payload for the frame 974 | set payload [::rmq::enc_field_table $clientProps]${mechanismVal}${responseVal}${localeVal} 975 | my send [::rmq::enc_frame 1 0 [::rmq::enc_method 10 11 $payload]] 976 | } 977 | 978 | method connectionTune {data} { 979 | ::rmq::debug "Connection.Tune" 980 | 981 | set channelMax [::rmq::dec_short $data _] 982 | if {$channelMax == 0} { 983 | set maxChannels $::rmq::MAX_CHANNELS 984 | } else { 985 | set maxChannels $channelMax 986 | } 987 | 988 | set sFrameMax [::rmq::dec_ulong [string range $data 2 end] _] 989 | if {$sFrameMax != 0} { 990 | set frameMax $sFrameMax 991 | } 992 | 993 | set heartbeat [::rmq::dec_ushort [string range $data 6 end] _] 994 | ::rmq::debug "Heartbeat interval of $heartbeat secs suggested by the server (our value $heartbeatSecs)" 995 | 996 | if {$heartbeatSecs != 0} { 997 | ::rmq::debug "Scheduling first heartbeat check in [expr {$heartbeatSecs / 2}] secs..." 998 | my ScheduleHeartbeatCheck 999 | } 1000 | 1001 | my connectionTuneOk $channelMax $frameMax $heartbeat 1002 | } 1003 | 1004 | method connectionTuneOk {channelMax frameMax heartbeat} { 1005 | ::rmq::debug "Connection.TuneOk" 1006 | 1007 | set channelMax [::rmq::enc_short $channelMax] 1008 | set frameMax [::rmq::enc_ulong $frameMax] 1009 | set heartbeat [::rmq::enc_ushort $heartbeatSecs] 1010 | set methodData "${channelMax}${frameMax}${heartbeat}" 1011 | 1012 | set payload [::rmq::enc_method 10 31 $methodData] 1013 | my send [::rmq::enc_frame 1 0 $payload] 1014 | 1015 | my connectionOpen 1016 | } 1017 | 1018 | method connectionUnblocked {data} { 1019 | ::rmq::debug "Connection.Unblocked" 1020 | 1021 | set blocked 0 1022 | if {$blockedCB ne ""} { 1023 | {*}$blockedCB [self] $blocked "" 1024 | } 1025 | } 1026 | } 1027 | 1028 | # vim: ts=4:sw=4:sts=4:noet 1029 | --------------------------------------------------------------------------------