pinesandbox 0.3.18 → 0.3.19

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.
@@ -176,7 +176,7 @@ module PineSandbox
176
176
  provider.available_locations
177
177
  end
178
178
 
179
- # Optional configuration discovery, not a runtime operation prerequisite.
179
+ # Optional configuration discovery, not a lifecycle operation prerequisite.
180
180
  def project_policy
181
181
  provider = @connection.attach_provider
182
182
  raise ArgumentError, "project policy requires a project API key provider" unless provider.respond_to?(:project_policy)
@@ -253,8 +253,8 @@ module PineSandbox
253
253
  location ? provider.register_computer(computer_id, location: location) : provider.register_computer(computer_id)
254
254
  end
255
255
 
256
- # Re-attach an EXISTING persistent Computer by its stored id + state key. A fresh
257
- # runtime is allocated and bound with the Computer's prior state restored.
256
+ # Re-attach an EXISTING persistent Computer by its stored id + state key. The
257
+ # Computer starts and is bound with its prior state restored.
258
258
  # The id may be given positionally or as `id:` (matching #create_computer).
259
259
  def start_computer(*args, **options)
260
260
  if args.first.is_a?(Computer::Credentials)
@@ -264,11 +264,11 @@ module PineSandbox
264
264
  credentials, *args = args
265
265
  options = credentials_options(credentials, options)
266
266
  end
267
- start_existing_computer(*args, **options, recover_runtime: false)
267
+ start_existing_computer(*args, **options, recover: false)
268
268
  end
269
269
 
270
270
  # Gracefully stop the binding observed for this ID, even when a caller lost
271
- # its Start response before saving the runtime token. Never follow a newer
271
+ # its Start response before saving the Computer token. Never follow a newer
272
272
  # binding during this invocation; project authorization is sufficient.
273
273
  def stop_computer(computer_id, **wait_options)
274
274
  raise ArgumentError, "id must be a lowercase UUIDv7" unless computer_id.to_s.match?(Computer::UUID_V7)
@@ -277,27 +277,27 @@ module PineSandbox
277
277
  raise ArgumentError, "Stop by ID requires a provider with current_binding"
278
278
  end
279
279
  reference = provider.current_binding(computer_id)
280
- runtime_id = reference["sandbox_id"]
281
- return true if runtime_id.nil?
280
+ sandbox_id = reference["sandbox_id"]
281
+ return true if sandbox_id.nil?
282
282
 
283
- RuntimeHandle.new(control_plane: @connection.control_plane, runtime_id: runtime_id).stop(**wait_options)
283
+ SandboxHandle.new(control_plane: @connection.control_plane, sandbox_id: sandbox_id).stop(**wait_options)
284
284
  rescue UnknownComputerError
285
285
  true
286
286
  end
287
287
 
288
- # Explicitly discard a failed runtime, wait for release, then Start from
289
- # committed state. This loses unsaved memory; it never replaces a healthy
288
+ # Explicitly shut down a Computer that failed, wait for the shutdown, then
289
+ # Start from committed state. This loses unsaved memory; it never replaces a healthy
290
290
  # paused or running Computer and never silently follows failed Resume.
291
291
  def recover_computer(*args, **options)
292
292
  if args.first.is_a?(Computer::Credentials)
293
293
  credentials, *args = args
294
294
  options = credentials_options(credentials, options)
295
295
  end
296
- start_existing_computer(*args, **options, recover_runtime: true)
296
+ start_existing_computer(*args, **options, recover: true)
297
297
  end
298
298
 
299
299
  def start_existing_computer(positional_id = nil, id: nil, key:, capture_keypair: nil, prior_capture_keypairs: [],
300
- binding_revision: 0, prior_keys: {}, ephemeral: false, location: nil, recover_runtime: false, **attach_opts)
300
+ binding_revision: 0, prior_keys: {}, ephemeral: false, location: nil, recover: false, **attach_opts)
301
301
  if positional_id && id && positional_id != id
302
302
  raise ArgumentError, "attach_computer got two different ids (positional and id:)"
303
303
  end
@@ -305,22 +305,22 @@ module PineSandbox
305
305
  raise ArgumentError, "attach_computer requires the Computer id" if id.nil?
306
306
 
307
307
  provider = @connection.attach_provider
308
- if recover_runtime && !provider.respond_to?(:current_binding)
308
+ if recover && !provider.respond_to?(:current_binding)
309
309
  raise ArgumentError, "saved-state recovery requires a provider with current_binding"
310
310
  end
311
311
  if provider.respond_to?(:current_binding)
312
312
  reference = provider.current_binding(id)
313
313
  binding_revision = reference.fetch("binding_revision")
314
- if (runtime_id = reference["sandbox_id"])
314
+ if (sandbox_id = reference["sandbox_id"])
315
315
  begin
316
- runtime = @connection.control_plane.get(runtime_id)
317
- unless runtime.status == :terminated && runtime.reason == "COMPUTE_RELEASED"
318
- unless recover_runtime || runtime.status == :stopping
319
- raise RuntimeRetainedError.new(runtime_id: runtime_id, state: runtime.status)
316
+ sandbox = @connection.control_plane.get(sandbox_id)
317
+ unless sandbox.status == :terminated && sandbox.reason == "COMPUTE_RELEASED"
318
+ unless recover || sandbox.status == :stopping
319
+ raise SandboxRetainedError.new(sandbox_id: sandbox_id, state: sandbox.status)
320
320
  end
321
- handle = RuntimeHandle.new(control_plane: @connection.control_plane, runtime_id: runtime_id)
322
- unless handle.release_failed_runtime(runtime)
323
- raise TimeoutError, "Old runtime release is unconfirmed; no replacement was allocated"
321
+ handle = SandboxHandle.new(control_plane: @connection.control_plane, sandbox_id: sandbox_id)
322
+ unless handle.release_failed_sandbox(sandbox)
323
+ raise TimeoutError, "The failed Computer's shutdown is unconfirmed; nothing was started"
324
324
  end
