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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61129562a7595873fe7d577f08d2d98b5a7d608a4c021c1b76eb3b66533deca8
4
- data.tar.gz: 1c78a3f84ad0edcffe46144084a959d7db5eadd0886baa9d348f72d8d89fb2d7
3
+ metadata.gz: c9826a2a05669fc0e17f6eba0e9c65974007862176b2944998982be28faf7f61
4
+ data.tar.gz: 4ff7da0c51c214ce05f27298669f8a3e787d7e9b61450d903c494aa66411bd4d
5
5
  SHA512:
6
- metadata.gz: f71390ff6a0309a3d7f2008cb44cb96a902494275bbfc0993f0f3ff2cc492cf3f75fddffb6eecaf7b11fdc6ea23a892a892ed0324a364d2bd2f412bc96a21d07
7
- data.tar.gz: 3984e3f511e04341b7ddaaf3089c9281c415988fa73590a248ca2d12a38ef080ffd934439b0a3d63a835eadb8965851833c379aeba88c77c5d9aaa4bc9e8c4ce
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
- groups, side = generate_selection(config, @argv.shift)
273
- if (extra = @argv.shift)
274
- @err.puts "twinshot: unexpected argument #{extra.inspect}"
275
- @err.puts generate_parser
276
- return EXIT_ERROR
277
- end
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
- targets = generate_targets(groups, side)
280
- if targets.empty?
281
- what = side ? "#{side}.generate" : "generate"
282
- scope = groups.size == 1 ? "for group #{groups.first.name}" : "in any selected group"
283
- @err.puts "twinshot: no #{what} configured #{scope} in #{File.basename(config.path)}"
284
- return EXIT_ERROR
285
- end
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
- run_generate(config, targets) ? EXIT_OK : EXIT_DIFF
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
- groups = select_groups(config)
304
- targets = generate_targets(groups)
305
- return run_compare(config, groups) if targets.empty?
306
-
307
- run_generate(config, targets) ? run_compare(config, groups) : EXIT_DIFF
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twinshot
4
- VERSION = "1.2.1"
4
+ VERSION = "1.2.2"
5
5
  end
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.1
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