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