microsandbox-rb 0.5.9 → 0.5.11
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/CHANGELOG.md +91 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +7 -3
- data/README.md +91 -26
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +27 -4
- data/ext/microsandbox/src/sandbox.rs +208 -61
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/errors.rb +6 -0
- data/lib/microsandbox/exec_handle.rb +14 -11
- data/lib/microsandbox/fs.rb +7 -7
- data/lib/microsandbox/image.rb +1 -1
- data/lib/microsandbox/network.rb +19 -19
- data/lib/microsandbox/patch.rb +8 -8
- data/lib/microsandbox/sandbox.rb +230 -84
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +2 -2
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +61 -0
- data/sig/microsandbox.rbs +31 -13
- metadata +2 -1
data/lib/microsandbox/patch.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Microsandbox
|
|
|
31
31
|
# @param replace [Boolean] allow shadowing a path already in the rootfs
|
|
32
32
|
# @return [Hash]
|
|
33
33
|
def text(path, content, mode: nil, replace: false)
|
|
34
|
-
h = {
|
|
34
|
+
h = {"kind" => "text", "path" => path.to_s, "content" => content.to_s, "replace" => replace ? true : false}
|
|
35
35
|
h["mode"] = Integer(mode) unless mode.nil?
|
|
36
36
|
h
|
|
37
37
|
end
|
|
@@ -43,7 +43,7 @@ module Microsandbox
|
|
|
43
43
|
# @param replace [Boolean]
|
|
44
44
|
# @return [Hash]
|
|
45
45
|
def file(path, content, mode: nil, replace: false)
|
|
46
|
-
h = {
|
|
46
|
+
h = {"kind" => "file", "path" => path.to_s, "content" => content.to_s, "replace" => replace ? true : false}
|
|
47
47
|
h["mode"] = Integer(mode) unless mode.nil?
|
|
48
48
|
h
|
|
49
49
|
end
|
|
@@ -52,7 +52,7 @@ module Microsandbox
|
|
|
52
52
|
# lower image layer is copied up first, then appended.
|
|
53
53
|
# @return [Hash]
|
|
54
54
|
def append(path, content)
|
|
55
|
-
{
|
|
55
|
+
{"kind" => "append", "path" => path.to_s, "content" => content.to_s}
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Copy a host file into the rootfs.
|
|
@@ -62,7 +62,7 @@ module Microsandbox
|
|
|
62
62
|
# @param replace [Boolean]
|
|
63
63
|
# @return [Hash]
|
|
64
64
|
def copy_file(src, dst, mode: nil, replace: false)
|
|
65
|
-
h = {
|
|
65
|
+
h = {"kind" => "copy_file", "src" => src.to_s, "dst" => dst.to_s, "replace" => replace ? true : false}
|
|
66
66
|
h["mode"] = Integer(mode) unless mode.nil?
|
|
67
67
|
h
|
|
68
68
|
end
|
|
@@ -70,13 +70,13 @@ module Microsandbox
|
|
|
70
70
|
# Copy a host directory (recursively) into the rootfs.
|
|
71
71
|
# @return [Hash]
|
|
72
72
|
def copy_dir(src, dst, replace: false)
|
|
73
|
-
{
|
|
73
|
+
{"kind" => "copy_dir", "src" => src.to_s, "dst" => dst.to_s, "replace" => replace ? true : false}
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Create a symlink at +link+ pointing to +target+.
|
|
77
77
|
# @return [Hash]
|
|
78
78
|
def symlink(target, link, replace: false)
|
|
79
|
-
{
|
|
79
|
+
{"kind" => "symlink", "target" => target.to_s, "link" => link.to_s, "replace" => replace ? true : false}
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# Create a directory (idempotent — no error if it already exists).
|
|
@@ -84,7 +84,7 @@ module Microsandbox
|
|
|
84
84
|
# @param mode [Integer, nil] directory mode (e.g. 0o755)
|
|
85
85
|
# @return [Hash]
|
|
86
86
|
def mkdir(path, mode: nil)
|
|
87
|
-
h = {
|
|
87
|
+
h = {"kind" => "mkdir", "path" => path.to_s}
|
|
88
88
|
h["mode"] = Integer(mode) unless mode.nil?
|
|
89
89
|
h
|
|
90
90
|
end
|
|
@@ -92,7 +92,7 @@ module Microsandbox
|
|
|
92
92
|
# Remove a file or directory (idempotent — no error if absent).
|
|
93
93
|
# @return [Hash]
|
|
94
94
|
def remove(path)
|
|
95
|
-
{
|
|
95
|
+
{"kind" => "remove", "path" => path.to_s}
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
end
|
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -1,41 +1,119 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Microsandbox
|
|
4
|
-
#
|
|
5
|
-
# {Sandbox.
|
|
6
|
-
|
|
4
|
+
# A controllable handle to a sandbox, returned by {Sandbox.get}, {Sandbox.list},
|
|
5
|
+
# and {Sandbox.list_with}. Carries a metadata snapshot (captured when fetched)
|
|
6
|
+
# plus the fine-grained lifecycle surface — `stop_with_timeout`, `request_stop`,
|
|
7
|
+
# `request_kill`, `request_drain`, `wait_until_stopped` — that mirrors the
|
|
8
|
+
# official SDKs' `SandboxHandle`. (The live {Sandbox} from {Sandbox.create}/
|
|
9
|
+
# {Sandbox.start} carries only the high-level `stop`/`kill`/`drain`/`wait`.)
|
|
10
|
+
#
|
|
11
|
+
# As of v0.5.8 this replaces the old read-only `SandboxInfo` (kept as a
|
|
12
|
+
# deprecated constant alias); `#status` here is a synchronous snapshot.
|
|
13
|
+
class SandboxHandle
|
|
14
|
+
def initialize(native)
|
|
15
|
+
@native = native
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
# @return [String]
|
|
8
|
-
|
|
9
|
-
# @return [Symbol] :running, :draining, :paused, :stopped, or :crashed
|
|
10
|
-
attr_reader :status
|
|
19
|
+
def name = @native.name
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@created_at_ms = data["created_at_ms"]
|
|
16
|
-
@updated_at_ms = data["updated_at_ms"]
|
|
17
|
-
end
|
|
21
|
+
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
22
|
+
# :stopped, or :crashed (a snapshot, captured when this handle was fetched)
|
|
23
|
+
def status = @native.status.to_sym
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# Whether the fetch-time {#status} snapshot is `:running` / `:stopped`.
|
|
26
|
+
# Like {#status}, these do NOT refresh: to observe a state change after
|
|
27
|
+
# {#request_stop}/{#request_kill}/{#request_drain}, use {#wait_until_stopped}
|
|
28
|
+
# or re-fetch the handle with {Sandbox.get}.
|
|
29
|
+
def running? = status == :running
|
|
30
|
+
def stopped? = status == :stopped
|
|
21
31
|
|
|
22
32
|
# @return [Time, nil]
|
|
23
33
|
def created_at
|
|
24
|
-
|
|
34
|
+
ms = @native.created_at_ms
|
|
35
|
+
ms && Time.at(ms / 1000.0)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# @return [Time, nil]
|
|
28
39
|
def updated_at
|
|
29
|
-
|
|
40
|
+
ms = @native.updated_at_ms
|
|
41
|
+
ms && Time.at(ms / 1000.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default).
|
|
45
|
+
# @return [nil]
|
|
46
|
+
def stop
|
|
47
|
+
@native.stop
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Gracefully stop with a custom escalation timeout.
|
|
52
|
+
# @param timeout [Numeric] seconds to wait before escalating to SIGKILL
|
|
53
|
+
# @return [nil]
|
|
54
|
+
def stop_with_timeout(timeout)
|
|
55
|
+
@native.stop_with_timeout(Sandbox.send(:coerce_duration, timeout, "timeout"))
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Force-kill the sandbox (SIGKILL).
|
|
60
|
+
# @return [nil]
|
|
61
|
+
def kill
|
|
62
|
+
@native.kill
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Force-kill, waiting up to `timeout` seconds for the process to disappear.
|
|
67
|
+
# @param timeout [Numeric]
|
|
68
|
+
# @return [nil]
|
|
69
|
+
def kill_with_timeout(timeout)
|
|
70
|
+
@native.kill_with_timeout(Sandbox.send(:coerce_duration, timeout, "timeout"))
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Send the graceful-shutdown request and return immediately, without waiting.
|
|
75
|
+
# Pair with {#wait_until_stopped}.
|
|
76
|
+
# @return [nil]
|
|
77
|
+
def request_stop
|
|
78
|
+
@native.request_stop
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Send the force-kill request and return immediately, without waiting.
|
|
83
|
+
# @return [nil]
|
|
84
|
+
def request_kill
|
|
85
|
+
@native.request_kill
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Request a graceful drain (SIGUSR1) and return immediately, without waiting.
|
|
90
|
+
# @return [nil]
|
|
91
|
+
def request_drain
|
|
92
|
+
@native.request_drain
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Block until the sandbox is observed in a terminal (non-running) state.
|
|
97
|
+
# @return [SandboxStopResult]
|
|
98
|
+
def wait_until_stopped
|
|
99
|
+
SandboxStopResult.new(@native.wait_until_stopped)
|
|
30
100
|
end
|
|
31
101
|
|
|
32
102
|
def inspect
|
|
33
|
-
"#<Microsandbox::
|
|
103
|
+
"#<Microsandbox::SandboxHandle name=#{name.inspect} status=#{status}>"
|
|
34
104
|
end
|
|
35
105
|
end
|
|
36
106
|
|
|
107
|
+
# @deprecated since v0.5.8. {Sandbox.get}/{Sandbox.list} now return a
|
|
108
|
+
# controllable {SandboxHandle}; this constant remains as an alias so code
|
|
109
|
+
# that referenced the old read-only metadata type by name (e.g. `is_a?`
|
|
110
|
+
# checks) still resolves. Note it is now the same class as {SandboxHandle},
|
|
111
|
+
# whose constructor takes a native handle, not the metadata Hash the old
|
|
112
|
+
# `SandboxInfo.new` accepted — construct via {Sandbox.get}/{Sandbox.list}.
|
|
113
|
+
SandboxInfo = SandboxHandle
|
|
114
|
+
|
|
37
115
|
# The terminal observation of a stopped sandbox, returned by
|
|
38
|
-
# {
|
|
116
|
+
# {SandboxHandle#wait_until_stopped}. Mirrors the official SDKs' `SandboxStopResult`.
|
|
39
117
|
class SandboxStopResult
|
|
40
118
|
# @return [String]
|
|
41
119
|
attr_reader :name
|
|
@@ -67,7 +145,7 @@ module Microsandbox
|
|
|
67
145
|
|
|
68
146
|
def inspect
|
|
69
147
|
"#<Microsandbox::SandboxStopResult name=#{@name.inspect} status=#{@status}" \
|
|
70
|
-
"#{
|
|
148
|
+
"#{" exit_code=#{@exit_code}" if @exit_code}#{" signal=#{@signal}" if @signal}>"
|
|
71
149
|
end
|
|
72
150
|
end
|
|
73
151
|
|
|
@@ -143,15 +221,21 @@ module Microsandbox
|
|
|
143
221
|
# @yieldparam sandbox [Sandbox]
|
|
144
222
|
# @return [Sandbox, Object] the sandbox, or the block's return value
|
|
145
223
|
def create(name,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
224
|
+
image: nil, cpus: nil, memory: nil, env: nil, workdir: nil,
|
|
225
|
+
shell: nil, user: nil, hostname: nil, labels: nil, scripts: nil,
|
|
226
|
+
entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
227
|
+
patches: nil,
|
|
228
|
+
from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil,
|
|
229
|
+
oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil,
|
|
230
|
+
pull_policy: nil, registry_auth: nil, registry_insecure: false,
|
|
231
|
+
registry_ca_certs: nil, secrets: nil,
|
|
232
|
+
detached: false, replace: false, replace_with_timeout: nil)
|
|
233
|
+
# A sandbox boots from exactly one rootfs source. The core would reject a
|
|
234
|
+
# contradictory pair, but only after a runtime round-trip; fail fast and
|
|
235
|
+
# clearly here (the Python SDK validates this the same way).
|
|
236
|
+
if image && from_snapshot
|
|
237
|
+
raise ArgumentError, "provide either image: or from_snapshot:, not both"
|
|
238
|
+
end
|
|
155
239
|
Microsandbox.ensure_runtime!
|
|
156
240
|
opts = {}
|
|
157
241
|
opts["image"] = image.to_s if image
|
|
@@ -183,7 +267,7 @@ module Microsandbox
|
|
|
183
267
|
opts["secrets"] = normalize_secrets(secrets) if secrets
|
|
184
268
|
opts["detached"] = true if detached
|
|
185
269
|
if replace_with_timeout
|
|
186
|
-
opts["replace_with_timeout"] =
|
|
270
|
+
opts["replace_with_timeout"] = coerce_duration(replace_with_timeout, "replace_with_timeout")
|
|
187
271
|
elsif replace
|
|
188
272
|
opts["replace"] = true
|
|
189
273
|
end
|
|
@@ -206,27 +290,27 @@ module Microsandbox
|
|
|
206
290
|
# @return [Sandbox]
|
|
207
291
|
def start(name, detached: false)
|
|
208
292
|
Microsandbox.ensure_runtime!
|
|
209
|
-
new(Native::Sandbox.start(name.to_s, {
|
|
293
|
+
new(Native::Sandbox.start(name.to_s, {"detached" => detached}))
|
|
210
294
|
end
|
|
211
295
|
|
|
212
|
-
# Fetch
|
|
213
|
-
# @return [
|
|
296
|
+
# Fetch a controllable handle for a sandbox by name (running or not).
|
|
297
|
+
# @return [SandboxHandle]
|
|
214
298
|
def get(name)
|
|
215
|
-
|
|
299
|
+
SandboxHandle.new(Native::Sandbox.get(name.to_s))
|
|
216
300
|
end
|
|
217
301
|
|
|
218
|
-
# List all sandboxes.
|
|
219
|
-
# @return [Array<
|
|
302
|
+
# List all sandboxes as controllable handles.
|
|
303
|
+
# @return [Array<SandboxHandle>]
|
|
220
304
|
def list
|
|
221
|
-
Native::Sandbox.list.map { |
|
|
305
|
+
Native::Sandbox.list.map { |h| SandboxHandle.new(h) }
|
|
222
306
|
end
|
|
223
307
|
|
|
224
308
|
# List sandboxes carrying all of the given labels (AND-matched).
|
|
225
309
|
# @param labels [Hash] required key => value labels
|
|
226
|
-
# @return [Array<
|
|
310
|
+
# @return [Array<SandboxHandle>]
|
|
227
311
|
def list_with(labels: {})
|
|
228
|
-
opts = {
|
|
229
|
-
Native::Sandbox.list_with(opts).map { |
|
|
312
|
+
opts = {"labels" => stringify(labels)}
|
|
313
|
+
Native::Sandbox.list_with(opts).map { |h| SandboxHandle.new(h) }
|
|
230
314
|
end
|
|
231
315
|
|
|
232
316
|
# Remove a (stopped) sandbox by name.
|
|
@@ -238,6 +322,19 @@ module Microsandbox
|
|
|
238
322
|
|
|
239
323
|
private
|
|
240
324
|
|
|
325
|
+
# Coerce a seconds value to a finite, non-negative Float. Rejects negatives,
|
|
326
|
+
# NaN, and infinities *here* (a clean ArgumentError) rather than letting them
|
|
327
|
+
# reach the native layer, where `Duration::from_secs_f64` panics across the
|
|
328
|
+
# FFI boundary on exactly those inputs. Shared by every duration option.
|
|
329
|
+
def coerce_duration(value, label)
|
|
330
|
+
seconds = Float(value)
|
|
331
|
+
unless seconds.finite? && seconds >= 0
|
|
332
|
+
raise ArgumentError,
|
|
333
|
+
"#{label} must be a finite, non-negative number of seconds (got #{value.inspect})"
|
|
334
|
+
end
|
|
335
|
+
seconds
|
|
336
|
+
end
|
|
337
|
+
|
|
241
338
|
def stringify(hash)
|
|
242
339
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
243
340
|
end
|
|
@@ -256,7 +353,7 @@ module Microsandbox
|
|
|
256
353
|
unless username && password
|
|
257
354
|
# Report only the keys given, never the values — auth carries secrets.
|
|
258
355
|
raise ArgumentError,
|
|
259
|
-
|
|
356
|
+
"registry_auth needs :username and :password (got keys: #{auth.keys.inspect})"
|
|
260
357
|
end
|
|
261
358
|
opts["registry_username"] = username.to_s
|
|
262
359
|
opts["registry_password"] = password.to_s
|
|
@@ -290,8 +387,10 @@ module Microsandbox
|
|
|
290
387
|
end
|
|
291
388
|
|
|
292
389
|
# Normalize volumes (Hash of guest_path => spec) into [guest, kind, source]
|
|
293
|
-
# triples for the native layer. A
|
|
294
|
-
#
|
|
390
|
+
# triples (or [guest, kind, source, options] quads) for the native layer. A
|
|
391
|
+
# spec is a host path String (read-write bind mount), or a Hash
|
|
392
|
+
# { bind: "/host" } / { named: "volume-name" } optionally carrying mount
|
|
393
|
+
# options: { bind: "/host", ro: true } / { named: "v", options: %w[ro noexec] }.
|
|
295
394
|
def normalize_volumes(volumes)
|
|
296
395
|
volumes.map do |guest, spec|
|
|
297
396
|
guest = guest.to_s
|
|
@@ -299,19 +398,39 @@ module Microsandbox
|
|
|
299
398
|
when String
|
|
300
399
|
[guest, "bind", spec]
|
|
301
400
|
when Hash
|
|
302
|
-
|
|
303
|
-
[
|
|
304
|
-
|
|
305
|
-
[
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
401
|
+
triple =
|
|
402
|
+
if (named = spec[:named] || spec["named"])
|
|
403
|
+
[guest, "named", named.to_s]
|
|
404
|
+
elsif (bind = spec[:bind] || spec["bind"])
|
|
405
|
+
[guest, "bind", bind.to_s]
|
|
406
|
+
else
|
|
407
|
+
raise ArgumentError, "volume spec for #{guest.inspect} needs :bind or :named"
|
|
408
|
+
end
|
|
409
|
+
# Optional 4th element: comma-joined mount options. Omitted when empty
|
|
410
|
+
# so existing [guest,kind,source] consumers and the common read-write
|
|
411
|
+
# case are byte-for-byte unchanged.
|
|
412
|
+
opts = mount_options(spec)
|
|
413
|
+
opts.empty? ? triple : triple + [opts.join(",")]
|
|
309
414
|
else
|
|
310
415
|
raise ArgumentError, "invalid volume spec for #{guest.inspect}: #{spec.inspect}"
|
|
311
416
|
end
|
|
312
417
|
end
|
|
313
418
|
end
|
|
314
419
|
|
|
420
|
+
# Collect mount options from a volume spec Hash. `ro:`/`readonly:` makes the
|
|
421
|
+
# mount read-only (host virtiofs rejects writes + guest kernel returns EROFS);
|
|
422
|
+
# `noexec:`/`nosuid:`/`nodev:` set the matching flags; an explicit `options:`
|
|
423
|
+
# array passes through verbatim. Unknown options are rejected natively.
|
|
424
|
+
def mount_options(spec)
|
|
425
|
+
opts = []
|
|
426
|
+
opts << "ro" if spec[:ro] || spec["ro"] || spec[:readonly] || spec["readonly"]
|
|
427
|
+
opts << "noexec" if spec[:noexec] || spec["noexec"]
|
|
428
|
+
opts << "nosuid" if spec[:nosuid] || spec["nosuid"]
|
|
429
|
+
opts << "nodev" if spec[:nodev] || spec["nodev"]
|
|
430
|
+
Array(spec[:options] || spec["options"]).each { |o| opts << o.to_s }
|
|
431
|
+
opts.uniq
|
|
432
|
+
end
|
|
433
|
+
|
|
315
434
|
# Normalize a list of patches (each a Hash from the {Patch} factory, or a
|
|
316
435
|
# plain Hash) into string-keyed Hashes for the native layer. Values are
|
|
317
436
|
# passed through unchanged (mode stays Integer, content stays String).
|
|
@@ -358,33 +477,39 @@ module Microsandbox
|
|
|
358
477
|
# @param env [Hash, nil] extra environment variables
|
|
359
478
|
# @param timeout [Numeric, nil] kill after N seconds
|
|
360
479
|
# @param tty [Boolean] allocate a pseudo-terminal
|
|
361
|
-
# @param stdin [String, nil]
|
|
480
|
+
# @param stdin [String, Symbol, nil] bytes to feed to stdin, or +:pipe+ to
|
|
481
|
+
# open a streaming stdin pipe (write/close it via {ExecHandle#stdin}; only
|
|
482
|
+
# useful with the streaming variants)
|
|
362
483
|
# @return [ExecOutput]
|
|
363
484
|
def exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
364
485
|
ExecOutput.new(@native.exec(command.to_s, Array(args).map(&:to_s),
|
|
365
|
-
|
|
486
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
366
487
|
end
|
|
367
488
|
|
|
368
489
|
# Run a shell script (pipes, redirects, etc. allowed) and collect output.
|
|
369
490
|
# @return [ExecOutput]
|
|
370
491
|
def shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
371
492
|
ExecOutput.new(@native.shell(script.to_s,
|
|
372
|
-
|
|
493
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
373
494
|
end
|
|
374
495
|
|
|
375
496
|
# Run a command and stream its output as it arrives.
|
|
497
|
+
#
|
|
498
|
+
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
499
|
+
# then returns a writable sink; close it to send EOF (a process like +cat+
|
|
500
|
+
# that reads until EOF will otherwise block forever).
|
|
376
501
|
# @return [ExecHandle]
|
|
377
502
|
# @see ExecHandle
|
|
378
503
|
def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
379
504
|
ExecHandle.new(@native.exec_stream(command.to_s, Array(args).map(&:to_s),
|
|
380
|
-
|
|
505
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
381
506
|
end
|
|
382
507
|
|
|
383
508
|
# Run a shell script and stream its output as it arrives.
|
|
384
509
|
# @return [ExecHandle]
|
|
385
510
|
def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
386
511
|
ExecHandle.new(@native.shell_stream(script.to_s,
|
|
387
|
-
|
|
512
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
388
513
|
end
|
|
389
514
|
|
|
390
515
|
# Attach an interactive terminal to a command in the sandbox.
|
|
@@ -469,7 +594,7 @@ module Microsandbox
|
|
|
469
594
|
# @param interval [Numeric] seconds between snapshots
|
|
470
595
|
# @return [MetricsStream] an {Enumerable} of {Metrics}
|
|
471
596
|
def metrics_stream(interval: 1.0)
|
|
472
|
-
MetricsStream.new(@native.metrics_stream(
|
|
597
|
+
MetricsStream.new(@native.metrics_stream(coerce_duration(interval, "interval")))
|
|
473
598
|
end
|
|
474
599
|
|
|
475
600
|
# Stream captured logs as they appear.
|
|
@@ -492,48 +617,46 @@ module Microsandbox
|
|
|
492
617
|
LogStream.new(@native.log_stream(opts))
|
|
493
618
|
end
|
|
494
619
|
|
|
495
|
-
# Gracefully stop the sandbox (
|
|
496
|
-
#
|
|
620
|
+
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and
|
|
621
|
+
# wait for it to terminate. For a custom timeout or fire-and-return
|
|
622
|
+
# `request_*` control, fetch a {SandboxHandle} via {Sandbox.get}.
|
|
497
623
|
# @return [nil]
|
|
498
|
-
def stop
|
|
499
|
-
@native.stop
|
|
624
|
+
def stop
|
|
625
|
+
@native.stop
|
|
500
626
|
nil
|
|
501
627
|
end
|
|
502
628
|
|
|
503
|
-
#
|
|
504
|
-
# @
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
@native.kill(timeout && Float(timeout))
|
|
508
|
-
nil
|
|
629
|
+
# Gracefully stop, then wait for the process to exit.
|
|
630
|
+
# @return [ExitStatus]
|
|
631
|
+
def stop_and_wait
|
|
632
|
+
ExitStatus.new(@native.stop_and_wait)
|
|
509
633
|
end
|
|
510
634
|
|
|
511
|
-
#
|
|
512
|
-
# for the sandbox to terminate. Pair with {#wait_until_stopped}.
|
|
635
|
+
# Force-kill the sandbox (SIGKILL).
|
|
513
636
|
# @return [nil]
|
|
514
|
-
def
|
|
515
|
-
@native.
|
|
637
|
+
def kill
|
|
638
|
+
@native.kill
|
|
516
639
|
nil
|
|
517
640
|
end
|
|
518
641
|
|
|
519
|
-
#
|
|
642
|
+
# Trigger a graceful drain (SIGUSR1).
|
|
520
643
|
# @return [nil]
|
|
521
|
-
def
|
|
522
|
-
@native.
|
|
644
|
+
def drain
|
|
645
|
+
@native.drain
|
|
523
646
|
nil
|
|
524
647
|
end
|
|
525
648
|
|
|
526
|
-
#
|
|
527
|
-
# @return [
|
|
528
|
-
def
|
|
529
|
-
@native.
|
|
530
|
-
nil
|
|
649
|
+
# Wait for the sandbox process to exit.
|
|
650
|
+
# @return [ExitStatus]
|
|
651
|
+
def wait
|
|
652
|
+
ExitStatus.new(@native.wait)
|
|
531
653
|
end
|
|
532
654
|
|
|
533
|
-
#
|
|
534
|
-
# @return [
|
|
535
|
-
|
|
536
|
-
|
|
655
|
+
# The live status, fetched from the backend (a round-trip per call).
|
|
656
|
+
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
657
|
+
# :stopped, or :crashed
|
|
658
|
+
def status
|
|
659
|
+
@native.status.to_sym
|
|
537
660
|
end
|
|
538
661
|
|
|
539
662
|
# @return [Boolean] whether this handle owns the sandbox process lifecycle
|
|
@@ -556,14 +679,37 @@ module Microsandbox
|
|
|
556
679
|
|
|
557
680
|
private
|
|
558
681
|
|
|
559
|
-
|
|
682
|
+
# Instance-side shim for the shared, class-private duration validator (used
|
|
683
|
+
# by #exec/#shell timeouts, #stop/#kill, and #metrics_stream).
|
|
684
|
+
def coerce_duration(value, label)
|
|
685
|
+
self.class.send(:coerce_duration, value, label)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: false)
|
|
560
689
|
opts = {}
|
|
561
690
|
opts["cwd"] = cwd.to_s if cwd
|
|
562
691
|
opts["user"] = user.to_s if user
|
|
563
692
|
opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
|
|
564
|
-
opts["timeout"] =
|
|
693
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
565
694
|
opts["tty"] = true if tty
|
|
566
|
-
|
|
695
|
+
# `stdin: :pipe` opens a streaming stdin pipe — write to it via
|
|
696
|
+
# {ExecHandle#stdin} and close to send EOF. It is only meaningful for the
|
|
697
|
+
# streaming variants (which return an ExecHandle); a blocking exec/shell
|
|
698
|
+
# collects to completion and has nowhere to hand back the sink, so a piped
|
|
699
|
+
# process that reads stdin would block forever waiting for EOF. Reject it
|
|
700
|
+
# there. Any other truthy value is fed as a fixed byte buffer (closed
|
|
701
|
+
# automatically). nil means no stdin.
|
|
702
|
+
case stdin
|
|
703
|
+
when nil then nil
|
|
704
|
+
when :pipe
|
|
705
|
+
unless pipe_ok
|
|
706
|
+
raise ArgumentError,
|
|
707
|
+
"stdin: :pipe is only valid for exec_stream/shell_stream — a blocking " \
|
|
708
|
+
"exec/shell cannot expose a writable stdin sink; pass a String to feed bytes"
|
|
709
|
+
end
|
|
710
|
+
opts["stdin_pipe"] = true
|
|
711
|
+
else opts["stdin"] = stdin.to_s
|
|
712
|
+
end
|
|
567
713
|
if rlimits
|
|
568
714
|
opts["rlimits"] = rlimits.map do |resource, limit|
|
|
569
715
|
soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
|
|
@@ -43,7 +43,7 @@ module Microsandbox
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def inspect
|
|
46
|
-
"#<Microsandbox::SnapshotInfo digest=#{@digest.inspect}#{
|
|
46
|
+
"#<Microsandbox::SnapshotInfo digest=#{@digest.inspect}#{" name=#{@name.inspect}" if @name}>"
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -142,7 +142,7 @@ module Microsandbox
|
|
|
142
142
|
# @param dest [String, nil] explicit destination directory
|
|
143
143
|
# @return [SnapshotInfo]
|
|
144
144
|
def import(archive_path, dest: nil)
|
|
145
|
-
SnapshotInfo.new(Native::Snapshot.import(archive_path.to_s, dest
|
|
145
|
+
SnapshotInfo.new(Native::Snapshot.import(archive_path.to_s, dest&.to_s))
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
private
|
data/lib/microsandbox/ssh.rb
CHANGED
|
@@ -218,7 +218,7 @@ module Microsandbox
|
|
|
218
218
|
# @yieldparam client [SshClient]
|
|
219
219
|
# @return [SshClient, Object]
|
|
220
220
|
def open_client(user: "root", term: nil, sftp: true)
|
|
221
|
-
opts = {
|
|
221
|
+
opts = {"user" => user.to_s, "sftp" => sftp ? true : false}
|
|
222
222
|
opts["term"] = term.to_s if term
|
|
223
223
|
client = SshClient.new(@native.ssh_open_client(opts))
|
|
224
224
|
return client unless block_given?
|
|
@@ -237,7 +237,7 @@ module Microsandbox
|
|
|
237
237
|
# @param sftp [Boolean] enable the SFTP subsystem (default true)
|
|
238
238
|
# @return [SshServer]
|
|
239
239
|
def prepare_server(host_key_path: nil, authorized_keys_path: nil, user: nil, sftp: true)
|
|
240
|
-
opts = {
|
|
240
|
+
opts = {"sftp" => sftp ? true : false}
|
|
241
241
|
opts["host_key_path"] = host_key_path.to_s if host_key_path
|
|
242
242
|
opts["authorized_keys_path"] = authorized_keys_path.to_s if authorized_keys_path
|
|
243
243
|
opts["user"] = user.to_s if user
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Microsandbox
|
|
4
|
-
# Gem version. Tracks the upstream microsandbox runtime (currently `v0.5.
|
|
4
|
+
# Gem version. Tracks the upstream microsandbox runtime (currently `v0.5.8`,
|
|
5
5
|
# the pinned core-crate tag); the patch segment advances for gem-only revisions
|
|
6
6
|
# that add bindings atop the same core. Must equal the native ext's Cargo crate
|
|
7
7
|
# version (`Native.version`), enforced by spec/unit/version_spec.rb.
|
|
8
|
-
VERSION = "0.5.
|
|
8
|
+
VERSION = "0.5.11"
|
|
9
9
|
end
|
data/lib/microsandbox/volume.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Microsandbox
|
|
|
7
7
|
# (`kind`, `used_bytes`, …) are populated when returned from {Volume.get}/{Volume.list}.
|
|
8
8
|
class VolumeInfo
|
|
9
9
|
attr_reader :name, :path, :quota_mib, :used_bytes, :capacity_bytes,
|
|
10
|
-
|
|
10
|
+
:disk_format, :disk_fstype, :labels
|
|
11
11
|
|
|
12
12
|
def initialize(data)
|
|
13
13
|
@name = data["name"]
|
|
@@ -33,7 +33,7 @@ module Microsandbox
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def inspect
|
|
36
|
-
"#<Microsandbox::VolumeInfo name=#{@name.inspect}#{
|
|
36
|
+
"#<Microsandbox::VolumeInfo name=#{@name.inspect}#{" kind=#{@kind}" if @kind}>"
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -49,7 +49,7 @@ module Microsandbox
|
|
|
49
49
|
# @param labels [Hash, nil]
|
|
50
50
|
# @return [VolumeInfo]
|
|
51
51
|
def create(name, kind: "dir", size_mib: nil, quota_mib: nil, labels: nil)
|
|
52
|
-
opts = {
|
|
52
|
+
opts = {"kind" => kind.to_s}
|
|
53
53
|
opts["size_mib"] = Integer(size_mib) if size_mib
|
|
54
54
|
opts["quota_mib"] = Integer(quota_mib) if quota_mib
|
|
55
55
|
opts["labels"] = labels.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if labels
|