microsandbox-rb 0.17.1 → 0.17.3

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.
@@ -102,6 +102,14 @@ module Microsandbox
102
102
  define_error(:CloudHttpError, "cloud-http")
103
103
  define_error(:UnsupportedError, "unsupported")
104
104
 
105
+ # Process model ------------------------------------------------------------
106
+ # A handle (a Sandbox, a SandboxHandle, an ExecHandle, a stream, an SSH or
107
+ # agent client, a VolumeFs, ...) was used in a process forked from the one
108
+ # that created it. Handles are bound to their creating process; a forked
109
+ # child opens its own (`Sandbox.get(name).connect`, `Volume.fs(name)`, ...).
110
+ # The message names both pids. Ruby-only: no other SDK has a counterpart.
111
+ define_error(:ForkedHandleError, "forked-handle")
112
+
105
113
  # As of runtime v0.6.8 the core keys Unsupported errors by a structured
106
114
  # (operation, reason) pair. The native layer renders both into the message
107
115
  # ("sandbox.kill is not supported by this backend: ...") and also attaches
@@ -496,7 +496,10 @@ module Microsandbox
496
496
  def create(name, **kwargs, &block)
497
497
  opts = build_create_opts(**kwargs)
498
498
  Microsandbox.ensure_runtime!
499
- sandbox = new(Native::Sandbox.create(name.to_s, opts))
499
+ # Everything Ruby-level happens BEFORE `acquire`: its interrupt mask must
500
+ # cover only the native call plus the wrap, never caller-supplied code.
501
+ sandbox_name = name.to_s
502
+ sandbox = acquire { Native::Sandbox.create(sandbox_name, opts) }
500
503
  return sandbox unless block_given?
501
504
 
502
505
  begin
@@ -553,7 +556,8 @@ module Microsandbox
553
556
  def connect_or_create(name, **kwargs, &block)
554
557
  opts = build_create_opts(**kwargs)
555
558
  Microsandbox.ensure_runtime!
556
- sandbox = new(Native::Sandbox.connect_or_create(name.to_s, opts))
559
+ sandbox_name = name.to_s # outside the interrupt mask, like in {create}
560
+ sandbox = acquire { Native::Sandbox.connect_or_create(sandbox_name, opts) }
557
561
  return sandbox unless block_given?
558
562
 
559
563
  begin
@@ -588,6 +592,65 @@ module Microsandbox
588
592
  PullSession.new(Native::Sandbox.create_with_progress(name.to_s, opts))
589
593
  end
590
594
 
595
+ # @api private
596
+ # Wrap the native sandbox the block acquires, and stop it again if an
597
+ # interrupt cuts in before the caller has it.
598
+ #
599
+ # A native call cannot be interrupted while it runs, so a `Thread#raise`
600
+ # (what `Timeout.timeout` uses) or a `Thread#kill` that arrives during the
601
+ # block is delivered after it — at the next interrupt checkpoint, which is
602
+ # still in here, before the caller ever sees the object. Left alone the
603
+ # sandbox would be garbage and only GC would stop it, "eventually"; for
604
+ # `Timeout.timeout { Sandbox.create(...) }` that is not good enough. So:
605
+ #
606
+ # - the acquisition runs under `Thread.handle_interrupt(Object => :never)`,
607
+ # which holds a queued exception (or kill) back until the wrapper is
608
+ # assigned. The mask covers only the uninterruptible native call plus
609
+ # the wrap, so it changes no observable timing; runtime provisioning
610
+ # ({Microsandbox.ensure_runtime!}) and the block forms' `yield` stay
611
+ # outside it, with normal interrupt handling. For the same reason the
612
+ # block passed here must contain NOTHING but the native call: argument
613
+ # conversion (`name.to_s`, option Hashes) is the caller's own code and
614
+ # has to run before `acquire`, where it stays interruptible;
615
+ # - the `ensure` plus the `acquired` flag — not `rescue Exception; raise` —
616
+ # is what also covers `Thread#kill`, which no `rescue` can catch.
617
+ #
618
+ # A SIGINT (Ctrl-C on the main thread) is the exception: it is not queued
619
+ # like those two and cannot be masked. One that lands during the native
620
+ # call is raised the moment the call returns — the VM checks interrupts
621
+ # as it pops the native method's frame, before any Ruby code here can
622
+ # hold the result — so no assignment order would catch it; the sandbox
623
+ # is then torn down when the unreferenced object is garbage-collected.
624
+ #
625
+ # Ownership: {connect_or_create} can hand back a sandbox that was already
626
+ # running for someone else, and {start} restarts a persisted one. The
627
+ # cleanup therefore applies EXACTLY the predicate the block form of
628
+ # {connect_or_create} uses — {Sandbox#owns_lifecycle?} — in all three
629
+ # callers, {create} included: a sandbox this call does not own is never
630
+ # stopped here. Its reference is just let go (GC closes the handle, the
631
+ # sandbox keeps running).
632
+ # @yieldreturn [Native::Sandbox]
633
+ # @return [Sandbox]
634
+ def acquire
635
+ sandbox = nil
636
+ acquired = false
637
+ begin
638
+ Thread.handle_interrupt(Object => :never) do
639
+ sandbox = new(yield)
640
+ end
641
+ acquired = true
642
+ ensure
643
+ if sandbox && !acquired && sandbox.owns_lifecycle?
644
+ begin
645
+ sandbox.stop
646
+ rescue Microsandbox::Error
647
+ # best-effort cleanup; the interrupt is what the caller must see
648
+ end
649
+ end
650
+ end
651
+ sandbox
652
+ end
653
+
591
654
  # @api private
