Class: KATCP::Client
- Inherits:
-
Object
- Object
- KATCP::Client
- Defined in:
- lib/katcp/client.rb
Overview
Facilitates talking to a KATCP server.
Direct Known Subclasses
Constant Summary collapse
- DEFAULT_SOCKET_TIMEOUT =
Default timeout for socket operations (in seconds)
0.25
Instance Method Summary collapse
-
#client_list ⇒ Object
call-seq: client_list -> KATCP::Response.
-
#close ⇒ Object
Close socket if it exists and is not already closed.
-
#configure(*args) ⇒ Object
call-seq: configure(*args) -> KATCP::Response.
-
#connect ⇒ Object
Connect socket and start listener thread.
-
#connected? ⇒ Boolean
Returns true if socket has been created and not closed.
-
#halt ⇒ Object
call-seq: halt -> KATCP::Response.
-
#help(*args) ⇒ Object
call-seq: help -> KATCP::Response help(name) -> KATCP::Response.
-
#host ⇒ Object
Return remote hostname.
-
#informs(clear = false) ⇒ Object
call-seq: inform(clear=false) -> Array.
-
#initialize(*args) ⇒ Client
constructor
call-seq: Client.new([remote_host, remote_port=7147, local_host=nil, local_port=nil,] opts={}) -> Client.
-
#inspect ⇒ Object
Provides more detailed String representation of
self. -
#log_level(*args) ⇒ Object
call-seq: log_level -> KATCP::Response log_level(priority) -> KATCP::Response.
-
#method_missing(sym, *args) ⇒ Object
Translates calls to missing methods into KATCP requests.
-
#mode(*args) ⇒ Object
call-seq: mode -> KATCP::Response mode(new_mode) -> KATCP::Response.
-
#port ⇒ Object
Return remote port.
-
#request(name, *arguments) ⇒ Object
call-seq: request(name, *arguments) -> KATCP::Response.
-
#restart ⇒ Object
call-seq: restart -> KATCP::Response.
-
#sensor_dump(*args) ⇒ Object
call-seq: sensor_dump(*args) -> KATCP::Response.
-
#sensor_list(*args) ⇒ Object
call-seq: sensor_list(*args) -> KATCP::Response.
-
#sensor_sampling(sensor, *args) ⇒ Object
call-seq: sensor_sampling(sensor) -> KATCP::Response sensor_sampling(sensor, strategy, *parameters) -> KATCP::Response.
-
#sensor_value(sensor) ⇒ Object
call-seq: sensor_value(sensor) -> KATCP::Response.
-
#to_s ⇒ Object
Provides terse string representation of
self. -
#watchdog ⇒ Object
(also: #ping)
call-seq: watchdog -> KATCP::Response.
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_list ⇒ Object
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 |
#close ⇒ Object
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 |
#connect ⇒ Object
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
206 207 208 |
# File 'lib/katcp/client.rb', line 206 def connected? !@socket.nil? && !@socket.closed? end |
#halt ⇒ Object
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 |
#host ⇒ Object
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 |
#inspect ⇒ Object
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 |
#port ⇒ Object
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 |
#restart ⇒ Object
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_s ⇒ Object
Provides terse string representation of self.
309 310 311 |
# File 'lib/katcp/client.rb', line 309 def to_s "#{@remote_host}:#{@remote_port}" end |
#watchdog ⇒ Object 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 |