Class: KATCP::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/katcp/client.rb

Overview

Facilitates talking to a KATCP server.

Direct Known Subclasses

RoachClient

Constant Summary collapse

DEFAULT_SOCKET_TIMEOUT =

Default timeout for socket operations (in seconds)

0.25

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Client

call-seq: Client.new([remote_host, remote_port=7147, local_host=nil, local_port=nil,] opts={}) -> Client

Creates a KATCP client that connects to a KATCP server at remote_host on remote_port. If local_host and local_port are specified, then those parameters are used on the local end to establish the connection. Positional parameters can be used OR parameters can be passed via the opts Hash.

Supported keys for the opts Hash are:

:remote_host    Specifies hostname of KATCP server
:remote_port    Specifies port used by KATCP server
                (default ENV['KATCP_PORT'] || 7147)
:local_host     Specifies local interface to bind to (default nil)
:local_port     Specifies local port to bind to (default nil)
:socket_timeout Specifies timeout for socket operations
                (default DEFAULT_SOCKET_TIMEOUT)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/katcp/client.rb', line 36

def initialize(*args)
  # If final arg is a Hash, pop it off
  @opts = (Hash === args[-1]) ? args.pop : {}

  # Save parameters
  remote_host, remote_port, local_host, local_port = args
  @remote_host = remote_host ? remote_host.to_s : @opts[:remote_host].to_s
  @remote_port = remote_port || @opts[:remote_port] || ENV['KATCP_PORT'] || 7147
  @local_host = local_host || @opts[:local_host]
  @local_port = local_port || @opts[:local_port]

  # Make sure @remote_port is Integer, if not use default of 7147
  @remote_port = Integer(@remote_port) rescue 7147

  # Create sockaddr from remote host and port.  This can raise
  # "SocketError: getaddrinfo: Name or service not known".
  @sockaddr = Socket.sockaddr_in(@remote_port, @remote_host)

  # Init attribute(s)
  @informs = []

  # @reqlock is the Monitor object used to serialize requests sent to the
  # KATCP server.  Threads should not write to @socket or read from @rxq
  # or change @reqname unless they have acquired (i.e. synchonized on)
  # @reqlock.
  @reqlock = Monitor.new

  # @reqname is the request name currently being processed (nil if no
  # current request).
  @reqname = nil

  # @rxq is an inter-thread queue
  @rxq = Queue.new

  # Timeout value for socket operations
  @socket_timeout = @opts[:socket_timeout] || DEFAULT_SOCKET_TIMEOUT

  # No socket yet
  @socket = nil

  # Try to connect socket and start listener thread, but stifle exception
  # if it fails because we need object creation to succeed even if connect
  # doesn't.  Each request attempt will try to reconnect if needed.
  # TODO Warn if connection fails?
  connect rescue self
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args) ⇒ Object

Translates calls to missing methods into KATCP requests. Raises an exception if the response status is not OK.



302
303
304
305
306
# File 'lib/katcp/client.rb', line 302

def method_missing(sym, *args)
  resp = request(sym, *args)
  raise resp.to_s unless resp.ok?
  resp
end

Instance Method Details

#client_listObject

call-seq:

client_list -> KATCP::Response

Issues a client_list request to the server.



322
323
324
# File 'lib/katcp/client.rb', line 322

def client_list
  request(:client_list)
end

#closeObject

Close socket if it exists and is not already closed. Subclasses can override #close to perform additional cleanup as needed, but they must either close the socket themselves or call super.



200
201
202
203
# File 'lib/katcp/client.rb', line 200

def close
  @socket.close if connected?
  self
end

#configure(*args) ⇒ Object

call-seq:

configure(*args) -> KATCP::Response

Issues a configure request to the server.



330
331
332
# File 'lib/katcp/client.rb', line 330

def configure(*args)
  request(:configure, *args)
end

#connectObject

Connect socket and start listener thread



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/katcp/client.rb', line 84

