Class: MaxCube::Network::TCP::Client

Inherits:
Object
  • Object
show all
Includes:
Commands
Defined in:
lib/maxcube/network/tcp/client.rb,
lib/maxcube/network/tcp/client/commands.rb

Overview

Fundamental class that provides TCP communication with Cube gateway and connected devices. After connecting to Cube (#connect), interactive shell is launched.

Communication with Cube is performed via messages, whereas client works with hashes, which have particular message contents divided and is human readable. An issue is how to pass contents of hashes as arguments of message serialization. For simple hashes client provides and option to pass arguments explicitly on command line. This would be difficult to accomplish for large hashes with subhashes, so YAML files are used in these cases, which are able to be generated both automatically and manually. This file has to be loaded into internal hash before each such message.

Client interactive shell contains quite detailed usage message.

Defined Under Namespace

Modules: Commands

Constant Summary

DEFAULT_VERBOSE =

Default verbose mode on startup.

true
DEFAULT_PERSIST =

Default persist mode on startup.

true
ARGS_FROM_HASH =

Command line token that enables loading arguments (hash) from file.

'-'.freeze

Constants included from Commands

Commands::COMMANDS

Instance Method Summary collapse

Methods included from Commands

#assign_hash, #cmd_clear, #cmd_config, #cmd_data, #cmd_delete, #cmd_dump, #cmd_history, #cmd_list, #cmd_load, #cmd_metadata, #cmd_ntp, #cmd_pair, #cmd_persist, #cmd_quit, #cmd_reset, #cmd_save, #cmd_send, #cmd_url, #cmd_usage, #cmd_verbose, #cmd_wake, #list_hashes, #load_hash, #parse_hash, #toggle, #usage_cmd

Constructor Details

#initialize(verbose: DEFAULT_VERBOSE, persist: DEFAULT_PERSIST) ⇒ Client

Creates all necessary internal variables. Internal hash is invalid on startup.

Parameters:

  • verbose (Boolean)

    verbose mode on startup.

  • persist (Boolean)

    persist mode on startup.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/maxcube/network/tcp/client.rb', line 37

def initialize(verbose: DEFAULT_VERBOSE, persist: DEFAULT_PERSIST)
  @parser = Messages::TCP::Parser.new
  @serializer = Messages::TCP::Serializer.new
  @queue = Queue.new

  @buffer = { recv: { hashes: [], data: [] },
              sent: { hashes: [], data: [] } }
  @history = { recv: { hashes: [], data: [] },
               sent: { hashes: [], data: [] } }

  @hash = nil
  @hash_set = false

  @data_dir = Pathname.new(MaxCube.data_dir)
  @load_data_dir = @data_dir + 'load'
  @save_data_dir = @data_dir + 'save'

  @verbose = verbose
  @persist = persist
end

Instance Method Details

#args_from_hash?(args) ⇒ Boolean (private)

Returns whether to enable loading arguments (hash) from file.

Parameters:

  • args (Array<String>)

    arguments from command line.

Returns:

  • (Boolean)

    whether to enable loading arguments (hash) from file.



218
219
220
# File 'lib/maxcube/network/tcp/client.rb', line 218

def args_from_hash?(args)
  args.first == ARGS_FROM_HASH
end

#buffer(dir_key, data_key, history = false) ⇒ Array<Hash>, String (private)

Returns only current or all (without or with history) collected part of buffer and history (contents of buffer is moved to history on clear command).

Parameters:

  • dir_key (:recv, :sent)

    received or sent data.

  • data_key (:hashes, :data)

    hashes or raw data (set of messages).

  • history (Boolean) (defaults to: false)

    whether to include history.

Returns:

  • (Array<Hash>, String)

    demanded data.



146
147
148
149
# File 'lib/maxcube/network/tcp/client.rb', line 146

def buffer(dir_key, data_key, history = false)
  return @buffer[dir_key][data_key] unless history
  @history[dir_key][data_key] + @buffer[dir_key][data_key]
end

#closeObject

Closes client gracefully.



119
120
121
122
123
124
# File 'lib/maxcube/network/tcp/client.rb', line 119

def close
  STDIN.close
  send_msg('q')
  @socket.close
  @thread.join
end

#command(line) ⇒ Object (private)

Executes command from shell command line. It calls a method dynamically according to MaxCube::Network::TCP::Client::Commands::COMMANDS, or displays usage message MaxCube::Network::TCP::Client::Commands#cmd_usage.

Parameters:

  • line (String)

    command line from STDIN



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/maxcube/network/tcp/client.rb', line 155

def command(line)
  cmd, *args = line.chomp.split
  return nil unless cmd

  return send("cmd_#{cmd}", *args) if COMMANDS.key?(cmd)

  keys = COMMANDS.find { |_, v| v.include?(cmd) }
  return send("cmd_#{keys.first}", *args) if keys

  puts "Unrecognized command: '#{cmd}'"
  cmd_usage
rescue ArgumentError
  puts "Invalid arguments: #{args}"
  cmd_usage
end

#connect(host = LOCALHOST, port = PORT) ⇒ Object

Connects to concrete address and starts interactive shell (#shell). Calls #receiver in separate thread to receive all incoming messages.

Parameters:

  • host (defaults to: LOCALHOST)

    remote host address.

  • port (defaults to: PORT)

    remote host port.



62
63
64
65
66
# File 'lib/maxcube/network/tcp/client.rb', line 62

def connect(host = LOCALHOST, port = PORT)
  @socket = TCPSocket.new(host, port)
  @thread = Thread.new(self, &:receiver)
  shell
end

Prints hash in human readable way.

Parameters:

  • hash (Hash)

    input hash.



282
283
284
# File 'lib/maxcube/network/tcp/client.rb', line 282

def print_hash(hash)
  puts hash.to_yaml
end

#receiverObject

Routine started in separate thread that receives and parses all incoming messages in loop and stores them info thread-safe queue. Parsing is done via Messages::TCP::Parser#parse_tcp_msg. It should close gracefully on any IOError or on shell's initiative.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/maxcube/network/tcp/client.rb', line 75

def receiver
  puts '<Starting receiver thread ...>'
  while (data = @socket.gets)
    hashes = @parser.parse_tcp_data(data)
    if @verbose
      hashes.each { |h| print_hash(h) }
      puts
    end
    @queue << [data, hashes]
  end
  raise IOError
rescue IOError
  STDIN.close
  puts '<Closing receiver thread ...>'
rescue Messages::InvalidMessage => e
  puts e.to_s.capitalize
end

#refresh_bufferObject (private)

Moves contents of receiver's queue to internal buffer. Queue is being filled from #receiver. Operation is thread-safe.



131
132
133
134
135
136
137
# File 'lib/maxcube/network/tcp/client.rb', line 131

def refresh_buffer
  until @queue.empty?
    data, hashes = @queue.pop
    @buffer[:recv][:data] << data
    @buffer[:recv][:hashes] << hashes
  end
end

#send_msg(type, *args, **opts) ⇒ Object (private)

Performs message serialization and sends it to Cube. It builds the hash to serialize from by #send_msg_hash, and serializes it with Messages::TCP::Serializer#serialize_tcp_hash.

Both sent message and built hash are buffered.

It catches all Messages::InvalidMessage exceptions.

Parameters:

  • type (String)

    message type.

  • args (Array<String>)

    arguments from command line.

  • opts (Hash)

    options that modifies interpreting of args.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/maxcube/network/tcp/client.rb', line 258

def send_msg(type, *args, **opts)
  hash = send_msg_hash(type, *args, **opts)
  return unless hash

  if hash.key?(:type)
    unless type == hash[:type]
      puts "\nInternal hash message type mismatch: '#{hash[:type]}'" \
           " (should be '#{type}')"
      return
    end
  else
    hash[:type] = type
  end
  msg = @serializer.serialize_tcp_hash(hash)

  @buffer[:sent][:data] << msg
  @buffer[:sent][:hashes] << [hash]
  @socket.write(msg)
rescue Messages::InvalidMessage => e
  puts e.to_s.capitalize
end

#send_msg_hash(type, *args, **opts) ⇒ Hash (private)

Returns hash with contents necessary for serialization of message of given message type. It is either built from command line args (#send_msg_hash_from_keys_args), or loaded from YAML file (#send_msg_hash_from_internal).

Parameters:

  • type (String)

    message type.

  • args (Array<String>)

    arguments from command line.

  • opts (Hash)

    options that modifies interpreting of args.

Options Hash (**opts):

  • :load_only (Boolean)

    means that hash must be loaded from file (contents are too complex). Specifying ARGS_FROM_HASH is optional in this case.

Returns:

  • (Hash)

    resulting hash.



234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/maxcube/network/tcp/client.rb', line 234

def send_msg_hash(type, *args, **opts)
  if opts[:load_only] && !args_from_hash?(args)
    args.unshift(ARGS_FROM_HASH)
  end
  return {} if args.empty?

  if args_from_hash?(args)
    return send_msg_hash_from_internal(*args, **opts)
  end

  send_msg_hash_from_keys_args(type, *args, **opts)
end

#send_msg_hash_from_internal(*args, **_opts) ⇒ Hash? (private)

Returns hash via MaxCube::Network::TCP::Client::Commands#cmd_load. It is used to combine sending a message with loading a hash from file. On success and in non-persistive mode, it simultaneously invalidates internal hash flag.

Parameters:

  • args (Array<String>)

    arguments from command line.

Returns:

  • (Hash, nil)

    loaded hash, or nil on failure.



206
207
208
209
210
# File 'lib/maxcube/network/tcp/client.rb', line 206

def send_msg_hash_from_internal(*args, **_opts)
  return nil unless cmd_load(*args.drop(1))
  @hash_set = false unless @persist
  @hash
end

#send_msg_hash_from_keys_args(type, *args, **opts) ⇒ Hash? (private)

Zips args with appropriate keys according to Messages::Handler#msg_type_hash_keys and Messages::Handler#msg_type_hash_opt_keys.

Parameters:

  • type (String)

    message type.

  • args (Array<String>)

    arguments from command line.

  • opts (Hash)

    options that modifies interpreting of args.

Options Hash (**opts):

  • :last_array (Boolean)

    whether to insert all rest arguments into array that will be stored into the last key.

  • :array_nonempty (Boolean)

    whether to require last_array not to be empty.

Returns:

  • (Hash, nil)

    resulting hash, or nil on failure.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/maxcube/network/tcp/client.rb', line 184

def send_msg_hash_from_keys_args(type, *args, **opts)
  keys = @serializer.msg_type_hash_keys(type) +
         @serializer.msg_type_hash_opt_keys(type)
  if opts[:last_array]
    hash_args = args.first(keys.size - 1)
    ary_args = args.drop(keys.size - 1)
    ary_args = nil if opts[:array_nonempty] && ary_args.empty?
    args = hash_args << ary_args
  end
  if keys.size < args.size
    return puts 'Additional arguments: ' \
                "#{args.last(args.size - keys.size)}"
  end
  keys.zip(args).to_h.reject { |_, v| v.nil? }
end

#shellObject

Interactive shell that maintains all operations with Cube. It is yet only simple STDIN parser without any command history and other features that possess all decent shells. It calls #command on every input. It provides quite detailed usage message (MaxCube::Network::TCP::Client::Commands#cmd_usage).

It should close gracefully from user's will, when connection closes, or when soft interrupt appears. Calls #close when closing.



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/maxcube/network/tcp/client.rb', line 104

def shell
  puts "Welcome to interactive shell!\n" \
       "Type 'help' for list of commands.\n\n"
  STDIN.each do |line|
    refresh_buffer
    command(line)
    puts
  end
  raise Interrupt
rescue IOError, Interrupt
  puts "\nClosing shell ..."
  close
end