onec-interactive-runtime-core 0.1.19__py3-none-any.whl
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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from multiprocessing.connection import Connection
|
|
6
|
+
from threading import Event
|
|
7
|
+
from time import monotonic
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from onec_runtime.errors import (
|
|
12
|
+
CommandTimeout,
|
|
13
|
+
ControllerUnavailable,
|
|
14
|
+
InvalidMessageSequence,
|
|
15
|
+
LeaseConflict,
|
|
16
|
+
LeaseExpired,
|
|
17
|
+
StaleLeaseEpoch,
|
|
18
|
+
)
|
|
19
|
+
from onec_runtime.fault_injection import FaultPoint
|
|
20
|
+
from onec_runtime.lease import LeaseAuthority, validate_lease_ttl
|
|
21
|
+
from onec_runtime.recovery_journal import RecoveryJournal
|
|
22
|
+
from onec_runtime.supervisor_model import LeaseHandle
|
|
23
|
+
from onec_runtime.supervisor_protocol import (
|
|
24
|
+
ControlMessage,
|
|
25
|
+
MessageKind,
|
|
26
|
+
MessageReceiver,
|
|
27
|
+
MessageSender,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
WORKER_POLL_INTERVAL_S = 0.05
|
|
32
|
+
HEARTBEAT_INTERVAL_S = 0.25
|
|
33
|
+
DEBUGGEE_START_TIMEOUT_S = 90.0
|
|
34
|
+
SAFE_RUNTIME_STATES = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"idle",
|
|
37
|
+
"main_pending",
|
|
38
|
+
"captured",
|
|
39
|
+
"evaluating_capture",
|
|
40
|
+
"debug_stopped",
|
|
41
|
+
"flushing",
|
|
42
|
+
"partial_writeback_failure",
|
|
43
|
+
"breakpoint_restore_failure",
|
|
44
|
+
"resuming",
|
|
45
|
+
"recovering",
|
|
46
|
+
"lost",
|
|
47
|
+
"completed",
|
|
48
|
+
"failed",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BarrierMode(str, Enum):
|
|
54
|
+
CRASH = "crash"
|
|
55
|
+
HANG = "hang"
|
|
56
|
+
LEASE_EXPIRY = "lease_expiry"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _BarrierShutdown(BaseException):
|
|
60
|
+
"""Unwinds driver execution when shutdown arrives at a phase barrier."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Publish = Callable[[MessageKind, dict[str, object]], None]
|
|
64
|
+
BarrierWait = Callable[..., None]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PhaseBarrier:
|
|
68
|
+
"""Publishes a durable phase boundary before entering its wait mode."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
point: FaultPoint,
|
|
74
|
+
publish: Publish,
|
|
75
|
+
flush: Callable[[], None],
|
|
76
|
+
wait: BarrierWait,
|
|
77
|
+
mode: BarrierMode = BarrierMode.CRASH,
|
|
78
|
+
payload: dict[str, object] | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
self._point = point
|
|
81
|
+
self._publish = publish
|
|
82
|
+
self._flush = flush
|
|
83
|
+
self._wait = wait
|
|
84
|
+
self._mode = mode
|
|
85
|
+
self._payload = payload or {}
|
|
86
|
+
self._fired = False
|
|
87
|
+
|
|
88
|
+
def __call__(self, point: FaultPoint) -> None:
|
|
89
|
+
if self._fired or point is not self._point:
|
|
90
|
+
return
|
|
91
|
+
self._fired = True
|
|
92
|
+
self._flush()
|
|
93
|
+
self._publish(
|
|
94
|
+
MessageKind.PHASE_REACHED,
|
|
95
|
+
{"point": point.value, **self._payload},
|
|
96
|
+
)
|
|
97
|
+
if self._mode is BarrierMode.HANG:
|
|
98
|
+
self._wait(heartbeat_enabled=False)
|
|
99
|
+
else:
|
|
100
|
+
self._wait()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RuntimeDriver(Protocol):
|
|
104
|
+
def prepare_debug_ui(self) -> None:
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
|
|
107
|
+
def attach_runtime(self) -> None:
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
def execute_phase(self, point: FaultPoint, barrier: PhaseBarrier) -> object:
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
|
|
113
|
+
def status(self) -> dict[str, object]:
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
|
|
116
|
+
def close(self) -> None:
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ControllerWorker:
|
|
121
|
+
"""Single-writer child control plane for one supervised generation."""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
generation_id: int,
|
|
126
|
+
driver: RuntimeDriver,
|
|
127
|
+
*,
|
|
128
|
+
connection: Connection | None = None,
|
|
129
|
+
emit: Callable[[ControlMessage], None] | None = None,
|
|
130
|
+
clock: Callable[[], float] = monotonic,
|
|
131
|
+
journal: RecoveryJournal | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
if emit is None and connection is None:
|
|
134
|
+
raise ValueError("ControllerWorker requires a connection or emit callback")
|
|
135
|
+
self._generation_id = generation_id
|
|
136
|
+
self._driver = driver
|
|
137
|
+
self._connection = connection
|
|
138
|
+
self._emit = emit if emit is not None else connection.send # type: ignore[union-attr]
|
|
139
|
+
self._clock = clock
|
|
140
|
+
self._journal = journal or RecoveryJournal()
|
|
141
|
+
self._leases = LeaseAuthority(generation_id, clock=clock)
|
|
142
|
+
self._sender = MessageSender(generation_id)
|
|
143
|
+
self._receiver = MessageReceiver(generation_id)
|
|
144
|
+
self._running = True
|
|
145
|
+
self._at_barrier = False
|
|
146
|
+
self._barrier_mode: BarrierMode | None = None
|
|
147
|
+
self._progress_sequence = 0
|
|
148
|
+
self._last_heartbeat_at = clock()
|
|
149
|
+
|
|
150
|
+
def run(self) -> None:
|
|
151
|
+
try:
|
|
152
|
+
try:
|
|
153
|
+
self._run()
|
|
154
|
+
finally:
|
|
155
|
+
self._driver.close()
|
|
156
|
+
except Exception as error:
|
|
157
|
+
self._publish(
|
|
158
|
+
MessageKind.FATAL,
|
|
159
|
+
{"error_type": type(error).__name__},
|
|
160
|
+
)
|
|
161
|
+
raise
|
|
162
|
+
|
|
163
|
+
def _run(self) -> None:
|
|
164
|
+
connection = self._require_connection()
|
|
165
|
+
self._driver.prepare_debug_ui()
|
|
166
|
+
self._advance_progress()
|
|
167
|
+
self._publish(MessageKind.DEBUG_READY, {})
|
|
168
|
+
|
|
169
|
+
deadline = self._clock() + DEBUGGEE_START_TIMEOUT_S
|
|
170
|
+
while True:
|
|
171
|
+
remaining = deadline - self._clock()
|
|
172
|
+
if remaining <= 0:
|
|
173
|
+
raise CommandTimeout("Timed out waiting for debuggee start")
|
|
174
|
+
if not connection.poll(min(WORKER_POLL_INTERVAL_S, remaining)):
|
|
175
|
+
continue
|
|
176
|
+
message = connection.recv()
|
|
177
|
+
self._receiver.accept(message)
|
|
178
|
+
if message.kind is not MessageKind.DEBUGGEE_STARTED:
|
|
179
|
+
raise InvalidMessageSequence(
|
|
180
|
+
f"Unexpected {message.kind.value} before debuggee start"
|
|
181
|
+
)
|
|
182
|
+
self._advance_progress()
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
self._driver.attach_runtime()
|
|
186
|
+
self._advance_progress()
|
|
187
|
+
self._publish(MessageKind.READY, {})
|
|
188
|
+
self._last_heartbeat_at = self._clock()
|
|
189
|
+
|
|
190
|
+
while self._running:
|
|
191
|
+
self._expire_lease()
|
|
192
|
+
if connection.poll(WORKER_POLL_INTERVAL_S):
|
|
193
|
+
self.handle_message(connection.recv())
|
|
194
|
+
self._emit_heartbeat_if_due()
|
|
195
|
+
|
|
196
|
+
def handle_message(self, message: ControlMessage) -> None:
|
|
197
|
+
self._expire_lease()
|
|
198
|
+
self._receiver.accept(message)
|
|
199
|
+
|
|
200
|
+
if message.kind is MessageKind.GRANT_LEASE:
|
|
201
|
+
self._grant_lease(message.payload)
|
|
202
|
+
elif message.kind is MessageKind.RENEW_LEASE:
|
|
203
|
+
self._renew_lease(message.payload)
|
|
204
|
+
elif message.kind is MessageKind.EXECUTE_PHASE:
|
|
205
|
+
try:
|
|
206
|
+
self._execute_phase(message.payload)
|
|
207
|
+
except _BarrierShutdown:
|
|
208
|
+
return
|
|
209
|
+
elif message.kind is MessageKind.STATUS:
|
|
210
|
+
self._status()
|
|
211
|
+
elif message.kind is MessageKind.SHUTDOWN:
|
|
212
|
+
self._running = False
|
|
213
|
+
else:
|
|
214
|
+
raise InvalidMessageSequence(
|
|
215
|
+
f"Unexpected worker command {message.kind.value}"
|
|
216
|
+
)
|
|
217
|
+
self._advance_progress()
|
|
218
|
+
|
|
219
|
+
def poll_once(self) -> None:
|
|
220
|
+
self._expire_lease()
|
|
221
|
+
|
|
222
|
+
def _grant_lease(self, payload: dict[str, object]) -> None:
|
|
223
|
+
try:
|
|
224
|
+
handle = self._leases.grant(
|
|
225
|
+
self._owner(payload),
|
|
226
|
+
ttl_s=validate_lease_ttl(payload["ttl_s"]),
|
|
227
|
+
)
|
|
228
|
+
except (LeaseConflict, LeaseExpired) as error:
|
|
229
|
+
self._publish_lease_conflict(error)
|
|
230
|
+
return
|
|
231
|
+
self._publish_lease_granted(handle)
|
|
232
|
+
|
|
233
|
+
def _renew_lease(self, payload: dict[str, object]) -> None:
|
|
234
|
+
try:
|
|
235
|
+
handle = self._leases.renew(
|
|
236
|
+
self._lease_handle(payload),
|
|
237
|
+
ttl_s=validate_lease_ttl(payload["ttl_s"]),
|
|
238
|
+
)
|
|
239
|
+
except (LeaseConflict, LeaseExpired, StaleLeaseEpoch) as error:
|
|
240
|
+
self._publish_lease_conflict(error)
|
|
241
|
+
return
|
|
242
|
+
self._publish_lease_granted(handle)
|
|
243
|
+
|
|
244
|
+
def _execute_phase(self, payload: dict[str, object]) -> None:
|
|
245
|
+
if self._at_barrier:
|
|
246
|
+
self._publish_lease_conflict(
|
|
247
|
+
ControllerUnavailable("A phase barrier is already active")
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
try:
|
|
251
|
+
self._leases.validate(self._lease_handle(payload))
|
|
252
|
+
except (LeaseConflict, LeaseExpired, StaleLeaseEpoch) as error:
|
|
253
|
+
self._publish_lease_conflict(error)
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
point = FaultPoint(str(payload["point"]))
|
|
257
|
+
mode = BarrierMode(str(payload["barrier_mode"]))
|
|
258
|
+
barrier_payload: dict[str, object] = {}
|
|
259
|
+
operation_id_value = payload.get("operation_id")
|
|
260
|
+
operation_id = None
|
|
261
|
+
if operation_id_value is not None:
|
|
262
|
+
operation_id = self._positive_int(operation_id_value, "operation_id")
|
|
263
|
+
barrier_payload["operation_id"] = operation_id
|
|
264
|
+
|
|
265
|
+
barrier = PhaseBarrier(
|
|
266
|
+
point=point,
|
|
267
|
+
mode=mode,
|
|
268
|
+
publish=self._publish,
|
|
269
|
+
flush=self._journal.flush,
|
|
270
|
+
wait=self._wait_at_barrier,
|
|
271
|
+
payload=barrier_payload,
|
|
272
|
+
)
|
|
273
|
+
self._at_barrier = True
|
|
274
|
+
self._barrier_mode = mode
|
|
275
|
+
try:
|
|
276
|
+
self._driver.execute_phase(point, barrier)
|
|
277
|
+
finally:
|
|
278
|
+
self._barrier_mode = None
|
|
279
|
+
self._at_barrier = False
|
|
280
|
+
if not self._running:
|
|
281
|
+
return
|
|
282
|
+
result_payload: dict[str, object] = {"phase": point.value}
|
|
283
|
+
if operation_id is not None:
|
|
284
|
+
result_payload["operation_id"] = operation_id
|
|
285
|
+
self._publish(MessageKind.OPERATION_RESULT, result_payload)
|
|
286
|
+
|
|
287
|
+
def _status(self) -> None:
|
|
288
|
+
lease = self._leases.status()
|
|
289
|
+
driver_status = self._driver.status()
|
|
290
|
+
state = driver_status.get("state")
|
|
291
|
+
safe_state = (
|
|
292
|
+
state
|
|
293
|
+
if type(state) is str and state in SAFE_RUNTIME_STATES
|
|
294
|
+
else "unknown"
|
|
295
|
+
)
|
|
296
|
+
self._publish(
|
|
297
|
+
MessageKind.STATUS_RESULT,
|
|
298
|
+
{
|
|
299
|
+
"state": safe_state,
|
|
300
|
+
"lease_epoch": lease.lease_epoch,
|
|
301
|
+
"expires_in_s": lease.expires_in_s,
|
|
302
|
+
"lease_expired": lease.expired,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _wait_at_barrier(self, *, heartbeat_enabled: bool = True) -> None:
|
|
307
|
+
if not heartbeat_enabled:
|
|
308
|
+
Event().wait()
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
connection = self._require_connection()
|
|
312
|
+
while self._running:
|
|
313
|
+
self._expire_lease()
|
|
314
|
+
if connection.poll(WORKER_POLL_INTERVAL_S):
|
|
315
|
+
self.handle_message(connection.recv())
|
|
316
|
+
if not self._running:
|
|
317
|
+
raise _BarrierShutdown
|
|
318
|
+
self._emit_heartbeat_if_due()
|
|
319
|
+
raise _BarrierShutdown
|
|
320
|
+
|
|
321
|
+
def _expire_lease(self) -> None:
|
|
322
|
+
expired = self._leases.expire_if_due()
|
|
323
|
+
if expired is None:
|
|
324
|
+
return
|
|
325
|
+
self._publish(
|
|
326
|
+
MessageKind.LEASE_EXPIRED,
|
|
327
|
+
{"lease_epoch": expired.lease_epoch},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _emit_heartbeat_if_due(self) -> None:
|
|
331
|
+
now = self._clock()
|
|
332
|
+
if now - self._last_heartbeat_at < HEARTBEAT_INTERVAL_S:
|
|
333
|
+
return
|
|
334
|
+
self._publish(
|
|
335
|
+
MessageKind.HEARTBEAT,
|
|
336
|
+
{
|
|
337
|
+
"progress_sequence": self._progress_sequence,
|
|
338
|
+
"busy_deadline": None,
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
self._last_heartbeat_at = now
|
|
342
|
+
|
|
343
|
+
def _publish_lease_granted(self, handle: LeaseHandle) -> None:
|
|
344
|
+
self._publish(
|
|
345
|
+
MessageKind.LEASE_GRANTED,
|
|
346
|
+
{
|
|
347
|
+
"lease_epoch": handle.lease_epoch,
|
|
348
|
+
"expires_in_s": self._leases.status().expires_in_s,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _publish_lease_conflict(self, error: Exception) -> None:
|
|
353
|
+
self._publish(
|
|
354
|
+
MessageKind.LEASE_CONFLICT,
|
|
355
|
+
{"error_type": type(error).__name__},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _publish(self, kind: MessageKind, payload: dict[str, object]) -> None:
|
|
359
|
+
self._emit(self._sender.create(kind, **payload))
|
|
360
|
+
|
|
361
|
+
def _lease_handle(self, payload: dict[str, object]) -> LeaseHandle:
|
|
362
|
+
return LeaseHandle(
|
|
363
|
+
generation_id=self._generation_id,
|
|
364
|
+
owner_id=self._owner(payload),
|
|
365
|
+
lease_epoch=self._positive_int(payload["lease_epoch"], "lease_epoch"),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _owner(payload: dict[str, object]) -> UUID:
|
|
370
|
+
return UUID(str(payload["owner_id"]))
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def _positive_int(value: object, field_name: str) -> int:
|
|
374
|
+
if type(value) is not int:
|
|
375
|
+
raise ValueError(f"{field_name} must be an integer")
|
|
376
|
+
if value <= 0:
|
|
377
|
+
raise ValueError(f"{field_name} must be positive")
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
def _advance_progress(self) -> None:
|
|
381
|
+
self._progress_sequence += 1
|
|
382
|
+
|
|
383
|
+
def _require_connection(self) -> Connection:
|
|
384
|
+
if self._connection is None:
|
|
385
|
+
raise RuntimeError("ControllerWorker command loop requires a connection")
|
|
386
|
+
return self._connection
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def controller_worker_main(
|
|
390
|
+
generation_id: int,
|
|
391
|
+
connection: Connection,
|
|
392
|
+
driver: RuntimeDriver,
|
|
393
|
+
journal: RecoveryJournal | None = None,
|
|
394
|
+
) -> None:
|
|
395
|
+
ControllerWorker(
|
|
396
|
+
generation_id,
|
|
397
|
+
driver,
|
|
398
|
+
connection=connection,
|
|
399
|
+
journal=journal,
|
|
400
|
+
).run()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Safe diagnostic representations of 1C launch arguments."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def redact_command(command: Sequence[str]) -> tuple[str, ...]:
|
|
7
|
+
"""Hide /P values; consume /N values even when they look like switches."""
|
|
8
|
+
result: list[str] = []
|
|
9
|
+
credential: str | None = None
|
|
10
|
+
for argument in command:
|
|
11
|
+
if credential is not None:
|
|
12
|
+
result.append("<redacted>" if credential == "/p" and argument else argument)
|
|
13
|
+
credential = None
|
|
14
|
+
else:
|
|
15
|
+
result.append(argument)
|
|
16
|
+
if argument.casefold() in ("/n", "/p"):
|
|
17
|
+
credential = argument.casefold()
|
|
18
|
+
return tuple(result)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Minimal deterministic codec for the 32-bit 1C container used by Worker EPF.
|
|
4
|
+
|
|
5
|
+
The layout and field interpretation are derived from the MIT-licensed
|
|
6
|
+
``saby-integration/v8unpack`` container reader/writer (copyright 2015
|
|
7
|
+
Infactum). This module intentionally implements only the bounded format used
|
|
8
|
+
by the one-object-module runtime Worker; it is not a general 1C file library.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
from struct import Struct
|
|
14
|
+
import zlib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_END_MARKER = 0x7FFFFFFF
|
|
18
|
+
_DEFAULT_BLOCK_SIZE = 0x200
|
|
19
|
+
_CONTAINER_HEADER = Struct("<4i")
|
|
20
|
+
_INDEX_ENTRY = Struct("<3i")
|
|
21
|
+
_FILE_ATTRIBUTES = Struct("<QQi")
|
|
22
|
+
_BLOCK_HEADER_SIZE = 31
|
|
23
|
+
_INDEX_OFFSET = _CONTAINER_HEADER.size
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EpfContainerError(ValueError):
|
|
27
|
+
"""The byte sequence is outside the supported bounded container format."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def raw_deflate(payload: bytes) -> bytes:
|
|
31
|
+
compressor = zlib.compressobj(wbits=-15)
|
|
32
|
+
return compressor.compress(payload) + compressor.flush()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def raw_inflate(payload: bytes) -> bytes:
|
|
36
|
+
try:
|
|
37
|
+
decompressor = zlib.decompressobj(wbits=-15)
|
|
38
|
+
result = decompressor.decompress(payload) + decompressor.flush()
|
|
39
|
+
except zlib.error as error:
|
|
40
|
+
raise EpfContainerError("Invalid raw-deflate stream") from error
|
|
41
|
+
if not decompressor.eof:
|
|
42
|
+
raise EpfContainerError("Invalid raw-deflate stream is truncated")
|
|
43
|
+
if decompressor.unused_data or decompressor.unconsumed_tail:
|
|
44
|
+
raise EpfContainerError("Invalid raw-deflate stream contains trailing data")
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_container(entries: Mapping[str, bytes]) -> bytes:
|
|
49
|
+
ordered = tuple(sorted(entries.items()))
|
|
50
|
+
if not ordered:
|
|
51
|
+
raise EpfContainerError("Container must contain at least one entry")
|
|
52
|
+
if len({name for name, _payload in ordered}) != len(ordered):
|
|
53
|
+
raise EpfContainerError("Container entry names must be unique")
|
|
54
|
+
|
|
55
|
+
stream = BytesIO()
|
|
56
|
+
stream.write(
|
|
57
|
+
_CONTAINER_HEADER.pack(
|
|
58
|
+
_END_MARKER,
|
|
59
|
+
_DEFAULT_BLOCK_SIZE,
|
|
60
|
+
len(ordered),
|
|
61
|
+
0,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
stream.write(b"\x00" * (_BLOCK_HEADER_SIZE + _DEFAULT_BLOCK_SIZE))
|
|
65
|
+
|
|
66
|
+
table: list[tuple[int, int]] = []
|
|
67
|
+
for name, payload in ordered:
|
|
68
|
+
if not name or "\x00" in name:
|
|
69
|
+
raise EpfContainerError("Container entry name is invalid")
|
|
70
|
+
if not isinstance(payload, bytes):
|
|
71
|
+
raise TypeError("Container entries must contain bytes")
|
|
72
|
+
attributes = (
|
|
73
|
+
_FILE_ATTRIBUTES.pack(0, 0, 0)
|
|
74
|
+
+ name.encode("utf-16-le")
|
|
75
|
+
+ b"\x00" * 4
|
|
76
|
+
)
|
|
77
|
+
attribute_offset = _write_document(stream, attributes)
|
|
78
|
+
data_offset = _write_document(
|
|
79
|
+
stream,
|
|
80
|
+
payload,
|
|
81
|
+
minimum_block_size=_DEFAULT_BLOCK_SIZE,
|
|
82
|
+
)
|
|
83
|
+
table.append((attribute_offset, data_offset))
|
|
84
|
+
|
|
85
|
+
toc = b"".join(
|
|
86
|
+
_INDEX_ENTRY.pack(attribute_offset, data_offset, _END_MARKER)
|
|
87
|
+
for attribute_offset, data_offset in table
|
|
88
|
+
)
|
|
89
|
+
_write_document(
|
|
90
|
+
stream,
|
|
91
|
+
toc,
|
|
92
|
+
minimum_block_size=_DEFAULT_BLOCK_SIZE,
|
|
93
|
+
offset=_INDEX_OFFSET,
|
|
94
|
+
)
|
|
95
|
+
return stream.getvalue()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_container(payload: bytes) -> dict[str, bytes]:
|
|
99
|
+
if len(payload) < _INDEX_OFFSET + _BLOCK_HEADER_SIZE:
|
|
100
|
+
raise EpfContainerError("Container is truncated")
|
|
101
|
+
try:
|
|
102
|
+
end_marker, block_size, count, reserved = _CONTAINER_HEADER.unpack_from(
|
|
103
|
+
payload, 0
|
|
104
|
+
)
|
|
105
|
+
except Exception as error:
|
|
106
|
+
raise EpfContainerError("Container header is invalid") from error
|
|
107
|
+
if (
|
|
108
|
+
end_marker != _END_MARKER
|
|
109
|
+
or block_size != _DEFAULT_BLOCK_SIZE
|
|
110
|
+
or count <= 0
|
|
111
|
+
or reserved != 0
|
|
112
|
+
):
|
|
113
|
+
raise EpfContainerError("Container header is unsupported")
|
|
114
|
+
|
|
115
|
+
toc = _read_document(payload, _INDEX_OFFSET)
|
|
116
|
+
if not toc or len(toc) % _INDEX_ENTRY.size:
|
|
117
|
+
raise EpfContainerError("Container table of contents is invalid")
|
|
118
|
+
|
|
119
|
+
result: dict[str, bytes] = {}
|
|
120
|
+
# Platform-built nested module containers may retain a header count that is
|
|
121
|
+
# one lower than the actual TOC entry count. v8unpack likewise treats the
|
|
122
|
+
# bounded TOC document as authoritative.
|
|
123
|
+
toc_count = len(toc) // _INDEX_ENTRY.size
|
|
124
|
+
if toc_count not in {count, count + 1}:
|
|
125
|
+
raise EpfContainerError("Container entry count does not match its bounded TOC")
|
|
126
|
+
for index in range(toc_count):
|
|
127
|
+
position = index * _INDEX_ENTRY.size
|
|
128
|
+
attribute_offset, data_offset, marker = _INDEX_ENTRY.unpack_from(toc, position)
|
|
129
|
+
if marker != _END_MARKER:
|
|
130
|
+
raise EpfContainerError("Container table marker is invalid")
|
|
131
|
+
attributes = _read_document(payload, attribute_offset)
|
|
132
|
+
if len(attributes) < _FILE_ATTRIBUTES.size + 4:
|
|
133
|
+
raise EpfContainerError("Container entry attributes are truncated")
|
|
134
|
+
name_bytes = attributes[_FILE_ATTRIBUTES.size :]
|
|
135
|
+
try:
|
|
136
|
+
name = name_bytes.decode("utf-16-le").split("\x00", 1)[0]
|
|
137
|
+
except UnicodeDecodeError as error:
|
|
138
|
+
raise EpfContainerError("Container entry name is invalid") from error
|
|
139
|
+
if not name or name in result:
|
|
140
|
+
raise EpfContainerError("Container entry name is empty or duplicated")
|
|
141
|
+
result[name] = _read_document(payload, data_offset)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _write_document(
|
|
146
|
+
stream: BytesIO,
|
|
147
|
+
data: bytes,
|
|
148
|
+
*,
|
|
149
|
+
minimum_block_size: int = 0,
|
|
150
|
+
offset: int | None = None,
|
|
151
|
+
) -> int:
|
|
152
|
+
if offset is None:
|
|
153
|
+
stream.seek(0, 2)
|
|
154
|
+
offset = stream.tell()
|
|
155
|
+
else:
|
|
156
|
+
stream.seek(offset)
|
|
157
|
+
current_block_size = max(minimum_block_size, len(data))
|
|
158
|
+
if current_block_size > _END_MARKER:
|
|
159
|
+
raise EpfContainerError("Container document is too large")
|
|
160
|
+
header = (
|
|
161
|
+
f"\r\n{len(data):08x} {current_block_size:08x} "
|
|
162
|
+
f"{_END_MARKER:08x} \r\n"
|
|
163
|
+
).encode("ascii")
|
|
164
|
+
if len(header) != _BLOCK_HEADER_SIZE:
|
|
165
|
+
raise AssertionError("Unexpected 1C block header size")
|
|
166
|
+
stream.write(header)
|
|
167
|
+
stream.write(data)
|
|
168
|
+
stream.write(b"\x00" * (current_block_size - len(data)))
|
|
169
|
+
return offset
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _read_document(payload: bytes, offset: int) -> bytes:
|
|
173
|
+
if offset < _INDEX_OFFSET or offset + _BLOCK_HEADER_SIZE > len(payload):
|
|
174
|
+
raise EpfContainerError("Container document offset is outside the file")
|
|
175
|
+
header = payload[offset : offset + _BLOCK_HEADER_SIZE]
|
|
176
|
+
if (
|
|
177
|
+
header[:2] != b"\r\n"
|
|
178
|
+
or header[10:11] != b" "
|
|
179
|
+
or header[19:20] != b" "
|
|
180
|
+
or header[28:29] != b" "
|
|
181
|
+
or header[29:] != b"\r\n"
|
|
182
|
+
):
|
|
183
|
+
raise EpfContainerError("Container document header is invalid")
|
|
184
|
+
try:
|
|
185
|
+
document_size = int(header[2:10], 16)
|
|
186
|
+
current_block_size = int(header[11:19], 16)
|
|
187
|
+
next_offset = int(header[20:28], 16)
|
|
188
|
+
except ValueError as error:
|
|
189
|
+
raise EpfContainerError("Container document size is invalid") from error
|
|
190
|
+
if current_block_size < document_size or next_offset != _END_MARKER:
|
|
191
|
+
raise EpfContainerError("Chained container documents are unsupported")
|
|
192
|
+
start = offset + _BLOCK_HEADER_SIZE
|
|
193
|
+
end = start + current_block_size
|
|
194
|
+
if end > len(payload):
|
|
195
|
+
raise EpfContainerError("Container document is truncated")
|
|
196
|
+
return payload[start : start + document_size]
|