microsandbox-rb 0.17.0 → 0.17.1
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 +136 -1
- data/Cargo.lock +45 -46
- data/DESIGN.md +6 -37
- data/README.md +176 -667
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +101 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +108 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +66 -0
- data/docs/runtime.md +152 -0
- data/docs/surface.md +70 -0
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/lib.rs +1 -0
- data/ext/microsandbox/src/sandbox.rs +61 -31
- data/ext/microsandbox/src/volume.rs +43 -12
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +8 -0
- data/lib/microsandbox/sandbox.rb +53 -9
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +88 -31
- metadata +14 -1
data/lib/microsandbox/volume.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Microsandbox
|
|
|
38
38
|
|
|
39
39
|
# @return [Time, nil]
|
|
40
40
|
def created_at
|
|
41
|
-
|
|
41
|
+
Microsandbox.time_from_ms(@created_at_ms)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# A host-side filesystem view over this volume (read/write its contents
|
|
@@ -55,8 +55,10 @@ module Microsandbox
|
|
|
55
55
|
|
|
56
56
|
# A host-side filesystem view over a named volume, from {Volume.fs} or
|
|
57
57
|
# {VolumeInfo#fs}. Reads and writes the volume's contents directly on the host,
|
|
58
|
-
# without booting a sandbox. All paths are relative to the volume root
|
|
59
|
-
#
|
|
58
|
+
# without booting a sandbox. All paths are relative to the volume root (a
|
|
59
|
+
# leading `/` is the volume root) and may not contain a `..` segment — every
|
|
60
|
+
# operation raises {FilesystemError} for one. Mirrors the `VolumeFs` of the
|
|
61
|
+
# official Python/Node SDKs.
|
|
60
62
|
class VolumeFs
|
|
61
63
|
def initialize(native)
|
|
62
64
|
@native = native
|
|
@@ -65,13 +67,13 @@ module Microsandbox
|
|
|
65
67
|
# Read a file as raw bytes (ASCII-8BIT).
|
|
66
68
|
# @return [String]
|
|
67
69
|
def read(path)
|
|
68
|
-
@native.read(path
|
|
70
|
+
@native.read(contained(path))
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
# Read a file as a UTF-8 string.
|
|
72
74
|
# @return [String]
|
|
73
75
|
def read_text(path)
|
|
74
|
-
@native.read_text(path
|
|
76
|
+
@native.read_text(contained(path))
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
# Write data to a file, creating parent directories as needed.
|
|
@@ -80,60 +82,75 @@ module Microsandbox
|
|
|
80
82
|
# @return [nil]
|
|
81
83
|
def write(path, data)
|
|
82
84
|
bytes = Microsandbox.coerce_write_bytes(data)
|
|
83
|
-
@native.write(path
|
|
85
|
+
@native.write(contained(path), bytes)
|
|
84
86
|
nil
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
# List the entries of a directory.
|
|
88
90
|
# @return [Array<FsEntry>]
|
|
89
91
|
def list(path)
|
|
90
|
-
@native.list(path
|
|
92
|
+
@native.list(contained(path)).map { |entry| FsEntry.new(entry) }
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
# Create a directory (and any missing parents).
|
|
94
96
|
# @return [nil]
|
|
95
97
|
def mkdir(path)
|
|
96
|
-
@native.mkdir(path
|
|
98
|
+
@native.mkdir(contained(path))
|
|
97
99
|
nil
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
# Remove a single file.
|
|
101
103
|
# @return [nil]
|
|
102
104
|
def remove_file(path)
|
|
103
|
-
@native.remove_file(path
|
|
105
|
+
@native.remove_file(contained(path))
|
|
104
106
|
nil
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
# Remove a directory recursively.
|
|
108
110
|
# @return [nil]
|
|
109
111
|
def remove_dir(path)
|
|
110
|
-
@native.remove_dir(path
|
|
112
|
+
@native.remove_dir(contained(path))
|
|
111
113
|
nil
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
# @return [Boolean] whether the path exists in the volume
|
|
115
117
|
def exists?(path)
|
|
116
|
-
@native.exists(path
|
|
118
|
+
@native.exists(contained(path))
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
# Copy a file within the volume.
|
|
120
122
|
# @return [nil]
|
|
121
123
|
def copy(src, dst)
|
|
122
|
-
@native.copy(src
|
|
124
|
+
@native.copy(contained(src), contained(dst))
|
|
123
125
|
nil
|
|
124
126
|
end
|
|
125
127
|
|
|
126
128
|
# Rename/move a file or directory within the volume.
|
|
127
129
|
# @return [nil]
|
|
128
130
|
def rename(src, dst)
|
|
129
|
-
@native.rename(src
|
|
131
|
+
@native.rename(contained(src), contained(dst))
|
|
130
132
|
nil
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
# Stat a path.
|
|
134
136
|
# @return [FsMetadata]
|
|
135
137
|
def stat(path)
|
|
136
|
-
FsMetadata.new(@native.stat(path
|
|
138
|
+
FsMetadata.new(@native.stat(contained(path)))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Volume paths are relative to the volume root and never need a `..`
|
|
144
|
+
# segment, so one is refused here with a clear message instead of being
|
|
145
|
+
# handed to the resolver. The native layer enforces the same rule; this
|
|
146
|
+
# check only exists so the error names the cause.
|
|
147
|
+
def contained(path)
|
|
148
|
+
s = path.to_s
|
|
149
|
+
if s.split("/").include?("..")
|
|
150
|
+
raise Microsandbox::FilesystemError,
|
|
151
|
+
"path traversal outside volume root (`..` segments are not allowed in volume paths)"
|
|
152
|
+
end
|
|
153
|
+
s
|
|
137
154
|
end
|
|
138
155
|
end
|
|
139
156
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Entry point named after the gem. `require "microsandbox"` resolves to
|
|
4
|
+
# whichever installed gem ships that file first, and the separate
|
|
5
|
+
# `microsandbox` gem on RubyGems ships one too; outside Bundler,
|
|
6
|
+
# `require "microsandbox-rb"` always loads this gem.
|
|
7
|
+
require_relative "microsandbox"
|
data/lib/microsandbox.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
3
5
|
require_relative "microsandbox/version"
|
|
4
6
|
|
|
5
7
|
# Load the compiled native extension. Precompiled platform gems stage the
|
|
@@ -47,6 +49,13 @@ require_relative "microsandbox/sandbox"
|
|
|
47
49
|
# puts sb.exec("python", ["-c", "print('Hello, World!')"]).stdout
|
|
48
50
|
# end
|
|
49
51
|
module Microsandbox
|
|
52
|
+
# Serializes runtime provisioning within this process: {install}, {setup} and
|
|
53
|
+
# the first-use check in {ensure_runtime!}. The installer replaces files in
|
|
54
|
+
# place, so two threads racing through a first `Sandbox.create` must not both
|
|
55
|
+
# run it. A Monitor (re-entrant) because ensure_runtime! calls install.
|
|
56
|
+
RUNTIME_SETUP_LOCK = Monitor.new
|
|
57
|
+
private_constant :RUNTIME_SETUP_LOCK
|
|
58
|
+
|
|
50
59
|
class << self
|
|
51
60
|
# @return [String] the gem version
|
|
52
61
|
def version
|
|
@@ -74,7 +83,7 @@ module Microsandbox
|
|
|
74
83
|
# gem supplies the runtime.
|
|
75
84
|
# @return [nil]
|
|
76
85
|
def install
|
|
77
|
-
Native.install
|
|
86
|
+
RUNTIME_SETUP_LOCK.synchronize { Native.install }
|
|
78
87
|
nil
|
|
79
88
|
end
|
|
80
89
|
|
|
@@ -93,7 +102,7 @@ module Microsandbox
|
|
|
93
102
|
opts["version"] = version.to_s if version
|
|
94
103
|
opts["force"] = true if force
|
|
95
104
|
opts["skip_verify"] = true if skip_verify
|
|
96
|
-
Native.setup(opts)
|
|
105
|
+
RUNTIME_SETUP_LOCK.synchronize { Native.setup(opts) }
|
|
97
106
|
nil
|
|
98
107
|
end
|
|
99
108
|
|
|
@@ -130,34 +139,38 @@ module Microsandbox
|
|
|
130
139
|
# @return [nil]
|
|
131
140
|
def ensure_runtime!
|
|
132
141
|
return if @runtime_ready
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
RUNTIME_SETUP_LOCK.synchronize do
|
|
143
|
+
# Re-check under the lock: another thread may have finished meanwhile.
|
|
144
|
+
return if @runtime_ready
|
|
145
|
+
# A cloud backend has no local msb/libkrunfw runtime to provision: skip the
|
|
146
|
+
# presence check and the first-use download entirely. Resolving the kind
|
|
147
|
+
# uses the same lazy env/profile/config ladder every operation already
|
|
148
|
+
# consults, so this adds no work for local hosts (the common case).
|
|
149
|
+
return if default_backend_kind == :cloud
|
|
150
|
+
# The binaries gem won the resolver: its vendored msb (and the libkrunfw
|
|
151
|
+
# beside it) are exactly the version this gem was built for, so there is
|
|
152
|
+
# nothing to verify or download. (If `MSB_PATH` overrides it, the user owns
|
|
153
|
+
# the runtime and we fall through to the existing behaviour.)
|
|
154
|
+
if bundled_runtime_active?
|
|
155
|
+
@runtime_ready = true
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
# Opted out: the caller manages the runtime out of band, so don't fetch,
|
|
159
|
+
# verify, or repair it here. Memoize the decision (the env var is stable for
|
|
160
|
+
# the process); the operation resolves `msb` itself and surfaces any problem.
|
|
161
|
+
if auto_install_disabled?
|
|
162
|
+
@runtime_ready = true
|
|
163
|
+
return
|
|
164
|
+
end
|
|
153
165
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
unless installed?
|
|
167
|
+
warn "[microsandbox] runtime (msb + libkrunfw) not found; " \
|
|
168
|
+
"downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip, " \
|
|
169
|
+
"or install the microsandbox-rb-binaries gem to ship it with your bundle)..."
|
|
170
|
+
end
|
|
171
|
+
install
|
|
172
|
+
@runtime_ready = true
|
|
158
173
|
end
|
|
159
|
-
install
|
|
160
|
-
@runtime_ready = true
|
|
161
174
|
nil
|
|
162
175
|
end
|
|
163
176
|
|
|
@@ -176,7 +189,9 @@ module Microsandbox
|
|
|
176
189
|
# setter is a no-op — use `MSB_PATH` to override a bundled runtime.
|
|
177
190
|
# @param path [String]
|
|
178
191
|
# @return [void]
|
|
192
|
+
# @raise [ArgumentError] for nil or an empty path (the slot cannot be cleared)
|
|
179
193
|
def runtime_path=(path)
|
|
194
|
+
require_runtime_path!(:runtime_path=, path)
|
|
180
195
|
if @bundled_msb_path && path.to_s != @bundled_msb_path
|
|
181
196
|
warn "[microsandbox] runtime_path= ignored: the microsandbox-rb-binaries gem already " \
|
|
182
197
|
"claimed the set-once SDK slot (#{@bundled_msb_path}); set MSB_PATH to override it"
|
|
@@ -190,7 +205,9 @@ module Microsandbox
|
|
|
190
205
|
# Mirrors {runtime_path=} for libkrunfw.
|
|
191
206
|
# @param path [String]
|
|
192
207
|
# @return [void]
|
|
208
|
+
# @raise [ArgumentError] for nil or an empty path (the slot cannot be cleared)
|
|
193
209
|
def libkrunfw_path=(path)
|
|
210
|
+
require_runtime_path!(:libkrunfw_path=, path)
|
|
194
211
|
Native.set_runtime_libkrunfw_path(path.to_s)
|
|
195
212
|
end
|
|
196
213
|
|
|
@@ -272,6 +289,36 @@ module Microsandbox
|
|
|
272
289
|
raise TypeError, "data must be a String (got #{data.class})"
|
|
273
290
|
end
|
|
274
291
|
|
|
292
|
+
# A Time for an epoch-milliseconds Integer from the native layer, or nil.
|
|
293
|
+
# Integer arithmetic on purpose: `Time.at(ms / 1000.0)` goes through a
|
|
294
|
+
# Float and lands one millisecond low for about half of all timestamps.
|
|
295
|
+
# @api private
|
|
296
|
+
# @param ms [Integer, nil]
|
|
297
|
+
# @return [Time, nil]
|
|
298
|
+
def time_from_ms(ms)
|
|
299
|
+
ms && Time.at(ms.div(1000), ms % 1000, :millisecond)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Run the block, then close +resource+ — the shared tail of the block forms
|
|
303
|
+
# that own a resource whose close can fail (a write sink flushes on close).
|
|
304
|
+
# If the block raised, its exception is the one the caller needs: a failure
|
|
305
|
+
# from the close that follows is dropped instead of replacing it. If the
|
|
306
|
+
# block finished (or left via break/throw), a close failure propagates.
|
|
307
|
+
# @api private
|
|
308
|
+
def close_after(resource)
|
|
309
|
+
raised = false
|
|
310
|
+
yield
|
|
311
|
+
rescue Exception # standard:disable Lint/RescueException -- re-raised unchanged
|
|
312
|
+
raised = true
|
|
313
|
+
raise
|
|
314
|
+
ensure
|
|
315
|
+
begin
|
|
316
|
+
resource.close
|
|
317
|
+
rescue
|
|
318
|
+
raise unless raised
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
275
322
|
private
|
|
276
323
|
|
|
277
324
|
# Auto-provisioning is on by default; any non-empty, non-"0"/"false" value
|
|
@@ -358,12 +405,22 @@ module Microsandbox
|
|
|
358
405
|
private_constant :TRUSTED_BINARIES_OWNERS
|
|
359
406
|
|
|
360
407
|
# Name of the loaded gem whose files define Microsandbox::Binaries, or nil
|
|
361
|
-
# when it came from a bare load path.
|
|
408
|
+
# when it came from a bare load path. Gem roots can nest — a host project
|
|
409
|
+
# that is itself a gem, with its bundle installed under the project
|
|
410
|
+
# directory, is a prefix of every gem in that bundle — so the owner is the
|
|
411
|
+
# gem with the LONGEST matching root, not whichever one is listed first.
|
|
362
412
|
def bundled_runtime_owner
|
|
363
413
|
source = Binaries.method(:msb_path).source_location&.first
|
|
364
414
|
return nil unless source
|
|
365
|
-
|
|
366
|
-
|
|
415
|
+
roots = Gem.loaded_specs.values.select { |s| source.start_with?(File.join(s.full_gem_path, "")) }
|
|
416
|
+
roots.max_by { |s| s.full_gem_path.length }&.name
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The runtime path slots are set-once in the core and have no "unset": nil
|
|
420
|
+
# or "" would be stored as an empty path for the rest of the process.
|
|
421
|
+
def require_runtime_path!(setter, path)
|
|
422
|
+
return unless path.nil? || path.to_s.empty?
|
|
423
|
+
raise ArgumentError, "#{setter} needs a non-empty path (the slot is set-once and cannot be cleared)"
|
|
367
424
|
end
|
|
368
425
|
|
|
369
426
|
# Whether the binaries gem's msb is what the resolver actually returns — i.e.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: microsandbox-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.17.
|
|
4
|
+
version: 0.17.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ya-luotao
|
|
@@ -43,6 +43,17 @@ files:
|
|
|
43
43
|
- DESIGN.md
|
|
44
44
|
- LICENSE
|
|
45
45
|
- README.md
|
|
46
|
+
- SECURITY.md
|
|
47
|
+
- docs/configuration.md
|
|
48
|
+
- docs/errors.md
|
|
49
|
+
- docs/execution.md
|
|
50
|
+
- docs/filesystem.md
|
|
51
|
+
- docs/images.md
|
|
52
|
+
- docs/lifecycle.md
|
|
53
|
+
- docs/observability.md
|
|
54
|
+
- docs/releasing.md
|
|
55
|
+
- docs/runtime.md
|
|
56
|
+
- docs/surface.md
|
|
46
57
|
- ext/microsandbox/Cargo.toml
|
|
47
58
|
- ext/microsandbox/extconf.rb
|
|
48
59
|
- ext/microsandbox/src/agent.rs
|
|
@@ -51,6 +62,7 @@ files:
|
|
|
51
62
|
- ext/microsandbox/src/error.rs
|
|
52
63
|
- ext/microsandbox/src/exec.rs
|
|
53
64
|
- ext/microsandbox/src/fs_stream.rs
|
|
65
|
+
- ext/microsandbox/src/host_path.rs
|
|
54
66
|
- ext/microsandbox/src/image.rs
|
|
55
67
|
- ext/microsandbox/src/lib.rs
|
|
56
68
|
- ext/microsandbox/src/runtime.rs
|
|
@@ -59,6 +71,7 @@ files:
|
|
|
59
71
|
- ext/microsandbox/src/ssh.rs
|
|
60
72
|
- ext/microsandbox/src/stream.rs
|
|
61
73
|
- ext/microsandbox/src/volume.rs
|
|
74
|
+
- lib/microsandbox-rb.rb
|
|
62
75
|
- lib/microsandbox.rb
|
|
63
76
|
- lib/microsandbox/agent.rb
|
|
64
77
|
- lib/microsandbox/backend_info.rb
|