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
data/bin/datalackey-make
ADDED
@@ -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.
|