nl-logic_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # NL::LogicClient
2
+
3
+ This is the Ruby client for the Nitrogen Logic [Automation Controller][1].
4
+ It's used internally by the Automation Controller's web browser-based interface
5
+ to connect to the automation logic system.
6
+
7
+ # Copying
8
+
9
+ ©2011-2021 Mike Bourgeous. Released under [AGPLv3][0].
10
+
11
+ Use in new projects is not recommended.
12
+
13
+ # Usage
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'nl-logic_client'
19
+ ```
20
+
21
+ See `bin/set_multi.rb`, `bin/list_exports.rb`, and `bin/show_info.rb` for usage
22
+ examples.
23
+
24
+ [0]: https://www.gnu.org/licenses/agpl-3.0.html
25
+ [1]: http://www.nitrogenlogic.com/products/automation_controller.html
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "nl/logic_client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ # Prints a list of exported parameters on the specified logic controller.
3
+ # (C)2011 Mike Bourgeous
4
+
5
+ require 'bundler/setup'
6
+ require 'nl/logic_client'
7
+
8
+ $list_exports_succeeded = false
9
+
10
+ def list_exports hostname=nil
11
+ hostname ||= 'localhost'
12
+
13
+ errback = proc {
14
+ puts "Connection to the server failed."
15
+ EM::stop_event_loop
16
+ }
17
+ NL::LC.get_connection(hostname, errback) do |c|
18
+ cmd = c.get_exports do |exports|
19
+ $list_exports_succeeded = true
20
+ if ARGV[1] == "--kvp"
21
+ puts *(exports.map { |e| e.to_kvp })
22
+ else
23
+ puts *exports
24
+ end
25
+ cmd2 = c.do_command 'bye' do
26
+ EM::stop_event_loop
27
+ end
28
+ cmd2.errback do
29
+ EM::stop_event_loop
30
+ end
31
+ end
32
+ cmd.errback do
33
+ EM::stop_event_loop
34
+ end
35
+ end
36
+ end
37
+
38
+ if __FILE__ == $0
39
+ EM::run {
40
+ list_exports ARGV[0]
41
+ }
42
+
43
+ exit $list_exports_succeeded ? 0 : 7
44
+ end
45
+
data/bin/set_multi.rb ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'nl/logic_client'
5
+
6
+ EM.run {
7
+ NL::LC.get_connection(
8
+ ARGV[0] || 'localhost',
9
+ proc { puts 'Error connecting to server'; EM.stop_event_loop }
10
+ ) do |c|
11
+ p c
12
+ c.set_multi [{:objid => 0, :index => 0, :value => '0x55'}] do |count, list|
13
+ p "#{count} of #{list.length}"
14
+ p list
15
+ EM.stop_event_loop
16
+ end
17
+ end
18
+ }
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/bin/show_info.rb ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # Displays information about the running graph.
3
+ # (C)2011 Mike Bourgeous
4
+
5
+ require 'bundler/setup'
6
+ require 'nl/logic_client'
7
+
8
+ $succeeded = false
9
+
10
+ class ShowInfoClient < NL::LC::Client
11
+ def post_init
12
+ super
13
+ get_info do |info|
14
+ info.each do |k, v|
15
+ puts "#{k}=#{v}"
16
+ end
17
+ do_command 'bye'
18
+ close_connection_after_writing
19
+ end
20
+ end
21
+
22
+ def connection_completed
23
+ super
24
+ $succeeded = true
25
+ end
26
+
27
+ def unbind
28
+ super
29
+ puts "An error occurred while getting the graph info." unless $succeeded
30
+ EM::stop_event_loop
31
+ end
32
+ end
33
+
34
+ def show_info hostname=nil
35
+ hostname ||= 'localhost'
36
+ EM.connect(hostname, 14309, ShowInfoClient)
37
+ end
38
+
39
+ if __FILE__ == $0
40
+ EM::run {
41
+ EM.error_handler { |e|
42
+ puts "Error: "
43
+ p e
44
+ }
45
+ show_info ARGV[0]
46
+ }
47
+
48
+ exit $succeeded ? 0 : 7
49
+ end
@@ -0,0 +1,586 @@
1
+ # Ruby client interface for the logic system protocol, powered by EventMachine.
2
+ # (C)2012-2016 Mike Bourgeous
3
+
4
+ require 'eventmachine'
5
+ require_relative 'logic_client/version'
6
+
7
+ module NL
8
+ module LogicClient
9
+ LS_PORT = 14309
10
+
11
+ module KeyValueParser
12
+ KVREGEX = %r{(\A|^|\s)("(\\.|[^"])*"|[^" \t\r\n=][^ \t\r\n=]*)=("(\\.|[^"])*("|$)|[^ \t\r\n]+)}
13
+
14
+ # Replacement on the left, original on the right
15
+ UNESCAPES = {
16
+ 't' => "\t",
17
+ 'n' => "\n",
18
+ 'r' => "\r",
19
+ 'v' => "\v",
20
+ 'f' => "\f",
21
+ 'a' => "\a",
22
+ '"' => '"'
23
+ }
24
+
25
+ # Original on the left, replacement on the right
26
+ ESCAPES = UNESCAPES.invert
27
+
28
+ # TODO: Replace this key-value parser with the C-based one from knc. See
29
+ # nl-knd_client.
30
+
31
+ # Removes surrounding quotes from and parses C-style escapes within the
32
+ # given string. Does not handle internal quoting the way a shell would.
33
+ # Returns an unescaped copy, leaving the original string unmodified.
34
+ def self.unescape(str, dequote=true, esc="\\")
35
+ if str.length == 0
36
+ return str
37
+ end
38
+
39
+ newstr = ''
40
+ i = 0
41
+ quoted = false
42
+ if dequote && str[0] == '"'
43
+ quoted = true
44
+ i += 1
45
+ end
46
+
47
+ until i == str.length
48
+ if str[i] == esc
49
+ # Remove a lone escape at the end of the string
50
+ break if i == str.length - 1
51
+
52
+ i += 1
53
+ c = str[i]
54
+ case c
55
+ when 'x'
56
+ puts "Hexadecimal escape", c.inspect
57
+ # Hexadecimal escape TODO: Get up to two hex digits
58
+ # TODO: Safe \u unicode escape
59
+ when esc
60
+ # Escape character
61
+ newstr << esc
62
+ else
63
+ # Standard escape
64
+ if UNESCAPES.has_key? c
65
+ # Matching character -- process escape sequence
66
+ newstr << UNESCAPES[c]
67
+ else
68
+ # No matching character -- pass escape sequence unmodified
69
+ newstr << esc
70
+ newstr << c
71
+ end
72
+ end
73
+ else
74
+ # Ordinary character
75
+ unless i == str.length - 1 && quoted && str[i] == '"'
76
+ newstr << str[i]
77
+ end
78
+ end
79
+ i += 1
80
+ end
81
+
82
+ newstr
83
+ end
84
+
85
+ # Parses a multi-element quoted key-value pair string into a hash. Keys
86
+ # and values should be quoted independently (e.g. "a"="b" not "a=b").
87
+ def self.kvp(str)
88
+ pairs = {}
89
+ str.scan(KVREGEX) do |match|
90
+ if match[1] != nil && match[3] != nil
91
+ pairs[unescape(match[1])] = unescape(match[3])
92
+ end
93
+ end
94
+ pairs
95
+ end
96
+ end
97
+
98
+ KVP = KeyValueParser
99
+
100
+ # Represents a deferred logic system command. Use the callback and errback
101
+ # methods from EM::Deferrable to add callbacks to be executed on command
102
+ # success/failure.
103
+ class Command
104
+ include EM::Deferrable
105
+
106
+ attr_reader :lines, :data, :data_size, :message, :name, :argstring
107
+
108
+ def initialize(name, *args)
109
+ @name = name
110
+ @args = args
111
+ @argstring = (args.length > 0 && " #{@args.map {|s| s.to_s.gsub(',', '') if s != nil }.join(',')}") || ''
112
+ @linecount = 0
113
+ @lines = []
114
+ @data_size = 0
115
+ @data = nil
116
+ @message = ""
117
+
118
+ timeout(5)
119
+ end
120
+
121
+ # Returns true if successful, false if failed, nil if the
122
+ # command isn't finished.
123
+ def success?
124
+ case @deferred_status
125
+ when :succeeded
126
+ true
127
+ when :failed
128
+ false
129
+ else
130
+ nil
131
+ end
132
+ end
133
+
134
+ # Returns true if this command is waiting for data.
135
+ def want_data?
136
+ @data_size > 0
137
+ end
138
+
139
+ # Called by Client when an OK line is received
140
+ def ok_line(message)
141
+ @message = message
142
+
143
+ # TODO: Change to a protocol more like the one I designed for HATS
144
+ # Server, with different response types for text/data/status.
145
+ case @name
146
+ when 'stats', 'subs', 'lst', 'lstk', 'help'
147
+ @linecount = message.to_i
148
+ when 'download'
149
+ @data_size = message.gsub(/[^0-9]*(\d+).*/, '\1').to_i
150
+ end
151
+
152
+ if @linecount == 0 && @data_size == 0
153
+ succeed self
154
+ return true
155
+ end
156
+
157
+ return false
158
+ end
159
+
160
+ # Called by Client when an ERR line is received
161
+ def err_line(message)
162
+ @message = message
163
+ fail self
164
+ end
165
+
166
+ # Called by Client to add a line
167
+ # Returns true when enough lines have been received
168
+ def add_line(line)
169
+ @lines << line
170
+ @linecount -= 1
171
+ if @linecount == 0
172
+ succeed self
173
+ end
174
+ return @linecount <= 0
175
+ end
176
+
177
+ # Called by client to add binary data
178
+ def add_data(data)
179
+ @data = @data || ''.force_encoding('BINARY')
180
+ @data << data
181
+ @data_size -= data.bytesize
182
+ if @data_size == 0
183
+ succeed self
184
+ elsif @data_size < 0
185
+ raise "Too many bytes given to data-receiving Command."
186
+ end
187
+ return @data_size <= 0
188
+ end
189
+
190
+ # Returns the command line sent to the logic system for this command
191
+ def to_s
192
+ "#{@name}#{@argstring}"
193
+ end
194
+
195
+ # Returns a description of the command's contents, useful for debugging
196
+ def inspect
197
+ %Q{#<Command: cmd=#{@name} n_lines=#{@lines.length} n_data=#{@data && @data.length}>}
198
+ end
199
+ end
200
+
201
+ # Internally represents a subscription to a value on the logic system
202
+ class Subscription
203
+ attr_reader :value
204
+ attr_reader :id
205
+
206
+ # Initializes a subscription. The block will be called with
207
+ # object ID, parameter ID, and parameter value.
208
+ # TODO: Use line as parameter instead?
209
+ def initialize(obj, id, &block)
210
+ if obj == nil || id == nil || block == nil
211
+ raise "Nil parameter to Subscription constructor"
212
+ end
213
+
214
+ unless obj.respond_to?(:to_i) and id.respond_to?(:to_i)
215
+ raise "Object and ID must be integers"
216
+ end
217
+
218
+ @value = nil
219
+ @obj = obj.to_i
220
+ @id = id.to_i
221
+ @cb = block
222
+ end
223
+
224
+ # Calls the callback when the value is changed. The call to
225
+ # the callback is deferred using the event reactor.
226
+ def value=(val)
227
+ @value = val
228
+ EM.next_tick {
229
+ cb.call @obj, @id, @value
230
+ }
231
+ # TODO: Support multiple callbacks per parameter using
232
+ # a reference count to unsubscribe
233
+ end
234
+
235
+ # Parses the key-value pair line from a subscription message
236
+ def parse(kvpline)
237
+ # TODO: Parse the message (move kvp tools from KNC
238
+ # client.rb into a private utility gem?)
239
+ end
240
+ end
241
+
242
+ # Converts a string value to the given logic system type name (int,
243
+ # float, string, data).
244
+ def self.string_to_type(str, type)
245
+ case type
246
+ when 'int'
247
+ return str.to_i
248
+ when 'float'
249
+ return str.to_f
250
+ when 'string'
251
+ return KVP.unescape(str)
252
+ when 'data'
253
+ raise "Data type not yet supported."
254
+ else
255
+ raise "Unsupported data type: #{type}."
256
+ end
257
+ end
258
+
259
+ # Represents an exported parameter to be returned by get_exports
260
+ class Export
261
+ attr_reader :objid, :obj_name, :param_name, :index, :type, :value
262
+ attr_reader :min, :max, :def, :hide_in_ui, :read_only
263
+
264
+ # Parses an export key-value line received from the logic system server
265
+ def initialize(line)
266
+ kvmap = KVP.kvp line
267
+
268
+ @objid = kvmap['objid'].to_i
269
+ @index = kvmap['index'].to_i
270
+ @type = kvmap['type']
271
+ @value = LC.string_to_type(kvmap['value'], @type)
272
+ @min = LC.string_to_type(kvmap['min'], @type)
273
+ @max = LC.string_to_type(kvmap['max'], @type)
274
+ @def = LC.string_to_type(kvmap['def'], @type)
275
+ @obj_name = kvmap['obj_name']
276
+ @param_name = kvmap['param_name']
277
+ @hide_in_ui = kvmap['hide_in_ui'] == 'true'
278
+ @read_only = kvmap['read_only'] == 'true'
279
+ end
280
+
281
+ # Formats this export's info as it would come from the logic system server
282
+ def to_s
283
+ # TODO: Quote strings/escape them the same way as the logic system
284
+ "#{@objid},#{@index},#{@type},#{@value.inspect} (#{@obj_name}: #{@param_name})"
285
+ end
286
+
287
+ # Formats this export's info as key-value pairs
288
+ def to_kvp
289
+ %Q{objid=#{@objid} index=#{@index} type=#{@type.inspect} read_only=#{@read_only} } <<
290
+ %Q{hide_in_ui=#{@hide_in_ui} min=#{@min.inspect} max=#{@max.inspect} def=#{@def.inspect} } <<
291
+ %Q{obj_name=#{@obj_name.inspect} param_name=#{@param_name.inspect} value=#{@value.inspect}}
292
+ end
293
+
294
+ # Stores this export's info in a hash
295
+ def to_h
296
+ { :objid => @objid, :obj_name => @obj_name, :param_name => @param_name,
297
+ :min => @min, :max => @max, :def => @def, :hide_in_ui => @hide_in_ui,
298
+ :read_only => @read_only, :index => @index, :type => @type, :value => @value
299
+ }
300
+ end
301
+ end
302
+
303
+ # Manages a connection to a logic system
304
+ class Client < EM::Connection
305
+ include EM::P::LineText2
306
+
307
+ attr_reader :verstr, :version
308
+
309
+ # Conmap parameter is the hash entry in LC::@@connections
310
+ def initialize(conmap=nil)
311
+ super
312
+ @binary = :none
313
+ @commands = []
314
+ @active_command = nil
315
+ @subscriptions = {}
316
+ @con = conmap
317
+ @verstr = ''
318
+ @version = nil
319
+ end
320
+
321
+ # Override this method to implement a connection-completed callback (be
322
+ # sure to call super)
323
+ def connection_completed
324
+ cmd = get_version do |msg|
325
+ @verstr = msg
326
+ @version = msg[/[0-9]+\.[0-9]+\.[0-9]+/]
327
+ end
328
+ cmd.errback do |cmd|
329
+ close_connection
330
+ end
331
+
332
+ @con_success = true
333
+ if @con
334
+ @con[:connected] = true
335
+ @con[:callbacks].each do |cb|
336
+ cb.call(self) if cb && cb.respond_to?(:call)
337
+ end
338
+ @con[:callbacks].clear
339
+ @con[:errbacks].clear
340
+ end
341
+ super
342
+ end
343
+
344
+ def unbind
345
+ # TODO: Call the error handlers for any pending commands
346
+ # TODO: Add the ability to register unbind handlers
347
+
348
+ unless @con_success
349
+ if @con
350
+ @con[:errbacks].each do |eb|
351
+ eb.call() if eb && eb.respond_to?(:call)
352
+ end
353
+ end
354
+ end
355
+
356
+ if @con
357
+ LC.connections.delete(@con[:hostname])
358
+ end
359
+ end
360
+
361
+ def receive_line(data)
362
+ # Feed lines into any command waiting for data
363
+ if @active_command
364
+ @active_command = nil if @active_command.add_line data
365
+ return
366
+ end
367
+
368
+ # No active command, so this must be the beginning of a response (e.g. OK, ERR, SUB)
369
+ type, message = data.split(" - ", 2)
370
+
371
+ case type
372
+ when "OK"
373
+ if @commands.length == 0
374
+ puts '=== ERROR - Received OK when no command was waiting ==='
375
+ else
376
+ cmd = @commands.shift
377
+ unless cmd.ok_line message
378
+ @active_command = cmd
379
+ set_binary_mode(cmd.data_size) if cmd.want_data?
380
+ end
381
+ end
382
+ when "ERR"
383
+ if @commands.length == 0
384
+ puts '=== ERROR - Received ERR when no command was waiting ==='
385
+ end
386
+ @commands.shift.err_line message
387
+ when "SUB"
388
+ # TODO: Check for a callback in the subscription table
389
+ puts "TODO: Implement subscription handling"
390
+ else
391
+ puts "=== ERROR - Unknown response '#{data}' ==="
392
+ # TODO: Clear pending commands or disconnect at this point?
393
+ end
394
+ end
395
+
396
+ def receive_binary_data(data)
397
+ if @active_command
398
+ raise "Received data for a command not expecting it!" unless @active_command.want_data?
399
+ @active_command = nil if @active_command.add_data data
400
+ end
401
+ end
402
+
403
+ # Defers execution of a command. The block, if specified, will be called
404
+ # with a Command object upon successful completion of the command. For
405
+ # more control over a command's lifecycle, including specifying an error
406
+ # callback, see the Command class. Returns the Command object used for
407
+ # this command. TODO: Add a timeout that calls any error handlers if the
408
+ # command doesn't return quickly.
409
+ def do_command(command, *args, &block)
410
+ if command.is_a? Command
411
+ cmd = command
412
+ else
413
+ cmd = Command.new command, *args
414
+ end
415
+
416
+ if block != nil
417
+ cmd.callback { |*args|
418
+ block.call *args
419
+ }
420
+ end
421
+
422
+ send_data "#{cmd.to_s}\n"
423
+ @commands << cmd
424
+
425
+ return cmd
426
+ end
427
+
428
+ # Calls the given block with the message received from the ver command.
429
+ # Returns the Command used to process the request.
430
+ def get_version(&block)
431
+ return do_command('ver') { |cmd|
432
+ block.call cmd.message
433
+ }
434
+ end
435
+
436
+ # Calls the given block with the current list of subscriptions (an array
437
+ # of lines received from the server). Returns the Command used to
438
+ # process the request.
439
+ def get_subscriptions(&block)
440
+ return do_command("subs") { |cmd|
441
+ block.call cmd.lines
442
+ }
443
+ end
444
+
445
+ # Calls the given block with the list of exported parameters (an array of
446
+ # lines received from the server). Returns the Command used to process
447
+ # the request.
448
+ def get_exports(&block)
449
+ return do_command("lstk") { |cmd|
450
+ block.call cmd.lines.map { |line| Export.new line }
451
+ }
452
+ end
453
+
454
+ # Calls the given block with a hash containing information about the
455
+ # currently-running logic graph. Returns the Command used to process the
456
+ # request.
457
+ def get_info(&block)
458
+ return do_command("inf") { |cmd|
459
+ info = KVP.kvp cmd.message
460
+ begin
461
+ info['id'] = info['id'].to_i
462
+ info['numobjs'] = info['numobjs'].to_i
463
+ info['period'] = info['period'].to_i
464
+ info['avg'] = info['avg'].to_i
465
+ info['revision'] = info['revision'].split('.', 2).map { |v| v.to_i }
466
+ rescue
467
+ end
468
+ block.call info
469
+ }
470
+ end
471
+
472
+ # Calls the given block with the requested value. Returns the Command
473
+ # used to process the request.
474
+ def get(objid, param_index, &block)
475
+ return do_command("get", objid, param_index) { |cmd|
476
+ type, value = cmd.message.split(' - ', 2)
477
+ block.call LC.string_to_type(value, type)
478
+ }
479
+ end
480
+
481
+ # Calls the given block with a Command object after successfully setting
482
+ # the given value. Returns the Command used to process the request.
483
+ def set(objid, param_index, value, &block)
484
+ return do_command("set", objid, param_index, value) { |cmd|
485
+ block.call cmd if block
486
+ }
487
+ end
488
+
489
+ # Sets multiple parameters and calls the given block with the number of
490
+ # sucessful sets, and a copy of the multi array with results added to the
491
+ # individual hashes. Multi should be an array of hashes, with each hash
492
+ # containing :objid, :index, and :value. A success/error result for each
493
+ # value will be stored in :result, and the associated Command object
494
+ # stored in :command. Returns the first Command object in the sequence
495
+ # (not particularly useful), or nil if multi is empty.
496
+ def set_multi(multi, &block)
497
+ raise ArgumentError, "Pass an array of hashes to set_multi" unless multi.is_a? Array
498
+
499
+ if multi.length == 0
500
+ block.call(0, multi) if block
501
+ return nil
502
+ end
503
+
504
+ iter = multi.each
505
+ count = 0
506
+
507
+ cb = proc { |cmd|
508
+ begin
509
+ v = iter.next
510
+ v[:command] = cmd
511
+ v[:result] = cmd.success?
512
+ count += 1 if cmd.success?
513
+
514
+ v = iter.peek
515
+ nc = set(v[:objid], v[:index], v[:value])
516
+ nc.callback { |*a| cb.call *a }
517
+ nc.errback { |*a| cb.call *a }
518
+ rescue ::StopIteration
519
+ block.call count, multi
520
+ end
521
+ }
522
+
523
+ v = iter.peek
524
+ nc = set(v[:objid], v[:index], v[:value])
525
+ nc.callback { |*a| cb.call *a }
526
+ nc.errback { |*a| cb.call *a }
527
+
528
+ return nc
529
+ end
530
+
531
+ # TODO: Methods for more commands
532
+ end
533
+
534
+ # Key=hostname, value=hash containing:
535
+ # { :connected => bool,
536
+ # :client => Client,
537
+ # :callbacks => [],
538
+ # :errbacks => []
539
+ # }
540
+ @@connections = {}
541
+ def self.connections
542
+ @@connections
543
+ end
544
+
545
+ # If a connection to the given exact hostname exists, then the given
546
+ # block will be called with the corresponding LC::Client object as its
547
+ # parameter. Otherwise a connection request to the given host name is
548
+ # queued, and block and errback will be added to the list of success
549
+ # and error callbacks that will be called when the connection is made
550
+ # or fails. Success callbacks are called with the Client object as
551
+ # their first parameter. Error callbacks are called with no
552
+ # parameters.
553
+ def self.get_connection(hostname, errback=nil, &block)
554
+ raise "You must pass a success block to get_connection." if block == nil
555
+ raise "EventMachine reactor must be running." unless EM.reactor_running?
556
+
557
+ con = @@connections[hostname]
558
+ unless con
559
+ con = {
560
+ :hostname => hostname,
561
+ :connected => false,
562
+ :callbacks => [],
563
+ :errbacks => []
564
+ }
565
+ con[:client] = EM.connect(hostname, LS_PORT, Client, con)
566
+ @@connections[hostname] = con
567
+ end
568
+
569
+ if con[:connected]
570
+ block.call con[:client]
571
+ else
572
+ con[:callbacks] << block if block
573
+ con[:errbacks] << errback if errback
574
+ end
575
+ end
576
+
577
+ # If a connection to the given exact hostname exists, then the
578
+ # connection's Client object will be returned. Otherwise, nil will be
579
+ # returned.
580
+ def self.get_client(hostname)
581
+ @@connections[hostname] && @@connections[hostname][:client]
582
+ end
583
+ end
584
+
585
+ LC = LogicClient
586
+ end