def connect
  # Close existing connection (if any)
  close

  # Create new socket.
  @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)

  # Do connect in timeout block
  begin
    Timeout::timeout(@socket_timeout) do
      @socket.connect(@sockaddr)
    end
  rescue => e
    # Close socket
    @socket.close
    # Change e to TimeoutError instead of Timeout::Error
    if Timeout::Error === e
      e = TimeoutError.new(
        'connection timed out in %.3f seconds' % @socket_timeout)
    end
    raise e
  end

  # Start thread that reads data from server.
  Thread.new do
    catch :giveup do
      while true
        begin
          req_timeouts = 0
          while req_timeouts < 2
            # Use select to wait with timeout for data or error
            rd_wr_ex = select([@socket], nil, nil, @socket_timeout)

            # Handle timeout
            if rd_wr_ex.nil?
              # Timeout, increment req_timeout if we're expecting a reply,
              # then try again
              req_timeouts += 1 if @reqname
              next
            end

            # OK to (try to) read!
            line = nil
            begin
              # TODO: Monkey-patch gets so that it recognizes "\r" or "\n"
              # as line endings.  Currently only recognizes fixed strings,
              # so for now go with "\n".
              line = @socket.gets("\n")
            rescue
              # Uh-oh, send double-bang error response, and give up
              @rxq.enq(['!!socket-error'])
              throw :giveup
            end

            # If EOF
            if line.nil?
              # Send double-bang error response, and give up
              @rxq.enq(['!!socket-eof'])
              throw :giveup
            end

            # Split line into words and unescape each word
            words = line.chomp.split(/[ \t]+/).map! {|w| w.katcp_unescape!}
            # Handle requests, replies, and informs based on first character
            # of first word.
            case words[0][0,1]
            # Request
            when '?'
              # TODO Send 'unsupported' reply (or support requests from server?)
            # Reply
            when '!'
              # TODO: Raise exception if name is not same as @reqname?
              # TODO: Raise exception on non-ok?
              # Enqueue words to @rxq
              @rxq.enq(words)
            # Inform
            when '#'
              # If the name is same as @reqname
              if @reqname && @reqname == words[0][1..-1]
                # Enqueue words to @rxq
                @rxq.enq(words)
              else
                # Must be asynchronous inform message, add to list.
                line.katcp_unescape!
                line.chomp!
                @informs << line
              end
            else
              # Malformed line
              # TODO: Log error better?
              warn "malformed line: #{line.inspect}"
            end # case words[0][0,1]

            # Reset req_timeouts counter
            req_timeouts = 0

          end # while req_timeouts < 2

          # Got 2 timeouts in a request!
          # Send double-bang timeout response
          @rxq.enq(['!!socket-timeout'])
          throw :giveup

        rescue Exception => e
          $stderr.puts e; $stderr.flush
        end # begin
      end # while true
    end # catch :giveup
  end # Thread.new block

  self
end

#connected?Boolean

Returns true if socket has been created and not closed

Returns:

  • (Boolean)


206
207
208
# File 'lib/katcp/client.rb', line 206

def connected?
  !@socket.nil? && !@socket.closed?
end

#haltObject

call-seq:

halt -> KATCP::Response

Issues a halt request to the server. Shuts down the system.



338
339
340
# File 'lib/katcp/client.rb', line 338

def halt
  request(:halt)
end

#help(*args) ⇒ Object

call-seq:

help -> KATCP::Response
help(name) -> KATCP::Response

Issues a help request to the server. If name is a Symbol, all ‘_’ characters are changed to ‘-’. Response inform lines are sorted.



348
349
350
351
352
353
354
355
# File 'lib/katcp/client.rb', line 348

def help(*args)
  # Change '_' to '-' in Symbol args
  args.map! do |arg|
    arg = arg.to_s.gsub('-', '_') if Symbol === arg
    arg
  end
  request(:help, *args).sort!
end

#hostObject

Return remote hostname



211
212
213
# File 'lib/katcp/client.rb', line 211

def host
  @remote_host
end

#informs(clear = false) ⇒ Object

call-seq:

inform(clear=false) -> Array

Returns Array of inform messages. If clear is true, clear messages.



294
295
296
297
298
# File 'lib/katcp/client.rb', line 294

def informs(clear=false)
  msgs = @informs
  @informs = [] if clear
  msgs
end

#inspectObject

Provides more detailed String representation of self



314
315
316
# File 'lib/katcp/client.rb', line 314

def inspect
  "#<#{self.class.name} #{to_s} (#{@informs.length} inform messages)>"
end

#log_level(*args) ⇒ Object

call-seq:

log_level -> KATCP::Response
log_level(priority) -> KATCP::Response