592
655
  # Shared keyword-option builder for {create}/{connect_or_create}/
593
656
  # {create_with_progress}. Pure validation and normalization: the callers
@@ -705,7 +768,9 @@ module Microsandbox
705
768
  # @return [Sandbox]
706
769
  def start(name, detached: false)
707
770
  Microsandbox.ensure_runtime!
708
- new(Native::Sandbox.start(name.to_s, {"detached" => detached}))
771
+ sandbox_name = name.to_s # outside the interrupt mask, like in {create}
772
+ opts = {"detached" => detached}
773
+ acquire { Native::Sandbox.start(sandbox_name, opts) }
709
774
  end
710
775
 
711
776
  # Fetch a controllable handle for a sandbox by name (running or not).
@@ -1998,10 +2063,16 @@ module Microsandbox
1998
2063
  # {Sandbox} is live — stop it when done. Memoized: the native result can be
1999
2064
  # taken only once, so a later call returns the same {Sandbox}, or re-raises
2000
2065
  # the same error if the create failed.
2066
+ #
2067
+ # Interrupt-safe like {Sandbox.create}: if a `Timeout.timeout` (or another
2068
+ # `Thread#raise`/`Thread#kill`) arrives while this is waiting, the sandbox
2069
+ # the session produced is stopped before the exception propagates rather
2070
+ # than left running with nothing referring to it. The result is consumed
2071
+ # by then, so a later call raises instead of returning a sandbox.
2001
2072
  # @return [Sandbox]
2002
2073
  def sandbox
2003
2074
  raise @failure if @failure
2004
- @sandbox ||= Sandbox.new(@native.result)
2075
+ @sandbox ||= Sandbox.acquire { @native.result }
2005
2076
  rescue Microsandbox::Error => e
2006
2077
  @failure ||= e
2007
2078
  raise
@@ -8,7 +8,7 @@ module Microsandbox
8
8
  # Versioning section of the README for the full gem-to-runtime map. Must equal
9
9
  # the native ext's Cargo crate version (`Native.version`), enforced by
10
10
  # spec/unit/version_spec.rb.
11
- VERSION = "0.17.1"
11
+ VERSION = "0.17.3"
12
12
 
13
13
  # The upstream microsandbox runtime release this gem build embeds — the `tag`
14
14
  # pinned on the `microsandbox`/`microsandbox-network` git deps in
data/lib/microsandbox.rb CHANGED
@@ -240,6 +240,15 @@ module Microsandbox
240
240
  # avoid calling {set_default_backend} inside the block (the restore on exit
241
241
  # would overwrite that change). Mirrors the official SDKs' scoped-backend helper.
242
242
  #
243
+ # The restore can fail in one situation: the previous backend belonged to a
244
+ # parent process (the block is left in a child forked while it was running,
245
+ # or it is a forked child's first use of the SDK) and cannot be chosen
246
+ # again there — it came from the environment or config file, which no
247
+ # longer resolve. The block's temporary selection then stays in effect. If
248
+ # the block itself raised, its exception is the one that propagates and the
249
+ # restore failure is only warned about; otherwise the restore failure is
250
+ # raised.
251
+ #
243
252
  # @param kind ["local","cloud", Symbol]
244
253
  # @param url [String, nil]
