sandbox-adapter 0.1.1 → 0.1.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: e5d358100e225bf920045edf88deedbeaacb2d1f90c57d83b9b6865d70df356a
4
- data.tar.gz: 7b0744d3df1da971ae6d149845544735fb55cdaeea933ac9ff41811374d5f0c6
3
+ metadata.gz: 8621cc441678bf373fa97d36a3f9c34545c0482a306f6a15da3e9f873d486cec
4
+ data.tar.gz: d51db640d37a85125008f8ab5a71324e04fa4bdb514724792e4c9c0b1828d69a
5
5
  SHA512:
6
- metadata.gz: a7839666cd9fb2a1b33c8d2ab83b9df921f196a830fa89523f9aa3338588d390ce20c9f2d8a1e08257a3313fabe24f0e95ea86a977f54f34556896e1d4b31bc1
7
- data.tar.gz: 3ecb9325f46085582afe337c36bea87a375a71f5e9056b2ff8c99976bfc355fa86bade972f8eab6db0d003e3da63eeffe707dc3b625c51d72389b8fcec2448a1
6
+ metadata.gz: fd94042bf70fa5df59e241c48f4437136d8edd19a8946fe9710df436f344ae0847969da8fa90ca0fea7374964d19bb6ed44f95cd4d2fbafbb2517ff4bccba5ef
7
+ data.tar.gz: 8d53d6726db2b45498f9e3a2b1aa27516bc1443a53cae2e7af26c9f3cc040c62baae39814bcd69bdaa62e0ddb8db019836d9c5e6b7e309cf8ee034bb6b1d43d3
@@ -30,20 +30,35 @@ class Sandbox::Adapter::Box
30
30
  end
31
31
 
32
32
  # Returns the exit status. env (symbol-keyed) is exported, dir is the working directory.
33
- def run(command, env: {}, dir: nil, &)
34
- @transport.exec(shell(script(command, env:, dir:)), &)
33
+ # `within` bounds a command that must not outlive a round (starting or following a detached one), on a
34
+ # transport that can.
35
+ def run(command, env: {}, dir: nil, within: nil, &)
36
+ line = shell(script(command, env:, dir:))
37
+ return @transport.exec(line, within:, &) if within && @transport.respond_to?(:bounded?)
38
+ @transport.exec(line, &)
35
39
  end
36
40
 
37
- def run!(command, **, &)
38
- status = run(command, **, &)
39
- raise Failed, "exit #{status}: #{self.class.headline(command)}" unless status == 0
41
+ def run!(command, **, &block)
42
+ said = []
43
+ status = run(command, **) { |line| keep(said, line); block&.call(line) }
44
+ raise Failed, self.class.failure(status, command, said) unless status == 0
45
+ end
46
+
47
+ # How many of the last lines a failure carries.
48
+ TAIL = 5
49
+
50
+ # "exit 1: what the script does", then the last lines it printed: they say what went wrong.
51
+ def self.failure(status, command, said)
52
+ [ "exit #{status}: #{headline(command)}", said.join("\n").strip.presence ].compact.join("\n")
40
53
  end
41
54
 
42
55
  # The first line of a script that says what it does. A shell directive is not one: every script here
43
- # opens with `set -e`, and a failure named after it says nothing about what failed.
56
+ # opens with `set -e`, and a failure named after it says nothing about what failed. A comment first
57
+ # names a script whose first command would not (a function it defines).
44
58
  DIRECTIVE = /\A(set|shopt|export|umask)\b/
45
59
  def self.headline(script)
46
- script.to_s.lines.map(&:strip).reject { _1.empty? || _1.match?(DIRECTIVE) }.first.to_s
60
+ line = script.to_s.lines.map(&:strip).reject { _1.empty? || _1.match?(DIRECTIVE) }.first.to_s
61
+ line.start_with?("#") ? line.delete_prefix("#").strip : line
47
62
  end
48
63
 
49
64
  # Starts `command` in its own process group under `id`, survives this connection, and streams its output from
@@ -72,9 +87,10 @@ class Sandbox::Adapter::Box
72
87
  end
73
88
  end
74
89
 
75
- def exec!(id, command, **, &)
76
- status = exec(id, command, **, &)
77
- raise Failed, "exit #{status}: #{self.class.headline(command)}" unless status == 0
90
+ def exec!(id, command, **, &block)
91
+ said = []
92
+ status = exec(id, command, **) { |line| keep(said, line); block&.call(line) }
93
+ raise Failed, self.class.failure(status, command, said) unless status == 0
78
94
  end
79
95
 
80
96
  # The bytes of a file on the box, over SFTP on a fresh connection.
@@ -104,6 +120,11 @@ class Sandbox::Adapter::Box
104
120
 
105
121
  private
106
122
 
123
+ def keep(said, line)
124
+ said << line
125
+ said.shift while said.size > TAIL
126
+ end
127
+
107
128
  # The script runs under `as` when there is one; the login stays what it is. A login shell, for the PATH
108
129
  # a box's profile sets, unless the transport brings its own environment (this machine).
109
130
  def shell(script)
@@ -141,15 +162,16 @@ class Sandbox::Adapter::Box
141
162
  end
142
163
 
143
164
  # The script and its log live under scratch/id. setsid makes the wrapper the group leader (its pid is the pgid);
144
- # nohup keeps it alive when this session ends; the wrapper records the exit status last. Started already
165
+ # nohup keeps it alive when this session ends; the wrapper records the exit status last, and a handler
166
+ # on TERM keeps it alive through a cancel to record the command's 143. Started already
145
167
  # (a link dropped after the launch) is left alone.
146
168
  def start(id, script)
147
169
  d = "#{@dir}/#{id}"
148
- run!(<<~SH, env: { NVOI_SCRIPT: script })
170
+ run!(<<~SH, env: { NVOI_SCRIPT: script }, within: ROUND)
149
171
  if [ -f #{d}/pid ] && [ ! -f #{d}/exit ] && kill -0 -- -$(cat #{d}/pid) 2>/dev/null; then exit 0; fi
150
172
  mkdir -p -m 700 #{d} && rm -f #{d}/exit
151
173
  printf '%s\\n' "$NVOI_SCRIPT" > #{d}/script && chmod 600 #{d}/script
152
- setsid nohup bash -c 'echo $$ > #{d}/pid; bash #{d}/script > #{d}/log 2>&1; echo $? > #{d}/exit' >/dev/null 2>&1 &
174
+ setsid nohup bash -c 'trap : TERM; echo $$ > #{d}/pid; bash #{d}/script > #{d}/log 2>&1; echo $? > #{d}/exit' >/dev/null 2>&1 &
153
175
  for i in $(seq 1 50); do [ -f #{d}/pid ] && exit 0; sleep 0.1; done; exit 1
154
176
  SH
155
177
  end
@@ -173,7 +195,7 @@ class Sandbox::Adapter::Box
173
195
  # A round can end while the command is mid-line - apt prints "Generating locales..." and waits - and
174
196
  # the marker is then written onto the end of that line. What comes before it is output; the marker is
175
197
  # still the marker. Reading it only at the start of a line loses the round's status altogether.
176
- run(cmd) do |line|
198
+ run(cmd, within: ROUND + 30) do |line|
177
199
  out, marker, code = line.match(MARKER)&.captures
178
200
  yield out if out.present? && status.nil? && !more
179
201
  if marker == "more" then more = true
@@ -1,5 +1,5 @@
1
1
  module Sandbox
2
2
  module Adapter
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sandbox-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - nvoi