microsandbox-rb 0.5.8 → 0.5.10
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 +125 -0
- data/Cargo.lock +90 -45
- data/DESIGN.md +32 -19
- data/README.md +108 -36
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/extconf.rb +6 -2
- data/ext/microsandbox/src/agent.rs +166 -0
- data/ext/microsandbox/src/backend.rs +170 -0
- data/ext/microsandbox/src/conv.rs +19 -1
- data/ext/microsandbox/src/error.rs +6 -0
- data/ext/microsandbox/src/image.rs +7 -7
- data/ext/microsandbox/src/lib.rs +31 -4
- data/ext/microsandbox/src/sandbox.rs +666 -61
- data/ext/microsandbox/src/ssh.rs +317 -0
- data/ext/microsandbox/src/volume.rs +6 -1
- data/lib/microsandbox/agent.rb +181 -0
- 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 +300 -0
- data/lib/microsandbox/patch.rb +98 -0
- data/lib/microsandbox/sandbox.rb +285 -77
- data/lib/microsandbox/snapshot.rb +2 -2
- data/lib/microsandbox/ssh.rb +247 -0
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +3 -3
- data/lib/microsandbox.rb +65 -0
- data/sig/microsandbox.rbs +164 -14
- metadata +8 -1
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
|
|
|
@@ -108,8 +186,15 @@ module Microsandbox
|
|
|
108
186
|
# @param entrypoint [Array<String>, nil] image entrypoint override
|
|
109
187
|
# @param ports [Hash, nil] host_port => guest_port TCP publications
|
|
110
188
|
# @param ports_udp [Hash, nil] host_port => guest_port UDP publications
|
|
111
|
-
# @param network [
|
|
112
|
-
#
|
|
189
|
+
# @param network [String, Symbol, NetworkPolicy, Hash, nil] network policy.
|
|
190
|
+
# A preset name ("public_only" (default), "none", "allow_all",
|
|
191
|
+
# "non_local"), a {NetworkPolicy} (e.g. {NetworkPolicy.custom}), or a Hash
|
|
192
|
+
# describing a custom policy (`default_egress:`, `default_ingress:`,
|
|
193
|
+
# `rules:`, `deny_domains:`, `deny_domain_suffixes:`). See {NetworkPolicy}
|
|
194
|
+
# and {Rule}.
|
|
195
|
+
# @param patches [Array<Hash>, nil] rootfs patches applied before boot, each
|
|
196
|
+
# built with the {Patch} factory (e.g. `Patch.text(...)`, `Patch.mkdir(...)`).
|
|
197
|
+
# Not compatible with disk-image roots.
|
|
113
198
|
# @param log_level ["error","warn","info","debug","trace", nil] guest log verbosity
|
|
114
199
|
# @param quiet_logs [Boolean] suppress sandbox process logs
|
|
115
200
|
# @param security ["default", "restricted", nil] exec security profile
|
|
@@ -136,14 +221,21 @@ module Microsandbox
|
|
|
136
221
|
# @yieldparam sandbox [Sandbox]
|
|
137
222
|
# @return [Sandbox, Object] the sandbox, or the block's return value
|
|
138
223
|
def create(name,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
147
239
|
Microsandbox.ensure_runtime!
|
|
148
240
|
opts = {}
|
|
149
241
|
opts["image"] = image.to_s if image
|
|
@@ -161,7 +253,8 @@ module Microsandbox
|
|
|
161
253
|
opts["ports"] = intify_ports(ports) if ports
|
|
162
254
|
opts["ports_udp"] = intify_ports(ports_udp) if ports_udp
|
|
163
255
|
opts["volumes"] = normalize_volumes(volumes) if volumes
|
|
164
|
-
opts["
|
|
256
|
+
opts["patches"] = normalize_patches(patches) if patches
|
|
257
|
+
apply_network_opts(opts, network) unless network.nil?
|
|
165
258
|
opts["log_level"] = log_level.to_s if log_level
|
|
166
259
|
opts["quiet_logs"] = true if quiet_logs
|
|
167
260
|
opts["security"] = security.to_s if security
|
|
@@ -174,7 +267,7 @@ module Microsandbox
|
|
|
174
267
|
opts["secrets"] = normalize_secrets(secrets) if secrets
|
|
175
268
|
opts["detached"] = true if detached
|
|
176
269
|
if replace_with_timeout
|
|
177
|
-
opts["replace_with_timeout"] =
|
|
270
|
+
opts["replace_with_timeout"] = coerce_duration(replace_with_timeout, "replace_with_timeout")
|
|
178
271
|
elsif replace
|
|
179
272
|
opts["replace"] = true
|
|
180
273
|
end
|
|
@@ -197,27 +290,27 @@ module Microsandbox
|
|
|
197
290
|
# @return [Sandbox]
|
|
198
291
|
def start(name, detached: false)
|
|
199
292
|
Microsandbox.ensure_runtime!
|
|
200
|
-
new(Native::Sandbox.start(name.to_s, {
|
|
293
|
+
new(Native::Sandbox.start(name.to_s, {"detached" => detached}))
|
|
201
294
|
end
|
|
202
295
|
|
|
203
|
-
# Fetch
|
|
204
|
-
# @return [
|
|
296
|
+
# Fetch a controllable handle for a sandbox by name (running or not).
|
|
297
|
+
# @return [SandboxHandle]
|
|
205
298
|
def get(name)
|
|
206
|
-
|
|
299
|
+
SandboxHandle.new(Native::Sandbox.get(name.to_s))
|
|
207
300
|
end
|
|
208
301
|
|
|
209
|
-
# List all sandboxes.
|
|
210
|
-
# @return [Array<
|
|
302
|
+
# List all sandboxes as controllable handles.
|
|
303
|
+
# @return [Array<SandboxHandle>]
|
|
211
304
|
def list
|
|
212
|
-
Native::Sandbox.list.map { |
|
|
305
|
+
Native::Sandbox.list.map { |h| SandboxHandle.new(h) }
|
|
213
306
|
end
|
|
214
307
|
|
|
215
308
|
# List sandboxes carrying all of the given labels (AND-matched).
|
|
216
309
|
# @param labels [Hash] required key => value labels
|
|
217
|
-
# @return [Array<
|
|
310
|
+
# @return [Array<SandboxHandle>]
|
|
218
311
|
def list_with(labels: {})
|
|
219
|
-
opts = {
|
|
220
|
-
Native::Sandbox.list_with(opts).map { |
|
|
312
|
+
opts = {"labels" => stringify(labels)}
|
|
313
|
+
Native::Sandbox.list_with(opts).map { |h| SandboxHandle.new(h) }
|
|
221
314
|
end
|
|
222
315
|
|
|
223
316
|
# Remove a (stopped) sandbox by name.
|
|
@@ -229,6 +322,19 @@ module Microsandbox
|
|
|
229
322
|
|
|
230
323
|
private
|
|
231
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
|
+
|
|
232
338
|
def stringify(hash)
|
|
233
339
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
|
|
234
340
|
end
|
|
@@ -247,7 +353,7 @@ module Microsandbox
|
|
|
247
353
|
unless username && password
|
|
248
354
|
# Report only the keys given, never the values — auth carries secrets.
|
|
249
355
|
raise ArgumentError,
|
|
250
|
-
|
|
356
|
+
"registry_auth needs :username and :password (got keys: #{auth.keys.inspect})"
|
|
251
357
|
end
|
|
252
358
|
opts["registry_username"] = username.to_s
|
|
253
359
|
opts["registry_password"] = password.to_s
|
|
@@ -302,6 +408,33 @@ module Microsandbox
|
|
|
302
408
|
end
|
|
303
409
|
end
|
|
304
410
|
end
|
|
411
|
+
|
|
412
|
+
# Normalize a list of patches (each a Hash from the {Patch} factory, or a
|
|
413
|
+
# plain Hash) into string-keyed Hashes for the native layer. Values are
|
|
414
|
+
# passed through unchanged (mode stays Integer, content stays String).
|
|
415
|
+
def normalize_patches(patches)
|
|
416
|
+
Array(patches).map do |p|
|
|
417
|
+
unless p.is_a?(Hash)
|
|
418
|
+
raise ArgumentError, "patch must be a Hash (use Microsandbox::Patch.*): #{p.inspect}"
|
|
419
|
+
end
|
|
420
|
+
p.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Route the `network:` argument to either the preset path
|
|
425
|
+
# (`opts["network"]`, the original string-preset behavior) or the custom
|
|
426
|
+
# policy path (`opts["network_policy"]`). Accepts a preset String/Symbol, a
|
|
427
|
+
# {NetworkPolicy}, or a plain Hash.
|
|
428
|
+
def apply_network_opts(opts, network)
|
|
429
|
+
norm = NetworkPolicy.coerce(network)
|
|
430
|
+
return if norm.empty? # e.g. network: {} — leave the default policy in place
|
|
431
|
+
|
|
432
|
+
if norm.keys == ["preset"]
|
|
433
|
+
opts["network"] = norm["preset"]
|
|
434
|
+
else
|
|
435
|
+
opts["network_policy"] = norm
|
|
436
|
+
end
|
|
437
|
+
end
|
|
305
438
|
end
|
|
306
439
|
|
|
307
440
|
def initialize(native)
|
|
@@ -322,33 +455,78 @@ module Microsandbox
|
|
|
322
455
|
# @param env [Hash, nil] extra environment variables
|
|
323
456
|
# @param timeout [Numeric, nil] kill after N seconds
|
|
324
457
|
# @param tty [Boolean] allocate a pseudo-terminal
|
|
325
|
-
# @param stdin [String, nil]
|
|
458
|
+
# @param stdin [String, Symbol, nil] bytes to feed to stdin, or +:pipe+ to
|
|
459
|
+
# open a streaming stdin pipe (write/close it via {ExecHandle#stdin}; only
|
|
460
|
+
# useful with the streaming variants)
|
|
326
461
|
# @return [ExecOutput]
|
|
327
462
|
def exec(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
328
463
|
ExecOutput.new(@native.exec(command.to_s, Array(args).map(&:to_s),
|
|
329
|
-
|
|
464
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
330
465
|
end
|
|
331
466
|
|
|
332
467
|
# Run a shell script (pipes, redirects, etc. allowed) and collect output.
|
|
333
468
|
# @return [ExecOutput]
|
|
334
469
|
def shell(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
335
470
|
ExecOutput.new(@native.shell(script.to_s,
|
|
336
|
-
|
|
471
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:)))
|
|
337
472
|
end
|
|
338
473
|
|
|
339
474
|
# Run a command and stream its output as it arrives.
|
|
475
|
+
#
|
|
476
|
+
# Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
|
|
477
|
+
# then returns a writable sink; close it to send EOF (a process like +cat+
|
|
478
|
+
# that reads until EOF will otherwise block forever).
|
|
340
479
|
# @return [ExecHandle]
|
|
341
480
|
# @see ExecHandle
|
|
342
481
|
def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
343
482
|
ExecHandle.new(@native.exec_stream(command.to_s, Array(args).map(&:to_s),
|
|
344
|
-
|
|
483
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
345
484
|
end
|
|
346
485
|
|
|
347
486
|
# Run a shell script and stream its output as it arrives.
|
|
348
487
|
# @return [ExecHandle]
|
|
349
488
|
def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
|
|
350
489
|
ExecHandle.new(@native.shell_stream(script.to_s,
|
|
351
|
-
|
|
490
|
+
exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: true)))
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Attach an interactive terminal to a command in the sandbox.
|
|
494
|
+
#
|
|
495
|
+
# Puts the **host** terminal into raw mode and forwards keystrokes (and
|
|
496
|
+
# SIGWINCH resizes) to the guest until the command exits or the detach
|
|
497
|
+
# sequence is typed. Requires a real TTY on stdin/stdout, so it is for CLI
|
|
498
|
+
# use, not library/automation code (use {#exec}/{#exec_stream} there). Blocks
|
|
499
|
+
# until the session ends. Mirrors the official SDKs' `attach`.
|
|
500
|
+
#
|
|
501
|
+
# @param command [String] the program to run
|
|
502
|
+
# @param args [Array<String>] its arguments
|
|
503
|
+
# @param cwd [String, nil] working directory
|
|
504
|
+
# @param user [String, nil] user to run as
|
|
505
|
+
# @param env [Hash, nil] extra environment variables
|
|
506
|
+
# @param detach_keys [String, nil] detach sequence (e.g. "ctrl-p,ctrl-q";
|
|
507
|
+
# default "ctrl-]")
|
|
508
|
+
# @param rlimits [Hash, nil] resource limits (see {#exec})
|
|
509
|
+
# @return [Integer] the command's exit code (or the code at detach)
|
|
510
|
+
def attach(command, args = [], cwd: nil, user: nil, env: nil, detach_keys: nil, rlimits: nil)
|
|
511
|
+
opts = {}
|
|
512
|
+
opts["cwd"] = cwd.to_s if cwd
|
|
513
|
+
opts["user"] = user.to_s if user
|
|
514
|
+
opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
|
|
515
|
+
opts["detach_keys"] = detach_keys.to_s if detach_keys
|
|
516
|
+
if rlimits
|
|
517
|
+
opts["rlimits"] = rlimits.map do |resource, limit|
|
|
518
|
+
soft, hard = limit.is_a?(Array) ? [limit[0], limit[1]] : [limit, limit]
|
|
519
|
+
[resource.to_s, Integer(soft), Integer(hard)]
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
@native.attach(command.to_s, Array(args).map(&:to_s), opts)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Attach an interactive terminal running the sandbox's default shell.
|
|
526
|
+
# See {#attach} for the host-TTY requirements.
|
|
527
|
+
# @return [Integer] the shell's exit code (or the code at detach)
|
|
528
|
+
def attach_shell
|
|
529
|
+
@native.attach_shell
|
|
352
530
|
end
|
|
353
531
|
|
|
354
532
|
# Guest filesystem operations.
|
|
@@ -357,6 +535,15 @@ module Microsandbox
|
|
|
357
535
|
@fs ||= FS.new(@native)
|
|
358
536
|
end
|
|
359
537
|
|
|
538
|
+
# SSH access to the sandbox — open a native in-process SSH client or prepare
|
|
539
|
+
# a reusable server endpoint.
|
|
540
|
+
# @return [SshOps]
|
|
541
|
+
# @example
|
|
542
|
+
# sb.ssh.open_client { |c| puts c.exec("hostname").stdout }
|
|
543
|
+
def ssh
|
|
544
|
+
SshOps.new(@native)
|
|
545
|
+
end
|
|
546
|
+
|
|
360
547
|
# Latest resource-usage snapshot.
|
|
361
548
|
# @return [Metrics]
|
|
362
549
|
def metrics
|
|
@@ -385,7 +572,7 @@ module Microsandbox
|
|
|
385
572
|
# @param interval [Numeric] seconds between snapshots
|
|
386
573
|
# @return [MetricsStream] an {Enumerable} of {Metrics}
|
|
387
574
|
def metrics_stream(interval: 1.0)
|
|
388
|
-
MetricsStream.new(@native.metrics_stream(
|
|
575
|
+
MetricsStream.new(@native.metrics_stream(coerce_duration(interval, "interval")))
|
|
389
576
|
end
|
|
390
577
|
|
|
391
578
|
# Stream captured logs as they appear.
|
|
@@ -408,48 +595,46 @@ module Microsandbox
|
|
|
408
595
|
LogStream.new(@native.log_stream(opts))
|
|
409
596
|
end
|
|
410
597
|
|
|
411
|
-
# Gracefully stop the sandbox (
|
|
412
|
-
#
|
|
598
|
+
# Gracefully stop the sandbox (SIGTERM→SIGKILL escalation, 10s default) and
|
|
599
|
+
# wait for it to terminate. For a custom timeout or fire-and-return
|
|
600
|
+
# `request_*` control, fetch a {SandboxHandle} via {Sandbox.get}.
|
|
413
601
|
# @return [nil]
|
|
414
|
-
def stop
|
|
415
|
-
@native.stop
|
|
602
|
+
def stop
|
|
603
|
+
@native.stop
|
|
416
604
|
nil
|
|
417
605
|
end
|
|
418
606
|
|
|
419
|
-
#
|
|
420
|
-
# @
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
@native.kill(timeout && Float(timeout))
|
|
424
|
-
nil
|
|
607
|
+
# Gracefully stop, then wait for the process to exit.
|
|
608
|
+
# @return [ExitStatus]
|
|
609
|
+
def stop_and_wait
|
|
610
|
+
ExitStatus.new(@native.stop_and_wait)
|
|
425
611
|
end
|
|
426
612
|
|
|
427
|
-
#
|
|
428
|
-
# for the sandbox to terminate. Pair with {#wait_until_stopped}.
|
|
613
|
+
# Force-kill the sandbox (SIGKILL).
|
|
429
614
|
# @return [nil]
|
|
430
|
-
def
|
|
431
|
-
@native.
|
|
615
|
+
def kill
|
|
616
|
+
@native.kill
|
|
432
617
|
nil
|
|
433
618
|
end
|
|
434
619
|
|
|
435
|
-
#
|
|
620
|
+
# Trigger a graceful drain (SIGUSR1).
|
|
436
621
|
# @return [nil]
|
|
437
|
-
def
|
|
438
|
-
@native.
|
|
622
|
+
def drain
|
|
623
|
+
@native.drain
|
|
439
624
|
nil
|
|
440
625
|
end
|
|
441
626
|
|
|
442
|
-
#
|
|
443
|
-
# @return [
|
|
444
|
-
def
|
|
445
|
-
@native.
|
|
446
|
-
nil
|
|
627
|
+
# Wait for the sandbox process to exit.
|
|
628
|
+
# @return [ExitStatus]
|
|
629
|
+
def wait
|
|
630
|
+
ExitStatus.new(@native.wait)
|
|
447
631
|
end
|
|
448
632
|
|
|
449
|
-
#
|
|
450
|
-
# @return [
|
|
451
|
-
|
|
452
|
-
|
|
633
|
+
# The live status, fetched from the backend (a round-trip per call).
|
|
634
|
+
# @return [Symbol] :created, :starting, :running, :draining, :paused,
|
|
635
|
+
# :stopped, or :crashed
|
|
636
|
+
def status
|
|
637
|
+
@native.status.to_sym
|
|
453
638
|
end
|
|
454
639
|
|
|
455
640
|
# @return [Boolean] whether this handle owns the sandbox process lifecycle
|
|
@@ -472,14 +657,37 @@ module Microsandbox
|
|
|
472
657
|
|
|
473
658
|
private
|
|
474
659
|
|
|
475
|
-
|
|
660
|
+
# Instance-side shim for the shared, class-private duration validator (used
|
|
661
|
+
# by #exec/#shell timeouts, #stop/#kill, and #metrics_stream).
|
|
662
|
+
def coerce_duration(value, label)
|
|
663
|
+
self.class.send(:coerce_duration, value, label)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def exec_opts(cwd:, user:, env:, timeout:, tty:, stdin:, rlimits:, pipe_ok: false)
|
|
476
667
|
opts = {}
|
|
477
668
|
opts["cwd"] = cwd.to_s if cwd
|
|
478
669
|
opts["user"] = user.to_s if user
|
|
479
670
|
opts["env"] = env.each_with_object({}) { |(k, v), a| a[k.to_s] = v.to_s } if env
|
|
480
|
-
opts["timeout"] =
|
|
671
|
+
opts["timeout"] = coerce_duration(timeout, "timeout") if timeout
|
|
481
672
|
opts["tty"] = true if tty
|
|
482
|
-
|
|
673
|
+
# `stdin: :pipe` opens a streaming stdin pipe — write to it via
|
|
674
|
+
# {ExecHandle#stdin} and close to send EOF. It is only meaningful for the
|
|
675
|
+
# streaming variants (which return an ExecHandle); a blocking exec/shell
|
|
676
|
+
# collects to completion and has nowhere to hand back the sink, so a piped
|
|
677
|
+
# process that reads stdin would block forever waiting for EOF. Reject it
|
|
678
|
+
# there. Any other truthy value is fed as a fixed byte buffer (closed
|
|
679
|
+
# automatically). nil means no stdin.
|
|
680
|
+
case stdin
|
|
681
|
+
when nil then nil
|
|
682
|
+
when :pipe
|
|
683
|
+
unless pipe_ok
|
|
684
|
+
raise ArgumentError,
|
|
685
|
+
"stdin: :pipe is only valid for exec_stream/shell_stream — a blocking " \
|
|
686
|
+
"exec/shell cannot expose a writable stdin sink; pass a String to feed bytes"
|
|
687
|
+
end
|
|
688
|
+
opts["stdin_pipe"] = true
|
|
689
|
+
else opts["stdin"] = stdin.to_s
|
|
690
|
+
end
|
|
483
691
|
if rlimits
|
|
484
692
|
opts["rlimits"] = rlimits.map do |resource, limit|
|
|
485
693
|
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
|