245
254
  # @param api_key [String, nil]
@@ -248,10 +257,20 @@ module Microsandbox
248
257
  # @return [Object] the block's return value
249
258
  def with_backend(kind, url: nil, api_key: nil, profile: nil)
250
259
  token = Native.push_default_backend(kind.to_s, url&.to_s, api_key&.to_s, profile&.to_s)
260
+ raised = false
251
261
  begin
252
262
  yield
263
+ rescue Exception # standard:disable Lint/RescueException -- only noted, re-raised at once
264
+ raised = true
265
+ raise
253
266
  ensure
254
- Native.pop_default_backend(token)
267
+ begin
268
+ Native.pop_default_backend(token)
269
+ rescue Microsandbox::Error => e
270
+ # Never let the restore failure replace what the block raised.
271
+ raise unless raised
272
+ warn "microsandbox: with_backend could not restore the previous backend: #{e.message}"
273
+ end
255
274
  end
256
275
  end
257
276
 
data/sig/microsandbox.rbs CHANGED
@@ -64,6 +64,7 @@ module Microsandbox
64
64
  class MetricsUnavailableError < Error end
65
65
  class UnsupportedOperationError < Error end
66
66
  class CloudHttpError < Error end
67
+ class ForkedHandleError < Error end
67
68
  class UnsupportedError < Error
68
69
  def operation: () -> String?
69
70
  def hint: () -> String?
@@ -82,6 +83,7 @@ module Microsandbox
82
83
  end
83
84
 
84
85
  class FsEntry
86
+ def initialize: (Hash[String, untyped] data) -> void
85
87
  def path: () -> String
86
88
  def name: () -> String
87
89
  def type: () -> Symbol
@@ -94,6 +96,7 @@ module Microsandbox
94
96
  end
95
97
 
96
98
  class FsMetadata
99
+ def initialize: (Hash[String, untyped] data) -> void
97
100
  def type: () -> Symbol
98
101
  def size: () -> Integer
99
102
  def mode: () -> Integer
@@ -106,6 +109,7 @@ module Microsandbox
106
109
  end
107
110
 
108
111
  class FS
112
+ def initialize: (untyped native) -> void
109
113
  def read: (String path) -> String
110
114
  def read_text: (String path) -> String
111
115
  def write: (String path, String data) -> nil
@@ -125,17 +129,20 @@ module Microsandbox
125
129
 
126
130
  class FsReadStream
127
131
  include Enumerable[String]
132
+ def initialize: (untyped native) -> void
128
133
  def each: () { (String) -> void } -> self
129
134
  | () -> Enumerator[String, self]
130
135
  def read: () -> String
131
136
  end
132
137
 
133
138
  class FsWriteSink
139
+ def initialize: (untyped native) -> void
134
140
  def write: (String data) -> self
135
141
  def close: () -> nil
136
142
  end
137
143
 
138
144
  class Metrics
145
+ def initialize: (Hash[String, untyped] data) -> void
139
146
  def cpu_percent: () -> Float
140
147
  def vcpu_time_ns: () -> Integer
141
148
  def memory_bytes: () -> Integer
@@ -154,6 +161,7 @@ module Microsandbox
154
161
  end
155
162
 
156
163
  class LogEntry
164
+ def initialize: (Hash[String, untyped] entry) -> void
157
165
  def source: () -> Symbol
158
166
  def session_id: () -> Integer?
159
167
  def cursor: () -> String
@@ -163,6 +171,7 @@ module Microsandbox
163
171
  end
164
172
 
165
173
  class SandboxHandle
174
+ def initialize: (untyped native) -> void
166
175
  def name: () -> String
167
176
  def id: () -> String
168
177
  def status: () -> Symbol
@@ -200,6 +209,7 @@ module Microsandbox
200
209
  SandboxInfo: singleton(SandboxHandle)
201
210
 
202
211
  class SandboxStopResult
212
+ def initialize: (Hash[String, untyped] data) -> void
203
213
  def name: () -> String
204
214
  def status: () -> Symbol
205
215
  def exit_code: () -> Integer?
@@ -290,6 +300,7 @@ module Microsandbox
290
300
  def self.list_with: (?labels: Hash[untyped, untyped], ?limit: Integer?, ?cursor: String?) -> SandboxPage
291
301
  def self.remove: (String name) -> nil
292
302
 
303
+ def initialize: (untyped native) -> void
293
304
  def name: () -> String
294
305
  def id: () -> String
