microsandbox-rb 0.17.2 → 0.17.4
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 +112 -1
- data/Cargo.lock +34 -32
- data/DESIGN.md +199 -8
- data/README.md +4 -2
- data/docs/errors.md +1 -0
- data/docs/runtime.md +115 -0
- data/docs/surface.md +6 -0
- data/ext/microsandbox/Cargo.toml +9 -2
- data/ext/microsandbox/src/agent.rs +13 -10
- data/ext/microsandbox/src/backend.rs +295 -34
- data/ext/microsandbox/src/error.rs +41 -23
- data/ext/microsandbox/src/exec.rs +32 -18
- data/ext/microsandbox/src/fs_stream.rs +11 -8
- data/ext/microsandbox/src/host_path.rs +4 -3
- data/ext/microsandbox/src/lib.rs +51 -2
- data/ext/microsandbox/src/runtime.rs +276 -39
- data/ext/microsandbox/src/sandbox.rs +175 -91
- data/ext/microsandbox/src/snapshot.rs +7 -4
- data/ext/microsandbox/src/ssh.rs +26 -23
- data/ext/microsandbox/src/stream.rs +13 -7
- data/ext/microsandbox/src/volume.rs +22 -16
- data/lib/microsandbox/errors.rb +8 -0
- data/lib/microsandbox/sandbox.rb +39 -20
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +20 -1
- data/sig/microsandbox.rbs +1 -0
- metadata +1 -1
data/docs/runtime.md
CHANGED
|
@@ -104,6 +104,121 @@ operations (create/start/stop/remove/get/list, one-shot exec, follow log
|
|
|
104
104
|
streaming); unsupported operations raise `Microsandbox::UnsupportedError`
|
|
105
105
|
(with `#operation` and `#hint`).
|
|
106
106
|
|
|
107
|
+
## Forking (Puma, Resque, Solid Queue)
|
|
108
|
+
|
|
109
|
+
Forking a process that has already used the SDK is supported: preload the
|
|
110
|
+
application, use the SDK in the master, fork workers, use it in the workers.
|
|
111
|
+
The first SDK call in a child sets up its own runtime threads and its own
|
|
112
|
+
backend (a new catalog connection pool, or a new HTTP client for the cloud
|
|
113
|
+
backend), chosen the same way as the parent's: an explicit
|
|
114
|
+
`set_default_backend`, an enclosing `with_backend` block, or — when neither
|
|
115
|
+
was used — the environment and config file as the child sees them at that
|
|
116
|
+
call. Everything addressed by name works in the child from then on:
|
|
117
|
+
`Sandbox.create`/`get`/`list`/`start`, `Volume`, `Image`, `Snapshot` — except
|
|
118
|
+
that on macOS a forked child must not do anything that builds a network
|
|
119
|
+
client (see [macOS](#macos-no-network-clients-in-a-forked-child) below).
|
|
120
|
+
|
|
121
|
+
If the parent's choice cannot be made again in the child (say the child's
|
|
122
|
+
environment now selects the cloud backend without credentials), SDK calls
|
|
123
|
+
there raise the configuration error until that is repaired. Selecting a
|
|
124
|
+
backend in the child — `set_default_backend`, or entering `with_backend` —
|
|
125
|
+
does not depend on the inherited choice and works regardless.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
Microsandbox::Sandbox.list # the parent has used the SDK
|
|
129
|
+
|
|
130
|
+
pid = fork do
|
|
131
|
+
# On macOS "alpine" must already be in the image cache (see below).
|
|
132
|
+
Microsandbox::Sandbox.create("job-#{Process.pid}", image: "alpine") do |sb|
|
|
133
|
+
puts sb.exec("echo", ["hello from the worker"]).stdout
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
Process.wait(pid)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
What does **not** cross a fork is an object created before it. A `Sandbox`,
|
|
140
|
+
`SandboxHandle`, `ExecHandle`, `ExecStdin`, `PullSession`, log/metrics/file
|
|
141
|
+
stream, `SshClient`/`SftpClient`/`SshServer`, `AgentClient` or `VolumeFs`
|
|
142
|
+
belongs to the process that created it. In a forked child, any call that acts
|
|
143
|
+
on one raises `Microsandbox::ForkedHandleError` (the message names the
|
|
144
|
+
pids involved); the original keeps working in the parent. Reading `name` or `id` — and
|
|
145
|
+
the snapshot fields of a `SandboxHandle` — still works, so a child can open
|
|
146
|
+
its own handle:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
sandbox = Microsandbox::Sandbox.create("shared", image: "alpine")
|
|
150
|
+
|
|
151
|
+
pid = fork do
|
|
152
|
+
begin
|
|
153
|
+
sandbox.exec("true") # the parent's handle
|
|
154
|
+
rescue Microsandbox::ForkedHandleError => e
|
|
155
|
+
warn e.message
|
|
156
|
+
end
|
|
157
|
+
mine = Microsandbox::Sandbox.get(sandbox.name).connect_or_start
|
|
158
|
+
puts mine.exec("echo", ["hello"]).stdout
|
|
159
|
+
end
|
|
160
|
+
Process.wait(pid)
|
|
161
|
+
sandbox.stop
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Things to know:
|
|
165
|
+
|
|
166
|
+
- **Cleanup code inherited by the child.** The block forms
|
|
167
|
+
(`Sandbox.create(...) { }`, `connect_or_create`) swallow the
|
|
168
|
+
`ForkedHandleError` their teardown hits when a forked child leaves the
|
|
169
|
+
block, so the child does not stop the parent's sandbox. An `ensure
|
|
170
|
+
sandbox.stop` of your own raises it in the child; rescue it there, or run
|
|
171
|
+
the cleanup only in the process that created the sandbox.
|
|
172
|
+
- **Leaving a `with_backend` block in the child.** When the backend to
|
|
173
|
+
restore was the parent's — the child was forked inside the block, or the
|
|
174
|
+
block is the child's first use of the SDK — leaving the block installs a
|
|
175
|
+
backend chosen the way that one had been. The block's own temporary backend
|
|
176
|
+
is not needed for that: if only it can no longer be chosen in the child (a
|
|
177
|
+
cloud profile that is gone there), leaving the block still restores the
|
|
178
|
+
previous backend. If the choice to restore no longer resolves in the child,
|
|
179
|
+
the block is closed anyway and its temporary selection stays in effect.
|
|
180
|
+
`with_backend` then raises the configuration error — unless the block
|
|
181
|
+
itself raised, in which case the block's exception propagates and the
|
|
182
|
+
restore failure is reported as a warning.
|
|
183
|
+
- **A child keeps the parent's sandboxes alive.** A child inherits the
|
|
184
|
+
parent's open file descriptors, among them the pipe through which a sandbox
|
|
185
|
+
notices that its owner is gone. If the parent exits while such a child is
|
|
186
|
+
still running, the sandbox is not torn down until that child exits as well.
|
|
187
|
+
Fork before creating sandboxes, or create them in the children.
|
|
188
|
+
- **Do not fork while another thread is inside an SDK call.** That thread
|
|
189
|
+
does not exist in the child, and a lock it held at that instant stays
|
|
190
|
+
locked there.
|
|
191
|
+
- **Memory.** A child leaks, once, the native state it inherited (the
|
|
192
|
+
parent's runtime, backend and whichever inherited handles it collects)
|
|
193
|
+
instead of destroying it — destroying it is what used to crash.
|
|
194
|
+
|
|
195
|
+
### macOS: no network clients in a forked child
|
|
196
|
+
|
|
197
|
+
On macOS, a forked child that has not called `exec` dies — `SIGSEGV` or
|
|
198
|
+
`SIGABRT`, not an exception, so it cannot be rescued — as soon as the SDK
|
|
199
|
+
builds a network client or reads registry credentials from the Keychain
|
|
200
|
+
there. macOS's system frameworks do not support being used after `fork`
|
|
201
|
+
without `exec`, and every HTTP client the SDK builds looks up the system proxy
|
|
202
|
+
settings through them. In a forked child on macOS, do not:
|
|
203
|
+
|
|
204
|
+
- use a cloud backend at all, however it is selected (`set_default_backend`,
|
|
205
|
+
`with_backend`, `MSB_BACKEND`/`MSB_API_KEY`, a profile);
|
|
206
|
+
- `Sandbox.create` with an image that is not in the image cache yet, or with
|
|
207
|
+
`pull_policy: :always`;
|
|
208
|
+
- `Image.load`;
|
|
209
|
+
- install the runtime: `Microsandbox.install`, `Microsandbox.setup`, or the
|
|
210
|
+
first-use install `ensure_runtime!` performs.
|
|
211
|
+
|
|
212
|
+
It makes no difference whether the parent used the SDK, and
|
|
213
|
+
`OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` does not help. Local work is
|
|
214
|
+
unaffected, including `Sandbox.create` with an image that is already cached.
|
|
215
|
+
On Linux the cloud backend works in a forked child.
|
|
216
|
+
|
|
217
|
+
Do that work in a process that was not forked: in the parent before it forks
|
|
218
|
+
(install the runtime and pull the images the workers will use — creating a
|
|
219
|
+
sandbox from the image pulls it), or in a fresh process started with
|
|
220
|
+
`Process.spawn` (or `fork` followed by `exec`).
|
|
221
|
+
|
|
107
222
|
## One runtime line per home
|
|
108
223
|
|
|
109
224
|
The runtime home (`MSB_HOME`, default `~/.microsandbox`) belongs to one
|
data/docs/surface.md
CHANGED
|
@@ -57,6 +57,12 @@ counterpart, and [DESIGN.md](../DESIGN.md) explains how the layers fit.
|
|
|
57
57
|
- **Registries** — `registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
58
58
|
`create`.
|
|
59
59
|
- **Errors** — the typed hierarchy in [errors.md](errors.md).
|
|
60
|
+
- **Process model** — the SDK may be used before and after `fork`; objects
|
|
61
|
+
created before a fork are refused in the child with `ForkedHandleError`
|
|
62
|
+
(see [runtime.md](runtime.md#forking-puma-resque-solid-queue)). On macOS a
|
|
63
|
+
forked child must not build network clients — cloud backends, image pulls,
|
|
64
|
+
`Image.load`, runtime install — or it crashes (see
|
|
65
|
+
[runtime.md](runtime.md#macos-no-network-clients-in-a-forked-child)).
|
|
60
66
|
|
|
61
67
|
Create options span resources (`cpus`/`memory`/`max_cpus`/`max_memory`),
|
|
62
68
|
`env`/`workdir`/`labels`/`user`/`hostname`/`shell`, named `scripts`, the image
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -8,7 +8,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
|
|
|
8
8
|
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
9
|
# The core-crate dependencies below stay on their own tag, which is
|
|
10
10
|
# Microsandbox::RUNTIME_VERSION and moves independently of this version.
|
|
11
|
-
version = "0.17.
|
|
11
|
+
version = "0.17.4"
|
|
12
12
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
13
13
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
14
14
|
license = "Apache-2.0"
|
|
@@ -25,12 +25,19 @@ crate-type = ["cdylib"]
|
|
|
25
25
|
[dependencies]
|
|
26
26
|
# "rb-sys" exposes `magnus::rb_sys::protect` (rb_protect), which the GVL bridge
|
|
27
27
|
# uses to check interrupts without letting Ruby longjmp over Rust frames.
|
|
28
|
-
|
|
28
|
+
# Default features are off: `old-api` (the free functions that conjure a `Ruby`
|
|
29
|
+
# handle unchecked in release builds) must not creep back, and `io` only gates
|
|
30
|
+
# `magnus::io`, which the extension does not use.
|
|
31
|
+
magnus = { version = "0.9", default-features = false, features = ["rb-sys"] }
|
|
29
32
|
# Low-level Ruby C API — used directly for rb_thread_call_without_gvl2 (GVL
|
|
30
33
|
# release during blocking sandbox calls) and rb_thread_check_ints. Kept in
|
|
31
34
|
# lock-step with magnus's own rb-sys.
|
|
32
35
|
rb-sys = "0.9"
|
|
33
36
|
|
|
37
|
+
# `pthread_atfork`, for the fork generation (runtime::ProcessIdentity). Already
|
|
38
|
+
# in the dependency graph; listed so the binding can call it directly.
|
|
39
|
+
libc = "0.2"
|
|
40
|
+
|
|
34
41
|
# The microsandbox core runtime, wrapped via FFI exactly like the official
|
|
35
42
|
# Python (pyo3) and Node (napi) SDKs. Pinned to the matching upstream tag so the
|
|
36
43
|
# gem is self-contained and buildable anywhere (CI, release containers, end-user
|
|
@@ -14,7 +14,7 @@ use microsandbox::agent::AgentClient as CoreAgentClient;
|
|
|
14
14
|
use microsandbox::{AgentBridge, BridgeFrame, MicrosandboxError};
|
|
15
15
|
|
|
16
16
|
use crate::error;
|
|
17
|
-
use crate::runtime::{block_on, ruby};
|
|
17
|
+
use crate::runtime::{block_on, ruby, ForkGuarded};
|
|
18
18
|
|
|
19
19
|
/// Map an agent-client error onto the Ruby exception hierarchy (via the core
|
|
20
20
|
/// `MicrosandboxError::AgentClient` wrapper, exactly like the Python binding).
|
|
@@ -24,13 +24,13 @@ fn to_ruby_agent(err: microsandbox::AgentClientError) -> Error {
|
|
|
24
24
|
|
|
25
25
|
#[magnus::wrap(class = "Microsandbox::Native::AgentClient", free_immediately, size)]
|
|
26
26
|
pub struct AgentClient {
|
|
27
|
-
inner: Arc<AgentBridge
|
|
27
|
+
inner: ForkGuarded<Arc<AgentBridge>>,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
impl AgentClient {
|
|
31
31
|
fn from_bridge(bridge: AgentBridge) -> Self {
|
|
32
32
|
Self {
|
|
33
|
-
inner: Arc::new(bridge),
|
|
33
|
+
inner: ForkGuarded::new("Microsandbox::AgentClient", Arc::new(bridge)),
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -60,6 +60,9 @@ impl AgentClient {
|
|
|
60
60
|
|
|
61
61
|
/// Resolve a sandbox's agent relay socket path without connecting.
|
|
62
62
|
fn socket_path(name: String) -> Result<String, Error> {
|
|
63
|
+
// Synchronous, and the core derives the path from the default
|
|
64
|
+
// backend's config.
|
|
65
|
+
crate::backend::ensure_this_process_backend()?;
|
|
63
66
|
let path = CoreAgentClient::socket_path(&name).map_err(error::to_ruby)?;
|
|
64
67
|
Ok(path.to_string_lossy().into_owned())
|
|
65
68
|
}
|
|
@@ -71,7 +74,7 @@ impl AgentClient {
|
|
|
71
74
|
/// Send one frame and await a single response frame ({id, flags, body}).
|
|
72
75
|
fn request(&self, flags: u8, body: RString) -> Result<RHash, Error> {
|
|
73
76
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
74
|
-
let inner = Arc::clone(
|
|
77
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
75
78
|
let frame =
|
|
76
79
|
block_on(async move { inner.request(flags, body).await })?.map_err(to_ruby_agent)?;
|
|
77
80
|
Ok(frame_to_hash(frame))
|
|
@@ -80,7 +83,7 @@ impl AgentClient {
|
|
|
80
83
|
/// Open a streaming session; returns {id, handle}.
|
|
81
84
|
fn stream_open(&self, flags: u8, body: RString) -> Result<RHash, Error> {
|
|
82
85
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
83
|
-
let inner = Arc::clone(
|
|
86
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
84
87
|
let (id, handle) = block_on(async move { inner.stream_open(flags, body).await })?
|
|
85
88
|
.map_err(to_ruby_agent)?;
|
|
86
89
|
let hash = ruby().hash_new();
|
|
@@ -91,7 +94,7 @@ impl AgentClient {
|
|
|
91
94
|
|
|
92
95
|
/// Pull the next frame from a stream; nil at end-of-stream.
|
|
93
96
|
fn stream_next(&self, handle: u64) -> Result<Option<RHash>, Error> {
|
|
94
|
-
let inner = Arc::clone(
|
|
97
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
95
98
|
let frame =
|
|
96
99
|
block_on(async move { inner.stream_next(handle).await })?.map_err(to_ruby_agent)?;
|
|
97
100
|
Ok(frame.map(frame_to_hash))
|
|
@@ -99,7 +102,7 @@ impl AgentClient {
|
|
|
99
102
|
|
|
100
103
|
/// Close a stream handle. Idempotent.
|
|
101
104
|
fn stream_close(&self, handle: u64) -> Result<(), Error> {
|
|
102
|
-
let inner = Arc::clone(
|
|
105
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
103
106
|
block_on(async move { inner.stream_close(handle).await })?;
|
|
104
107
|
Ok(())
|
|
105
108
|
}
|
|
@@ -107,19 +110,19 @@ impl AgentClient {
|
|
|
107
110
|
/// Send a follow-up frame on an existing correlation id.
|
|
108
111
|
fn send(&self, id: u32, flags: u8, body: RString) -> Result<(), Error> {
|
|
109
112
|
let body = unsafe { body.as_slice() }.to_vec();
|
|
110
|
-
let inner = Arc::clone(
|
|
113
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
111
114
|
block_on(async move { inner.send(id, flags, body).await })?.map_err(to_ruby_agent)
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/// Cached handshake `core.ready` frame body bytes (CBOR).
|
|
115
118
|
fn ready_bytes(&self) -> Result<RString, Error> {
|
|
116
|
-
let bytes = self.inner.ready_bytes().map_err(to_ruby_agent)?;
|
|
119
|
+
let bytes = self.inner.get()?.ready_bytes().map_err(to_ruby_agent)?;
|
|
117
120
|
Ok(ruby().str_from_slice(&bytes))
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/// Close the connection. Idempotent.
|
|
121
124
|
fn close(&self) -> Result<(), Error> {
|
|
122
|
-
let inner = Arc::clone(
|
|
125
|
+
let inner = Arc::clone(self.inner.get()?);
|
|
123
126
|
block_on(async move { inner.close().await })?;
|
|
124
127
|
Ok(())
|
|
125
128
|
}
|