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.
@@ -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)