Query or set the minimum reported log priority.



362
363
364
# File 'lib/katcp/client.rb', line 362

def log_level(*args)
  request(:log_level, *args)
end

#mode(*args) ⇒ Object

call-seq:

mode -> KATCP::Response
mode(new_mode) -> KATCP::Response

Query or set the current mode.



371
372
373
# File 'lib/katcp/client.rb', line 371

def mode(*args)
  request(:mode, *args)
end

#portObject

Return remote port



216
217
218
# File 'lib/katcp/client.rb', line 216

def port
  @remote_port
end

#request(name, *arguments) ⇒ Object

call-seq:

request(name, *arguments) -> KATCP::Response

Sends request name with arguments to server. Returns KATCP::Response object.

TODO: Raise exception if reply is not OK?


229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/katcp/client.rb', line 229

def request(name, *arguments)
  # (Re-)connect if @socket is in an invalid state
  connect if @socket.nil? || @socket.closed?

  # Massage name to allow Symbols and to allow '_' between words (since
  # that is more natural for Symbols) in place of '-'
  reqname = name.to_s.gsub('_','-')

  # Escape arguments
  reqargs = arguments.map! {|arg| arg.to_s.katcp_escape}

  # TODO Find a more elegant way to code this retry loop?
  attempts = 0
  while true
    attempts += 1

    # Create response
    resp = Response.new

    # Give "words" scope outside of synchronize block
    words = nil

    # Get lock on @reqlock
    @reqlock.synchronize do
      # Store request name
      @reqname = reqname
      # Send request
      req = "?#{[reqname, *reqargs].join(' ')}\n"
      @socket.print req
      # Loop on reply queue until done or error
      begin
        words = @rxq.deq
        resp << words
      end until words[0][0,1] == '!'
      # Clear request name
      @reqname = nil
    end # @reqlock.synchronize

    # Break out of retry loop unless double-bang reply
    break unless words[0][0,2] == '!!'

    # Double-bang reply!!

    # If we've already attempted more than once (i.e. twice)
    if attempts > 1
      # Raise exception
      case words[0]
      when '!!socket-timeout'; raise TimeoutError.new(resp)
      when '!!socket-error'; raise SocketError.new(resp)
      when '!!socket-eof'; raise SocketEOF.new(resp)
      else raise RuntimeError.new(resp)
      end
    end

    # Reconnect and try again
    connect
  end # while true

  resp
end

#restartObject

call-seq:

restart -> KATCP::Response

Issues a restart request to the server to restart the remote system.



379
380
381
# File 'lib/katcp/client.rb', line 379

def restart
  request(:restart)
end

#sensor_dump(*args) ⇒ Object

call-seq:

sensor_dump(*args) -> KATCP::Response

Dumps the sensor tree. [obsolete?]



387
388
389
# File 'lib/katcp/client.rb', line 387

def sensor_dump(*args)
  request(:sensor_dump, *args)
end

#sensor_list(*args) ⇒ Object

call-seq:

sensor_list(*args) -> KATCP::Response

Queries for list of available sensors. Response inform lines are sorted.



395
396
397
# File 'lib/katcp/client.rb', line 395

def sensor_list(*args)
  request(:sensor_list, *args).sort!
end

#sensor_sampling(sensor, *args) ⇒ Object

call-seq:

sensor_sampling(sensor) -> KATCP::Response
sensor_sampling(sensor, strategy, *parameters) -> KATCP::Response

Quesry or set sampling parameters for a sensor.



404
405
406
# File 'lib/katcp/client.rb', line 404

def sensor_sampling(sensor, *args)
  request(:sensor_sampling, sensor, *args)
end

#sensor_value(sensor) ⇒ Object

call-seq:

sensor_value(sensor) -> KATCP::Response

Query a sensor.



412
413
414
# File 'lib/katcp/client.rb', line 412

def sensor_value(sensor)
  request(:sensor_value, sensor)
end

#to_sObject

Provides terse string representation of self.



309
310
311
# File 'lib/katcp/client.rb', line 309

def to_s
  "#{@remote_host}:#{@remote_port}"
end

#watchdogObject Also known as: ping

call-seq:

watchdog -> KATCP::Response

Issues a watchdog request to the server.



420
421
422
# File 'lib/katcp/client.rb', line 420

def watchdog
  request(:watchdog)
end