twinshot 1.2.1 → 1.2.2
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 +4 -4
- data/lib/twinshot/cli.rb +46 -23
- data/lib/twinshot/lockfile.rb +94 -0
- data/lib/twinshot/version.rb +1 -1
- data/lib/twinshot.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9826a2a05669fc0e17f6eba0e9c65974007862176b2944998982be28faf7f61
|
|
4
|
+
data.tar.gz: 4ff7da0c51c214ce05f27298669f8a3e787d7e9b61450d903c494aa66411bd4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7997f0505c7414ed6305a3039d0058c5c6259e025cf30a154425586094a566056b4b22ca9fd6e5cf43a0d3327c245661c7d3f0839a7dcc0c80bcc0d607bbd7f
|
|
7
|
+
data.tar.gz: 332282358ce1a8bca3faa57798b577b48c334a2028f4aa3318ba45fd0acab3d099afbc184359e1b76ed927205538fb48362bd8a5c3d64001518be3152ef73901
|
data/lib/twinshot/cli.rb
CHANGED
|
@@ -4,6 +4,8 @@ require "json"
|
|
|
4
4
|
require "optparse"
|
|
5
5
|
require "shellwords"
|
|
6
6
|
require "fileutils"
|
|
7
|
+
require "English"
|
|
8
|
+
require "colorize"
|
|
7
9
|
require_relative "progress"
|
|
8
10
|
|
|
9
11
|
module Twinshot
|
|
@@ -65,7 +67,7 @@ module Twinshot
|
|
|
65
67
|
@err.puts global_parser
|
|
66
68
|
EXIT_ERROR
|
|
67
69
|
end
|
|
68
|
-
rescue OptionParser::ParseError, Config::Error, Compare::Error => e
|
|
70
|
+
rescue OptionParser::ParseError, Config::Error, Compare::Error, Lockfile::Error => e
|
|
69
71
|
@err.puts "twinshot: #{e.message}"
|
|
70
72
|
EXIT_ERROR
|
|
71
73
|
end
|
|
@@ -196,7 +198,7 @@ module Twinshot
|
|
|
196
198
|
end
|
|
197
199
|
|
|
198
200
|
config = Config.load(@config_path)
|
|
199
|
-
run_compare(config, select_groups(config))
|
|
201
|
+
Lockfile.under(config) { run_compare(config, select_groups(config)) }
|
|
200
202
|
end
|
|
201
203
|
|
|
202
204
|
# The compare run shared by compare and review: compares the groups,
|
|
@@ -269,22 +271,24 @@ module Twinshot
|
|
|
269
271
|
end
|
|
270
272
|
|
|
271
273
|
config = Config.load(@config_path)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
Lockfile.under(config) do
|
|
275
|
+
groups, side = generate_selection(config, @argv.shift)
|
|
276
|
+
if (extra = @argv.shift)
|
|
277
|
+
@err.puts "twinshot: unexpected argument #{extra.inspect}"
|
|
278
|
+
@err.puts generate_parser
|
|
279
|
+
next EXIT_ERROR
|
|
280
|
+
end
|
|
278
281
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
targets = generate_targets(groups, side)
|
|
283
|
+
if targets.empty?
|
|
284
|
+
what = side ? "#{side}.generate" : "generate"
|
|
285
|
+
scope = groups.size == 1 ? "for group #{groups.first.name}" : "in any selected group"
|
|
286
|
+
@err.puts "twinshot: no #{what} configured #{scope} in #{File.basename(config.path)}"
|
|
287
|
+
next EXIT_ERROR
|
|
288
|
+
end
|
|
286
289
|
|
|
287
|
-
|
|
290
|
+
run_generate(config, targets) ? EXIT_OK : EXIT_DIFF
|
|
291
|
+
end
|
|
288
292
|
end
|
|
289
293
|
|
|
290
294
|
# The review command is generate + compare in one run: both sides of the selected
|
|
@@ -300,11 +304,18 @@ module Twinshot
|
|
|
300
304
|
end
|
|
301
305
|
|
|
302
306
|
config = Config.load(@config_path)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
Lockfile.under(config) do
|
|
308
|
+
groups = select_groups(config)
|
|
309
|
+
targets = generate_targets(groups)
|
|
310
|
+
next run_compare(config, groups) if targets.empty?
|
|
311
|
+
|
|
312
|
+
unless run_generate(config, targets)
|
|
313
|
+
@err.puts err_paint("twinshot: generate failed — skipping compare " \
|
|
314
|
+
"(a report over stale trees would be misleading)")
|
|
315
|
+
next EXIT_DIFF
|
|
316
|
+
end
|
|
317
|
+
run_compare(config, groups)
|
|
318
|
+
end
|
|
308
319
|
end
|
|
309
320
|
|
|
310
321
|
# Trees (group, side, command) with a generate command, in oracle-then-
|
|
@@ -319,15 +330,27 @@ module Twinshot
|
|
|
319
330
|
end
|
|
320
331
|
end
|
|
321
332
|
|
|
333
|
+
# Red for failures on stderr — but only when stderr is a tty, like
|
|
334
|
+
# Progress paints stdout (piped/CI logs stay plain).
|
|
335
|
+
def err_paint(text)
|
|
336
|
+
return text.to_s unless @err.tty?
|
|
337
|
+
|
|
338
|
+
text.to_s.red
|
|
339
|
+
end
|
|
340
|
+
|
|
322
341
|
# Says the full list of trees about to be regenerated first, then runs
|
|
323
342
|
# the commands one by one from the config's directory; a failing command
|
|
324
|
-
# doesn't stop the rest. true — all succeeded.
|
|
343
|
+
# doesn't stop the rest, it just says so on stderr. true — all succeeded.
|
|
325
344
|
def run_generate(config, targets)
|
|
326
345
|
@out.puts "Generate:"
|
|
327
346
|
targets.each { |group, tree, _command| @out.puts "- #{group.name}/#{tree}" }
|
|
328
347
|
targets.map do |group, tree, command|
|
|
329
348
|
@out.puts "twinshot: [#{group.name}/#{tree}] #{command}"
|
|
330
|
-
system(command, chdir: config.root)
|
|
349
|
+
next true if system(command, chdir: config.root)
|
|
350
|
+
|
|
351
|
+
@err.puts err_paint("twinshot: [#{group.name}/#{tree}] generate command " \
|
|
352
|
+
"failed with exit #{$CHILD_STATUS.exitstatus}")
|
|
353
|
+
false
|
|
331
354
|
end.all?
|
|
332
355
|
end
|
|
333
356
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Twinshot
|
|
6
|
+
# One run at a time per project: while a command works on a config's
|
|
7
|
+
# trees, tmp/twinshot.pid in the config's directory holds the running
|
|
8
|
+
# pid. A second run refuses to start with a clear error; a pid file
|
|
9
|
+
# left behind by a crashed run is never removed automatically — it is
|
|
10
|
+
# reported so the user can remove it by hand.
|
|
11
|
+
class Lockfile
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Where the lock lives, relative to the config's directory.
|
|
15
|
+
RELATIVE_PATH = File.join("tmp", "twinshot.pid").freeze
|
|
16
|
+
|
|
17
|
+
# Runs the block under the lock: acquires first, releases after —
|
|
18
|
+
# also on failure. Raises Error before the block starts when another
|
|
19
|
+
# run holds the lock or a stale pid file is in the way.
|
|
20
|
+
def self.under(config)
|
|
21
|
+
lock = new(File.join(config.root, RELATIVE_PATH))
|
|
22
|
+
lock.acquire
|
|
23
|
+
begin
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
lock.release
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(path)
|
|
31
|
+
@path = path
|
|
32
|
+
@held = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Creates the pid file atomically (O_EXCL), so two runs starting at
|
|
36
|
+
# the same moment can't both win; the loser gets the "already
|
|
37
|
+
# running" error from the winner's pid.
|
|
38
|
+
def acquire
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
40
|
+
File.open(@path, File::CREAT | File::EXCL | File::WRONLY) { |f| f.write(Process.pid.to_s) }
|
|
41
|
+
@held = true
|
|
42
|
+
rescue Errno::EEXIST
|
|
43
|
+
raise Error, taken_message if File.exist?(@path)
|
|
44
|
+
|
|
45
|
+
retry # vanished between EEXIST and the check — try to take it again
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Removes the pid file. Only the run that created it releases, and a
|
|
49
|
+
# file already gone (a stray `rm`, say) is fine.
|
|
50
|
+
def release
|
|
51
|
+
return unless @held
|
|
52
|
+
|
|
53
|
+
File.delete(@path)
|
|
54
|
+
rescue Errno::ENOENT
|
|
55
|
+
nil
|
|
56
|
+
ensure
|
|
57
|
+
@held = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The error when the pid file is already there: a live pid means
|
|
63
|
+
# another run is active; a dead or unreadable pid means a leftover
|
|
64
|
+
# from a crashed run — the user removes it manually.
|
|
65
|
+
def taken_message
|
|
66
|
+
pid = Integer(File.read(@path).strip, 10)
|
|
67
|
+
raise ArgumentError if pid <= 0 # a real process pid is always positive
|
|
68
|
+
|
|
69
|
+
return "another twinshot run is already active (pid #{pid}, see #{RELATIVE_PATH}) — wait for it to finish" if alive?(pid)
|
|
70
|
+
|
|
71
|
+
"#{RELATIVE_PATH} holds pid #{pid}, but that process is not running — " \
|
|
72
|
+
"it may be left over from a crashed run; remove #{RELATIVE_PATH} manually and try again"
|
|
73
|
+
rescue Errno::ENOENT, ArgumentError
|
|
74
|
+
"#{RELATIVE_PATH} exists but holds no readable pid — " \
|
|
75
|
+
"it may be left over from a crashed run; remove #{RELATIVE_PATH} manually and try again"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Signal 0 is the existence probe, and it is cross-platform: POSIX
|
|
79
|
+
# sends nothing and just checks, while Ruby's Windows build maps it
|
|
80
|
+
# onto OpenProcess. EPERM (Unix) and EACCES (Windows) mean the
|
|
81
|
+
# process is there but not ours; ESRCH — on both — means the pid is
|
|
82
|
+
# gone. One caveat: on Windows a child that exited but was not
|
|
83
|
+
# reaped can still answer (like a Unix zombie); that only makes the
|
|
84
|
+
# check err on the safe side — the error tells the user to wait.
|
|
85
|
+
def alive?(pid)
|
|
86
|
+
Process.kill(0, pid)
|
|
87
|
+
true
|
|
88
|
+
rescue Errno::EPERM, Errno::EACCES
|
|
89
|
+
true
|
|
90
|
+
rescue Errno::ESRCH
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/twinshot/version.rb
CHANGED
data/lib/twinshot.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "twinshot/compare"
|
|
|
8
8
|
require_relative "twinshot/progress"
|
|
9
9
|
require_relative "twinshot/report"
|
|
10
10
|
require_relative "twinshot/opener"
|
|
11
|
+
require_relative "twinshot/lockfile"
|
|
11
12
|
require_relative "twinshot/cli"
|
|
12
13
|
|
|
13
14
|
# Twinshot: 1:1 artifact parity between a verified oracle tree and a current
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: twinshot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleg Orlov
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- lib/twinshot/compare.rb
|
|
93
93
|
- lib/twinshot/compare_worker.rb
|
|
94
94
|
- lib/twinshot/config.rb
|
|
95
|
+
- lib/twinshot/lockfile.rb
|
|
95
96
|
- lib/twinshot/logo.svg
|
|
96
97
|
- lib/twinshot/opener.rb
|
|
97
98
|
- lib/twinshot/opener/darwin.rb
|