295
306
  def exec: (String command, ?Array[String] args, ?cwd: String?, ?user: String?,
@@ -354,17 +365,20 @@ module Microsandbox
354
365
 
355
366
  class LogStream
356
367
  include Enumerable[LogEntry]
368
+ def initialize: (untyped native) -> void
357
369
  def each: () { (LogEntry) -> void } -> self
358
370
  | () -> Enumerator[LogEntry, self]
359
371
  end
360
372
 
361
373
  class MetricsStream
362
374
  include Enumerable[Metrics]
375
+ def initialize: (untyped native) -> void
363
376
  def each: () { (Metrics) -> void } -> self
364
377
  | () -> Enumerator[Metrics, self]
365
378
  end
366
379
 
367
380
  class ExecEvent
381
+ def initialize: (Hash[String, untyped] event) -> void
368
382
  def type: () -> Symbol
369
383
  def pid: () -> Integer?
370
384
  def code: () -> Integer?
@@ -379,18 +393,21 @@ module Microsandbox
379
393
  end
380
394
 
381
395
  class ExitStatus
396
+ def initialize: (Hash[String, untyped] data) -> void
382
397
  def exit_code: () -> Integer?
383
398
  def success?: () -> bool
384
399
  def failure?: () -> bool
385
400
  end
386
401
 
387
402
  class ExecStdin
403
+ def initialize: (untyped native) -> void
388
404
  def write: (String data) -> self
389
405
  def close: () -> nil
390
406
  end
391
407
 
392
408
  class ExecHandle
393
409
  include Enumerable[ExecEvent]
410
+ def initialize: (untyped native) -> void
394
411
  def id: () -> String
395
412
  def each: () { (ExecEvent) -> void } -> self
396
413
  | () -> Enumerator[ExecEvent, self]
@@ -403,6 +420,7 @@ module Microsandbox
403
420
  end
404
421
 
405
422
  class ImageInfo
423
+ def initialize: (Hash[String, untyped] data) -> void
406
424
  def reference: () -> String
407
425
  def size_bytes: () -> Integer?
408
426
  def manifest_digest: () -> String?
@@ -414,6 +432,7 @@ module Microsandbox
414
432
  end
415
433
 
416
434
  class ImageDetail
435
+ def initialize: (Hash[String, untyped] data) -> void
417
436
  def handle: () -> ImageInfo
418
437
  def config: () -> Hash[String, untyped]?
419
438
  def layers: () -> Array[Hash[String, untyped]]
@@ -421,6 +440,7 @@ module Microsandbox
421
440
  end
422
441
 
423
442
  class ImagePruneReport
443
+ def initialize: (Hash[String, untyped] data) -> void
424
444
  def image_refs_removed: () -> Integer
425
445
  def manifests_removed: () -> Integer
426
446
  def layers_removed: () -> Integer
@@ -441,6 +461,7 @@ module Microsandbox
441
461
  end
442
462
 
443
463
  class VolumeInfo
464
+ def initialize: (Hash[String, untyped] data) -> void
444
465
  def name: () -> String
445
466
  def path: () -> String?
446
467
  def kind: () -> Symbol?
@@ -481,6 +502,7 @@ module Microsandbox
481
502
  end
482
503
 
483
504
  class SnapshotInfo
505
+ def initialize: (Hash[String, untyped] data) -> void
484
506
  def digest: () -> String
485
507
  def path: () -> String
486
508
  def name: () -> String?
@@ -506,6 +528,7 @@ module Microsandbox
506
528
  end
507
529
 
508
530
  class SnapshotVerifyReport
531
+ def initialize: (Hash[String, untyped] data) -> void
509
532
  def digest: () -> String
510
533
  def path: () -> String
511
534
  def status: () -> Symbol
@@ -569,6 +592,7 @@ module Microsandbox
569
592
  end
570
593
 
571
594
  class BackendInfo
595
+ def initialize: (Hash[String, untyped] data) -> void
572
596
  def kind: () -> Symbol
573
597
  def api_url: () -> String?
574
598
  def source: () -> Symbol
@@ -643,6 +667,7 @@ module Microsandbox
643
667
 
644
668
  class AgentStream
645
669
  include Enumerable[AgentFrame]
670
+ def initialize: (untyped native, Integer id, Integer handle) -> void
646
671
  def id: () -> Integer
647
672
  def recv: () -> AgentFrame?
648
673
  def each: () { (AgentFrame) -> void } -> self
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.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao