Class: Kobako::Context

Inherits:
Object
  • Object
show all
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

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.

Parameters:

  • path (Symbol, String)
  • object (Object)

Returns:

  • (self)

Raises:

  • (ArgumentError)


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.

Parameters:

  • value (Object)
  • failed: (Boolean)

Returns:



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.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


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.

Returns:



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.

Parameters:

  • code (String)


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.

Parameters:

  • verb (Symbol)

Yields:

Yield Returns:

Returns:



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.message}"
  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.

Parameters:

  • path (Symbol, String)

Returns:

  • (Object)

Raises:

  • (KeyError)


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.

Parameters:



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.

Parameters:



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.

Parameters:

Returns:



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.message}").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.

Parameters:

Returns:



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.

Parameters:

Returns:



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.trap_message}")
end