kobako 0.9.1 → 0.10.0
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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +36 -0
- data/Cargo.lock +3 -1
- data/Cargo.toml +6 -0
- data/README.md +35 -14
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +11 -1
- data/ext/kobako/src/runtime/ambient.rs +78 -0
- data/ext/kobako/src/runtime/cache.rs +168 -4
- data/ext/kobako/src/runtime/dispatch.rs +2 -2
- data/ext/kobako/src/runtime/exports.rs +32 -21
- data/ext/kobako/src/runtime/instance_pre.rs +97 -0
- data/ext/kobako/src/runtime/invocation.rs +13 -70
- data/ext/kobako/src/runtime.rs +373 -369
- data/lib/kobako/catalog/handles.rb +26 -7
- data/lib/kobako/catalog/namespaces.rb +22 -4
- data/lib/kobako/catalog/snippets.rb +9 -1
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec.rb +5 -6
- data/lib/kobako/errors.rb +11 -1
- data/lib/kobako/namespace.rb +16 -2
- data/lib/kobako/pool.rb +182 -0
- data/lib/kobako/sandbox.rb +16 -14
- data/lib/kobako/transport/dispatcher.rb +29 -10
- data/lib/kobako/transport/request.rb +1 -9
- data/lib/kobako/transport/response.rb +9 -2
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +17 -3
- data/sig/kobako/catalog/handles.rbs +4 -0
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +4 -0
- data/sig/kobako/transport/request.rbs +0 -3
- data/sig/kobako/transport/response.rbs +3 -0
- metadata +5 -1
|
@@ -53,14 +53,9 @@ module Kobako
|
|
|
53
53
|
# is reserved for the codec's wire-decode path, where the id is
|
|
54
54
|
# the only thing the bytes carry.
|
|
55
55
|
def alloc(object)
|
|
56
|
+
reject_unwrappable!(object)
|
|
57
|
+
ensure_capacity!
|
|
56
58
|
id = @next_id
|
|
57
|
-
cap = Kobako::Handle::MAX_ID
|
|
58
|
-
if id > cap
|
|
59
|
-
raise HandlerExhaustedError,
|
|
60
|
-
"Out of handle allocations: too many host objects were referenced " \
|
|
61
|
-
"in a single invocation (limit #{cap})"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
59
|
@entries[id] = object
|
|
65
60
|
@next_id = id + 1
|
|
66
61
|
Kobako::Handle.restore(id)
|
|
@@ -94,6 +89,30 @@ module Kobako
|
|
|
94
89
|
|
|
95
90
|
private
|
|
96
91
|
|
|
92
|
+
# Refuse to mint a Capability Handle for a reflective gadget
|
|
93
|
+
# ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): a +Binding+ /
|
|
94
|
+
# +Method+ / +UnboundMethod+ would hand the guest a callable proxy onto
|
|
95
|
+
# host reflection (a returned +Binding+ reaches +Binding#eval+). Raising
|
|
96
|
+
# here keeps the rule at the single mint point, so it holds on both the
|
|
97
|
+
# Service-return (B-14) and the +#run+ host→guest auto-wrap (B-34) paths.
|
|
98
|
+
def reject_unwrappable!(object)
|
|
99
|
+
case object
|
|
100
|
+
when Binding, Method, UnboundMethod
|
|
101
|
+
raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Guard {#alloc} against issuing an ID past the B-21 cap. Returns +nil+
|
|
106
|
+
# on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
|
|
107
|
+
def ensure_capacity!
|
|
108
|
+
cap = Kobako::Handle::MAX_ID
|
|
109
|
+
return unless @next_id > cap
|
|
110
|
+
|
|
111
|
+
raise HandlerExhaustedError,
|
|
112
|
+
"Out of handle allocations: too many host objects were referenced " \
|
|
113
|
+
"in a single invocation (limit #{cap})"
|
|
114
|
+
end
|
|
115
|
+
|
|
97
116
|
# Single source of truth for the "unknown Handle id" raise used by
|
|
98
117
|
# {#fetch}. Returns +nil+ on success; raises +Kobako::SandboxError+
|
|
99
118
|
# when +id+ is not currently bound.
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require_relative "handles"
|
|
4
4
|
require_relative "../codec"
|
|
5
5
|
require_relative "../errors"
|
|
6
|
-
require_relative "../transport/request"
|
|
7
6
|
require_relative "../namespace"
|
|
8
7
|
|
|
9
8
|
module Kobako
|
|
@@ -38,6 +37,7 @@ module Kobako
|
|
|
38
37
|
@namespaces = {} # : Hash[String, Kobako::Namespace]
|
|
39
38
|
@handler = handler
|
|
40
39
|
@sealed = false
|
|
40
|
+
@encoded = nil # : String?
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Declare or retrieve the Namespace named +name+ (idempotent —
|
|
@@ -81,14 +81,32 @@ module Kobako
|
|
|
81
81
|
# Arrays, so none of the kobako ext types actually fire. Structure:
|
|
82
82
|
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
83
83
|
# +String+ of msgpack bytes.
|
|
84
|
+
#
|
|
85
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
86
|
+
# subsequent invocation: B-33 seals Service registration (B-07 /
|
|
87
|
+
# B-08) at the first invocation, so the preamble is exactly the
|
|
88
|
+
# bindings that existed at that moment — a bind reaching a
|
|
89
|
+
# +Kobako::Namespace+ after the seal raises +ArgumentError+ (E-45)
|
|
90
|
+
# and never alters Frame 1.
|
|
84
91
|
def encode
|
|
85
|
-
|
|
92
|
+
return @encoded if @encoded
|
|
93
|
+
|
|
94
|
+
bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
|
|
95
|
+
@encoded = bytes if @sealed
|
|
96
|
+
bytes
|
|
86
97
|
end
|
|
87
98
|
|
|
88
|
-
# Mark the registry as sealed
|
|
89
|
-
#
|
|
99
|
+
# Mark the registry as sealed and propagate the seal to every
|
|
100
|
+
# declared +Kobako::Namespace+
|
|
101
|
+
# ({docs/behavior.md B-33}[link:../../../docs/behavior.md]). Called
|
|
102
|
+
# by +Sandbox+ on the first invocation. After sealing, #define
|
|
103
|
+
# raises ArgumentError (E-18) and +Namespace#bind+ raises
|
|
104
|
+
# ArgumentError (E-45). Idempotent.
|
|
90
105
|
def seal!
|
|
106
|
+
return self if @sealed
|
|
107
|
+
|
|
91
108
|
@sealed = true
|
|
109
|
+
@namespaces.each_value(&:seal!)
|
|
92
110
|
self
|
|
93
111
|
end
|
|
94
112
|
|
|
@@ -31,6 +31,7 @@ module Kobako
|
|
|
31
31
|
|
|
32
32
|
def initialize
|
|
33
33
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
34
|
+
@encoded = nil # : String?
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Serialize the registered snippets to wire bytes. Each entry
|
|
@@ -42,8 +43,14 @@ module Kobako
|
|
|
42
43
|
# carriers — this collection-tier method reads their attributes
|
|
43
44
|
# externally via +entry_payload+ rather than asking each entry to
|
|
44
45
|
# self-encode.
|
|
46
|
+
#
|
|
47
|
+
# The bytes are memoized — the table is replayed verbatim on every
|
|
48
|
+
# invocation after B-33 seals it, so Frame 3 never changes between
|
|
49
|
+
# encodes; {#register} drops the memo while the table is still open.
|
|
45
50
|
def encode
|
|
46
|
-
|
|
51
|
+
return @encoded if @encoded
|
|
52
|
+
|
|
53
|
+
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
# Register one preloaded snippet in either of two forms
|
|
@@ -68,6 +75,7 @@ module Kobako
|
|
|
68
75
|
# missing keywords, wrong types, malformed +name+ (E-34), or
|
|
69
76
|
# duplicate +code:+ +name+ (E-33).
|
|
70
77
|
def register(code: nil, name: nil, binary: nil)
|
|
78
|
+
@encoded = nil
|
|
71
79
|
if binary
|
|
72
80
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
73
81
|
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -64,7 +64,11 @@ module Kobako
|
|
|
64
64
|
case value
|
|
65
65
|
when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
|
|
66
66
|
when Array then value.each { |v| validate_utf8!(v) }
|
|
67
|
-
when Hash
|
|
67
|
+
when Hash
|
|
68
|
+
value.each do |key, val|
|
|
69
|
+
validate_utf8!(key)
|
|
70
|
+
validate_utf8!(val)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
end
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "codec/error"
|
|
4
|
+
require_relative "codec/utils"
|
|
5
|
+
require_relative "codec/factory"
|
|
6
|
+
require_relative "codec/encoder"
|
|
7
|
+
require_relative "codec/decoder"
|
|
4
8
|
|
|
5
9
|
module Kobako
|
|
6
10
|
# Host-side MessagePack codec for the kobako wire contract — the
|
|
@@ -17,15 +21,10 @@ module Kobako
|
|
|
17
21
|
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
18
22
|
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
19
23
|
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
20
|
-
# mirrors this layer as the +codec+ module in the +kobako-
|
|
24
|
+
# mirrors this layer as the +codec+ module in the +kobako-core+ crate;
|
|
21
25
|
# the ext-code constants live as module-private values on {Factory}
|
|
22
26
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
23
27
|
# +codec::EXT_ERRENV+ on that side.
|
|
24
28
|
module Codec
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
|
-
|
|
28
|
-
require_relative "codec/utils"
|
|
29
|
-
require_relative "codec/factory"
|
|
30
|
-
require_relative "codec/encoder"
|
|
31
|
-
require_relative "codec/decoder"
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# call that failed and was not rescued inside the
|
|
22
22
|
# script).
|
|
23
23
|
#
|
|
24
|
-
#
|
|
24
|
+
# Two further branches sit outside the invocation taxonomy:
|
|
25
25
|
#
|
|
26
26
|
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
27
|
# when the wasm runtime cannot be built from the
|
|
@@ -29,6 +29,9 @@ module Kobako
|
|
|
29
29
|
# ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
|
|
30
30
|
# Not an invocation outcome, so it never passes
|
|
31
31
|
# through the two-step attribution decision.
|
|
32
|
+
# * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
|
|
33
|
+
# when the checkout wait exceeds +checkout_timeout+
|
|
34
|
+
# ({docs/behavior.md E-46}[link:../../docs/behavior.md]).
|
|
32
35
|
#
|
|
33
36
|
# Subclasses pinned by docs/behavior.md Error Classes:
|
|
34
37
|
#
|
|
@@ -137,4 +140,11 @@ module Kobako
|
|
|
137
140
|
# snippet failures while callers wanting bytecode-specific handling
|
|
138
141
|
# can `rescue Kobako::BytecodeError` directly.
|
|
139
142
|
class BytecodeError < SandboxError; end
|
|
143
|
+
|
|
144
|
+
# Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
|
|
145
|
+
# wait exceeded the configured +checkout_timeout+ while every slot was
|
|
146
|
+
# held ({docs/behavior.md E-46}[link:../../docs/behavior.md]). No
|
|
147
|
+
# Sandbox state is touched — retrying succeeds as soon as a holder
|
|
148
|
+
# returns its Sandbox.
|
|
149
|
+
class PoolTimeoutError < Error; end
|
|
140
150
|
end
|
data/lib/kobako/namespace.rb
CHANGED
|
@@ -18,15 +18,20 @@ module Kobako
|
|
|
18
18
|
def initialize(name)
|
|
19
19
|
@name = name
|
|
20
20
|
@members = {} # : Hash[String, untyped]
|
|
21
|
+
@sealed = false
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Bind +object+ under +member+ inside this Namespace. +member+ is a
|
|
24
25
|
# constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
|
|
25
26
|
# object that responds to the methods guest code will invoke. Returns
|
|
26
27
|
# +self+ for chaining. Raises +ArgumentError+ when +member+ does not
|
|
27
|
-
# match the constant pattern,
|
|
28
|
-
# bound ({docs/behavior.md B-11}[link:../../docs/behavior.md])
|
|
28
|
+
# match the constant pattern, when a Member of the same name is
|
|
29
|
+
# already bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]),
|
|
30
|
+
# or when the owning Sandbox's first invocation has sealed Service
|
|
31
|
+
# registration ({docs/behavior.md E-45}[link:../../docs/behavior.md]).
|
|
29
32
|
def bind(member, object)
|
|
33
|
+
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
34
|
+
|
|
30
35
|
member_str = validate_member_name!(member)
|
|
31
36
|
raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
|
|
32
37
|
|
|
@@ -34,6 +39,15 @@ module Kobako
|
|
|
34
39
|
self
|
|
35
40
|
end
|
|
36
41
|
|
|
42
|
+
# Mark this Namespace as sealed ({docs/behavior.md B-33}[link:../../docs/behavior.md]).
|
|
43
|
+
# Called by +Kobako::Catalog::Namespaces#seal!+ on the owning
|
|
44
|
+
# Sandbox's first invocation; afterwards {#bind} raises
|
|
45
|
+
# +ArgumentError+ (E-45). Idempotent; returns +self+.
|
|
46
|
+
def seal!
|
|
47
|
+
@sealed = true
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
# Member lookup; raises +KeyError+ when no Member is registered
|
|
38
52
|
# under +member+.
|
|
39
53
|
def fetch(member)
|
data/lib/kobako/pool.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "sandbox"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
|
|
8
|
+
# handed out one exclusive holder at a time
|
|
9
|
+
# ({docs/behavior.md B-46..B-48}[link:../../docs/behavior.md]).
|
|
10
|
+
#
|
|
11
|
+
# Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
|
|
12
|
+
# and holds the optional block as the per-Sandbox setup hook; a
|
|
13
|
+
# checkout prefers an idle Sandbox and constructs a new one only when
|
|
14
|
+
# none is idle and fewer than +slots+ exist (B-46). +#with+ blocks up
|
|
15
|
+
# to +checkout_timeout+ seconds when every slot is held (E-46), applies
|
|
16
|
+
# the +TrapError+ discard-and-recreate contract at checkin (B-47), and
|
|
17
|
+
# the Pool releases everything with its own reachability — there is no
|
|
18
|
+
# teardown verb (B-48).
|
|
19
|
+
class Pool
|
|
20
|
+
# The +#with+ wait bound applied when +checkout_timeout+ is not given
|
|
21
|
+
# ({docs/behavior.md B-46}[link:../../docs/behavior.md]).
|
|
22
|
+
DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
|
|
23
|
+
|
|
24
|
+
# Build a Pool of up to +slots+ Sandboxes
|
|
25
|
+
# ({docs/behavior.md B-46}[link:../../docs/behavior.md]). +slots+ is
|
|
26
|
+
# a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
|
|
27
|
+
# seconds (+nil+ waits indefinitely); every other keyword is
|
|
28
|
+
# forwarded verbatim to +Kobako::Sandbox.new+. The optional block
|
|
29
|
+
# runs exactly once per constructed Sandbox — it is the setup window
|
|
30
|
+
# for +#define+ / +#preload+ before that Sandbox's first checkout.
|
|
31
|
+
# No Sandbox is constructed here. Raises +ArgumentError+ for an
|
|
32
|
+
# invalid +slots+ / +checkout_timeout+
|
|
33
|
+
# ({docs/behavior.md E-47}[link:../../docs/behavior.md]).
|
|
34
|
+
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
|
35
|
+
validate_slots!(slots)
|
|
36
|
+
@slots = slots
|
|
37
|
+
@checkout_timeout = normalize_checkout_timeout(checkout_timeout)
|
|
38
|
+
@sandbox_options = sandbox_options
|
|
39
|
+
@setup = setup
|
|
40
|
+
@idle = [] # : Array[Kobako::Sandbox]
|
|
41
|
+
@constructed = 0
|
|
42
|
+
@mutex = Mutex.new
|
|
43
|
+
@slot_freed = ConditionVariable.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Yield one exclusively-held Sandbox to the block and return the
|
|
47
|
+
# block's value ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
|
|
48
|
+
# Blocks while every slot is held; raises +Kobako::PoolTimeoutError+
|
|
49
|
+
# once the wait exceeds +checkout_timeout+
|
|
50
|
+
# ({docs/behavior.md E-46}[link:../../docs/behavior.md]). The Sandbox
|
|
51
|
+
# returns to the pool at block exit — unless the block raised
|
|
52
|
+
# +Kobako::TrapError+, in which case the unrecoverable Sandbox is
|
|
53
|
+
# discarded and its slot refills by a fresh construction on next
|
|
54
|
+
# demand.
|
|
55
|
+
def with
|
|
56
|
+
sandbox = checkout
|
|
57
|
+
begin
|
|
58
|
+
yield sandbox
|
|
59
|
+
rescue TrapError
|
|
60
|
+
release_capacity!
|
|
61
|
+
sandbox = nil
|
|
62
|
+
raise
|
|
63
|
+
ensure
|
|
64
|
+
checkin(sandbox) if sandbox
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Acquire a Sandbox and hand it over in pre-invocation state — empty
|
|
71
|
+
# output buffers and truncation predicates false (B-47).
|
|
72
|
+
def checkout
|
|
73
|
+
acquire.tap(&:reset_invocation_state!)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The idle-first claim loop (B-46): an idle Sandbox wins, unclaimed
|
|
77
|
+
# capacity constructs, and a full pool waits for a checkin.
|
|
78
|
+
def acquire
|
|
79
|
+
timeout = @checkout_timeout
|
|
80
|
+
deadline = timeout && (monotonic_now + timeout)
|
|
81
|
+
loop do
|
|
82
|
+
action, sandbox = claim_or_wait(deadline)
|
|
83
|
+
return sandbox if action == :idle && sandbox
|
|
84
|
+
return construct_slot if action == :build
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Single locked decision point for one claim attempt. Waiting
|
|
89
|
+
# happens inside the lock (so a checkin can wake it); construction
|
|
90
|
+
# happens outside (so a slow setup block never holds the lock) —
|
|
91
|
+
# capacity is reserved here and released by +construct_slot+ on
|
|
92
|
+
# failure.
|
|
93
|
+
def claim_or_wait(deadline)
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
return [:idle, @idle.pop] unless @idle.empty?
|
|
96
|
+
|
|
97
|
+
if @constructed < @slots
|
|
98
|
+
@constructed += 1
|
|
99
|
+
return [:build, nil]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
await_slot!(deadline)
|
|
103
|
+
[:retry, nil]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Wait for a checkin or freed capacity; raises
|
|
108
|
+
# +Kobako::PoolTimeoutError+ once +deadline+ has passed (E-46). Must
|
|
109
|
+
# run while holding +@mutex+.
|
|
110
|
+
def await_slot!(deadline)
|
|
111
|
+
remaining = deadline && (deadline - monotonic_now)
|
|
112
|
+
if remaining && remaining <= 0
|
|
113
|
+
raise PoolTimeoutError,
|
|
114
|
+
"no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@slot_freed.wait(@mutex, remaining)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Construct and set up one pooled Sandbox against the capacity
|
|
121
|
+
# reserved by +claim_or_wait+. Construction and setup-block errors
|
|
122
|
+
# propagate to the checkout caller unchanged (B-46); the reserved
|
|
123
|
+
# capacity is released so a later checkout can retry.
|
|
124
|
+
def construct_slot
|
|
125
|
+
done = false
|
|
126
|
+
sandbox = Sandbox.new(**@sandbox_options)
|
|
127
|
+
@setup&.call(sandbox)
|
|
128
|
+
done = true
|
|
129
|
+
sandbox
|
|
130
|
+
ensure
|
|
131
|
+
release_capacity! unless done
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Return a Sandbox to the idle list and wake one waiting checkout.
|
|
135
|
+
def checkin(sandbox)
|
|
136
|
+
@mutex.synchronize do
|
|
137
|
+
@idle.push(sandbox)
|
|
138
|
+
@slot_freed.signal
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Give back reserved-but-unfilled capacity — a failed construction or
|
|
143
|
+
# a discarded Sandbox — and wake one waiting checkout to claim it.
|
|
144
|
+
def release_capacity!
|
|
145
|
+
@mutex.synchronize do
|
|
146
|
+
@constructed -= 1
|
|
147
|
+
@slot_freed.signal
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The wait deadline runs on the monotonic clock so a wall-clock jump
|
|
152
|
+
# cannot stretch or cut the checkout wait.
|
|
153
|
+
def monotonic_now
|
|
154
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# E-47 pre-flight for +slots+ — no coercion, a positive Integer is
|
|
158
|
+
# the only accepted shape.
|
|
159
|
+
def validate_slots!(slots)
|
|
160
|
+
return if slots.is_a?(Integer) && slots.positive?
|
|
161
|
+
|
|
162
|
+
raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Coerce +checkout_timeout+ into the Float seconds the wait loop
|
|
166
|
+
# consumes, or +nil+ to wait indefinitely — the E-39 normalisation
|
|
167
|
+
# idiom applied to E-47.
|
|
168
|
+
def normalize_checkout_timeout(checkout_timeout)
|
|
169
|
+
return nil if checkout_timeout.nil?
|
|
170
|
+
unless checkout_timeout.is_a?(Numeric)
|
|
171
|
+
raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
seconds = checkout_timeout.to_f
|
|
175
|
+
unless seconds.positive? && seconds.finite?
|
|
176
|
+
raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
seconds
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -206,6 +206,22 @@ module Kobako
|
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
# Reset all per-invocation observable state to its pre-invocation
|
|
210
|
+
# sentinels — both per-channel captures
|
|
211
|
+
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
212
|
+
# per-last-invocation usage record
|
|
213
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
214
|
+
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
215
|
+
# (between-invocation reset) so both paths agree on what
|
|
216
|
+
# "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
217
|
+
# so a pooled Sandbox hands over empty output buffers
|
|
218
|
+
# ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
|
|
219
|
+
def reset_invocation_state!
|
|
220
|
+
@stdout_capture = Capture::EMPTY
|
|
221
|
+
@stderr_capture = Capture::EMPTY
|
|
222
|
+
@usage = Usage::EMPTY
|
|
223
|
+
end
|
|
224
|
+
|
|
209
225
|
private
|
|
210
226
|
|
|
211
227
|
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
@@ -237,20 +253,6 @@ module Kobako
|
|
|
237
253
|
reset_invocation_state!
|
|
238
254
|
end
|
|
239
255
|
|
|
240
|
-
# Reset all per-invocation observable state to its pre-invocation
|
|
241
|
-
# sentinels — both per-channel captures
|
|
242
|
-
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
243
|
-
# per-last-invocation usage record
|
|
244
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
245
|
-
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
246
|
-
# (between-invocation reset) so both paths agree on what
|
|
247
|
-
# "pre-invocation state" means.
|
|
248
|
-
def reset_invocation_state!
|
|
249
|
-
@stdout_capture = Capture::EMPTY
|
|
250
|
-
@stderr_capture = Capture::EMPTY
|
|
251
|
-
@usage = Usage::EMPTY
|
|
252
|
-
end
|
|
253
|
-
|
|
254
256
|
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
255
257
|
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
256
258
|
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
@@ -46,13 +46,28 @@ module Kobako
|
|
|
46
46
|
# metaprogramming surface (+send+, +public_send+, +instance_eval+,
|
|
47
47
|
# +method+, +tap+, +instance_variable_get+, ...) rather than Service
|
|
48
48
|
# behaviour. A guest-supplied method name resolving to one of these is
|
|
49
|
-
# rejected
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# +Kernel#eval+ / +#system+
|
|
49
|
+
# rejected ({docs/behavior.md B-42}[link:../../../docs/behavior.md]):
|
|
50
|
+
# only methods the bound object itself exposes as Service behaviour are
|
|
51
|
+
# reachable, and +public_send(:send, ...)+ would otherwise let a guest
|
|
52
|
+
# pivot through +send+ into the private +Kernel#eval+ / +#system+
|
|
53
|
+
# surface (host RCE).
|
|
53
54
|
META_OWNERS = [BasicObject, Kernel, Object, Module, Class].freeze
|
|
54
55
|
private_constant :META_OWNERS
|
|
55
56
|
|
|
57
|
+
# Callable gadget types whose own public methods are reflection surface
|
|
58
|
+
# (+Proc#binding+ reaches +Binding#eval+, +Method#receiver+ / +#unbind+
|
|
59
|
+
# hand back the underlying object) rather than Service behaviour. Only
|
|
60
|
+
# {CALLABLE_ALLOW} is reachable on a target of these types; a bound
|
|
61
|
+
# lambda stays invocable, its reflective surface does not (B-42).
|
|
62
|
+
GADGET_OWNERS = [Proc, Method, UnboundMethod, Binding].freeze
|
|
63
|
+
private_constant :GADGET_OWNERS
|
|
64
|
+
|
|
65
|
+
# The sole methods reachable on a {GADGET_OWNERS} target: invoking it
|
|
66
|
+
# (+call+ / +[]+ / +yield+) and the harmless +arity+ / +lambda?+
|
|
67
|
+
# describers that aid guest-side debugging.
|
|
68
|
+
CALLABLE_ALLOW = %i[call [] yield arity lambda?].freeze
|
|
69
|
+
private_constant :CALLABLE_ALLOW
|
|
70
|
+
|
|
56
71
|
# Dispatch a single transport request and return the encoded
|
|
57
72
|
# Response bytes ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
|
|
58
73
|
# Invoked from the +Runtime#on_dispatch+ Proc that
|
|
@@ -131,14 +146,18 @@ module Kobako
|
|
|
131
146
|
end
|
|
132
147
|
|
|
133
148
|
# Guard the +public_send+ below against ambient reflection methods
|
|
134
|
-
# (
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
149
|
+
# ({docs/behavior.md B-42}[link:../../../docs/behavior.md]). A public
|
|
150
|
+
# method whose owner is a {META_OWNERS} or {GADGET_OWNERS} module is
|
|
151
|
+
# rejected, except {CALLABLE_ALLOW} on a gadget target (a bound lambda
|
|
152
|
+
# stays invocable). A name with no concrete public method is allowed
|
|
153
|
+
# only when the target opts into it via +respond_to?+ (dynamic
|
|
154
|
+
# +method_missing+ Services), since the dangerous methods are all
|
|
155
|
+
# concretely defined and therefore never reach that branch.
|
|
139
156
|
def reject_meta_method!(target, name)
|
|
140
157
|
owner = target.public_method(name).owner
|
|
141
|
-
|
|
158
|
+
gadget = GADGET_OWNERS.include?(owner)
|
|
159
|
+
return unless META_OWNERS.include?(owner) || gadget
|
|
160
|
+
return if gadget && CALLABLE_ALLOW.include?(name)
|
|
142
161
|
|
|
143
162
|
raise UndefinedTargetError, "method #{name.inspect} is not a Service method"
|
|
144
163
|
rescue NameError
|
|
@@ -5,16 +5,8 @@ require_relative "../codec"
|
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
7
|
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
8
|
-
# owns the Request value object and its +#encode+ / +.decode+ codec
|
|
9
|
-
# plus the +STATUS_OK+ / +STATUS_ERROR+ constants shared with Response.
|
|
8
|
+
# owns the Request value object and its +#encode+ / +.decode+ codec.
|
|
10
9
|
module Transport
|
|
11
|
-
# ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
|
|
12
|
-
|
|
13
|
-
# Response variant marker for the success branch.
|
|
14
|
-
STATUS_OK = 0
|
|
15
|
-
# Response variant marker for the fault branch.
|
|
16
|
-
STATUS_ERROR = 1
|
|
17
|
-
|
|
18
10
|
# Value object for a single guest-initiated Transport Request
|
|
19
11
|
# ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
|
|
20
12
|
#
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../codec"
|
|
4
4
|
require_relative "../fault"
|
|
5
|
-
require_relative "request"
|
|
6
5
|
|
|
7
6
|
module Kobako
|
|
8
7
|
# See lib/kobako/transport.rb for the umbrella module doc; this file
|
|
9
|
-
# owns the Response value object and its +#encode+ / +.decode+ codec
|
|
8
|
+
# owns the Response value object and its +#encode+ / +.decode+ codec,
|
|
9
|
+
# plus the +STATUS_OK+ / +STATUS_ERROR+ status bytes.
|
|
10
10
|
module Transport
|
|
11
|
+
# ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
|
|
12
|
+
|
|
13
|
+
# Response variant marker for the success branch.
|
|
14
|
+
STATUS_OK = 0
|
|
15
|
+
# Response variant marker for the fault branch.
|
|
16
|
+
STATUS_ERROR = 1
|
|
17
|
+
|
|
11
18
|
# Value object for a single host-side Transport Response
|
|
12
19
|
# ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
|
|
13
20
|
#
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"component": "kobako",
|
|
8
8
|
"include-component-in-tag": false,
|
|
9
9
|
"release-type": "ruby",
|
|
10
|
-
"exclude-paths": ["wasm"]
|
|
11
|
-
"release-as": "0.9.1"
|
|
10
|
+
"exclude-paths": ["wasm"]
|
|
12
11
|
},
|
|
13
12
|
"wasm/kobako-core": {
|
|
14
13
|
"component": "kobako-core",
|
|
@@ -74,13 +73,28 @@
|
|
|
74
73
|
"path": "/wasm/kobako-regexp/README.md"
|
|
75
74
|
}
|
|
76
75
|
]
|
|
76
|
+
},
|
|
77
|
+
"wasm/kobako-baker": {
|
|
78
|
+
"component": "kobako-baker",
|
|
79
|
+
"release-type": "rust",
|
|
80
|
+
"extra-files": [
|
|
81
|
+
{
|
|
82
|
+
"type": "toml",
|
|
83
|
+
"path": "/wasm/kobako-baker/Cargo.lock",
|
|
84
|
+
"jsonpath": "$.package[?(@.name=='kobako-baker')].version"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "generic",
|
|
88
|
+
"path": "/wasm/kobako-baker/README.md"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
77
91
|
}
|
|
78
92
|
},
|
|
79
93
|
"plugins": [
|
|
80
94
|
{
|
|
81
95
|
"type": "linked-versions",
|
|
82
96
|
"groupName": "kobako guest crates",
|
|
83
|
-
"components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp"]
|
|
97
|
+
"components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp", "kobako-baker"]
|
|
84
98
|
}
|
|
85
99
|
],
|
|
86
100
|
"extra-files": [
|
data/sig/kobako/errors.rbs
CHANGED