maxcube-client 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +32 -0
- data/Gemfile +5 -0
- data/LICENSE.md +21 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/bin/maxcube-client +31 -0
- data/bin/sample_server +13 -0
- data/bin/sample_socket +13 -0
- data/bin/setup +6 -0
- data/data/load/del +6 -0
- data/data/load/meta +20 -0
- data/data/load/ntp +6 -0
- data/data/load/set_temp +13 -0
- data/data/load/set_temp_mode +12 -0
- data/data/load/set_valve +11 -0
- data/data/load/url +4 -0
- data/data/load/wake +4 -0
- data/lib/maxcube/messages.rb +148 -0
- data/lib/maxcube/messages/handler.rb +154 -0
- data/lib/maxcube/messages/parser.rb +34 -0
- data/lib/maxcube/messages/serializer.rb +59 -0
- data/lib/maxcube/messages/tcp.rb +18 -0
- data/lib/maxcube/messages/tcp/handler.rb +70 -0
- data/lib/maxcube/messages/tcp/parser.rb +46 -0
- data/lib/maxcube/messages/tcp/serializer.rb +47 -0
- data/lib/maxcube/messages/tcp/type/a.rb +32 -0
- data/lib/maxcube/messages/tcp/type/c.rb +248 -0
- data/lib/maxcube/messages/tcp/type/f.rb +33 -0
- data/lib/maxcube/messages/tcp/type/h.rb +70 -0
- data/lib/maxcube/messages/tcp/type/l.rb +131 -0
- data/lib/maxcube/messages/tcp/type/m.rb +185 -0
- data/lib/maxcube/messages/tcp/type/n.rb +44 -0
- data/lib/maxcube/messages/tcp/type/q.rb +18 -0
- data/lib/maxcube/messages/tcp/type/s.rb +246 -0
- data/lib/maxcube/messages/tcp/type/t.rb +38 -0
- data/lib/maxcube/messages/tcp/type/u.rb +19 -0
- data/lib/maxcube/messages/tcp/type/z.rb +36 -0
- data/lib/maxcube/messages/udp.rb +9 -0
- data/lib/maxcube/messages/udp/handler.rb +40 -0
- data/lib/maxcube/messages/udp/parser.rb +50 -0
- data/lib/maxcube/messages/udp/serializer.rb +30 -0
- data/lib/maxcube/messages/udp/type/h.rb +24 -0
- data/lib/maxcube/messages/udp/type/i.rb +23 -0
- data/lib/maxcube/messages/udp/type/n.rb +21 -0
- data/lib/maxcube/network.rb +14 -0
- data/lib/maxcube/network/tcp.rb +11 -0
- data/lib/maxcube/network/tcp/client.rb +174 -0
- data/lib/maxcube/network/tcp/client/commands.rb +286 -0
- data/lib/maxcube/network/tcp/sample_server.rb +96 -0
- data/lib/maxcube/network/udp.rb +11 -0
- data/lib/maxcube/network/udp/client.rb +52 -0
- data/lib/maxcube/network/udp/sample_socket.rb +65 -0
- data/lib/maxcube/version.rb +4 -0
- data/maxcube-client.gemspec +29 -0
- metadata +155 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'handler'
|
2
|
+
require 'maxcube/messages/serializer'
|
3
|
+
|
4
|
+
module MaxCube
|
5
|
+
module Messages
|
6
|
+
module UDP
|
7
|
+
class Serializer
|
8
|
+
include Handler
|
9
|
+
include Messages::Serializer
|
10
|
+
|
11
|
+
MSG_TYPES = %w[I N h c R].freeze
|
12
|
+
|
13
|
+
MSG_PREFIX = (UDP::MSG_PREFIX + "*\x00").freeze
|
14
|
+
|
15
|
+
def serialize_udp_hash(hash)
|
16
|
+
check_udp_hash(hash)
|
17
|
+
serial_number = hash[:serial_number] || '*' * 10
|
18
|
+
msg = MSG_PREFIX + serial_number << @msg_type
|
19
|
+
check_udp_msg(msg)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def msg_msg_type(msg)
|
25
|
+
msg[18]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module UDP
|
5
|
+
class Parser
|
6
|
+
module MessageH
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = (Parser::KEYS + %i[port url path]).freeze
|
10
|
+
|
11
|
+
def parse_udp_h(_body)
|
12
|
+
port = read(2, true)
|
13
|
+
url, path = read.split(',')
|
14
|
+
{
|
15
|
+
port: port,
|
16
|
+
url: url,
|
17
|
+
path: path,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module UDP
|
5
|
+
class Parser
|
6
|
+
module MessageI
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = (Parser::KEYS + %i[unknown
|
10
|
+
rf_address firmware_version]).freeze
|
11
|
+
|
12
|
+
def parse_udp_i(_body)
|
13
|
+
{
|
14
|
+
unknown: read(1),
|
15
|
+
rf_address: read(3, true),
|
16
|
+
firmware_version: read(2, 'H*'),
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module UDP
|
5
|
+
class Parser
|
6
|
+
module MessageN
|
7
|
+
private
|
8
|
+
|
9
|
+
N_KEYS = %i[ip_address gateway subnet_mask dns1 dns2].freeze
|
10
|
+
KEYS = (Parser::KEYS + N_KEYS).freeze
|
11
|
+
|
12
|
+
def parse_udp_n(_body)
|
13
|
+
N_KEYS.map do |k|
|
14
|
+
[k, IPAddr.ntop(read(4))]
|
15
|
+
end.to_h
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'maxcube/network/tcp'
|
2
|
+
require_relative 'client/commands'
|
3
|
+
|
4
|
+
module MaxCube
|
5
|
+
module Network
|
6
|
+
module TCP
|
7
|
+
class Client
|
8
|
+
def initialize
|
9
|
+
@parser = Messages::TCP::Parser.new
|
10
|
+
@serializer = Messages::TCP::Serializer.new
|
11
|
+
@queue = Queue.new
|
12
|
+
|
13
|
+
@buffer = { recv: { hashes: [], data: [] },
|
14
|
+
sent: { hashes: [], data: [] } }
|
15
|
+
@history = { recv: { hashes: [], data: [] },
|
16
|
+
sent: { hashes: [], data: [] } }
|
17
|
+
|
18
|
+
@hash = nil
|
19
|
+
@hash_set = false
|
20
|
+
|
21
|
+
@data_dir = Pathname.new('../data')
|
22
|
+
@load_data_dir = @data_dir + 'load'
|
23
|
+
@save_data_dir = @data_dir + 'save'
|
24
|
+
|
25
|
+
@verbose = true
|
26
|
+
@persist = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def connect(host = LOCALHOST, port = PORT)
|
30
|
+
@socket = TCPSocket.new(host, port)
|
31
|
+
@thread = Thread.new(self, &:receiver)
|
32
|
+
shell
|
33
|
+
end
|
34
|
+
|
35
|
+
def receiver
|
36
|
+
puts '<Starting receiver thread ...>'
|
37
|
+
while (data = @socket.gets)
|
38
|
+
hashes = @parser.parse_tcp_data(data)
|
39
|
+
if @verbose
|
40
|
+
hashes.each { |h| print_hash(h) }
|
41
|
+
puts
|
42
|
+
end
|
43
|
+
@queue << [data, hashes]
|
44
|
+
end
|
45
|
+
raise IOError
|
46
|
+
rescue IOError
|
47
|
+
STDIN.close
|
48
|
+
puts '<Closing receiver thread ...>'
|
49
|
+
rescue Messages::InvalidMessage => e
|
50
|
+
puts e.to_s.capitalize
|
51
|
+
end
|
52
|
+
|
53
|
+
def shell
|
54
|
+
puts "Welcome to interactive shell!\n" \
|
55
|
+
"Type 'help' for list of commands.\n\n"
|
56
|
+
STDIN.each do |line|
|
57
|
+
refresh_buffer
|
58
|
+
command(line)
|
59
|
+
puts
|
60
|
+
end
|
61
|
+
raise Interrupt
|
62
|
+
rescue IOError, Interrupt
|
63
|
+
puts "\nClosing shell ..."
|
64
|
+
close
|
65
|
+
end
|
66
|
+
|
67
|
+
def close
|
68
|
+
STDIN.close
|
69
|
+
send_msg('q')
|
70
|
+
@socket.close
|
71
|
+
@thread.join
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def refresh_buffer
|
77
|
+
until @queue.empty?
|
78
|
+
data, hashes = @queue.pop
|
79
|
+
@buffer[:recv][:data] << data
|
80
|
+
@buffer[:recv][:hashes] << hashes
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def buffer(dir_key, data_key, history = false)
|
85
|
+
return @buffer[dir_key][data_key] unless history
|
86
|
+
@history[dir_key][data_key] + @buffer[dir_key][data_key]
|
87
|
+
end
|
88
|
+
|
89
|
+
def command(line)
|
90
|
+
cmd, *args = line.chomp.split
|
91
|
+
return nil unless cmd
|
92
|
+
|
93
|
+
return send("cmd_#{cmd}", *args) if COMMANDS.key?(cmd)
|
94
|
+
|
95
|
+
keys = COMMANDS.find { |_, v| v.include?(cmd) }
|
96
|
+
return send("cmd_#{keys.first}", *args) if keys
|
97
|
+
|
98
|
+
puts "Unrecognized command: '#{cmd}'"
|
99
|
+
cmd_usage
|
100
|
+
rescue ArgumentError
|
101
|
+
puts "Invalid arguments: #{args}"
|
102
|
+
cmd_usage
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_msg_hash_from_keys_args(type, *args, **opts)
|
106
|
+
keys = @serializer.msg_type_hash_keys(type) +
|
107
|
+
@serializer.msg_type_hash_opt_keys(type)
|
108
|
+
if opts[:last_array]
|
109
|
+
hash_args = args.first(keys.size - 1)
|
110
|
+
ary_args = args.drop(keys.size - 1)
|
111
|
+
ary_args = nil if opts[:array_nonempty] && ary_args.empty?
|
112
|
+
args = hash_args << ary_args
|
113
|
+
end
|
114
|
+
if keys.size < args.size
|
115
|
+
return puts 'Additional arguments: ' \
|
116
|
+
"#{args.last(args.size - keys.size)}"
|
117
|
+
end
|
118
|
+
keys.zip(args).to_h.reject { |_, v| v.nil? }
|
119
|
+
end
|
120
|
+
|
121
|
+
def send_msg_hash_from_internal(*args, **_opts)
|
122
|
+
return nil unless cmd_load(*args.drop(1))
|
123
|
+
@hash_set = false unless @persist
|
124
|
+
@hash
|
125
|
+
end
|
126
|
+
|
127
|
+
ARGS_FROM_HASH = '-'.freeze
|
128
|
+
|
129
|
+
def args_from_hash?(args)
|
130
|
+
args.first == ARGS_FROM_HASH
|
131
|
+
end
|
132
|
+
|
133
|
+
def send_msg_hash(type, *args, **opts)
|
134
|
+
if opts[:load_only] && !args_from_hash?(args)
|
135
|
+
args.unshift(ARGS_FROM_HASH)
|
136
|
+
end
|
137
|
+
return {} if args.empty?
|
138
|
+
|
139
|
+
if args_from_hash?(args)
|
140
|
+
return send_msg_hash_from_internal(*args, **opts)
|
141
|
+
end
|
142
|
+
|
143
|
+
send_msg_hash_from_keys_args(type, *args, **opts)
|
144
|
+
end
|
145
|
+
|
146
|
+
def send_msg(type, *args, **opts)
|
147
|
+
hash = send_msg_hash(type, *args, **opts)
|
148
|
+
return unless hash
|
149
|
+
|
150
|
+
if hash.key?(:type)
|
151
|
+
unless type == hash[:type]
|
152
|
+
puts "\nInternal hash message type mismatch: '#{hash[:type]}'" \
|
153
|
+
" (should be '#{type}')"
|
154
|
+
return
|
155
|
+
end
|
156
|
+
else
|
157
|
+
hash[:type] = type
|
158
|
+
end
|
159
|
+
msg = @serializer.serialize_tcp_hash(hash)
|
160
|
+
|
161
|
+
@buffer[:sent][:data] << msg
|
162
|
+
@buffer[:sent][:hashes] << [hash]
|
163
|
+
@socket.write(msg)
|
164
|
+
rescue Messages::InvalidMessage => e
|
165
|
+
puts e.to_s.capitalize
|
166
|
+
end
|
167
|
+
|
168
|
+
def print_hash(hash)
|
169
|
+
puts hash.to_yaml
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Network
|
4
|
+
module TCP
|
5
|
+
class Client
|
6
|
+
private
|
7
|
+
|
8
|
+
COMMANDS = {
|
9
|
+
'usage' => %w[? h help],
|
10
|
+
'data' => %w[B buffer d],
|
11
|
+
'history' => %w[H hist],
|
12
|
+
'clear' => %w[C],
|
13
|
+
'dump' => %w[D],
|
14
|
+
'list' => %w[l],
|
15
|
+
'config' => %w[c],
|
16
|
+
'send' => %w[cmd s set],
|
17
|
+
'pair' => %w[n],
|
18
|
+
'ntp' => %w[N f],
|
19
|
+
'url' => %w[U u],
|
20
|
+
'wake' => %w[w z],
|
21
|
+
'metadata' => %w[m meta],
|
22
|
+
'delete' => %w[del],
|
23
|
+
'reset' => %w[],
|
24
|
+
'verbose' => %w[V],
|
25
|
+
'save' => %w[S],
|
26
|
+
'load' => %w[L],
|
27
|
+
'persist' => %w[P],
|
28
|
+
'quit' => %w[q],
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def usage_line(command, args, description,
|
32
|
+
message = nil, response = nil)
|
33
|
+
cmds_str = (COMMANDS[command].dup << command).join('|')
|
34
|
+
cmds_str << ' ' << args unless args.empty?
|
35
|
+
|
36
|
+
description, *rest = description.split("\n")
|
37
|
+
rest << "[#{message} message]" if message
|
38
|
+
rest << "[#{response} response]" if response
|
39
|
+
rest = if rest.empty?
|
40
|
+
''
|
41
|
+
else
|
42
|
+
rest.map { |s| ' ' * 52 + s }.join("\n") << "\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
' ' << cmds_str << ' ' * (48 - cmds_str.size) <<
|
46
|
+
description << "\n" << rest
|
47
|
+
end
|
48
|
+
|
49
|
+
def cmd_usage
|
50
|
+
puts "\nUSAGE: <command> [<arguments...>]\nCOMMADS:\n" <<
|
51
|
+
usage_line('usage', '',
|
52
|
+
'Prints this message') <<
|
53
|
+
usage_line('data', '',
|
54
|
+
'Lists buffered received data (hashes)') <<
|
55
|
+
usage_line('history', '',
|
56
|
+
'Lists all received data incl. the cleared') <<
|
57
|
+
usage_line('clear', '',
|
58
|
+
"Clears collected data\n" \
|
59
|
+
'(resp. moves it to history)') <<
|
60
|
+
usage_line('dump', '',
|
61
|
+
"Shortcut for 'data' + 'clear'") <<
|
62
|
+
usage_line('list', '',
|
63
|
+
'Requests for new list of devices', 'l', 'L') <<
|
64
|
+
usage_line('config', '',
|
65
|
+
'Requests for configuration message', 'c', 'C') <<
|
66
|
+
usage_line('send', '{}',
|
67
|
+
'Sends settings to connected devices',
|
68
|
+
's', 'S') <<
|
69
|
+
usage_line('pair', '{<timeout>}',
|
70
|
+
'Sets device into pairing mode' \
|
71
|
+
" with optional timeout\n" \
|
72
|
+
'(request for a new device)', 'n', 'N') <<
|
73
|
+
usage_line('ntp', '{<NTP servers...>}',
|
74
|
+
'Requests for NTP servers' \
|
75
|
+
' and optionally updates them',
|
76
|
+
'f', 'F') <<
|
77
|
+
usage_line('url', '{<URL> <port>}',
|
78
|
+
'Configures Cube\'s portal URL', 'u') <<
|
79
|
+
usage_line('wake', '{<time> <scope> [<ID>]}',
|
80
|
+
'Wake-ups the Cube',
|
81
|
+
'z', 'A') <<
|
82
|
+
usage_line('metadata', '{}',
|
83
|
+
'Serializes metadata for the Cube',
|
84
|
+
'm', 'M') <<
|
85
|
+
usage_line('delete', '{<count> <force> <RF addresses...>}',
|
86
|
+
'Deletes one or more devices from the Cube (!)',
|
87
|
+
't', 'A') <<
|
88
|
+
usage_line('reset', '',
|
89
|
+
'Requests for factory reset (!)', 'a', 'A') <<
|
90
|
+
usage_line('verbose', '',
|
91
|
+
"Toggles verbose mode (whether is incoming data\n" \
|
92
|
+
'printed immediately or is not printed)') <<
|
93
|
+
usage_line('save', '[a|A|all]',
|
94
|
+
"Saves buffered [all] received and sent data\n" \
|
95
|
+
"into files at '#{@save_data_dir}'") <<
|
96
|
+
usage_line('load', '[<path>]',
|
97
|
+
'Loads first hash from YAML file' \
|
98
|
+
" to internal variable\n" \
|
99
|
+
"-> to pass data with outgoing message\n" \
|
100
|
+
'If path is relative,' \
|
101
|
+
" it looks in '#{@load_data_dir}'\n" \
|
102
|
+
"(loads previous valid hash if no file given)\n" \
|
103
|
+
'(command can be combined' \
|
104
|
+
" using '#{ARGS_FROM_HASH}'\n" \
|
105
|
+
" with other commands which have '{}' arguments)") <<
|
106
|
+
usage_line('persist', '',
|
107
|
+
'Toggles persistent mode' \
|
108
|
+
"(whether is internal hash\n" \
|
109
|
+
'not invalidated after use)') <<
|
110
|
+
usage_line('quit', '',
|
111
|
+
"Shuts the client down gracefully\n" \
|
112
|
+
'(SIGINT and EOF also work)', 'q') <<
|
113
|
+
"\n[<arg>] means optional argument <arg>" \
|
114
|
+
"\n[<args...>] means multiple arguments <args...> or none" \
|
115
|
+
"\n (<args...> requires at least one)" \
|
116
|
+
"\n{<arg>} means that either <arg>" \
|
117
|
+
" or '#{ARGS_FROM_HASH}' is expected" \
|
118
|
+
"\n (when '#{ARGS_FROM_HASH}' specified as first argument," \
|
119
|
+
' internal hash is used' \
|
120
|
+
"\n -> 'load' command is called with rest arguments)" \
|
121
|
+
"\n ({} means that only internal hash can be used," \
|
122
|
+
"\n '#{ARGS_FROM_HASH}' is not necessary in this case)"
|
123
|
+
end
|
124
|
+
|
125
|
+
def list_hashes(history)
|
126
|
+
buffer(:recv, :hashes, history).each_with_index do |h, i|
|
127
|
+
puts "<#{i + 1}>"
|
128
|
+
print_hash(h)
|
129
|
+
puts
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def cmd_data
|
134
|
+
list_hashes(false)
|
135
|
+
end
|
136
|
+
|
137
|
+
def cmd_history
|
138
|
+
list_hashes(true)
|
139
|
+
end
|
140
|
+
|
141
|
+
def cmd_clear
|
142
|
+
%i[data hashes].each do |sym|
|
143
|
+
@history[:recv][sym] += @buffer[:recv][sym]
|
144
|
+
@buffer[:recv][sym].clear
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def cmd_dump
|
149
|
+
cmd_data
|
150
|
+
cmd_clear
|
151
|
+
end
|
152
|
+
|
153
|
+
def cmd_list
|
154
|
+
send_msg('l')
|
155
|
+
end
|
156
|
+
|
157
|
+
def cmd_config
|
158
|
+
send_msg('c')
|
159
|
+
end
|
160
|
+
|
161
|
+
def cmd_send(*args)
|
162
|
+
send_msg('s', *args, load_only: true)
|
163
|
+
end
|
164
|
+
|
165
|
+
def cmd_pair(*args)
|
166
|
+
send_msg('n', *args)
|
167
|
+
end
|
168
|
+
|
169
|
+
def cmd_url(*args)
|
170
|
+
send_msg('u', *args)
|
171
|
+
end
|
172
|
+
|
173
|
+
def cmd_ntp(*args)
|
174
|
+
send_msg('f', *args, last_array: true)
|
175
|
+
end
|
176
|
+
|
177
|
+
def cmd_wake(*args)
|
178
|
+
send_msg('z', *args)
|
179
|
+
end
|
180
|
+
|
181
|
+
def cmd_metadata(*args)
|
182
|
+
send_msg('m', *args, load_only: true)
|
183
|
+
end
|
184
|
+
|
185
|
+
def cmd_delete(*args)
|
186
|
+
send_msg('t', *args, last_array: true, array_nonempty: true)
|
187
|
+
end
|
188
|
+
|
189
|
+
def cmd_reset
|
190
|
+
send_msg('a')
|
191
|
+
end
|
192
|
+
|
193
|
+
def toggle(name, flag)
|
194
|
+
puts "#{name}: #{flag} -> #{!flag}"
|
195
|
+
!flag
|
196
|
+
end
|
197
|
+
|
198
|
+
def cmd_verbose
|
199
|
+
@verbose = toggle('verbose', @verbose)
|
200
|
+
end
|
201
|
+
|
202
|
+
def cmd_save(what = nil)
|
203
|
+
buffer = !what
|
204
|
+
all = %w[a A all].include?(what)
|
205
|
+
unless all || buffer
|
206
|
+
puts "Unrecognized argument: '#{what}'"
|
207
|
+
return
|
208
|
+
end
|
209
|
+
|
210
|
+
dir = @save_data_dir + Time.now.strftime('%Y%m%d-%H%M')
|
211
|
+
dir.mkpath
|
212
|
+
|
213
|
+
%i[recv sent].each do |sym|
|
214
|
+
data_fn = dir + (sym.to_s << '.data')
|
215
|
+
File.open(data_fn, 'w') do |f|
|
216
|
+
f.puts(buffer(sym, :data, all).join)
|
217
|
+
end
|
218
|
+
|
219
|
+
hashes_fn = dir + (sym.to_s << '.yaml')
|
220
|
+
File.open(hashes_fn, 'w') do |f|
|
221
|
+
buffer(sym, :hashes, all).to_yaml(f)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
which = buffer ? 'Buffered' : 'All'
|
226
|
+
puts "#{which} received and sent raw data and hashes" \
|
227
|
+
" saved into '#{dir}'"
|
228
|
+
rescue SystemCallError => e
|
229
|
+
puts "Files could not been saved:\n#{e}"
|
230
|
+
end
|
231
|
+
|
232
|
+
def parse_hash(path)
|
233
|
+
unless path.file? && path.readable?
|
234
|
+
return puts "File is not readable: '#{path}'"
|
235
|
+
end
|
236
|
+
|
237
|
+
hash = YAML.load_file(path)
|
238
|
+
hash = hash.first while hash.is_a?(Array)
|
239
|
+
raise YAML::SyntaxError unless hash.is_a?(Hash)
|
240
|
+
hash
|
241
|
+
rescue YAML::SyntaxError => e
|
242
|
+
puts "File '#{path}' does not contain proper YAML hash", e
|
243
|
+
end
|
244
|
+
|
245
|
+
def load_hash(path = nil)
|
246
|
+
if path
|
247
|
+
path = Pathname.new(path)
|
248
|
+
path = @load_data_dir + path if path.relative?
|
249
|
+
return parse_hash(path)
|
250
|
+
end
|
251
|
+
return @hash if @hash && @hash_set
|
252
|
+
|
253
|
+
if @hash
|
254
|
+
puts 'Internal hash is not set'
|
255
|
+
else
|
256
|
+
puts 'No internal hash loaded yet'
|
257
|
+
cmd_usage
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def assign_hash(hash)
|
262
|
+
valid_hash = !hash.nil?
|
263
|
+
@hash = hash if valid_hash
|
264
|
+
@hash_set |= valid_hash
|
265
|
+
valid_hash
|
266
|
+
end
|
267
|
+
|
268
|
+
def cmd_load(path = nil)
|
269
|
+
hash = load_hash(path)
|
270
|
+
return false unless assign_hash(hash)
|
271
|
+
print_hash(hash)
|
272
|
+
true
|
273
|
+
end
|
274
|
+
|
275
|
+
def cmd_persist
|
276
|
+
@persist = toggle('persist', @persist)
|
277
|
+
@hash_set = @persist if @hash
|
278
|
+
end
|
279
|
+
|
280
|
+
def cmd_quit
|
281
|
+
raise Interrupt
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|