Class: Kobako::Context
- Inherits:
-
Object
- Object
- Kobako::Context
- Defined in:
- lib/kobako/context.rb,
sig/kobako/context.rbs
Overview
Kobako::Context — the per-invocation driver behind a single Sandbox#eval
/ #run. Each Context owns a fresh Catalog::Handles table and drives one
guest invocation: it builds the dispatch handler, runs the guest, records
the run's captures and usage, and decodes the outcome. One Context per
invocation owns all per-invocation state, so the reusable Sandbox holds
none and overlapping evals never touch each other's state.
Instance Method Summary collapse
-
#bind(path, object) ⇒ self
Override the object bound at an already-declared
pathfor this invocation only — the per-eval hook the#eval/#runblock uses to fill a fillable or shadow any static / per-invocation binding. -
#build_execution(value, failed:) ⇒ Kobako::Execution
Freeze this run's observables plus
value(+nil+ on a failed run) into the read-onlyExecutionthe caller receives or the error carries. -
#collect_overrides {|arg0| ... } ⇒ void
Run the per-eval override
block, handing it this Context so it can callctx.bind, then spend the Context so a capturedctxused after the block raises. -
#dispatch_handler ⇒ ^(String, Kobako::Transport::_GuestYielder) -> String
Build this invocation's guest→host dispatch handler — a
Procrouting each guest→host call through the statelessTransport::Dispatcher, capturing this Context as the path resolver (its#lookuplayers the per-invocation providers over the static bindings) plus@handler. -
#eval(code, &block) ⇒ void
Execute a guest mruby source string in a fresh
mrb_stateand return the decoded last expression. -
#initialize(runtime:, services:, snippets:, extensions:) ⇒ Context
constructor
Build a Context over the Sandbox-owned config — the
Runtime, the sealedCatalog::Services/Catalog::Snippetsregistries, and theCatalog::Extensionswhose callable backends this Context resolves for its own run. -
#invoke!(verb) { ... } ⇒ Kobako::Execution
Drive one invocation and settle it into a frozen
Execution. -
#lookup(path) ⇒ Object
Resolve a Service
pathto the object backing it this invocation, layering the per-evalctx.bindoverrides over this Context's per-invocation provider results over the Sandbox's static base bindings. -
#populate_observability!(snapshot) ⇒ void
Record this invocation's usage and both output captures from the ext
Snapshot. -
#run(run_envelope, &block) ⇒ void
Dispatch a
Transport::Runenvelope into a preloaded entrypoint and return the decoded result. -
#settle_outcome(snapshot, verb) ⇒ Kobako::Execution
Decode a completed run's outcome into its
Execution. -
#trap_class_for(err) ⇒ singleton(Kobako::TrapError)
Pick the
TrapErrorsubclass to re-raise based on +err+'s actual class. -
#trap_error_for(snapshot, verb) ⇒ Kobako::TrapError
Build the +TrapError+-family exception for a trapped
Snapshotfrom its neutral trap kind, tagged with the verb — the cap subclasses (+TimeoutError+ /MemoryLimitError) keep their identity, every other engine fault is the baseTrapError.
Constructor Details
#initialize(runtime:, services:, snippets:, extensions:) ⇒ Context
Build a Context over the Sandbox-owned config — the Runtime, the
sealed Catalog::Services / Catalog::Snippets registries, and the
Catalog::Extensions whose callable backends this Context resolves for
its own run. The Catalog::Handles table is this invocation's own, so
guest→host dispatch and host→guest auto-wrap share one allocator scoped
to the run; the resolved provider map is likewise per-invocation, so
concurrent invocations never share mutable state.
28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/kobako/context.rb', line 28 def initialize(runtime:, services:, snippets:, extensions:) @runtime = runtime @services = services @snippets = snippets @extensions = extensions @resolved = {} # : Hash[String, untyped] @overrides = {} # : Hash[String, untyped] @spent = false @handler = Catalog::Handles.new @stdout_capture = @stderr_capture = Capture::EMPTY @usage = Usage::EMPTY end |
Instance Method Details
#bind(path, object) ⇒ self
Override the object bound at an already-declared path for this
invocation only — the per-eval hook the #eval / #run block uses to
fill a fillable or shadow any static / per-invocation binding. path
must name a declared (Frame 1) binding; overriding an undeclared path
raises ArgumentError so the Frame 1 key set stays fixed. Valid only
while the block runs — the Context is spent once the block returns, so a
captured ctx used afterward raises ArgumentError too, putting both
misuses on one rescuable channel. Returns self.
49 50 51 52 53 54 55 56 57 |
# File 'lib/kobako/context.rb', line 49 def bind(path, object) raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent key = path.to_s raise ArgumentError, "cannot override undeclared path #{key.inspect}" unless @services.bound?(key) @overrides[key] = object self end |
#build_execution(value, failed:) ⇒ Kobako::Execution
Freeze this run's observables plus value (+nil+ on a failed run) into
the read-only Execution the caller receives or the error carries.
failed records the two apart so a nil value from a successful run
stays distinct from a failed one.
162 163 164 |
# File 'lib/kobako/context.rb', line 162 def build_execution(value, failed:) Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed) end |
#collect_overrides {|arg0| ... } ⇒ void
This method returns an undefined value.
Run the per-eval override block, handing it this Context so it can call
ctx.bind, then spend the Context so a captured ctx used after the
block raises. A block that raises propagates before the guest drives, so
the guest never runs and no Execution is produced.
102 103 104 105 106 |
# File 'lib/kobako/context.rb', line 102 def collect_overrides yield self ensure @spent = true end |
#dispatch_handler ⇒ ^(String, Kobako::Transport::_GuestYielder) -> String
Build this invocation's guest→host dispatch handler — a Proc routing
each guest→host call through the stateless Transport::Dispatcher,
capturing this Context as the path resolver (its #lookup layers the
per-invocation providers over the static bindings) plus @handler. Handed to
Runtime#eval / #run as a call argument, so the Runtime holds no
dispatch state and the Proc stays GC-rooted as a live argument for the
synchronous call. The ext hands the Proc a per-dispatch guest_yielder
— a String → String callable that re-enters the in-flight guest to run
a yielded block — which the Dispatcher forwards to the
Transport::Yielder it builds for the call.
118 119 120 121 122 |
# File 'lib/kobako/context.rb', line 118 def dispatch_handler lambda do |request_bytes, guest_yielder| Transport::Dispatcher.dispatch(request_bytes, self, @handler, guest_yielder) end end |
#eval(code, &block) ⇒ void
This method returns an undefined value.
Execute a guest mruby source string in a fresh mrb_state and return the
decoded last expression. A given block runs first, receiving this
Context to collect ctx.bind overrides before the guest drives.
79 80 81 82 83 84 |
# File 'lib/kobako/context.rb', line 79 def eval(code, &block) collect_overrides(&block) if block invoke!(:eval) do @runtime.eval(dispatch_handler, @services.encode, code.b, @snippets.encode) end end |
#invoke!(verb) { ... } ⇒ Kobako::Execution
Drive one invocation and settle it into a frozen Execution. verb
tags the TrapError message so the failing export is identifiable. This
invocation's callable Extension backends are resolved first — before the
guest runs — so a provider that raises propagates unwrapped and leaves
the guest unrun. Usage and captures are recorded before the trap check,
so a trapped Snapshot's error carries them just like a completed run's
return value. A could-not-start fault ran no invocation at all, so it
carries no Execution and gains only the verb prefix.
189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/kobako/context.rb', line 189 def invoke!(verb) @resolved = @extensions.resolve begin snapshot = yield rescue Kobako::TrapError => e raise trap_class_for(e), "Sandbox##{verb} failed: #{e.}" end populate_observability!(snapshot) return settle_outcome(snapshot, verb) unless snapshot.trapped? raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true)) end |
#lookup(path) ⇒ Object
Resolve a Service path to the object backing it this invocation,
layering the per-eval ctx.bind overrides over this Context's
per-invocation provider results over the Sandbox's static base bindings.
An unbound path raises KeyError; a fillable left unfilled resolves to
Kobako::Unresolved and is reported the same way, so an unresolved
capability fails closed as an undefined target rather than dispatching to
the sentinel. The Dispatcher maps either KeyError to an
undefined-target wire fault. Internal — the per-invocation dispatch
handler is the sole caller.
68 69 70 71 72 73 74 |
# File 'lib/kobako/context.rb', line 68 def lookup(path) key = path.to_s object = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } } raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(object) object end |
#populate_observability!(snapshot) ⇒ void
This method returns an undefined value.
Record this invocation's usage and both output captures from the ext
Snapshot. Every Snapshot carries them — value return or trap alike — so
#usage / #stdout / #stderr stay readable after a rescued trap.
127 128 129 130 131 |
# File 'lib/kobako/context.rb', line 127 def populate_observability!(snapshot) @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak) @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?) @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?) end |
#run(run_envelope, &block) ⇒ void
This method returns an undefined value.
Dispatch a Transport::Run envelope into a preloaded entrypoint and
return the decoded result. A given block runs first, receiving this
Context to collect ctx.bind overrides before the guest drives.
89 90 91 92 93 94 |
# File 'lib/kobako/context.rb', line 89 def run(run_envelope, &block) collect_overrides(&block) if block invoke!(:run) do @runtime.run(dispatch_handler, @services.encode, @snippets.encode, run_envelope.encode(@handler)) end end |
#settle_outcome(snapshot, verb) ⇒ Kobako::Execution
Decode a completed run's outcome into its Execution. A Capability
Handle in the result is restored to its host object first. Decode sits in
the rescue so a wire-violation trap or a Panic envelope both attach this
run's Execution, just like a guest-call trap does.
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/kobako/context.rb', line 170 def settle_outcome(snapshot, verb) value, carried = Codec.track_handles { Outcome.decode(snapshot.outcome) } value = Codec::HandleWalk.deep_restore(value, @handler) if carried build_execution(value, failed: false) rescue Kobako::TrapError => e raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.}").with_execution(build_execution(nil, failed: true)) rescue Kobako::SandboxError, Kobako::ServiceError => e raise e.with_execution(build_execution(nil, failed: true)) end |
#trap_class_for(err) ⇒ singleton(Kobako::TrapError)
Pick the TrapError subclass to re-raise based on +err+'s actual class.
Cap-trap subclasses (+TimeoutError+ / MemoryLimitError) preserve their
named identity; everything else collapses to the base Kobako::TrapError,
so #invoke! can add the verb prefix without erasing the named subclass.
137 138 139 140 141 142 143 |
# File 'lib/kobako/context.rb', line 137 def trap_class_for(err) case err when TimeoutError then TimeoutError when MemoryLimitError then MemoryLimitError else TrapError end end |
#trap_error_for(snapshot, verb) ⇒ Kobako::TrapError
Build the +TrapError+-family exception for a trapped Snapshot from its
neutral trap kind, tagged with the verb — the cap subclasses
(+TimeoutError+ / MemoryLimitError) keep their identity, every other
engine fault is the base TrapError.
149 150 151 152 153 154 155 156 |
# File 'lib/kobako/context.rb', line 149 def trap_error_for(snapshot, verb) klass = case snapshot.trap_kind when :timeout then TimeoutError when :memory_limit then MemoryLimitError else TrapError end klass.new("Sandbox##{verb} failed: #{snapshot.}") end |