tywrap 0.7.0 → 0.9.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.
- package/README.md +36 -11
- package/SECURITY.md +39 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/emit-call.d.ts.map +1 -1
- package/dist/core/emit-call.js +1 -1
- package/dist/core/emit-call.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +243 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +1 -3
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +1 -1
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +78 -55
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/frame-codec.d.ts +111 -0
- package/dist/runtime/frame-codec.d.ts.map +1 -0
- package/dist/runtime/frame-codec.js +352 -0
- package/dist/runtime/frame-codec.js.map +1 -0
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -23
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +5 -78
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +37 -72
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +134 -8
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +490 -65
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +90 -18
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +21 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +49 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +152 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -15
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -21
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +165 -17
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +430 -0
- package/runtime/python_bridge.py +213 -54
- package/runtime/tywrap_bridge_core.py +217 -147
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +9 -5
- package/src/core/emit-call.ts +1 -7
- package/src/core/generator.ts +315 -205
- package/src/core/mapper.ts +8 -8
- package/src/dev.ts +1 -3
- package/src/index.ts +7 -3
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +94 -65
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/frame-codec.ts +469 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +7 -6
- package/src/runtime/node.ts +17 -104
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +58 -93
- package/src/runtime/subprocess-transport.ts +585 -80
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +112 -22
- package/src/runtime/validators.ts +204 -0
- package/src/types/index.ts +21 -35
- package/src/tywrap.ts +157 -30
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +205 -16
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
package/runtime/python_bridge.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Reference tywrap Python
|
|
3
|
+
Reference tywrap Python subprocess bridge.
|
|
4
|
+
|
|
5
|
+
This module implements only the stdin/stdout subprocess loop for the Node/Bun/
|
|
6
|
+
Deno transport. `HttpBridge` connects to an HTTP server provided by the
|
|
7
|
+
operator; tywrap does not ship that server or an HTTP transport in this module.
|
|
8
|
+
The operator is responsible for securing any network exposure.
|
|
4
9
|
|
|
5
10
|
This module owns the I/O concerns and process identity for the Node/Bun/Deno
|
|
6
|
-
subprocess transport
|
|
11
|
+
subprocess transport:
|
|
7
12
|
- the stdin/stdout JSONL request/response loop (main())
|
|
8
13
|
- env-var size guards (TYWRAP_CODEC_MAX_BYTES / TYWRAP_REQUEST_MAX_BYTES)
|
|
9
14
|
- TYWRAP_CODEC_FALLBACK=json marker mode and TYWRAP_TORCH_ALLOW_COPY
|
|
@@ -14,7 +19,7 @@ subprocess transport and the HTTP transport:
|
|
|
14
19
|
the explicit byte-size limit error message)
|
|
15
20
|
|
|
16
21
|
SECURITY / TRUST MODEL: the bridge imports the requested module and getattrs the
|
|
17
|
-
requested function/class
|
|
22
|
+
requested function/class method, so call is an arbitrary
|
|
18
23
|
import+getattr+call surface. Two guards (implemented in tywrap_bridge_core) bound
|
|
19
24
|
that surface:
|
|
20
25
|
* TYWRAP_ALLOWED_MODULES (comma/space separated): when set, only those modules
|
|
@@ -37,9 +42,8 @@ import json
|
|
|
37
42
|
import os
|
|
38
43
|
import importlib # noqa: F401 (re-exported for compat / used by handlers via core)
|
|
39
44
|
|
|
40
|
-
from safe_codec import BridgeCodec, CodecError
|
|
41
|
-
|
|
42
45
|
import tywrap_bridge_core as core
|
|
46
|
+
from frame_codec import FrameError, Reassembler, encode_frames
|
|
43
47
|
|
|
44
48
|
# Re-export the shared protocol/serialization surface so existing importers of
|
|
45
49
|
# python_bridge keep working after the extraction (codex-flagged: runtime/ ships).
|
|
@@ -47,8 +51,9 @@ from tywrap_bridge_core import ( # noqa: F401
|
|
|
47
51
|
PROTOCOL,
|
|
48
52
|
PROTOCOL_VERSION,
|
|
49
53
|
CODEC_VERSION,
|
|
54
|
+
BridgeCodec,
|
|
55
|
+
CodecError,
|
|
50
56
|
ProtocolError,
|
|
51
|
-
InstanceHandleError,
|
|
52
57
|
ImportNotAllowedError,
|
|
53
58
|
AttributeNotAllowedError,
|
|
54
59
|
import_allowed_module,
|
|
@@ -89,8 +94,6 @@ except (OSError, ValueError, TypeError, AttributeError) as exc:
|
|
|
89
94
|
except (OSError, ValueError):
|
|
90
95
|
pass
|
|
91
96
|
|
|
92
|
-
instances = {}
|
|
93
|
-
|
|
94
97
|
FALLBACK_JSON = os.environ.get('TYWRAP_CODEC_FALLBACK', '').lower() == 'json'
|
|
95
98
|
TORCH_ALLOW_COPY = os.environ.get('TYWRAP_TORCH_ALLOW_COPY', '').lower() in ('1', 'true', 'yes')
|
|
96
99
|
BRIDGE_NAME = 'python-subprocess'
|
|
@@ -221,6 +224,24 @@ def get_request_max_bytes():
|
|
|
221
224
|
REQUEST_MAX_BYTES = get_request_max_bytes()
|
|
222
225
|
|
|
223
226
|
|
|
227
|
+
def get_transport_frame_bytes():
|
|
228
|
+
"""Read the private frame ceiling argument supplied by SubprocessTransport."""
|
|
229
|
+
if "--tywrap-max-frame-bytes" not in sys.argv:
|
|
230
|
+
return 100 * 1024 * 1024
|
|
231
|
+
try:
|
|
232
|
+
index = sys.argv.index("--tywrap-max-frame-bytes")
|
|
233
|
+
value = int(sys.argv[index + 1])
|
|
234
|
+
except (ValueError, IndexError) as exc:
|
|
235
|
+
raise CodecConfigError("--tywrap-max-frame-bytes must be a positive integer") from exc
|
|
236
|
+
if value <= 0:
|
|
237
|
+
raise CodecConfigError("--tywrap-max-frame-bytes must be a positive integer")
|
|
238
|
+
return value
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
MAX_FRAME_BYTES = get_transport_frame_bytes()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
224
245
|
def serialize(obj):
|
|
225
246
|
"""
|
|
226
247
|
Backward-compatible result serializer (subprocess identity).
|
|
@@ -262,7 +283,6 @@ def handle_meta():
|
|
|
262
283
|
real pid and bridge='python-subprocess'.
|
|
263
284
|
"""
|
|
264
285
|
return core.build_meta(
|
|
265
|
-
instances,
|
|
266
286
|
bridge=BRIDGE_NAME,
|
|
267
287
|
pid=os.getpid(),
|
|
268
288
|
python_version=sys.version.split()[0],
|
|
@@ -270,7 +290,7 @@ def handle_meta():
|
|
|
270
290
|
)
|
|
271
291
|
|
|
272
292
|
|
|
273
|
-
def dispatch_request(msg):
|
|
293
|
+
def dispatch_request(msg, *, has_envelope_markers=True):
|
|
274
294
|
"""
|
|
275
295
|
Dispatch a validated request to the correct handler (subprocess identity).
|
|
276
296
|
|
|
@@ -280,7 +300,6 @@ def dispatch_request(msg):
|
|
|
280
300
|
"""
|
|
281
301
|
out = core.dispatch_request(
|
|
282
302
|
msg,
|
|
283
|
-
instances,
|
|
284
303
|
bridge=BRIDGE_NAME,
|
|
285
304
|
pid=os.getpid(),
|
|
286
305
|
force_json_markers=FALLBACK_JSON,
|
|
@@ -289,6 +308,7 @@ def dispatch_request(msg):
|
|
|
289
308
|
torch_allow_copy=TORCH_ALLOW_COPY,
|
|
290
309
|
allowed_modules=ALLOWED_MODULES,
|
|
291
310
|
allow_private_attrs=ALLOW_PRIVATE_ATTRS,
|
|
311
|
+
has_envelope_markers=has_envelope_markers,
|
|
292
312
|
)
|
|
293
313
|
return out['id'], out['result']
|
|
294
314
|
|
|
@@ -305,10 +325,11 @@ def encode_response(out):
|
|
|
305
325
|
except CodecError as exc:
|
|
306
326
|
# Convert CodecError to ValueError for consistent error handling
|
|
307
327
|
raise ValueError(str(exc)) from exc
|
|
308
|
-
|
|
328
|
+
payload_utf8 = payload.encode('utf-8')
|
|
329
|
+
payload_bytes = len(payload_utf8)
|
|
309
330
|
if CODEC_MAX_BYTES is not None and payload_bytes > CODEC_MAX_BYTES:
|
|
310
331
|
raise PayloadTooLargeError(payload_bytes, CODEC_MAX_BYTES)
|
|
311
|
-
return payload
|
|
332
|
+
return payload, payload_utf8
|
|
312
333
|
|
|
313
334
|
|
|
314
335
|
def write_payload(payload: str) -> bool:
|
|
@@ -326,55 +347,193 @@ def write_payload(payload: str) -> bool:
|
|
|
326
347
|
return False
|
|
327
348
|
|
|
328
349
|
|
|
329
|
-
def
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
def write_response(payload: str, payload_utf8: bytes, response_id) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Write a fully-encoded JSONL response, fragmenting it into ``tywrap-frame/1``
|
|
353
|
+
frames when the payload exceeds the fixed per-frame ceiling.
|
|
354
|
+
|
|
355
|
+
Why: a single response can exceed the JS-side JSONL line ceiling
|
|
356
|
+
(``maxLineLength``). The packaged subprocess transport always enables
|
|
357
|
+
framing, so an oversize response is split into frames, each written as its
|
|
358
|
+
own JSONL line and flushed one at a time so the OS pipe provides
|
|
359
|
+
backpressure. The TS reassembler rebuilds the logical response before the
|
|
360
|
+
codec sees it. Small responses keep the single-line fast path.
|
|
361
|
+
|
|
362
|
+
Frames require an integer correlation id. A response with a non-integer id
|
|
363
|
+
(only reachable on a malformed request whose error envelope carries id=None)
|
|
364
|
+
is never large enough to chunk, so it is always written as a single line.
|
|
365
|
+
|
|
366
|
+
Returns False if the parent's stdin/our stdout closed mid-write (BrokenPipe),
|
|
367
|
+
so the caller can exit the loop cleanly.
|
|
368
|
+
"""
|
|
369
|
+
payload_bytes = len(payload_utf8)
|
|
370
|
+
if payload_bytes <= MAX_FRAME_BYTES:
|
|
371
|
+
return write_payload(payload)
|
|
372
|
+
|
|
373
|
+
if not isinstance(response_id, int) or isinstance(response_id, bool):
|
|
374
|
+
# Cannot correlate frames without an integer id; emit as one line. This
|
|
375
|
+
# only happens for tiny malformed-request error envelopes (id=None),
|
|
376
|
+
# which never exceed a sane frame ceiling.
|
|
377
|
+
return write_payload(payload)
|
|
378
|
+
|
|
379
|
+
frames = encode_frames(
|
|
380
|
+
payload,
|
|
381
|
+
id=response_id,
|
|
382
|
+
stream='response',
|
|
383
|
+
max_frame_bytes=MAX_FRAME_BYTES,
|
|
384
|
+
total_bytes=payload_bytes,
|
|
385
|
+
)
|
|
386
|
+
for frame in frames:
|
|
387
|
+
# One frame per JSONL line; flush per frame so the pipe backpressures
|
|
388
|
+
# and the TS reader can interleave reassembly with the write.
|
|
389
|
+
if not write_payload(_response_codec.encode(frame)):
|
|
390
|
+
return False
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Reassembles `tywrap-frame/1` REQUEST frames (W5) back into a single logical
|
|
395
|
+
# request line. Framing is always accepted and restricted to the 'request'
|
|
396
|
+
# stream. No reassembly-bytes cap here: the request size is
|
|
397
|
+
# already bounded by the TS codec's maxPayloadBytes on the sending side, and
|
|
398
|
+
# TYWRAP_REQUEST_MAX_BYTES is enforced on the complete payload after reassembly
|
|
399
|
+
# (process_request_line), preserving the W5 post-reassembly semantics.
|
|
400
|
+
_request_reassembler = Reassembler(expected_stream='request')
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _accept_request_frame(frame):
|
|
404
|
+
"""Accept one request frame, abandoning a different older serialized burst."""
|
|
405
|
+
frame_id = frame.get('id')
|
|
406
|
+
if frame.get('seq') == 0 and not _request_reassembler.is_pending(frame_id):
|
|
407
|
+
# The stdin loop is deliberately blocking/serial: an abandoned partial
|
|
408
|
+
# buffer persists while the worker is idle, then is freed by the next
|
|
409
|
+
# logical request (or by process restart). A background TTL timer would
|
|
410
|
+
# add concurrency solely to reclaim memory during an otherwise idle loop.
|
|
411
|
+
_request_reassembler.clear_pending()
|
|
412
|
+
return _request_reassembler.accept(frame)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _try_parse_frame_line(line):
|
|
416
|
+
"""
|
|
417
|
+
Return a frame dict if ``line`` is a ``tywrap-frame/1`` envelope, else None.
|
|
418
|
+
|
|
419
|
+
Why: a request frame is a JSON object carrying ``__tywrap_frame__``. We only
|
|
420
|
+
treat a line as a frame when it parses as such an
|
|
421
|
+
object; anything else (including invalid JSON) falls through to the normal
|
|
422
|
+
single-line request path, which reports the JSON error exactly as before.
|
|
423
|
+
Structural validity (protocol, seq/total ranges, etc.) is enforced by the
|
|
424
|
+
Reassembler, not here.
|
|
425
|
+
"""
|
|
426
|
+
try:
|
|
427
|
+
parsed = json.loads(line)
|
|
428
|
+
except json.JSONDecodeError:
|
|
429
|
+
return None
|
|
430
|
+
if isinstance(parsed, dict) and '__tywrap_frame__' in parsed:
|
|
431
|
+
return parsed
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def process_request_line(line):
|
|
436
|
+
"""
|
|
437
|
+
Process one complete logical request line and write its response.
|
|
438
|
+
|
|
439
|
+
``line`` is the full logical JSON request: either a single line read from
|
|
440
|
+
stdin, or the payload reassembled from ``tywrap-frame/1`` request frames.
|
|
441
|
+
TYWRAP_REQUEST_MAX_BYTES is enforced on this complete payload (so for a
|
|
442
|
+
chunked request the limit applies to the REASSEMBLED size, not per frame).
|
|
443
|
+
|
|
444
|
+
Returns True to keep the loop running, or False if the parent's stdin / our
|
|
445
|
+
stdout closed mid-write (BrokenPipe), so main() can exit cleanly.
|
|
446
|
+
"""
|
|
447
|
+
mid = None
|
|
448
|
+
out = None
|
|
449
|
+
try:
|
|
450
|
+
if REQUEST_MAX_BYTES is not None:
|
|
451
|
+
payload_bytes = len(line.encode('utf-8'))
|
|
452
|
+
if payload_bytes > REQUEST_MAX_BYTES:
|
|
453
|
+
raise RequestTooLargeError(payload_bytes, REQUEST_MAX_BYTES)
|
|
454
|
+
msg = json.loads(line)
|
|
455
|
+
if isinstance(msg, dict):
|
|
456
|
+
req_id = msg.get('id')
|
|
457
|
+
if isinstance(req_id, int):
|
|
458
|
+
# Why: preserve request ids even when handlers raise.
|
|
459
|
+
mid = req_id
|
|
336
460
|
try:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
msg = json.loads(line)
|
|
342
|
-
if isinstance(msg, dict):
|
|
343
|
-
req_id = msg.get('id')
|
|
344
|
-
if isinstance(req_id, int):
|
|
345
|
-
# Why: preserve request ids even when handlers raise.
|
|
346
|
-
mid = req_id
|
|
347
|
-
try:
|
|
348
|
-
mid, result = dispatch_request(msg)
|
|
349
|
-
out = {'id': mid, 'protocol': PROTOCOL, 'result': result}
|
|
350
|
-
except ProtocolError as e:
|
|
351
|
-
emit_protocol_diagnostic(str(e))
|
|
352
|
-
out = build_error_payload(mid, e, include_traceback=False)
|
|
353
|
-
except Exception as e: # noqa: BLE001
|
|
354
|
-
# Why: ensure any handler error becomes a protocol-compliant response.
|
|
355
|
-
out = build_error_payload(mid, e, include_traceback=True)
|
|
356
|
-
except RequestTooLargeError as e:
|
|
461
|
+
has_envelope_markers = '__tywrap' in line or '__type__' in line
|
|
462
|
+
mid, result = dispatch_request(msg, has_envelope_markers=has_envelope_markers)
|
|
463
|
+
out = {'id': mid, 'protocol': PROTOCOL, 'result': result}
|
|
464
|
+
except ProtocolError as e:
|
|
357
465
|
emit_protocol_diagnostic(str(e))
|
|
358
466
|
out = build_error_payload(mid, e, include_traceback=False)
|
|
359
|
-
except json.JSONDecodeError as e:
|
|
360
|
-
emit_protocol_diagnostic(f'Invalid JSON: {e}')
|
|
361
|
-
out = build_error_payload(mid, e, include_traceback=False)
|
|
362
467
|
except Exception as e: # noqa: BLE001
|
|
363
|
-
# Why:
|
|
364
|
-
out = build_error_payload(mid, e, include_traceback=
|
|
468
|
+
# Why: ensure any handler error becomes a protocol-compliant response.
|
|
469
|
+
out = build_error_payload(mid, e, include_traceback=True)
|
|
470
|
+
except RequestTooLargeError as e:
|
|
471
|
+
emit_protocol_diagnostic(str(e))
|
|
472
|
+
out = build_error_payload(mid, e, include_traceback=False)
|
|
473
|
+
except json.JSONDecodeError as e:
|
|
474
|
+
emit_protocol_diagnostic(f'Invalid JSON: {e}')
|
|
475
|
+
out = build_error_payload(mid, e, include_traceback=False)
|
|
476
|
+
except Exception as e: # noqa: BLE001
|
|
477
|
+
# Why: catch malformed input without breaking the JSONL protocol.
|
|
478
|
+
out = build_error_payload(mid, e, include_traceback=False)
|
|
365
479
|
|
|
480
|
+
try:
|
|
481
|
+
payload, payload_utf8 = encode_response(out)
|
|
482
|
+
# Correlate frames by the response id when chunking; out always
|
|
483
|
+
# carries the request id (or None for a malformed-request envelope).
|
|
484
|
+
response_id = out.get('id') if isinstance(out, dict) else None
|
|
485
|
+
if not write_response(payload, payload_utf8, response_id):
|
|
486
|
+
return False
|
|
487
|
+
except Exception as e: # noqa: BLE001
|
|
488
|
+
# Why: fallback error keeps responses well-formed even if serialization fails.
|
|
489
|
+
err_out = build_error_payload(mid, e, include_traceback=False)
|
|
366
490
|
try:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
491
|
+
# Error envelopes are tiny; write as a single line regardless of
|
|
492
|
+
# chunking so a serialization failure never recurses into framing.
|
|
493
|
+
if not write_payload(json.dumps(err_out)):
|
|
494
|
+
return False
|
|
495
|
+
except Exception:
|
|
496
|
+
return False
|
|
497
|
+
return True
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def main():
|
|
501
|
+
for line in sys.stdin:
|
|
502
|
+
line = line.strip()
|
|
503
|
+
if not line:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
# A line may be a `tywrap-frame/1` REQUEST frame. Reassemble per id; only
|
|
507
|
+
# the completed logical request is handed to process_request_line (which
|
|
508
|
+
# then enforces the request-size guard on the reassembled payload).
|
|
509
|
+
# Non-frame lines are normal requests.
|
|
510
|
+
frame = _try_parse_frame_line(line)
|
|
511
|
+
if frame is not None:
|
|
512
|
+
# JS serializes request frame bursts. A seq=0 frame for a different
|
|
513
|
+
# id proves any older partial stream was abandoned; a duplicate
|
|
514
|
+
# seq=0 for the same id must still fail loud in the reassembler.
|
|
373
515
|
try:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
516
|
+
reassembled = _accept_request_frame(frame)
|
|
517
|
+
except FrameError as exc:
|
|
518
|
+
# A framing-protocol violation desyncs the request stream and
|
|
519
|
+
# there is no correlatable response to write (the id may be
|
|
520
|
+
# malformed). Fail LOUD on stderr and stop the loop so the
|
|
521
|
+
# transport restarts the bridge rather than silently
|
|
522
|
+
# mis-parsing subsequent frames.
|
|
523
|
+
emit_protocol_diagnostic(f'Request frame error: {exc}')
|
|
524
|
+
return
|
|
525
|
+
if reassembled is None:
|
|
526
|
+
# More frames needed for this id; await the rest.
|
|
527
|
+
continue
|
|
528
|
+
if not process_request_line(reassembled):
|
|
377
529
|
return
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
# Same idle-retention tradeoff as _accept_request_frame: a partial burst
|
|
533
|
+
# is retained only until the next request or process restart.
|
|
534
|
+
_request_reassembler.clear_pending()
|
|
535
|
+
if not process_request_line(line):
|
|
536
|
+
return
|
|
378
537
|
|
|
379
538
|
|
|
380
539
|
if __name__ == '__main__':
|