datalackeytools 0.3.4
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/LICENSE.txt +17 -0
- data/bin/datalackey-make +1014 -0
- data/bin/datalackey-run +234 -0
- data/bin/datalackey-shell +1505 -0
- data/bin/datalackey-state +1005 -0
- data/bin/files2object +56 -0
- data/bin/object2files +44 -0
- data/lib/common.rb +9 -0
- data/lib/datalackeylib.rb +638 -0
- metadata +68 -0
@@ -0,0 +1,1005 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Copyright © 2019-2021 Ismo Kärkkäinen
|
5
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
|
+
|
7
|
+
require 'optparse'
|
8
|
+
require 'yaml'
|
9
|
+
require 'tempfile'
|
10
|
+
require_relative '../lib/datalackeylib'
|
11
|
+
|
12
|
+
$CMDOUT = $stdout
|
13
|
+
$USEROUT = $stderr
|
14
|
+
$OVERWRITE_ACTION = nil
|
15
|
+
$FOLLOW = 0
|
16
|
+
$QUIET = false
|
17
|
+
# For the case when we run datalackey.
|
18
|
+
$LACKEY = nil
|
19
|
+
$MEMORY = nil
|
20
|
+
$DIRECTORY = nil
|
21
|
+
$PERMISSIONS = nil
|
22
|
+
$ECHO = false
|
23
|
+
|
24
|
+
$root_dir = Dir.pwd
|
25
|
+
|
26
|
+
parser = OptionParser.new do |opts|
|
27
|
+
opts.summary_indent = ' '
|
28
|
+
opts.summary_width = 26
|
29
|
+
opts.banner = 'Usage: datalackey-state [options] state-files...'
|
30
|
+
opts.separator ''
|
31
|
+
opts.separator 'Options:'
|
32
|
+
opts.on('-o', '--stdout', 'Use stdout as command output, stderr for user.') do
|
33
|
+
$CMDOUT = $stdout
|
34
|
+
$USEROUT = $stderr
|
35
|
+
end
|
36
|
+
opts.on('-e', '--stderr', 'Use stderr as command output, stdout for user.') do
|
37
|
+
$CMDOUT = $stderr
|
38
|
+
$USEROUT = $stdout
|
39
|
+
end
|
40
|
+
opts.on('-q', '--quiet', 'Suppress normal command output.') do
|
41
|
+
$QUIET = true
|
42
|
+
end
|
43
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
44
|
+
$USEROUT.puts opts
|
45
|
+
exit 0
|
46
|
+
end
|
47
|
+
opts.separator 'Options for error checking and tracking execution:'
|
48
|
+
opts.on('--warn', 'Print warning when state is overwritten.') do
|
49
|
+
$OVERWRITE_ACTION = :warn
|
50
|
+
end
|
51
|
+
opts.on('--error', 'State overwriting is an error.') do
|
52
|
+
$OVERWRITE_ACTION = :error
|
53
|
+
end
|
54
|
+
opts.on('-f', '--follow [LEVEL]', Integer, 'Print state/signal/command.') do |level|
|
55
|
+
$FOLLOW = level
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
DatalackeyProcess.options_for_OptionParser(parser, true,
|
60
|
+
proc { |arg| $LACKEY = arg },
|
61
|
+
proc { |arg| $MEMORY = arg },
|
62
|
+
proc { |arg| $DIRECTORY = arg },
|
63
|
+
proc { |arg| $PERMISSIONS = arg },
|
64
|
+
proc { |arg| $ECHO = arg })
|
65
|
+
parser.parse!
|
66
|
+
|
67
|
+
if ARGV.empty?
|
68
|
+
$USEROUT.puts "No state files."
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
|
72
|
+
$userout_mutex = Mutex.new
|
73
|
+
def userout(message)
|
74
|
+
return false if message.is_a?(Array) && message.empty?
|
75
|
+
message = [ message ] unless message.is_a? Array
|
76
|
+
$userout_mutex.synchronize do
|
77
|
+
message.each { |m| $USEROUT.puts m }
|
78
|
+
end
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
class Machine
|
83
|
+
attr_accessor :variables
|
84
|
+
attr_reader :had_error, :state, :state_name, :machine
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@commands = Hash.new(nil)
|
88
|
+
@variables = Hash.new(nil)
|
89
|
+
@skip_expand = { }
|
90
|
+
@signal_mutex = Mutex.new
|
91
|
+
@signal = nil
|
92
|
+
@forced_state = nil
|
93
|
+
@state_mutex = Mutex.new
|
94
|
+
@label2signal = { }
|
95
|
+
@state = nil
|
96
|
+
@state_name = nil
|
97
|
+
@machine = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def register(name, object, expand_skip_count = 0)
|
101
|
+
raise ArgumentError, 'Command name is not a string.' unless name.is_a? String
|
102
|
+
@commands[name] = object
|
103
|
+
@skip_expand[name] = expand_skip_count if expand_skip_count > 0
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_binding
|
107
|
+
binding
|
108
|
+
end
|
109
|
+
|
110
|
+
def expand(item, info, seen = [])
|
111
|
+
if item.is_a? Array
|
112
|
+
info.push '['
|
113
|
+
result = []
|
114
|
+
item.each { |v| result.push expand(v, info, seen) }
|
115
|
+
info.push ']'
|
116
|
+
return result
|
117
|
+
end
|
118
|
+
if item.is_a? Hash
|
119
|
+
info.push '{'
|
120
|
+
result = { }
|
121
|
+
item.each_pair { |k, v| result[k] = expand(v, info, seen) }
|
122
|
+
info.push '}'
|
123
|
+
return result
|
124
|
+
end
|
125
|
+
info.concat([ ',', item ]) unless info.last == item
|
126
|
+
if item.is_a?(String) && item.start_with?('$') && ENV.key?(item[1...item.size])
|
127
|
+
item = ENV[item[1...item.size]]
|
128
|
+
info.concat [ '=>', item ]
|
129
|
+
return expand(item, info, seen)
|
130
|
+
end
|
131
|
+
if @variables.key? item
|
132
|
+
s = seen.clone
|
133
|
+
s.push item
|
134
|
+
raise "Loop: #{item} via #{s.join(' => ')}." if seen.include? item
|
135
|
+
item = @variables[item]
|
136
|
+
info.concat [ '=>', item ]
|
137
|
+
return expand(item, info, s)
|
138
|
+
end
|
139
|
+
item
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_signal(sig, target_state = nil)
|
143
|
+
@signal_mutex.synchronize do
|
144
|
+
if @signal.nil? || sig == 'error'
|
145
|
+
userout "Signal: #{sig}" if $FOLLOW > 0
|
146
|
+
@signal = sig
|
147
|
+
@had_error = @signal == 'error'
|
148
|
+
@forced_state = target_state
|
149
|
+
return true
|
150
|
+
end
|
151
|
+
end
|
152
|
+
false
|
153
|
+
end
|
154
|
+
|
155
|
+
def set_error(message)
|
156
|
+
userout message
|
157
|
+
set_signal('error')
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_signal
|
161
|
+
@signal_mutex.synchronize { @signal }
|
162
|
+
end
|
163
|
+
|
164
|
+
def label2signal
|
165
|
+
@state_mutex.synchronize { @label2signal }
|
166
|
+
end
|
167
|
+
|
168
|
+
def state_source
|
169
|
+
@state.nil? ? nil : @state.fetch(:load, { }).fetch(:source, nil)
|
170
|
+
end
|
171
|
+
|
172
|
+
def run_state(state, name)
|
173
|
+
userout "State: #{name}" if $FOLLOW > 0
|
174
|
+
state[:commands].each do |cmd|
|
175
|
+
info = []
|
176
|
+
begin
|
177
|
+
s_cmd = cmd.to_s
|
178
|
+
userout "Command: #{s_cmd}" if $FOLLOW > 2
|
179
|
+
if cmd.is_a? Hash
|
180
|
+
expanded = expand(cmd, info)
|
181
|
+
else
|
182
|
+
skip = @skip_expand.fetch(cmd.first, 0)
|
183
|
+
skip = cmd.size if cmd.size < skip
|
184
|
+
expanded = cmd[0...skip]
|
185
|
+
rest = cmd[skip...cmd.size]
|
186
|
+
rest = [ rest ] unless rest.is_a? Array
|
187
|
+
expanded.concat expand(rest, info)
|
188
|
+
end
|
189
|
+
s_exp = expanded.to_s
|
190
|
+
userout "Expanded: #{s_exp}" if $FOLLOW > 3 && s_cmd != s_exp
|
191
|
+
rescue RuntimeError => e
|
192
|
+
set_error e.to_s
|
193
|
+
return
|
194
|
+
end
|
195
|
+
handler = nil
|
196
|
+
name = nil
|
197
|
+
if expanded.is_a? Hash
|
198
|
+
@commands.each_key do |key|
|
199
|
+
next unless cmd.key? key # Registered names ought to be unique.
|
200
|
+
name = key
|
201
|
+
break
|
202
|
+
end
|
203
|
+
if name.nil?
|
204
|
+
set_error('Match for dictionary keys not found.')
|
205
|
+
break
|
206
|
+
end
|
207
|
+
else
|
208
|
+
if expanded.is_a? String
|
209
|
+
name = expanded
|
210
|
+
elsif expanded.is_a? Array
|
211
|
+
name = expanded.first
|
212
|
+
else
|
213
|
+
set_error("Unexpected type for command: #{expanded.class.name}")
|
214
|
+
break
|
215
|
+
end
|
216
|
+
end
|
217
|
+
handler = @commands[name]
|
218
|
+
set_error("Unknown command: #{name}") if handler.nil?
|
219
|
+
break if handler.nil? || !get_signal.nil?
|
220
|
+
userout "Command: #{name}" if $FOLLOW == 2
|
221
|
+
handler.handle(expanded)
|
222
|
+
break unless get_signal.nil? # Prevents following output for next command.
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def run(initial_state_name, states)
|
227
|
+
@state_mutex.synchronize { @machine = states }
|
228
|
+
last_state_name = '__terminate_all'
|
229
|
+
finalized = false
|
230
|
+
name = initial_state_name
|
231
|
+
signal2state = { } # Only changes when state has new global mapping.
|
232
|
+
loop do
|
233
|
+
userout($lackey_stderr.getlines) unless $QUIET
|
234
|
+
finalized = true if name == last_state_name
|
235
|
+
unless states.key? name
|
236
|
+
userout "Transition to unknown state: #{name}"
|
237
|
+
break
|
238
|
+
end
|
239
|
+
state = states[name]
|
240
|
+
signal2state = state.fetch('global_signal2state', signal2state)
|
241
|
+
@state_mutex.synchronize do
|
242
|
+
@label2signal = state.fetch('label2signal', { })
|
243
|
+
@state = state # Command may need e.g. the location of the state file.
|
244
|
+
@state_name = name
|
245
|
+
end
|
246
|
+
run_state(state, name)
|
247
|
+
@state_mutex.synchronize do
|
248
|
+
@label2signal = { }
|
249
|
+
@state = nil
|
250
|
+
@state_name = nil
|
251
|
+
end
|
252
|
+
sig = nil
|
253
|
+
name = nil
|
254
|
+
@signal_mutex.synchronize do
|
255
|
+
sig = @signal
|
256
|
+
@signal = nil
|
257
|
+
name = @forced_state
|
258
|
+
@forced_state = nil
|
259
|
+
end
|
260
|
+
name = nil unless sig == '__jump' # Jump may have been "interrupted".
|
261
|
+
if name.nil?
|
262
|
+
name = state.fetch('signal2state', { }).fetch(sig, nil)
|
263
|
+
name = signal2state.fetch(sig, nil) if name.nil?
|
264
|
+
break if name.nil?
|
265
|
+
end
|
266
|
+
end
|
267
|
+
return unless !finalized && states.key?(last_state_name)
|
268
|
+
run_state(states[last_state_name], last_state_name)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
$machine = Machine.new
|
272
|
+
|
273
|
+
$signal_sender = proc do |action, message, vars|
|
274
|
+
case action.first
|
275
|
+
when :error
|
276
|
+
$machine.set_error(message.join(' '))
|
277
|
+
true
|
278
|
+
when :exitcode
|
279
|
+
$machine.set_error(message.join(' ')) if vars.first != 0
|
280
|
+
true
|
281
|
+
else false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
class Command
|
286
|
+
attr_reader :action
|
287
|
+
|
288
|
+
def initialize(name, expand_skip = 0)
|
289
|
+
$machine.register(name, self, expand_skip)
|
290
|
+
@name = name
|
291
|
+
end
|
292
|
+
|
293
|
+
def handle(cmd)
|
294
|
+
cmd.flatten!
|
295
|
+
userout(cmd.to_s) if $FOLLOW > 4
|
296
|
+
$lackey.send(@action, cmd)
|
297
|
+
end
|
298
|
+
|
299
|
+
def assert(cmd, minlen, maxlen = nil)
|
300
|
+
if cmd.size < minlen
|
301
|
+
$machine.set_error "Command #{@name} too short: #{cmd.size} < #{minlen}"
|
302
|
+
return false
|
303
|
+
end
|
304
|
+
return true if maxlen.nil?
|
305
|
+
if maxlen < cmd.size
|
306
|
+
$machine.set_error "Command #{@name} too long: #{maxlen} < #{cmd.size}"
|
307
|
+
return false
|
308
|
+
end
|
309
|
+
true
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class CommentCommand < Command
|
314
|
+
def initialize
|
315
|
+
super('comment', 1000000)
|
316
|
+
end
|
317
|
+
|
318
|
+
def handle(cmd)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
CommentCommand.new
|
322
|
+
|
323
|
+
class SetCommand < Command
|
324
|
+
def initialize
|
325
|
+
super('set', 2)
|
326
|
+
end
|
327
|
+
|
328
|
+
def handle(cmd)
|
329
|
+
return unless assert(cmd, 3)
|
330
|
+
$machine.variables[cmd[1]] = cmd.size == 3 ? cmd[2] : cmd[2...cmd.size]
|
331
|
+
end
|
332
|
+
end
|
333
|
+
SetCommand.new
|
334
|
+
|
335
|
+
class DefaultCommand < Command
|
336
|
+
def initialize
|
337
|
+
super('default', 2)
|
338
|
+
end
|
339
|
+
|
340
|
+
def handle(cmd)
|
341
|
+
return unless assert(cmd, 3)
|
342
|
+
v = cmd[1]
|
343
|
+
return if $machine.variables.key? v
|
344
|
+
$machine.variables[v] = cmd.size == 3 ? cmd[2] : cmd[2...cmd.size]
|
345
|
+
end
|
346
|
+
end
|
347
|
+
DefaultCommand.new
|
348
|
+
|
349
|
+
class UnsetCommand < Command
|
350
|
+
def initialize
|
351
|
+
super('unset', 1000000)
|
352
|
+
end
|
353
|
+
|
354
|
+
def handle(cmd)
|
355
|
+
cmd.flatten!
|
356
|
+
cmd.shift
|
357
|
+
cmd.each { |name| $machine.variables.delete name }
|
358
|
+
end
|
359
|
+
end
|
360
|
+
UnsetCommand.new
|
361
|
+
|
362
|
+
class AssertVarCommand < Command
|
363
|
+
def initialize
|
364
|
+
super('assert_var', 1000000)
|
365
|
+
end
|
366
|
+
|
367
|
+
def handle(cmd)
|
368
|
+
cmd.flatten!
|
369
|
+
cmd.shift
|
370
|
+
unset = []
|
371
|
+
cmd.each { |name| unset.push(name) unless $machine.variables.key? name }
|
372
|
+
$machine.set_error("Not set: #{unset.join(' ')}") unless unset.empty?
|
373
|
+
end
|
374
|
+
end
|
375
|
+
AssertVarCommand.new
|
376
|
+
|
377
|
+
class AssertNotVar < Command
|
378
|
+
def initialize
|
379
|
+
super('assert_notvar', 1000000)
|
380
|
+
end
|
381
|
+
|
382
|
+
def handle(cmd)
|
383
|
+
cmd.flatten!
|
384
|
+
cmd.shift
|
385
|
+
set = []
|
386
|
+
cmd.each { |name| set.push(name) if $machine.variables.key? name }
|
387
|
+
$machine.set_error("Set: #{set.join(' ')}") unless set.empty?
|
388
|
+
end
|
389
|
+
end
|
390
|
+
AssertNotVar.new
|
391
|
+
|
392
|
+
class AssertDataCommand < Command
|
393
|
+
def initialize
|
394
|
+
super('assert_data')
|
395
|
+
end
|
396
|
+
|
397
|
+
def handle(cmd)
|
398
|
+
cmd.flatten!
|
399
|
+
cmd.shift
|
400
|
+
data = $lackey.data
|
401
|
+
missing = []
|
402
|
+
cmd.each { |name| missing.push(name) unless data.key? name }
|
403
|
+
$machine.set_error("Missing: #{missing.join(' ')}") unless missing.empty?
|
404
|
+
end
|
405
|
+
end
|
406
|
+
AssertDataCommand.new
|
407
|
+
|
408
|
+
class PushCommand < Command
|
409
|
+
def initialize
|
410
|
+
super('push', 2)
|
411
|
+
end
|
412
|
+
|
413
|
+
def handle(cmd)
|
414
|
+
return unless assert(cmd, 3)
|
415
|
+
# Creates a stack if not present, or turns scalar into stack.
|
416
|
+
stack = $machine.variables.fetch(cmd[1], [])
|
417
|
+
stack = [ stack ] unless stack.is_a? Array
|
418
|
+
stack.push(3 < cmd.size ? cmd[2...cmd.size] : cmd[2])
|
419
|
+
$machine.variables[cmd[1]] = stack
|
420
|
+
end
|
421
|
+
end
|
422
|
+
PushCommand.new
|
423
|
+
|
424
|
+
class PopCommand < Command
|
425
|
+
def initialize
|
426
|
+
super('pop', 3)
|
427
|
+
end
|
428
|
+
|
429
|
+
def handle(cmd)
|
430
|
+
return unless assert(cmd, 3, 3)
|
431
|
+
unless $machine.variables.key? cmd[1]
|
432
|
+
$machine.set_error("Stack not found: #{cmd[1]}")
|
433
|
+
return
|
434
|
+
end
|
435
|
+
# Interprets scalas as stack. Empty array treated as stack.
|
436
|
+
stack = $machine.variables[cmd[1]]
|
437
|
+
stack = [ stack ] unless stack.is_a? Array
|
438
|
+
return $machine.set_error("Stack empty: #{cmd[1]}") if stack.empty?
|
439
|
+
$machine.variables[cmd[2]] = stack.pop
|
440
|
+
$machine.variables.delete(cmd[1]) if stack.empty?
|
441
|
+
end
|
442
|
+
end
|
443
|
+
PopCommand.new
|
444
|
+
|
445
|
+
class RunBase < Command
|
446
|
+
@@actions_run_common = {
|
447
|
+
error: {
|
448
|
+
syntax: [ 'run', 'error', '?', 'argument', 'unknown' ],
|
449
|
+
command_error: [ 'run', 'error', '*' ],
|
450
|
+
run_internal: [ [ 'run', 'error', 'exception' ],
|
451
|
+
[ 'run', 'error', 'no-memory' ],
|
452
|
+
[ 'run', 'error', 'no-processes' ],
|
453
|
+
[ 'run', 'error', 'no-thread' ],
|
454
|
+
[ 'run', 'error', 'pipe' ] ]
|
455
|
+
},
|
456
|
+
note: {
|
457
|
+
run_error_input_failed: [ 'run', 'error', 'input', 'failed' ],
|
458
|
+
run_child_error_output_format: [
|
459
|
+
[ 'run', 'error', 'format' ], [ 'error', 'format' ] ]
|
460
|
+
},
|
461
|
+
exitcode: [ 'run', 'exit', '?' ],
|
462
|
+
bytes: [ 'run', 'bytes', '?', '*' ]
|
463
|
+
}
|
464
|
+
@@prev_bytes_mutex = Mutex.new
|
465
|
+
@@prev_bytes_id = nil
|
466
|
+
|
467
|
+
def initialize(name)
|
468
|
+
@msgmaker = proc do |action, message, vars|
|
469
|
+
message_maker(action, message, vars)
|
470
|
+
end
|
471
|
+
super(name)
|
472
|
+
end
|
473
|
+
|
474
|
+
def message_maker(action, message, vars)
|
475
|
+
out = []
|
476
|
+
case action.first
|
477
|
+
when :note
|
478
|
+
# Should signal error, presumably.
|
479
|
+
case action.last
|
480
|
+
when :run_error_input_failed
|
481
|
+
out.push "Output from #{message[0]} failed."
|
482
|
+
when :run_child_error_output_format
|
483
|
+
out.push "Output to #{message[0]} failed."
|
484
|
+
end
|
485
|
+
when :bytes
|
486
|
+
id = message[0]
|
487
|
+
@@prev_bytes_mutex.synchronize do
|
488
|
+
if id != @@prev_bytes_id
|
489
|
+
out.push "#{id}:"
|
490
|
+
@prev_bytes_id = id
|
491
|
+
end
|
492
|
+
end
|
493
|
+
out.push ''.concat(*vars)
|
494
|
+
end
|
495
|
+
userout(out) unless $QUIET
|
496
|
+
!out.empty?
|
497
|
+
end
|
498
|
+
|
499
|
+
def handle(cmd)
|
500
|
+
return unless assert(cmd, 4)
|
501
|
+
t = cmd[0] # Swap command and user-given identifier.
|
502
|
+
cmd[0] = cmd[1]
|
503
|
+
cmd[1] = t
|
504
|
+
# Locate executable unless absolute path.
|
505
|
+
idx = cmd.index 'program'
|
506
|
+
unless idx.nil? || idx + 1 == cmd.size
|
507
|
+
cmd[idx + 1] = DatalackeyProcess.locate_executable(cmd[idx + 1],
|
508
|
+
[ Dir.pwd, $root_dir, File.dirname($machine.state_source) ])
|
509
|
+
end # Missing program will be found normally and causes error.
|
510
|
+
userout(cmd.to_s) if $FOLLOW > 4
|
511
|
+
$lackey.send(@action, cmd, true)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
class RunCommand < RunBase
|
516
|
+
def initialize
|
517
|
+
super('run')
|
518
|
+
@action = PatternAction.new([
|
519
|
+
{ return: [ 'run', 'running', '?' ],
|
520
|
+
error: [ 'run', 'terminated', '?' ] },
|
521
|
+
@@actions_run_common ], [ @msgmaker, $signal_sender ])
|
522
|
+
end
|
523
|
+
|
524
|
+
def handle(cmd)
|
525
|
+
cmd.flatten!
|
526
|
+
super(cmd)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
RunCommand.new
|
530
|
+
|
531
|
+
class ProcessCommand < Command
|
532
|
+
def initialize(name)
|
533
|
+
@action = nil
|
534
|
+
super(name)
|
535
|
+
end
|
536
|
+
|
537
|
+
def handle(cmd)
|
538
|
+
cmd.flatten!
|
539
|
+
cmd.concat $lackey.launched.keys if cmd.size == 1
|
540
|
+
return if cmd.size == 1
|
541
|
+
super(cmd)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
class CloseCommand < ProcessCommand
|
546
|
+
def initialize
|
547
|
+
super('close')
|
548
|
+
end
|
549
|
+
|
550
|
+
def handle(cmd)
|
551
|
+
cmd.flatten!
|
552
|
+
cmd[0] = 'end-feed'
|
553
|
+
super(cmd)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
CloseCommand.new
|
557
|
+
|
558
|
+
class TerminateCommand < ProcessCommand
|
559
|
+
def initialize
|
560
|
+
super('terminate')
|
561
|
+
end
|
562
|
+
end
|
563
|
+
TerminateCommand.new
|
564
|
+
|
565
|
+
class FeedCommand < Command
|
566
|
+
def initialize
|
567
|
+
super('feed')
|
568
|
+
@action = PatternAction.new(
|
569
|
+
[ { error: [ 'error', '*' ] } ], [ $signal_sender ])
|
570
|
+
end
|
571
|
+
end
|
572
|
+
FeedCommand.new
|
573
|
+
|
574
|
+
class WaitProcessCommand < Command
|
575
|
+
def initialize
|
576
|
+
super('wait_process')
|
577
|
+
end
|
578
|
+
|
579
|
+
def handle(cmd)
|
580
|
+
cmd.flatten!
|
581
|
+
return unless assert(cmd, 2)
|
582
|
+
begin
|
583
|
+
deadline = Time.new + Float(cmd[1])
|
584
|
+
rescue ArgumentError
|
585
|
+
$machine.set_error "Not a number: #{cmd[1]}"
|
586
|
+
return
|
587
|
+
end
|
588
|
+
waited = 2 < cmd.size ? $lackey.launched.keys : cmd[2...cmd.size]
|
589
|
+
while (Time.new <=> deadline) == -1
|
590
|
+
sleep(0.2)
|
591
|
+
remains = false
|
592
|
+
current = $lackey.process.keys
|
593
|
+
waited.each { |id| remains ||= current.include? id }
|
594
|
+
return unless remains
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
WaitProcessCommand.new
|
599
|
+
|
600
|
+
class WaitDataCommand < Command
|
601
|
+
def initialize
|
602
|
+
super('wait_data')
|
603
|
+
end
|
604
|
+
|
605
|
+
def handle(cmd)
|
606
|
+
cmd.flatten!
|
607
|
+
unseen = cmd[1...cmd.size]
|
608
|
+
loop do
|
609
|
+
data = $lackey.data
|
610
|
+
unseen.delete_if { |u| data.key? u }
|
611
|
+
break if unseen.empty?
|
612
|
+
sleep(0.2)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
WaitDataCommand.new
|
617
|
+
|
618
|
+
class WaitSignalCommand < Command
|
619
|
+
def initialize
|
620
|
+
super('wait_signal')
|
621
|
+
end
|
622
|
+
|
623
|
+
def handle(cmd)
|
624
|
+
while $machine.get_signal.nil?
|
625
|
+
sleep(0.2)
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
WaitSignalCommand.new
|
630
|
+
|
631
|
+
class SignalCommand < Command
|
632
|
+
def initialize
|
633
|
+
super('signal')
|
634
|
+
end
|
635
|
+
|
636
|
+
def handle(cmd)
|
637
|
+
cmd.flatten!
|
638
|
+
return unless assert(cmd, 2, 2)
|
639
|
+
$machine.set_signal(cmd[1])
|
640
|
+
end
|
641
|
+
end
|
642
|
+
SignalCommand.new
|
643
|
+
|
644
|
+
class JumpCommand < Command
|
645
|
+
def initialize
|
646
|
+
super('jump')
|
647
|
+
end
|
648
|
+
|
649
|
+
def handle(cmd)
|
650
|
+
cmd.flatten!
|
651
|
+
return unless assert(cmd, 2, 2)
|
652
|
+
$machine.set_signal('__jump', cmd[1])
|
653
|
+
end
|
654
|
+
end
|
655
|
+
JumpCommand.new
|
656
|
+
|
657
|
+
class DeleteCommand < Command
|
658
|
+
def initialize
|
659
|
+
super('delete')
|
660
|
+
@action = nil
|
661
|
+
end
|
662
|
+
end
|
663
|
+
DeleteCommand.new
|
664
|
+
|
665
|
+
class RenameCommand < Command
|
666
|
+
def initialize
|
667
|
+
super('rename')
|
668
|
+
@action = nil
|
669
|
+
end
|
670
|
+
end
|
671
|
+
RenameCommand.new
|
672
|
+
|
673
|
+
class PrintCommand < Command
|
674
|
+
def initialize
|
675
|
+
super('print')
|
676
|
+
end
|
677
|
+
|
678
|
+
def handle(cmd)
|
679
|
+
return if $QUIET
|
680
|
+
# Array containing arrays will be treated as multi-line output.
|
681
|
+
cmd.shift
|
682
|
+
multi = false
|
683
|
+
cmd.each_index do |k|
|
684
|
+
next unless cmd[k].is_a? Array
|
685
|
+
multi = true
|
686
|
+
cmd[k] = cmd[k].flatten.join(' ')
|
687
|
+
end
|
688
|
+
userout cmd.join(multi ? "\n" : ' ')
|
689
|
+
end
|
690
|
+
end
|
691
|
+
PrintCommand.new
|
692
|
+
|
693
|
+
class ValueCommand < Command
|
694
|
+
def handle_with_map(cmd, map)
|
695
|
+
return if $QUIET
|
696
|
+
cmd.flatten!
|
697
|
+
cmd.shift
|
698
|
+
out = []
|
699
|
+
cmd.each { |id| out.push "#{id} : #{map.fetch(id, 'NOT FOUND')}" }
|
700
|
+
userout out
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
class SerialCommand < ValueCommand
|
705
|
+
def initialize
|
706
|
+
super('serial')
|
707
|
+
end
|
708
|
+
|
709
|
+
def handle(cmd)
|
710
|
+
handle_with_map(cmd, $lackey.data)
|
711
|
+
end
|
712
|
+
end
|
713
|
+
SerialCommand.new
|
714
|
+
|
715
|
+
class PIDCommand < ValueCommand
|
716
|
+
def initialize
|
717
|
+
super('pid')
|
718
|
+
end
|
719
|
+
|
720
|
+
def handle(cmd)
|
721
|
+
handle_with_map(cmd, $lackey.process)
|
722
|
+
end
|
723
|
+
end
|
724
|
+
PIDCommand.new
|
725
|
+
|
726
|
+
class ShellCommand < Command
|
727
|
+
def initialize
|
728
|
+
@rule = { 'argv' => '*', 'script' => '*', 'stdout' => '?', 'stderr' => '?', '*' => '*' }
|
729
|
+
super('script')
|
730
|
+
end
|
731
|
+
|
732
|
+
def handle(cmd)
|
733
|
+
# Get interpreter and check that it exists.
|
734
|
+
cmd.delete 'comment'
|
735
|
+
argv = cmd.fetch('argv', [])
|
736
|
+
cmd.delete 'argv'
|
737
|
+
script = cmd['script'] # Pre-requisite for getting this far, exists.
|
738
|
+
cmd.delete 'script'
|
739
|
+
vout = cmd.fetch('stdout', nil)
|
740
|
+
cmd.delete 'stdout'
|
741
|
+
verr = cmd.fetch('stderr', nil)
|
742
|
+
cmd.delete 'stderr'
|
743
|
+
vin = cmd.fetch('stdin', nil)
|
744
|
+
cmd.delete 'stdin'
|
745
|
+
if cmd.size != 1
|
746
|
+
$machine.set_error "Multiple interpreters: #{cmd.keys.sort.join(' ')}"
|
747
|
+
return
|
748
|
+
end
|
749
|
+
interpreter = cmd.keys.first
|
750
|
+
hashbangargs = cmd[cmd.keys.first]
|
751
|
+
hashbangargs = hashbangargs.join(' ') if hashbangargs.is_a? Array
|
752
|
+
unless File.exist?(interpreter) && File.executable?(interpreter)
|
753
|
+
$machine.set_error "Not found or not an executable: #{interpreter}"
|
754
|
+
return
|
755
|
+
end
|
756
|
+
# Write script into a temporary file.
|
757
|
+
temp = Tempfile.new('state', Dir.tmpdir)
|
758
|
+
begin
|
759
|
+
temp.puts "#!#{interpreter} #{hashbangargs}"
|
760
|
+
temp.puts script
|
761
|
+
temp.chmod(0o700)
|
762
|
+
temp.close
|
763
|
+
argv.prepend temp.path
|
764
|
+
rescue StandardError => e
|
765
|
+
$machine.set_error("Shell script prepare exception:\n#{e}")
|
766
|
+
temp.close!
|
767
|
+
return
|
768
|
+
end
|
769
|
+
wait = nil
|
770
|
+
stdin = nil
|
771
|
+
rd_out = nil
|
772
|
+
rd_err = nil
|
773
|
+
begin
|
774
|
+
# Run script with arguments.
|
775
|
+
stdin, stdout, stderr, wait = Open3.popen3(*argv)
|
776
|
+
rd_out = vout.nil? ? DiscardReader.new(stdout) : StoringReader.new(stdout)
|
777
|
+
rd_err = verr.nil? ? DiscardReader.new(stderr) : StoringReader.new(stderr)
|
778
|
+
stdin.puts(vin) unless vin.nil?
|
779
|
+
stdin.close
|
780
|
+
wait.join
|
781
|
+
$machine.variables[vout] = rd_out.getlines.join("\n") unless vout.nil?
|
782
|
+
$machine.variables[verr] = rd_err.getlines.join("\n") unless verr.nil?
|
783
|
+
exitcode = wait.value.exitstatus
|
784
|
+
wait = nil
|
785
|
+
$machine.set_error("Shell exit: #{exitcode}") if exitcode != 0
|
786
|
+
rescue StandardError => e
|
787
|
+
$machine.set_error("Shell run exception:\n#{e}")
|
788
|
+
ensure
|
789
|
+
stdin.close unless stdin.nil?
|
790
|
+
rd_out.close unless rd_out.nil?
|
791
|
+
rd_err.close unless rd_err.nil?
|
792
|
+
wait.join unless wait.nil?
|
793
|
+
temp.close!
|
794
|
+
end
|
795
|
+
end
|
796
|
+
end
|
797
|
+
ShellCommand.new
|
798
|
+
|
799
|
+
class RubyCommand < Command
|
800
|
+
def initialize
|
801
|
+
super('ruby')
|
802
|
+
end
|
803
|
+
|
804
|
+
def handle(cmd)
|
805
|
+
# Access to variables via @variables in binding.
|
806
|
+
script = cmd['ruby']
|
807
|
+
script = script.join("\n") if script.is_a? Array
|
808
|
+
begin
|
809
|
+
eval(script, $machine.get_binding)
|
810
|
+
rescue StandardError => e
|
811
|
+
$machine.set_error "Eval failed: #{e}"
|
812
|
+
end
|
813
|
+
end
|
814
|
+
end
|
815
|
+
RubyCommand.new
|
816
|
+
|
817
|
+
# Load and parse all state files here.
|
818
|
+
|
819
|
+
$overwrites = []
|
820
|
+
|
821
|
+
def add_states(states, new_states)
|
822
|
+
new_states.each_pair do |name, state|
|
823
|
+
$overwrites.push({ state: name }.merge(state[:load])) if states.key? name
|
824
|
+
states[name] = state
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
$built_ins = { '__setup' => %q(
|
829
|
+
---
|
830
|
+
- __terminate_all:
|
831
|
+
- close
|
832
|
+
- wait_process terminate_delay
|
833
|
+
- terminate
|
834
|
+
comment: "Default termination, close feeds, terminate all after 5 seconds."
|
835
|
+
- __setup:
|
836
|
+
- set pipes in JSON stdin out JSON stdout out bytes stderr
|
837
|
+
- set pipes_noerr in JSON stdin out JSON stdout
|
838
|
+
- set notify_all notify data notify process
|
839
|
+
- [ set, terminate_delay, 5 ]
|
840
|
+
global_signal2state:
|
841
|
+
error: __terminate_all
|
842
|
+
comment: |
|
843
|
+
First state to run. Default variable set-up. Program will add first state
|
844
|
+
in the first item of the first file with null key unless you set it when
|
845
|
+
replacing this. Several states in the first item results in random pick.
|
846
|
+
) }
|
847
|
+
|
848
|
+
$first_state = nil
|
849
|
+
|
850
|
+
def load_statefile(filename, current_directory, parent_includes = [])
|
851
|
+
if $built_ins.key? filename
|
852
|
+
fullname = filename
|
853
|
+
contents = YAML.safe_load($built_ins[filename])
|
854
|
+
directory = '(built-in)' # Built-in can include only another built-in.
|
855
|
+
else
|
856
|
+
begin
|
857
|
+
fullname = File.realpath(filename, current_directory)
|
858
|
+
contents = YAML.safe_load(File.read(fullname))
|
859
|
+
directory = File.dirname(fullname)
|
860
|
+
rescue Errno::ENOENT
|
861
|
+
userout "Could not find #{filename} from #{current_directory}"
|
862
|
+
return nil
|
863
|
+
rescue StandardError => e
|
864
|
+
userout e.to_s
|
865
|
+
userout "Failed to read #{filename} from #{current_directory}"
|
866
|
+
return nil
|
867
|
+
end
|
868
|
+
end
|
869
|
+
if parent_includes.include? fullname
|
870
|
+
userout "Include loop: #{filename} from #{current_directory}"
|
871
|
+
return nil
|
872
|
+
end
|
873
|
+
unless contents.is_a? Array
|
874
|
+
userout "Not a top level array: #{filename} from #{current_directory}"
|
875
|
+
return nil
|
876
|
+
end
|
877
|
+
states = { }
|
878
|
+
contents.each_index do |index|
|
879
|
+
item = contents[index]
|
880
|
+
common = { load: { source: fullname, index: index } }
|
881
|
+
begin
|
882
|
+
item.delete 'comment'
|
883
|
+
[ 'global_signal2state', 'signal2state', 'label2signal' ].each do |shared|
|
884
|
+
next unless item.key? shared
|
885
|
+
common[shared] = item[shared]
|
886
|
+
item.delete shared
|
887
|
+
end
|
888
|
+
rescue NoMethodError
|
889
|
+
userout "Item is not a mapping: #{fullname} : #{index}"
|
890
|
+
return nil
|
891
|
+
end
|
892
|
+
include_name = item.fetch('include', nil)
|
893
|
+
if include_name.nil?
|
894
|
+
# Separate each state while keeping common parts.
|
895
|
+
item.each_pair do |state, commands|
|
896
|
+
if $first_state.nil?
|
897
|
+
$first_state = state
|
898
|
+
if item.size != 1
|
899
|
+
userout "First state mapping with multiple states is ambiguous: #{fullname} : #{index}"
|
900
|
+
return nil
|
901
|
+
end
|
902
|
+
end
|
903
|
+
commands.each_index do |k|
|
904
|
+
commands[k] = commands[k].split(' ') if commands[k].is_a? String
|
905
|
+
end
|
906
|
+
separated = common.clone
|
907
|
+
separated[:commands] = commands
|
908
|
+
add_states(states, { state => separated })
|
909
|
+
end
|
910
|
+
else
|
911
|
+
parent_includes.push fullname
|
912
|
+
included = load_statefile(include_name, directory, parent_includes)
|
913
|
+
parent_includes.pop
|
914
|
+
if included.nil?
|
915
|
+
userout " .. #{fullname} : #{index}"
|
916
|
+
return nil
|
917
|
+
end
|
918
|
+
add_states(states, included)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
states
|
922
|
+
end
|
923
|
+
|
924
|
+
states = load_statefile('__setup', $root_dir)
|
925
|
+
$first_state = nil
|
926
|
+
ARGV.each do |filename|
|
927
|
+
new_states = load_statefile(filename, $root_dir)
|
928
|
+
exit(2) if new_states.nil?
|
929
|
+
add_states(states, new_states)
|
930
|
+
end
|
931
|
+
|
932
|
+
if !$OVERWRITE_ACTION.nil? && !$overwrites.empty?
|
933
|
+
$overwrites.each do |ow|
|
934
|
+
userout "#{ow[:state]} replaced by #{ow[:source]} : #{ow[:index]}"
|
935
|
+
end
|
936
|
+
exit(4) if $OVERWRITE_ACTION == :error
|
937
|
+
end
|
938
|
+
|
939
|
+
unless states['__setup'].key? 'signal2state'
|
940
|
+
states['__setup']['signal2state'] = { }
|
941
|
+
end
|
942
|
+
unless states['__setup']['signal2state'].key? nil
|
943
|
+
states['__setup']['signal2state'][nil] = $first_state
|
944
|
+
end
|
945
|
+
|
946
|
+
def label_signal_check(action, message, vars)
|
947
|
+
case action.first
|
948
|
+
when :stored
|
949
|
+
l2s = $machine.label2signal
|
950
|
+
$machine.set_signal(l2s[vars.first]) if l2s.key? vars.first
|
951
|
+
when :data_error then $machine.set_error(message.to_s)
|
952
|
+
when :error then $machine.set_error(message.to_s)
|
953
|
+
end
|
954
|
+
end
|
955
|
+
sig_check = proc { |act, msg, vs| label_signal_check(act, msg, vs) }
|
956
|
+
|
957
|
+
if $LACKEY.nil? && $MEMORY.nil? && $DIRECTORY.nil?
|
958
|
+
# Seemingly running under datalackey.
|
959
|
+
unless $PERMISSIONS.nil?
|
960
|
+
userout 'Cannot give --permissions/-p unless running datalackey.'
|
961
|
+
exit 1
|
962
|
+
end
|
963
|
+
if $stdin.tty?
|
964
|
+
userout 'Not running under datalackey, turning on memory storage.'
|
965
|
+
$MEMORY = true
|
966
|
+
end
|
967
|
+
end
|
968
|
+
|
969
|
+
if $LACKEY.nil? && $MEMORY.nil? && $DIRECTORY.nil?
|
970
|
+
$lackey_proc = DatalackeyParentProcess.new($CMDOUT, $stdin)
|
971
|
+
$lackey_stderr = DiscardReader.new($lackey_proc.stderr)
|
972
|
+
$lackey = DatalackeyIO.new(
|
973
|
+
$lackey_proc.stdin, $lackey_proc.stdout, sig_check)
|
974
|
+
else
|
975
|
+
begin
|
976
|
+
$DIRECTORY, $PERMISSIONS, $MEMORY =
|
977
|
+
DatalackeyProcess.verify_directory_permissions_memory(
|
978
|
+
$DIRECTORY, $PERMISSIONS, $MEMORY)
|
979
|
+
$lackey_proc = DatalackeyProcess.new(
|
980
|
+
$LACKEY, $DIRECTORY, $PERMISSIONS, $MEMORY)
|
981
|
+
rescue ArgumentError => e
|
982
|
+
userout e.to_s
|
983
|
+
exit 1
|
984
|
+
end
|
985
|
+
$lackey_stderr = StoringReader.new($lackey_proc.stderr)
|
986
|
+
echo = $ECHO ? proc { |json| userout json } : nil
|
987
|
+
$lackey = DatalackeyIO.new(
|
988
|
+
$lackey_proc.stdin, $lackey_proc.stdout, sig_check, echo, echo)
|
989
|
+
end
|
990
|
+
|
991
|
+
$machine.run('__setup', states)
|
992
|
+
|
993
|
+
$lackey_proc.finish
|
994
|
+
$lackey.close
|
995
|
+
if $LACKEY.nil? && $MEMORY.nil? && $DIRECTORY.nil?
|
996
|
+
$lackey_proc.stdout.close
|
997
|
+
$stderr.close
|
998
|
+
end
|
999
|
+
$lackey.finish
|
1000
|
+
$lackey_stderr.close
|
1001
|
+
userout $lackey_stderr.getlines
|
1002
|
+
if !$lackey_proc.exit_code.zero? && !$lackey_proc.exit_code.nil?
|
1003
|
+
userout("datalackey exit: #{$lackey_proc.exit_code}")
|
1004
|
+
end
|
1005
|
+
exit $machine.variables.fetch('__exitcode', $machine.had_error ? 1 : 0)
|