325
325
  end
326
326
  rescue NotFoundError
@@ -343,14 +343,14 @@ module PineSandbox
343
343
  alias_method :attach_computer, :start_computer
344
344
 
345
345
  # Adopt an ALREADY-bound, still-live Computer from its persisted `{id, key,
346
- # runtime_id, ct_}` — NO new runtime, NO re-bind. For driving a running Computer
346
+ # sandbox_id, ct_}` — NO new start, NO re-bind. For driving a running Computer
347
347
  # across requests (e.g. re-minting a desktop `dt_` for a reconnect) from a
348
- # backend that cached the `ct_`. Use #start_computer to restore onto a fresh runtime.
349
- # `sandbox_id:` is the deprecated spelling of `runtime_id:`.
350
- def adopt_existing(id:, key:, computer_token:, runtime_id: nil, sandbox_id: nil, status: :running,
348
+ # backend that cached the `ct_`. Use #start_computer to start it again with state
349
+ # restored.
350
+ def adopt_existing(id:, key:, sandbox_id:, computer_token:, status: :running,
351
351
  binding_revision: 0, ephemeral: false)
352
352
  Computer.new(id, key: key, binding_revision: binding_revision, ephemeral: ephemeral)
353
- .adopt(@connection, runtime_id: runtime_id || sandbox_id, computer_token: computer_token,
353
+ .adopt(@connection, sandbox_id: sandbox_id, computer_token: computer_token,
354
354
  status: status, binding_revision: binding_revision)
355
355
  end
356
356
 
@@ -366,7 +366,7 @@ module PineSandbox
366
366
  raise ArgumentError, "credentials and binding belong to different Computers"
367
367
  end
368
368
  computer = adopt_existing(id: credentials.id.dup, key: credentials.key.dup,
369
- runtime_id: binding.runtime_id.dup, computer_token: binding.computer_token.dup,
369
+ sandbox_id: binding.sandbox_id.dup, computer_token: binding.computer_token.dup,
370
370
  binding_revision: binding.binding_revision, ephemeral: credentials.ephemeral)
371
371
  configure_capture_keypairs(computer, credentials.capture_keypair, credentials.prior_capture_keypairs)
372
372
  credentials.prior_keys.each { |version, key| computer.add_prior_key(version, key.dup) }
@@ -374,18 +374,41 @@ module PineSandbox
374
374
  end
375
375
 
376
376
  # Block form: yields a ready Computer and stops it on exit.
377
- # Failed saves and unconfirmed deletion are raised to the caller.
377
+ # Failed saves and unconfirmed deletion are raised to the caller. When the
378
+ # block raises, its error propagates and a failed Stop is only warned about.
378
379
  def with_computer(**opts)
379
380
  computer = create_computer(**opts)
380
- yield computer
381
- ensure
382
- if computer && !computer.stop
383
- raise TimeoutError, "Computer #{computer.id} stop is unconfirmed; retry Stop with its saved binding"
381
+ failure = nil
382
+ begin
383
+ yield computer
384
+ rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised
385
+ failure = e
386
+ raise
387
+ ensure
388
+ # Also runs when the block leaves early with return, break or throw.
389
+ if failure
390
+ stop_after_failure(computer, failure)
391
+ elsif !computer.stop
392
+ raise TimeoutError, "Computer #{computer.id} stop is unconfirmed; retry Stop with its saved binding"
393
+ end
384
394
  end
385
395
  end
386
396
 
387
397
  private
388
398
 
399
+ # Stop after with_computer's block failed: the block's error is the one the
400
+ # caller gets, so a Stop that fails or stays unconfirmed is only reported.
401
+ # The failure is recorded where the block raised, never read from $!, which
402
+ # inside a caller's own rescue clause is the exception that clause handles.
403
+ def stop_after_failure(computer, failure)
404
+ return if computer.stop
405
+
406
+ warn "PineSandbox: Computer #{computer.id} stop is unconfirmed; retry Stop with its saved binding " \
407
+ "(while #{failure.class} propagates)"
408
+ rescue StandardError => e
409
+ warn "PineSandbox: Computer #{computer.id} stop failed (#{e.class}: #{e.message}) while #{failure.class} propagates"
410
+ end
411
+
389
412
  def credentials_options(credentials, options)
390
413
  unless credentials.is_a?(Computer::Credentials)
391
414
  raise ArgumentError, "credentials must be a PineSandbox::Computer::Credentials"
@@ -401,13 +424,13 @@ module PineSandbox
401
424
  computer.attach(@connection, **attach_opts)
402
425
  rescue StandardError => e
403
426
  revision = computer.binding_revision
404
- runtime_id = computer.last_authorized_runtime_id
405
- raise if revision <= initial_revision || runtime_id.to_s.empty?
427
+ sandbox_id = computer.last_authorized_sandbox_id
428
+ raise if revision <= initial_revision || sandbox_id.to_s.empty?
406
429
 
407
430
  raise AttachAuthorizationCommittedError.new(
408
- "attach authorization committed at revision #{revision} for runtime #{runtime_id}, " \
431
+ "attach authorization committed at revision #{revision} for Computer sandbox #{sandbox_id}, " \
409
432
  "but the bind failed: #{e.message}",
410
- binding_revision: revision, runtime_id: runtime_id,
433
+ binding_revision: revision, sandbox_id: sandbox_id,
411
434
  credentials: recovery_credentials,
412
435
  status: (e.respond_to?(:status) ? e.status : nil), cause: e
413
436
  )