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
onec_runtime/session.py
ADDED
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import stat
|
|
4
|
+
import warnings
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from hashlib import sha256
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from threading import Event, RLock, Thread, current_thread
|
|
12
|
+
from time import sleep
|
|
13
|
+
from typing import Iterator, cast
|
|
14
|
+
from uuid import UUID, uuid4
|
|
15
|
+
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import psutil
|
|
18
|
+
|
|
19
|
+
from onec_runtime.artifacts import ArtifactWriter
|
|
20
|
+
from onec_runtime.bootstrap import (
|
|
21
|
+
ExtensionHandshakeContract,
|
|
22
|
+
ServerGuardSession,
|
|
23
|
+
enable_server_kernel_loop,
|
|
24
|
+
observe_extension_handshake,
|
|
25
|
+
verify_extension_handshake,
|
|
26
|
+
verify_extension_safe_mode_disabled,
|
|
27
|
+
wait_for_managed_startup_stop,
|
|
28
|
+
wait_for_server_entry_then_service,
|
|
29
|
+
)
|
|
30
|
+
from onec_runtime.bsl import (
|
|
31
|
+
SourceUnitKind,
|
|
32
|
+
SourceUnitRef,
|
|
33
|
+
mapped_visible_source,
|
|
34
|
+
source_sha256,
|
|
35
|
+
)
|
|
36
|
+
from onec_runtime.bsl.module_universe import (
|
|
37
|
+
WorkerModuleUnit,
|
|
38
|
+
)
|
|
39
|
+
from onec_runtime.bsl.module_catalog import SessionCommonModuleCatalog
|
|
40
|
+
from onec_runtime.capture_source import (
|
|
41
|
+
CaptureBinding,
|
|
42
|
+
CapturePointRequest,
|
|
43
|
+
CaptureSourceConfig,
|
|
44
|
+
CommonModuleCaptureResolver,
|
|
45
|
+
ResolvedCapturePoint,
|
|
46
|
+
)
|
|
47
|
+
from onec_runtime.config import RuntimeConfig
|
|
48
|
+
from onec_runtime.configurator_agent import ExtensionAgentEditor, edit_extension
|
|
49
|
+
from onec_runtime.errors import (
|
|
50
|
+
CaptureSourceNotConfigured,
|
|
51
|
+
CommandTimeout,
|
|
52
|
+
ExtensionHandshakeError,
|
|
53
|
+
ManualExtensionUnavailable,
|
|
54
|
+
ManualExtensionVersionWarning,
|
|
55
|
+
ProcessStartError,
|
|
56
|
+
ProtocolError,
|
|
57
|
+
RdbgDebugUiNotRegistered,
|
|
58
|
+
TargetLost,
|
|
59
|
+
UnexpectedStop,
|
|
60
|
+
)
|
|
61
|
+
from onec_runtime.extension_bundle import (
|
|
62
|
+
ExtensionBundle,
|
|
63
|
+
ExtensionHandshakeEvidence,
|
|
64
|
+
ExtensionManifest,
|
|
65
|
+
packaged_extension_bundle,
|
|
66
|
+
)
|
|
67
|
+
from onec_runtime.extension_lifecycle import (
|
|
68
|
+
ExtensionLifecycle,
|
|
69
|
+
LifecycleDecision,
|
|
70
|
+
LifecycleMode,
|
|
71
|
+
)
|
|
72
|
+
from onec_runtime.extension_state import ExtensionStateStore
|
|
73
|
+
from onec_runtime.performance_profile import PhaseRecorder
|
|
74
|
+
from onec_runtime.processes import FileModeProcesses
|
|
75
|
+
from onec_runtime.prototype_runtime import (
|
|
76
|
+
ContinuationAttemptEvidence,
|
|
77
|
+
ContinuationAttemptSpec,
|
|
78
|
+
OperationState,
|
|
79
|
+
PrototypeRuntimeController,
|
|
80
|
+
)
|
|
81
|
+
from onec_runtime.rdbg.models import ModuleLocation, TargetId
|
|
82
|
+
from onec_runtime.rdbg.session import RdbgSession
|
|
83
|
+
from onec_runtime.rdbg.transport import RdbgTransport, TranscriptEntry
|
|
84
|
+
from onec_runtime.recovery_journal import RecoveryJournal
|
|
85
|
+
from onec_runtime.runtime_api import (
|
|
86
|
+
PrototypeRuntimeApi,
|
|
87
|
+
RuntimeNamespaceSnapshot,
|
|
88
|
+
RuntimeReply,
|
|
89
|
+
RuntimeStatus,
|
|
90
|
+
)
|
|
91
|
+
from onec_runtime.runtime_contracts import OperationExecutionProvenance
|
|
92
|
+
from onec_runtime.server_worker import (
|
|
93
|
+
NotebookWorkerArtifactBuilder,
|
|
94
|
+
)
|
|
95
|
+
from onec_runtime.startup_diagnostics import startup_log_hint
|
|
96
|
+
from onec_runtime.worker_universe import (
|
|
97
|
+
WorkerGenerationHandle,
|
|
98
|
+
WorkerModuleArtifactBuilder,
|
|
99
|
+
WorkerModuleArtifactCache,
|
|
100
|
+
)
|
|
101
|
+
from onec_runtime.worker_breakpoints import (
|
|
102
|
+
WorkerBreakpointReloadPolicy,
|
|
103
|
+
WorkerBreakpointReloadReport,
|
|
104
|
+
WorkerBreakpointStatus,
|
|
105
|
+
)
|
|
106
|
+
from onec_runtime.table_materialization import ReferenceMode
|
|
107
|
+
from onec_runtime.toolchain import (
|
|
108
|
+
ToolResult,
|
|
109
|
+
dump_target_extension_cfe,
|
|
110
|
+
dump_target_extension_files,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ExtensionMode(StrEnum):
|
|
115
|
+
AUTO = "auto"
|
|
116
|
+
MANUAL = "manual"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
120
|
+
class RuntimeSessionConfig:
|
|
121
|
+
"""Configuration owned by the headless runtime session."""
|
|
122
|
+
|
|
123
|
+
runtime: RuntimeConfig
|
|
124
|
+
evidence_root: Path | str | None = None
|
|
125
|
+
chunk_size: int = 2400
|
|
126
|
+
startup_profiler: PhaseRecorder | None = None
|
|
127
|
+
capture_source: CaptureSourceConfig | None = None
|
|
128
|
+
source_root: Path | str | None = None
|
|
129
|
+
extension_mode: ExtensionMode = ExtensionMode.AUTO
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
if not isinstance(self.extension_mode, ExtensionMode):
|
|
133
|
+
raise TypeError("extension_mode must be an ExtensionMode")
|
|
134
|
+
evidence_root = self.evidence_root
|
|
135
|
+
if evidence_root is None:
|
|
136
|
+
if not isinstance(self.runtime, RuntimeConfig):
|
|
137
|
+
raise ValueError("evidence_root requires a RuntimeConfig")
|
|
138
|
+
evidence_root = self.runtime.artifacts_dir
|
|
139
|
+
object.__setattr__(self, "evidence_root", Path(evidence_root).resolve())
|
|
140
|
+
if self.source_root is not None:
|
|
141
|
+
supplied_source_root = Path(self.source_root).absolute()
|
|
142
|
+
try:
|
|
143
|
+
# Resolve only after inspecting the original spelling: resolve()
|
|
144
|
+
# erases symlink/junction evidence, including in parent paths.
|
|
145
|
+
for component in (*reversed(supplied_source_root.parents), supplied_source_root):
|
|
146
|
+
status = component.lstat()
|
|
147
|
+
if (stat.S_ISLNK(status.st_mode) or
|
|
148
|
+
getattr(status, "st_file_attributes", 0) &
|
|
149
|
+
getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)):
|
|
150
|
+
raise ProtocolError("common-module source root is unsafe")
|
|
151
|
+
resolved_source_root = supplied_source_root.resolve(strict=True)
|
|
152
|
+
except OSError as error:
|
|
153
|
+
raise ProtocolError(
|
|
154
|
+
"common-module source root is unsafe"
|
|
155
|
+
) from error
|
|
156
|
+
object.__setattr__(
|
|
157
|
+
self,
|
|
158
|
+
"source_root",
|
|
159
|
+
resolved_source_root,
|
|
160
|
+
)
|
|
161
|
+
if self.chunk_size <= 0:
|
|
162
|
+
raise ValueError("chunk_size must be positive")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class RuntimeCaptureFence:
|
|
167
|
+
capture_intent_id: str
|
|
168
|
+
operation_id: str
|
|
169
|
+
source_revision: int
|
|
170
|
+
source_sha256: str
|
|
171
|
+
capture_generation: int
|
|
172
|
+
stop_sequence: int
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True, slots=True)
|
|
176
|
+
class RuntimeCaptureArming:
|
|
177
|
+
ticket_id: str
|
|
178
|
+
expected_controller_operation_id: int
|
|
179
|
+
expected_stop_sequence: int
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True, slots=True)
|
|
183
|
+
class _ActiveCaptureTicket:
|
|
184
|
+
ticket_id: str
|
|
185
|
+
capture_intent_id: str
|
|
186
|
+
operation_id: str
|
|
187
|
+
capture_generation: int
|
|
188
|
+
source_revision: int
|
|
189
|
+
source_sha256: str
|
|
190
|
+
stop_sequence: int
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True, slots=True)
|
|
194
|
+
class _AttemptBootstrapEvidence:
|
|
195
|
+
managed_stop: dict[str, object]
|
|
196
|
+
server_entry_stop: dict[str, object]
|
|
197
|
+
server_service_stop: dict[str, object]
|
|
198
|
+
guard: dict[str, object]
|
|
199
|
+
same_session: bool
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _OwnedAttemptFailure(Exception):
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
primary: BaseException,
|
|
206
|
+
*,
|
|
207
|
+
stage: str,
|
|
208
|
+
repairable: bool,
|
|
209
|
+
cleanup_succeeded: bool,
|
|
210
|
+
cleanup_notes: tuple[str, ...],
|
|
211
|
+
) -> None:
|
|
212
|
+
super().__init__(type(primary).__name__)
|
|
213
|
+
self.primary = primary
|
|
214
|
+
self.stage = stage
|
|
215
|
+
self.repairable = repairable
|
|
216
|
+
self.cleanup_succeeded = cleanup_succeeded
|
|
217
|
+
self.cleanup_notes = cleanup_notes
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
_STARTUP_CLEANUP_RETRY_NOTE = (
|
|
221
|
+
"Runtime startup cleanup is incomplete; retry with exception.retry_cleanup()"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _expose_startup_cleanup_retry(
|
|
226
|
+
error: BaseException,
|
|
227
|
+
retry_cleanup: Callable[[], None],
|
|
228
|
+
) -> None:
|
|
229
|
+
setattr(error, "retry_cleanup", retry_cleanup)
|
|
230
|
+
notes = getattr(error, "__notes__", ())
|
|
231
|
+
if _STARTUP_CLEANUP_RETRY_NOTE not in notes:
|
|
232
|
+
error.add_note(_STARTUP_CLEANUP_RETRY_NOTE)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class _StartupAttemptCleanup:
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
runtime: RuntimeConfig,
|
|
239
|
+
processes: FileModeProcesses,
|
|
240
|
+
transport: RdbgTransport | None,
|
|
241
|
+
rdbg: RdbgSession | None,
|
|
242
|
+
) -> None:
|
|
243
|
+
self._server_mode = runtime.is_server_infobase
|
|
244
|
+
self._processes = processes
|
|
245
|
+
self._transport = transport
|
|
246
|
+
self._rdbg = rdbg
|
|
247
|
+
self._server_session_terminated = not self._server_mode or rdbg is None
|
|
248
|
+
self._native_client_termination_requested = False
|
|
249
|
+
self._processes_closed = False
|
|
250
|
+
self._debug_ui_detached = not self._server_mode or rdbg is None
|
|
251
|
+
self._transport_closed = transport is None
|
|
252
|
+
self.cleanup_error_types: tuple[str, ...] = ()
|
|
253
|
+
|
|
254
|
+
def _raise_cleanup_error(self, errors: list[BaseException]) -> None:
|
|
255
|
+
self.cleanup_error_types = tuple(
|
|
256
|
+
type(error).__name__ for error in errors
|
|
257
|
+
)
|
|
258
|
+
if errors:
|
|
259
|
+
raise ProtocolError(
|
|
260
|
+
"Runtime startup cleanup failed: "
|
|
261
|
+
+ ", ".join(self.cleanup_error_types)
|
|
262
|
+
) from None
|
|
263
|
+
|
|
264
|
+
def retry_cleanup(self) -> None:
|
|
265
|
+
"""Retry incomplete startup cleanup in safe resource order."""
|
|
266
|
+
errors: list[BaseException] = []
|
|
267
|
+
self.cleanup_error_types = ()
|
|
268
|
+
if self._server_mode:
|
|
269
|
+
if not self._server_session_terminated:
|
|
270
|
+
try:
|
|
271
|
+
assert self._rdbg is not None
|
|
272
|
+
self._native_client_termination_requested = self._rdbg.terminate_bound_server_session()
|
|
273
|
+
except RdbgDebugUiNotRegistered:
|
|
274
|
+
# Native termination cannot use a UI the server has lost.
|
|
275
|
+
# Close only our client, then finish deregistration.
|
|
276
|
+
self._server_session_terminated = True
|
|
277
|
+
except BaseException as error:
|
|
278
|
+
errors.append(error)
|
|
279
|
+
else:
|
|
280
|
+
self._server_session_terminated = True
|
|
281
|
+
self._raise_cleanup_error(errors)
|
|
282
|
+
if not self._processes_closed:
|
|
283
|
+
try:
|
|
284
|
+
if self._native_client_termination_requested:
|
|
285
|
+
self._processes.close(graceful_client_timeout_s=3.0)
|
|
286
|
+
else:
|
|
287
|
+
self._processes.close()
|
|
288
|
+
except BaseException as error:
|
|
289
|
+
errors.append(error)
|
|
290
|
+
else:
|
|
291
|
+
self._processes_closed = True
|
|
292
|
+
self._raise_cleanup_error(errors)
|
|
293
|
+
if not self._debug_ui_detached:
|
|
294
|
+
try:
|
|
295
|
+
assert self._rdbg is not None
|
|
296
|
+
self._rdbg.detach()
|
|
297
|
+
except BaseException as error:
|
|
298
|
+
errors.append(error)
|
|
299
|
+
else:
|
|
300
|
+
self._debug_ui_detached = True
|
|
301
|
+
self._raise_cleanup_error(errors)
|
|
302
|
+
if not self._transport_closed:
|
|
303
|
+
try:
|
|
304
|
+
assert self._transport is not None
|
|
305
|
+
self._transport.close()
|
|
306
|
+
except BaseException as error:
|
|
307
|
+
errors.append(error)
|
|
308
|
+
else:
|
|
309
|
+
self._transport_closed = True
|
|
310
|
+
self._raise_cleanup_error(errors)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
if not self._transport_closed:
|
|
314
|
+
try:
|
|
315
|
+
assert self._transport is not None
|
|
316
|
+
self._transport.close()
|
|
317
|
+
except BaseException as error:
|
|
318
|
+
errors.append(error)
|
|
319
|
+
else:
|
|
320
|
+
self._transport_closed = True
|
|
321
|
+
if not self._processes_closed:
|
|
322
|
+
try:
|
|
323
|
+
self._processes.close()
|
|
324
|
+
except BaseException as error:
|
|
325
|
+
errors.append(error)
|
|
326
|
+
else:
|
|
327
|
+
self._processes_closed = True
|
|
328
|
+
self._raise_cleanup_error(errors)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _start_rdbg_with_registration_retry(
|
|
332
|
+
*,
|
|
333
|
+
transport: RdbgTransport,
|
|
334
|
+
location: ModuleLocation,
|
|
335
|
+
alias: str,
|
|
336
|
+
server_target_type: str,
|
|
337
|
+
break_on_next: bool,
|
|
338
|
+
progress: Callable[[str], None] | None,
|
|
339
|
+
on_candidate: Callable[[RdbgSession], None] | None = None,
|
|
340
|
+
attempts: int = 24,
|
|
341
|
+
retry_interval_s: float = 5.0,
|
|
342
|
+
) -> RdbgSession:
|
|
343
|
+
"""Register a fresh debugger UI before any owned 1C client is launched."""
|
|
344
|
+
if attempts < 1 or retry_interval_s < 0:
|
|
345
|
+
raise ValueError("RDBG registration retry bounds are invalid")
|
|
346
|
+
for number in range(1, attempts + 1):
|
|
347
|
+
rdbg = RdbgSession(
|
|
348
|
+
transport,
|
|
349
|
+
location,
|
|
350
|
+
alias=alias,
|
|
351
|
+
server_target_type=server_target_type,
|
|
352
|
+
break_on_next=break_on_next,
|
|
353
|
+
)
|
|
354
|
+
if on_candidate is not None:
|
|
355
|
+
on_candidate(rdbg)
|
|
356
|
+
try:
|
|
357
|
+
rdbg.initialize()
|
|
358
|
+
rdbg.set_service_breakpoint()
|
|
359
|
+
rdbg.verify_registration()
|
|
360
|
+
except RdbgDebugUiNotRegistered as error:
|
|
361
|
+
try:
|
|
362
|
+
rdbg.detach()
|
|
363
|
+
except BaseException as cleanup_error:
|
|
364
|
+
error.add_note(
|
|
365
|
+
"RDBG registration cleanup failed: "
|
|
366
|
+
f"{type(cleanup_error).__name__}"
|
|
367
|
+
)
|
|
368
|
+
raise
|
|
369
|
+
if number == attempts:
|
|
370
|
+
raise ProtocolError(
|
|
371
|
+
"RDBG debug UI registration did not stabilize after "
|
|
372
|
+
f"{attempts} attempts"
|
|
373
|
+
) from error
|
|
374
|
+
if progress is not None:
|
|
375
|
+
progress(
|
|
376
|
+
"Повторная регистрация интерфейса отладки 1С "
|
|
377
|
+
f"({number + 1}/{attempts})"
|
|
378
|
+
)
|
|
379
|
+
sleep(retry_interval_s)
|
|
380
|
+
except BaseException as error:
|
|
381
|
+
try:
|
|
382
|
+
rdbg.detach()
|
|
383
|
+
except BaseException as cleanup_error:
|
|
384
|
+
error.add_note(
|
|
385
|
+
"RDBG registration cleanup failed: "
|
|
386
|
+
f"{type(cleanup_error).__name__}"
|
|
387
|
+
)
|
|
388
|
+
raise
|
|
389
|
+
else:
|
|
390
|
+
return rdbg
|
|
391
|
+
raise AssertionError("RDBG registration retry bound was exceeded")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass(slots=True)
|
|
395
|
+
class _OnecInteractiveRuntimeTools:
|
|
396
|
+
runtime: RuntimeConfig
|
|
397
|
+
_editor: ExtensionAgentEditor | None = field(default=None, init=False)
|
|
398
|
+
|
|
399
|
+
@contextmanager
|
|
400
|
+
def mutation_session(self, log_dir: Path) -> Iterator[None]:
|
|
401
|
+
if self._editor is not None:
|
|
402
|
+
raise ProtocolError("extension agent session is already active")
|
|
403
|
+
with edit_extension(self.runtime, log_dir) as editor:
|
|
404
|
+
self._editor = editor
|
|
405
|
+
try:
|
|
406
|
+
yield
|
|
407
|
+
finally:
|
|
408
|
+
self._editor = None
|
|
409
|
+
|
|
410
|
+
def dump_files(self, destination: Path, log_path: Path) -> ToolResult:
|
|
411
|
+
if self._editor is not None:
|
|
412
|
+
with self._editor.designer_access():
|
|
413
|
+
return dump_target_extension_files(self.runtime, destination, log_path)
|
|
414
|
+
return dump_target_extension_files(self.runtime, destination, log_path)
|
|
415
|
+
|
|
416
|
+
def dump_cfe(self, destination: Path, log_path: Path) -> ToolResult:
|
|
417
|
+
if self._editor is not None:
|
|
418
|
+
with self._editor.designer_access():
|
|
419
|
+
return dump_target_extension_cfe(self.runtime, destination, log_path)
|
|
420
|
+
return dump_target_extension_cfe(self.runtime, destination, log_path)
|
|
421
|
+
|
|
422
|
+
def load_cfe(self, source: Path, log_path: Path) -> None:
|
|
423
|
+
if self._editor is None:
|
|
424
|
+
raise ProtocolError("extension agent session is not active")
|
|
425
|
+
self._editor.load_cfe(source)
|
|
426
|
+
|
|
427
|
+
def apply(self, log_path: Path) -> None:
|
|
428
|
+
if self._editor is None:
|
|
429
|
+
raise ProtocolError("extension agent session is not active")
|
|
430
|
+
self._editor.apply()
|
|
431
|
+
|
|
432
|
+
def disable_safe_mode(self, log_dir: Path) -> None:
|
|
433
|
+
if self._editor is None:
|
|
434
|
+
raise ProtocolError("extension agent session is not active")
|
|
435
|
+
self._editor.disable_safe_mode()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class _SessionContinuationAdmission:
|
|
439
|
+
def __init__(
|
|
440
|
+
self,
|
|
441
|
+
session: "RuntimeSession",
|
|
442
|
+
api_admission: object,
|
|
443
|
+
*,
|
|
444
|
+
active_ticket: _ActiveCaptureTicket | None,
|
|
445
|
+
active_points: tuple[object, ...],
|
|
446
|
+
active_locations: tuple[object, ...],
|
|
447
|
+
arming: RuntimeCaptureArming | None,
|
|
448
|
+
) -> None:
|
|
449
|
+
self._session = session
|
|
450
|
+
self._api_admission = api_admission
|
|
451
|
+
self._active_ticket = active_ticket
|
|
452
|
+
self._active_points = active_points
|
|
453
|
+
self._active_locations = active_locations
|
|
454
|
+
self.arming = arming
|
|
455
|
+
self._closed = False
|
|
456
|
+
|
|
457
|
+
def commit(self) -> None:
|
|
458
|
+
if not self._closed:
|
|
459
|
+
getattr(self._api_admission, "commit")()
|
|
460
|
+
self._closed = True
|
|
461
|
+
|
|
462
|
+
def rollback(self) -> None:
|
|
463
|
+
if self._closed:
|
|
464
|
+
return
|
|
465
|
+
getattr(self._api_admission, "rollback")()
|
|
466
|
+
with self._session._operation_lock:
|
|
467
|
+
self._session._active_capture_ticket = self._active_ticket
|
|
468
|
+
self._session._active_capture_points = self._active_points
|
|
469
|
+
self._session._active_capture_locations = self._active_locations
|
|
470
|
+
self._closed = True
|
|
471
|
+
|
|
472
|
+
def quarantine(self) -> None:
|
|
473
|
+
if self._closed:
|
|
474
|
+
return
|
|
475
|
+
try:
|
|
476
|
+
getattr(self._api_admission, "quarantine")()
|
|
477
|
+
finally:
|
|
478
|
+
with self._session._operation_lock:
|
|
479
|
+
self._session._active_capture_ticket = None
|
|
480
|
+
self._session._active_capture_points = ()
|
|
481
|
+
self._session._active_capture_locations = ()
|
|
482
|
+
self._closed = True
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def runtime_bootstrap_locations(
|
|
486
|
+
manifest: ExtensionManifest,
|
|
487
|
+
) -> tuple[ModuleLocation, ModuleLocation, ModuleLocation]:
|
|
488
|
+
breakpoints = manifest.breakpoints
|
|
489
|
+
return breakpoints.managed, breakpoints.server_entry, breakpoints.server_service
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _redacted_transcript_sink(
|
|
493
|
+
artifacts: ArtifactWriter,
|
|
494
|
+
) -> Callable[[TranscriptEntry], None]:
|
|
495
|
+
def sink(entry: TranscriptEntry) -> None:
|
|
496
|
+
artifacts.append_jsonl(
|
|
497
|
+
"rdbg-summary.jsonl",
|
|
498
|
+
{
|
|
499
|
+
"command": entry.command,
|
|
500
|
+
"status_code": entry.status_code,
|
|
501
|
+
"duration_ms": entry.duration_ms,
|
|
502
|
+
"request_sha256": sha256(entry.request).hexdigest(),
|
|
503
|
+
"response_sha256": sha256(entry.response).hexdigest(),
|
|
504
|
+
"error": bool(entry.error),
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return sink
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _stop_summary(stop: object, *, target_type: str) -> dict[str, object]:
|
|
512
|
+
location = getattr(stop, "location")
|
|
513
|
+
return {
|
|
514
|
+
"target_type": target_type,
|
|
515
|
+
"module_type": location.module_type,
|
|
516
|
+
"line": location.line,
|
|
517
|
+
"extension_name": location.extension_name,
|
|
518
|
+
"reason": getattr(stop, "reason"),
|
|
519
|
+
"stop_by_breakpoint": getattr(stop, "stop_by_breakpoint"),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _handshake_summary(evidence: ExtensionHandshakeEvidence) -> dict[str, object]:
|
|
524
|
+
location = evidence.location
|
|
525
|
+
return {
|
|
526
|
+
"target_type": evidence.target_type,
|
|
527
|
+
"product_id": evidence.product_id,
|
|
528
|
+
"artifact_version": evidence.artifact_version,
|
|
529
|
+
"protocol_version": evidence.protocol_version,
|
|
530
|
+
"location": {
|
|
531
|
+
"module_type": location.module_type,
|
|
532
|
+
"url": location.url,
|
|
533
|
+
"object_id": str(location.object_id),
|
|
534
|
+
"property_id": str(location.property_id),
|
|
535
|
+
"line": location.line,
|
|
536
|
+
"extension_name": location.extension_name,
|
|
537
|
+
"ext_id": location.ext_id,
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class RuntimeSession:
|
|
543
|
+
"""Own one headless 1C runtime/debugger pair."""
|
|
544
|
+
|
|
545
|
+
def __init__(
|
|
546
|
+
self,
|
|
547
|
+
config: RuntimeSessionConfig,
|
|
548
|
+
processes: FileModeProcesses,
|
|
549
|
+
transport: RdbgTransport,
|
|
550
|
+
rdbg: RdbgSession,
|
|
551
|
+
runtime_api: PrototypeRuntimeApi,
|
|
552
|
+
artifacts: ArtifactWriter,
|
|
553
|
+
*,
|
|
554
|
+
heartbeat_interval_s: float = 15.0,
|
|
555
|
+
) -> None:
|
|
556
|
+
if heartbeat_interval_s <= 0:
|
|
557
|
+
raise ValueError("heartbeat_interval_s must be positive")
|
|
558
|
+
self.config = config
|
|
559
|
+
self._processes = processes
|
|
560
|
+
self._transport = transport
|
|
561
|
+
self._rdbg = rdbg
|
|
562
|
+
self.runtime_api = runtime_api
|
|
563
|
+
self.artifacts = artifacts
|
|
564
|
+
self._common_module_catalog = (
|
|
565
|
+
None
|
|
566
|
+
if config.source_root is None
|
|
567
|
+
else SessionCommonModuleCatalog(
|
|
568
|
+
config.source_root,
|
|
569
|
+
profile="runtime-session-server-v1",
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
self._worker_file_revisions: dict[str, int] = {}
|
|
573
|
+
self._active_worker_file_units: dict[str, SourceUnitRef] = {}
|
|
574
|
+
self._closed = False
|
|
575
|
+
self._close_lock = RLock()
|
|
576
|
+
self._transport_closed = False
|
|
577
|
+
self._processes_closed = False
|
|
578
|
+
self._runtime_api_closed = False
|
|
579
|
+
self._debug_ui_detached = not config.runtime.is_server_infobase
|
|
580
|
+
self._server_session_terminated = not config.runtime.is_server_infobase
|
|
581
|
+
self._native_client_termination_requested = False
|
|
582
|
+
self._operation_lock = RLock()
|
|
583
|
+
self._capture_locations: dict[tuple[str, int], object] = {}
|
|
584
|
+
self._capture_source_resolver: CommonModuleCaptureResolver | None = None
|
|
585
|
+
self._capture_source_bindings: dict[
|
|
586
|
+
tuple[str, int], CaptureBinding
|
|
587
|
+
] = {}
|
|
588
|
+
self._active_capture_points: tuple[object, ...] = ()
|
|
589
|
+
self._active_capture_locations: tuple[object, ...] = ()
|
|
590
|
+
self._file_capture_points: tuple[ModuleLocation, ...] = ()
|
|
591
|
+
self._active_capture_ticket: _ActiveCaptureTicket | None = None
|
|
592
|
+
self._capture_resume_listeners: list[Callable[[RuntimeCaptureFence], None]] = []
|
|
593
|
+
self._attempt_bootstrap_evidence: _AttemptBootstrapEvidence | None = None
|
|
594
|
+
self._heartbeat_interval_s = heartbeat_interval_s
|
|
595
|
+
self._heartbeat_stop = Event()
|
|
596
|
+
self._heartbeat_thread = Thread(
|
|
597
|
+
target=self._heartbeat_loop,
|
|
598
|
+
name="onec-runtime-rdbg-heartbeat",
|
|
599
|
+
daemon=True,
|
|
600
|
+
)
|
|
601
|
+
self._heartbeat_thread.start()
|
|
602
|
+
|
|
603
|
+
def _heartbeat_loop(self) -> None:
|
|
604
|
+
while not self._heartbeat_stop.wait(self._heartbeat_interval_s):
|
|
605
|
+
if not self._operation_lock.acquire(blocking=False):
|
|
606
|
+
continue
|
|
607
|
+
lost_debug_ui = False
|
|
608
|
+
lost_owned_process = False
|
|
609
|
+
try:
|
|
610
|
+
if not self._closed:
|
|
611
|
+
try:
|
|
612
|
+
self._rdbg.heartbeat()
|
|
613
|
+
except RdbgDebugUiNotRegistered:
|
|
614
|
+
lost_debug_ui = True
|
|
615
|
+
except Exception:
|
|
616
|
+
# A transient transport failure must not permanently
|
|
617
|
+
# disable later keepalives. The next notebook command
|
|
618
|
+
# remains the authoritative place to surface it.
|
|
619
|
+
pass
|
|
620
|
+
ensure_running = getattr(self._processes, "ensure_running", None)
|
|
621
|
+
if callable(ensure_running):
|
|
622
|
+
try:
|
|
623
|
+
ensure_running()
|
|
624
|
+
except TargetLost:
|
|
625
|
+
lost_owned_process = True
|
|
626
|
+
except Exception:
|
|
627
|
+
# Process inspection can fail transiently too.
|
|
628
|
+
pass
|
|
629
|
+
finally:
|
|
630
|
+
self._operation_lock.release()
|
|
631
|
+
if lost_debug_ui or lost_owned_process:
|
|
632
|
+
try:
|
|
633
|
+
self.close()
|
|
634
|
+
except BaseException:
|
|
635
|
+
# The owner retains incomplete cleanup for an explicit retry.
|
|
636
|
+
warnings.warn(
|
|
637
|
+
"1C runtime lost its debug UI or owned process; "
|
|
638
|
+
"cleanup is incomplete; "
|
|
639
|
+
"retry runtime.close()",
|
|
640
|
+
RuntimeWarning,
|
|
641
|
+
stacklevel=2,
|
|
642
|
+
)
|
|
643
|
+
return
|
|
644
|
+
|
|
645
|
+
@classmethod
|
|
646
|
+
def start(
|
|
647
|
+
cls,
|
|
648
|
+
config: RuntimeSessionConfig,
|
|
649
|
+
*,
|
|
650
|
+
progress: Callable[[str], None] | None = None,
|
|
651
|
+
) -> "RuntimeSession":
|
|
652
|
+
runtime = config.runtime
|
|
653
|
+
artifacts = ArtifactWriter(config.evidence_root, "runtime-session")
|
|
654
|
+
profiler = config.startup_profiler or PhaseRecorder()
|
|
655
|
+
attempt_failures: list[dict[str, object]] = []
|
|
656
|
+
primary_error: BaseException | None = None
|
|
657
|
+
completed_session: RuntimeSession | None = None
|
|
658
|
+
try:
|
|
659
|
+
if progress is not None:
|
|
660
|
+
progress("Загрузка расширения 1С")
|
|
661
|
+
bundle = profiler.measure(
|
|
662
|
+
"extension.bundle",
|
|
663
|
+
lambda: packaged_extension_bundle(runtime.runtime_dir),
|
|
664
|
+
)
|
|
665
|
+
state_store = ExtensionStateStore(
|
|
666
|
+
runtime.runtime_dir / "extension-state",
|
|
667
|
+
runtime.infobase_identity,
|
|
668
|
+
)
|
|
669
|
+
lifecycle = ExtensionLifecycle(
|
|
670
|
+
runtime,
|
|
671
|
+
bundle,
|
|
672
|
+
state_store,
|
|
673
|
+
_OnecInteractiveRuntimeTools(runtime),
|
|
674
|
+
profiler=profiler,
|
|
675
|
+
)
|
|
676
|
+
manual_mode = config.extension_mode is ExtensionMode.MANUAL
|
|
677
|
+
if progress is not None:
|
|
678
|
+
progress("Проверка состояния расширения 1С")
|
|
679
|
+
decision = lifecycle.prepare_manual() if manual_mode else lifecycle.prepare()
|
|
680
|
+
# An RDBG UI can vanish after the pre-launch probe while the client
|
|
681
|
+
# starts. Keep one fresh full attempt separate from extension repair.
|
|
682
|
+
attempt_limit = (1 if manual_mode else 2) + 1
|
|
683
|
+
registration_retry_available = True
|
|
684
|
+
extension_repair_available = not manual_mode
|
|
685
|
+
|
|
686
|
+
for attempt_number in range(attempt_limit):
|
|
687
|
+
try:
|
|
688
|
+
runtime_session, evidence = cls._start_attempt(
|
|
689
|
+
config,
|
|
690
|
+
artifacts,
|
|
691
|
+
bundle,
|
|
692
|
+
lifecycle,
|
|
693
|
+
profiler,
|
|
694
|
+
verify_safe_mode=decision.mode in {
|
|
695
|
+
LifecycleMode.FAST, LifecycleMode.PROBED,
|
|
696
|
+
},
|
|
697
|
+
progress=progress,
|
|
698
|
+
)
|
|
699
|
+
except _OwnedAttemptFailure as failure:
|
|
700
|
+
attempt_failures.append(
|
|
701
|
+
{
|
|
702
|
+
"admitted": False,
|
|
703
|
+
"attempt": attempt_number + 1,
|
|
704
|
+
"cleanup_succeeded": failure.cleanup_succeeded,
|
|
705
|
+
"decision_mode": decision.mode.value,
|
|
706
|
+
"error_type": type(failure.primary).__name__,
|
|
707
|
+
"repairable": failure.repairable,
|
|
708
|
+
"stage": failure.stage,
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
manual_unavailable = (
|
|
712
|
+
manual_mode
|
|
713
|
+
and failure.stage in {"managed-bootstrap", "server-entry-wait"}
|
|
714
|
+
and isinstance(failure.primary, (CommandTimeout, UnexpectedStop))
|
|
715
|
+
)
|
|
716
|
+
if manual_unavailable:
|
|
717
|
+
unavailable = ManualExtensionUnavailable(
|
|
718
|
+
"The user-managed runtime extension is unavailable; "
|
|
719
|
+
"install and apply the CFE from the current release "
|
|
720
|
+
"and preserve its bootstrap ABI"
|
|
721
|
+
)
|
|
722
|
+
for note in failure.cleanup_notes:
|
|
723
|
+
unavailable.add_note(note)
|
|
724
|
+
retry_cleanup = getattr(
|
|
725
|
+
failure.primary, "retry_cleanup", None
|
|
726
|
+
)
|
|
727
|
+
if callable(retry_cleanup):
|
|
728
|
+
_expose_startup_cleanup_retry(
|
|
729
|
+
unavailable, retry_cleanup
|
|
730
|
+
)
|
|
731
|
+
raise unavailable from None
|
|
732
|
+
if (
|
|
733
|
+
registration_retry_available
|
|
734
|
+
and failure.cleanup_succeeded
|
|
735
|
+
and isinstance(failure.primary, RdbgDebugUiNotRegistered)
|
|
736
|
+
):
|
|
737
|
+
registration_retry_available = False
|
|
738
|
+
if progress is not None:
|
|
739
|
+
progress("Повторный запуск после потери интерфейса отладки 1С")
|
|
740
|
+
continue
|
|
741
|
+
may_repair = (
|
|
742
|
+
extension_repair_available
|
|
743
|
+
and not manual_mode
|
|
744
|
+
and decision.mode in {LifecycleMode.FAST, LifecycleMode.PROBED}
|
|
745
|
+
and decision.retry_allowed
|
|
746
|
+
and failure.repairable
|
|
747
|
+
and failure.cleanup_succeeded
|
|
748
|
+
)
|
|
749
|
+
if not may_repair:
|
|
750
|
+
raise failure.primary.with_traceback(
|
|
751
|
+
failure.primary.__traceback__
|
|
752
|
+
) from None
|
|
753
|
+
extension_repair_available = False
|
|
754
|
+
|
|
755
|
+
def repair() -> LifecycleDecision:
|
|
756
|
+
lifecycle.invalidate_marker()
|
|
757
|
+
if progress is not None:
|
|
758
|
+
progress("Повторная подготовка расширения через Конфигуратор 1С")
|
|
759
|
+
return lifecycle.prepare(force_slow=True)
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
decision = profiler.measure("extension.retry", repair)
|
|
763
|
+
except BaseException as repair_error: # noqa: BLE001
|
|
764
|
+
failure.primary.add_note(
|
|
765
|
+
"Runtime startup repair failed: "
|
|
766
|
+
f"{type(repair_error).__name__}"
|
|
767
|
+
)
|
|
768
|
+
retry_cleanup = getattr(repair_error, "retry_cleanup", None)
|
|
769
|
+
if callable(retry_cleanup):
|
|
770
|
+
_expose_startup_cleanup_retry(failure.primary, retry_cleanup)
|
|
771
|
+
raise failure.primary.with_traceback(
|
|
772
|
+
failure.primary.__traceback__
|
|
773
|
+
) from None
|
|
774
|
+
continue
|
|
775
|
+
|
|
776
|
+
try:
|
|
777
|
+
manifest = bundle.manifest
|
|
778
|
+
manual_extension: dict[str, object] | None = None
|
|
779
|
+
if manual_mode:
|
|
780
|
+
observed_version = lifecycle.accept_manual_handshake(
|
|
781
|
+
(evidence[0], evidence[1])
|
|
782
|
+
)
|
|
783
|
+
version_matches = observed_version == manifest.artifact_version
|
|
784
|
+
if not version_matches:
|
|
785
|
+
warnings.warn(
|
|
786
|
+
"User-managed runtime extension artifact version "
|
|
787
|
+
f"{observed_version!r} differs from packaged release "
|
|
788
|
+
f"{manifest.artifact_version!r}; continuing in manual mode",
|
|
789
|
+
ManualExtensionVersionWarning,
|
|
790
|
+
stacklevel=2,
|
|
791
|
+
)
|
|
792
|
+
manual_extension = {
|
|
793
|
+
"packaged_artifact_version": manifest.artifact_version,
|
|
794
|
+
"observed_artifact_version": observed_version,
|
|
795
|
+
"version_matches": version_matches,
|
|
796
|
+
"protocol_version": manifest.protocol_version,
|
|
797
|
+
}
|
|
798
|
+
else:
|
|
799
|
+
lifecycle.commit_handshake((evidence[0], evidence[1]))
|
|
800
|
+
details = runtime_session._attempt_bootstrap_evidence
|
|
801
|
+
if details is None:
|
|
802
|
+
raise ProtocolError("Runtime bootstrap evidence is unavailable")
|
|
803
|
+
bootstrap_payload: dict[str, object] = {
|
|
804
|
+
"status": "PASS",
|
|
805
|
+
"lifecycle": {
|
|
806
|
+
"mode": decision.mode.value,
|
|
807
|
+
"target_state": decision.target_state.value,
|
|
808
|
+
},
|
|
809
|
+
"bundle": {
|
|
810
|
+
"source": "packaged-release",
|
|
811
|
+
"artifact_sha256": manifest.fingerprints.artifact_sha256,
|
|
812
|
+
"cfe_sha256": manifest.cfe_sha256,
|
|
813
|
+
"identity_sha256": manifest.fingerprints.identity_sha256,
|
|
814
|
+
},
|
|
815
|
+
"attempt_failures": attempt_failures,
|
|
816
|
+
"handshakes": [
|
|
817
|
+
_handshake_summary(item) for item in evidence
|
|
818
|
+
],
|
|
819
|
+
"managed_stop": details.managed_stop,
|
|
820
|
+
"server_entry_stop": details.server_entry_stop,
|
|
821
|
+
"server_service_stop": details.server_service_stop,
|
|
822
|
+
"guard": details.guard,
|
|
823
|
+
"same_session": details.same_session,
|
|
824
|
+
}
|
|
825
|
+
if manual_extension is not None:
|
|
826
|
+
bootstrap_payload["manual_extension"] = manual_extension
|
|
827
|
+
artifacts.write_json("bootstrap.json", bootstrap_payload)
|
|
828
|
+
except BaseException as error:
|
|
829
|
+
try:
|
|
830
|
+
runtime_session.close()
|
|
831
|
+
except BaseException as cleanup_error:
|
|
832
|
+
error.add_note(
|
|
833
|
+
"Runtime startup cleanup failed: "
|
|
834
|
+
f"{type(cleanup_error).__name__}"
|
|
835
|
+
)
|
|
836
|
+
_expose_startup_cleanup_retry(
|
|
837
|
+
error, runtime_session.close
|
|
838
|
+
)
|
|
839
|
+
raise
|
|
840
|
+
if config.capture_source is not None:
|
|
841
|
+
try:
|
|
842
|
+
runtime_session.configure_capture_source(
|
|
843
|
+
config.capture_source.project,
|
|
844
|
+
config.capture_source.source_root,
|
|
845
|
+
)
|
|
846
|
+
except BaseException as error:
|
|
847
|
+
try:
|
|
848
|
+
runtime_session.close()
|
|
849
|
+
except BaseException as cleanup_error: # noqa: BLE001
|
|
850
|
+
error.add_note(
|
|
851
|
+
"Runtime startup cleanup failed: "
|
|
852
|
+
f"{type(cleanup_error).__name__}"
|
|
853
|
+
)
|
|
854
|
+
_expose_startup_cleanup_retry(
|
|
855
|
+
error, runtime_session.close
|
|
856
|
+
)
|
|
857
|
+
raise
|
|
858
|
+
if progress is not None:
|
|
859
|
+
try:
|
|
860
|
+
progress("Сеанс 1С готов")
|
|
861
|
+
except BaseException as error:
|
|
862
|
+
try:
|
|
863
|
+
runtime_session.close()
|
|
864
|
+
except BaseException as cleanup_error:
|
|
865
|
+
error.add_note(
|
|
866
|
+
"Runtime startup cleanup failed: "
|
|
867
|
+
f"{type(cleanup_error).__name__}"
|
|
868
|
+
)
|
|
869
|
+
_expose_startup_cleanup_retry(
|
|
870
|
+
error, runtime_session.close
|
|
871
|
+
)
|
|
872
|
+
raise
|
|
873
|
+
completed_session = runtime_session
|
|
874
|
+
return runtime_session
|
|
875
|
+
|
|
876
|
+
raise AssertionError("runtime startup retry bound was exceeded")
|
|
877
|
+
except BaseException as error:
|
|
878
|
+
primary_error = error
|
|
879
|
+
raise
|
|
880
|
+
finally:
|
|
881
|
+
try:
|
|
882
|
+
artifacts.write_json(
|
|
883
|
+
"bootstrap-phases.json",
|
|
884
|
+
[event.as_dict() for event in profiler.events],
|
|
885
|
+
)
|
|
886
|
+
artifacts.write_json("bootstrap-attempts.json", attempt_failures)
|
|
887
|
+
except BaseException as evidence_error:
|
|
888
|
+
if primary_error is not None:
|
|
889
|
+
primary_error.add_note(
|
|
890
|
+
"Runtime startup phase evidence write failed: "
|
|
891
|
+
f"{type(evidence_error).__name__}"
|
|
892
|
+
)
|
|
893
|
+
else:
|
|
894
|
+
public_error = ProtocolError(
|
|
895
|
+
"Runtime startup phase evidence could not be written"
|
|
896
|
+
)
|
|
897
|
+
if completed_session is not None:
|
|
898
|
+
try:
|
|
899
|
+
completed_session.close()
|
|
900
|
+
except BaseException as cleanup_error:
|
|
901
|
+
public_error.add_note(
|
|
902
|
+
"Runtime startup cleanup failed: "
|
|
903
|
+
f"{type(cleanup_error).__name__}"
|
|
904
|
+
)
|
|
905
|
+
_expose_startup_cleanup_retry(
|
|
906
|
+
public_error, completed_session.close
|
|
907
|
+
)
|
|
908
|
+
raise public_error from None
|
|
909
|
+
|
|
910
|
+
@classmethod
|
|
911
|
+
def _start_attempt(
|
|
912
|
+
cls,
|
|
913
|
+
config: RuntimeSessionConfig,
|
|
914
|
+
artifacts: ArtifactWriter,
|
|
915
|
+
bundle: ExtensionBundle,
|
|
916
|
+
lifecycle: ExtensionLifecycle,
|
|
917
|
+
profiler: PhaseRecorder,
|
|
918
|
+
*,
|
|
919
|
+
verify_safe_mode: bool = False,
|
|
920
|
+
progress: Callable[[str], None] | None = None,
|
|
921
|
+
) -> tuple["RuntimeSession", tuple[ExtensionHandshakeEvidence, ...]]:
|
|
922
|
+
del lifecycle, profiler
|
|
923
|
+
runtime = config.runtime
|
|
924
|
+
processes = FileModeProcesses(runtime)
|
|
925
|
+
transport: RdbgTransport | None = None
|
|
926
|
+
rdbg: RdbgSession | None = None
|
|
927
|
+
registration_candidates: list[RdbgSession] = []
|
|
928
|
+
runtime_session: RuntimeSession | None = None
|
|
929
|
+
stage = "process-start"
|
|
930
|
+
try:
|
|
931
|
+
managed_location, entry_location, service_location = (
|
|
932
|
+
runtime_bootstrap_locations(bundle.manifest)
|
|
933
|
+
)
|
|
934
|
+
if progress is not None:
|
|
935
|
+
progress("Запуск сервера отладки 1С")
|
|
936
|
+
debug_port = processes.start_debug_server()
|
|
937
|
+
transport = RdbgTransport(
|
|
938
|
+
runtime.debug_host,
|
|
939
|
+
debug_port,
|
|
940
|
+
transcript=_redacted_transcript_sink(artifacts),
|
|
941
|
+
)
|
|
942
|
+
rdbg = _start_rdbg_with_registration_retry(
|
|
943
|
+
transport=transport,
|
|
944
|
+
location=managed_location,
|
|
945
|
+
alias=runtime.infobase_debug_alias,
|
|
946
|
+
server_target_type=runtime.server_target_type,
|
|
947
|
+
break_on_next=not runtime.is_server_infobase,
|
|
948
|
+
progress=progress,
|
|
949
|
+
on_candidate=registration_candidates.append,
|
|
950
|
+
)
|
|
951
|
+
launch_token = "onec-runtime:" + uuid4().hex if runtime.is_server_infobase else None
|
|
952
|
+
if progress is not None:
|
|
953
|
+
progress("Запуск 1С:Предприятия")
|
|
954
|
+
processes.start_debuggee(
|
|
955
|
+
debug_port,
|
|
956
|
+
execute_external=False,
|
|
957
|
+
thick_client=False,
|
|
958
|
+
startup_parameter=launch_token,
|
|
959
|
+
)
|
|
960
|
+
stage = "managed-bootstrap"
|
|
961
|
+
if progress is not None:
|
|
962
|
+
progress("Ожидание клиентского сеанса 1С")
|
|
963
|
+
managed_stop = wait_for_managed_startup_stop(
|
|
964
|
+
rdbg,
|
|
965
|
+
managed_location,
|
|
966
|
+
timeout_s=150.0,
|
|
967
|
+
on_poll=processes.ensure_running,
|
|
968
|
+
)
|
|
969
|
+
if rdbg.target is None or rdbg.target.target_type != "ManagedClient":
|
|
970
|
+
raise ProtocolError("Runtime bootstrap did not stop on ManagedClient")
|
|
971
|
+
client_target = rdbg.target
|
|
972
|
+
if runtime.is_server_infobase:
|
|
973
|
+
rdbg.bind_server_session(launch_token=launch_token)
|
|
974
|
+
contract = ExtensionHandshakeContract(
|
|
975
|
+
bundle.manifest.product_id,
|
|
976
|
+
bundle.manifest.artifact_version,
|
|
977
|
+
bundle.manifest.protocol_version,
|
|
978
|
+
)
|
|
979
|
+
if config.extension_mode is ExtensionMode.MANUAL:
|
|
980
|
+
def handshake(
|
|
981
|
+
actual: ServerGuardSession,
|
|
982
|
+
*,
|
|
983
|
+
target_type: str,
|
|
984
|
+
location: ModuleLocation,
|
|
985
|
+
) -> ExtensionHandshakeEvidence:
|
|
986
|
+
return observe_extension_handshake(
|
|
987
|
+
actual, target_type=target_type, location=location
|
|
988
|
+
)
|
|
989
|
+
else:
|
|
990
|
+
def handshake(
|
|
991
|
+
actual: ServerGuardSession,
|
|
992
|
+
*,
|
|
993
|
+
target_type: str,
|
|
994
|
+
location: ModuleLocation,
|
|
995
|
+
) -> ExtensionHandshakeEvidence:
|
|
996
|
+
return verify_extension_handshake(
|
|
997
|
+
actual, contract, target_type=target_type, location=location
|
|
998
|
+
)
|
|
999
|
+
stage = "managed-handshake"
|
|
1000
|
+
if progress is not None:
|
|
1001
|
+
progress("Проверка расширения в клиентском сеансе")
|
|
1002
|
+
managed_handshake = handshake(
|
|
1003
|
+
rdbg,
|
|
1004
|
+
target_type="ManagedClient",
|
|
1005
|
+
location=managed_location,
|
|
1006
|
+
)
|
|
1007
|
+
stage = "server-guard"
|
|
1008
|
+
guard = enable_server_kernel_loop(rdbg)
|
|
1009
|
+
server_handshake: ExtensionHandshakeEvidence | None = None
|
|
1010
|
+
server_entry_target: TargetId | None = None
|
|
1011
|
+
|
|
1012
|
+
def verify_server_entry(actual: object) -> None:
|
|
1013
|
+
nonlocal server_handshake, server_entry_target, stage
|
|
1014
|
+
stage = "server-entry-handshake"
|
|
1015
|
+
if rdbg.target is None or rdbg.target.target_type != runtime.server_target_type:
|
|
1016
|
+
raise ProtocolError(
|
|
1017
|
+
f"Runtime server entry did not stop on {runtime.server_target_type}"
|
|
1018
|
+
)
|
|
1019
|
+
server_entry_target = rdbg.target.target_id
|
|
1020
|
+
server_handshake = handshake(
|
|
1021
|
+
cast(ServerGuardSession, actual),
|
|
1022
|
+
target_type=runtime.server_target_type,
|
|
1023
|
+
location=entry_location,
|
|
1024
|
+
)
|
|
1025
|
+
stage = "server-service"
|
|
1026
|
+
|
|
1027
|
+
stage = "server-entry-wait"
|
|
1028
|
+
if progress is not None:
|
|
1029
|
+
progress("Подключение серверного сеанса 1С")
|
|
1030
|
+
entry_stop, service_stop = wait_for_server_entry_then_service(
|
|
1031
|
+
rdbg,
|
|
1032
|
+
entry_location,
|
|
1033
|
+
service_location,
|
|
1034
|
+
timeout_s=150.0,
|
|
1035
|
+
on_entry=verify_server_entry,
|
|
1036
|
+
server_target_type=runtime.server_target_type,
|
|
1037
|
+
)
|
|
1038
|
+
if server_handshake is None or server_entry_target is None:
|
|
1039
|
+
raise ProtocolError("Runtime server entry handshake was not observed")
|
|
1040
|
+
if rdbg.target is None or rdbg.target.target_type != runtime.server_target_type:
|
|
1041
|
+
raise ProtocolError(f"Runtime bootstrap did not stop on {runtime.server_target_type}")
|
|
1042
|
+
server_target = rdbg.target
|
|
1043
|
+
if (
|
|
1044
|
+
client_target.target_id.seance_id is None
|
|
1045
|
+
or server_entry_target.seance_id is None
|
|
1046
|
+
or client_target.target_id.seance_id
|
|
1047
|
+
!= server_entry_target.seance_id
|
|
1048
|
+
):
|
|
1049
|
+
raise ProtocolError(
|
|
1050
|
+
"Runtime client/server targets belong to different sessions"
|
|
1051
|
+
)
|
|
1052
|
+
if server_target.target_id != server_entry_target:
|
|
1053
|
+
raise ProtocolError(
|
|
1054
|
+
"Runtime server entry/service stops belong to different server targets"
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
if verify_safe_mode:
|
|
1058
|
+
stage = "safe-mode-check"
|
|
1059
|
+
if progress is not None:
|
|
1060
|
+
progress("Проверка безопасного режима расширения 1С")
|
|
1061
|
+
verify_extension_safe_mode_disabled(rdbg)
|
|
1062
|
+
|
|
1063
|
+
journal = RecoveryJournal(artifacts.append_jsonl)
|
|
1064
|
+
controller = PrototypeRuntimeController(
|
|
1065
|
+
rdbg,
|
|
1066
|
+
service_location,
|
|
1067
|
+
command_timeout_s=90.0,
|
|
1068
|
+
journal=journal,
|
|
1069
|
+
)
|
|
1070
|
+
notebook_worker_builder = NotebookWorkerArtifactBuilder(runtime)
|
|
1071
|
+
worker_module_builder = WorkerModuleArtifactBuilder(
|
|
1072
|
+
notebook_worker_builder,
|
|
1073
|
+
cache=WorkerModuleArtifactCache(),
|
|
1074
|
+
packer_version="worker-epf-v1",
|
|
1075
|
+
target_profile="runtime-session-server-v1",
|
|
1076
|
+
)
|
|
1077
|
+
api = PrototypeRuntimeApi(
|
|
1078
|
+
controller,
|
|
1079
|
+
journal=journal,
|
|
1080
|
+
notebook_worker_builder=notebook_worker_builder,
|
|
1081
|
+
worker_module_builder=worker_module_builder,
|
|
1082
|
+
)
|
|
1083
|
+
runtime_session = cls(config, processes, transport, rdbg, api, artifacts)
|
|
1084
|
+
runtime_session._attempt_bootstrap_evidence = _AttemptBootstrapEvidence(
|
|
1085
|
+
managed_stop=_stop_summary(
|
|
1086
|
+
managed_stop,
|
|
1087
|
+
target_type="ManagedClient",
|
|
1088
|
+
),
|
|
1089
|
+
server_entry_stop=_stop_summary(
|
|
1090
|
+
entry_stop,
|
|
1091
|
+
target_type=runtime.server_target_type,
|
|
1092
|
+
),
|
|
1093
|
+
server_service_stop=_stop_summary(
|
|
1094
|
+
service_stop,
|
|
1095
|
+
target_type=runtime.server_target_type,
|
|
1096
|
+
),
|
|
1097
|
+
guard=guard.as_dict(),
|
|
1098
|
+
same_session=True,
|
|
1099
|
+
)
|
|
1100
|
+
return runtime_session, (managed_handshake, server_handshake)
|
|
1101
|
+
except BaseException as error:
|
|
1102
|
+
if rdbg is None and registration_candidates:
|
|
1103
|
+
# The helper may have failed while detaching its last candidate.
|
|
1104
|
+
# Keep that exact UI available to the owned cleanup retry.
|
|
1105
|
+
rdbg = registration_candidates[-1]
|
|
1106
|
+
if stage in {"managed-bootstrap", "server-entry-wait"} and isinstance(
|
|
1107
|
+
error, (CommandTimeout, UnexpectedStop, TargetLost)
|
|
1108
|
+
):
|
|
1109
|
+
log_path = runtime.logs_dir / "1c-messages.log"
|
|
1110
|
+
hint = startup_log_hint(log_path)
|
|
1111
|
+
if hint is not None:
|
|
1112
|
+
error = ProcessStartError(
|
|
1113
|
+
f"1С:Предприятие: {hint}; подробности: {log_path}"
|
|
1114
|
+
)
|
|
1115
|
+
cleanup_errors: list[BaseException] = []
|
|
1116
|
+
cleanup_retry: Callable[[], None] | None = None
|
|
1117
|
+
cleanup_error_types: tuple[str, ...] = ()
|
|
1118
|
+
if runtime_session is not None:
|
|
1119
|
+
try:
|
|
1120
|
+
runtime_session.close()
|
|
1121
|
+
except BaseException as cleanup_error:
|
|
1122
|
+
cleanup_errors.append(cleanup_error)
|
|
1123
|
+
cleanup_retry = runtime_session.close
|
|
1124
|
+
else:
|
|
1125
|
+
cleanup = _StartupAttemptCleanup(
|
|
1126
|
+
runtime, processes, transport, rdbg
|
|
1127
|
+
)
|
|
1128
|
+
try:
|
|
1129
|
+
cleanup.retry_cleanup()
|
|
1130
|
+
except BaseException as cleanup_error:
|
|
1131
|
+
cleanup_errors.append(cleanup_error)
|
|
1132
|
+
cleanup_error_types = cleanup.cleanup_error_types
|
|
1133
|
+
cleanup_retry = cleanup.retry_cleanup
|
|
1134
|
+
# Keep only our cleanup diagnostics, separate from arbitrary primary notes.
|
|
1135
|
+
cleanup_notes: list[str] = []
|
|
1136
|
+
recorded_error_types = cleanup_error_types or tuple(
|
|
1137
|
+
type(recorded_cleanup_error).__name__
|
|
1138
|
+
for recorded_cleanup_error in cleanup_errors
|
|
1139
|
+
)
|
|
1140
|
+
for error_type in recorded_error_types:
|
|
1141
|
+
if not (
|
|
1142
|
+
len(error_type) <= 64
|
|
1143
|
+
and error_type.isascii()
|
|
1144
|
+
and error_type.isidentifier()
|
|
1145
|
+
):
|
|
1146
|
+
error_type = "Exception"
|
|
1147
|
+
note = f"Runtime startup cleanup failed: {error_type}"
|
|
1148
|
+
cleanup_notes.append(note)
|
|
1149
|
+
error.add_note(note)
|
|
1150
|
+
if cleanup_retry is not None:
|
|
1151
|
+
_expose_startup_cleanup_retry(error, cleanup_retry)
|
|
1152
|
+
repairable = (
|
|
1153
|
+
stage in {"managed-handshake", "server-entry-handshake"}
|
|
1154
|
+
and isinstance(error, ExtensionHandshakeError)
|
|
1155
|
+
) or (
|
|
1156
|
+
stage == "safe-mode-check"
|
|
1157
|
+
and isinstance(error, ExtensionHandshakeError)
|
|
1158
|
+
) or (
|
|
1159
|
+
stage == "managed-bootstrap"
|
|
1160
|
+
and isinstance(error, (CommandTimeout, UnexpectedStop))
|
|
1161
|
+
)
|
|
1162
|
+
raise _OwnedAttemptFailure(
|
|
1163
|
+
error,
|
|
1164
|
+
stage=stage,
|
|
1165
|
+
repairable=repairable,
|
|
1166
|
+
cleanup_succeeded=not cleanup_errors,
|
|
1167
|
+
cleanup_notes=tuple(cleanup_notes),
|
|
1168
|
+
) from None
|
|
1169
|
+
|
|
1170
|
+
def execute_bsl(
|
|
1171
|
+
self,
|
|
1172
|
+
source: str,
|
|
1173
|
+
*,
|
|
1174
|
+
source_unit: SourceUnitRef | None = None,
|
|
1175
|
+
on_execution_provenance: (
|
|
1176
|
+
Callable[[OperationExecutionProvenance], None] | None
|
|
1177
|
+
) = None,
|
|
1178
|
+
) -> RuntimeReply:
|
|
1179
|
+
with self._operation_lock:
|
|
1180
|
+
arguments: dict[str, object] = {}
|
|
1181
|
+
if source_unit is not None:
|
|
1182
|
+
arguments["source_unit"] = source_unit
|
|
1183
|
+
if on_execution_provenance is not None:
|
|
1184
|
+
arguments["on_execution_provenance"] = (
|
|
1185
|
+
on_execution_provenance
|
|
1186
|
+
)
|
|
1187
|
+
return self.runtime_api.execute_bsl(source, **arguments) # type: ignore[arg-type]
|
|
1188
|
+
|
|
1189
|
+
def configure_capture_source(self, project: str, source_root: Path | str) -> None:
|
|
1190
|
+
with self._operation_lock:
|
|
1191
|
+
if self._active_capture_ticket is not None:
|
|
1192
|
+
raise ProtocolError(
|
|
1193
|
+
"capture source cannot change during an active capture"
|
|
1194
|
+
)
|
|
1195
|
+
resolver = CommonModuleCaptureResolver(project, source_root)
|
|
1196
|
+
self.runtime_api.configure_capture_points(())
|
|
1197
|
+
self._file_capture_points = ()
|
|
1198
|
+
self._capture_source_resolver = resolver
|
|
1199
|
+
self._capture_source_bindings = {}
|
|
1200
|
+
self._capture_locations = {}
|
|
1201
|
+
|
|
1202
|
+
def clear_capture_source(self) -> None:
|
|
1203
|
+
with self._operation_lock:
|
|
1204
|
+
if self._active_capture_ticket is not None:
|
|
1205
|
+
raise ProtocolError(
|
|
1206
|
+
"capture source cannot change during an active capture"
|
|
1207
|
+
)
|
|
1208
|
+
self.runtime_api.configure_capture_points(())
|
|
1209
|
+
self._file_capture_points = ()
|
|
1210
|
+
self._capture_source_resolver = None
|
|
1211
|
+
self._capture_source_bindings = {}
|
|
1212
|
+
self._capture_locations = {}
|
|
1213
|
+
|
|
1214
|
+
def resolve_capture_points(
|
|
1215
|
+
self, points: Sequence[CapturePointRequest]
|
|
1216
|
+
) -> tuple[ResolvedCapturePoint, ...]:
|
|
1217
|
+
with self._operation_lock:
|
|
1218
|
+
resolver = self._capture_source_resolver
|
|
1219
|
+
if resolver is None:
|
|
1220
|
+
raise CaptureSourceNotConfigured(
|
|
1221
|
+
"Symbolic capture source is not configured; call "
|
|
1222
|
+
"configure_capture_source(project, source_root) first"
|
|
1223
|
+
)
|
|
1224
|
+
bindings = resolver.resolve(points)
|
|
1225
|
+
indexed = {
|
|
1226
|
+
(binding.point.name.casefold(), binding.point.line): binding
|
|
1227
|
+
for binding in bindings
|
|
1228
|
+
}
|
|
1229
|
+
self._capture_source_bindings = indexed
|
|
1230
|
+
self._capture_locations = {
|
|
1231
|
+
key: binding.location for key, binding in indexed.items()
|
|
1232
|
+
}
|
|
1233
|
+
return tuple(binding.point for binding in bindings)
|
|
1234
|
+
|
|
1235
|
+
def verify_capture_points(
|
|
1236
|
+
self, points: Sequence[ResolvedCapturePoint]
|
|
1237
|
+
) -> tuple[CaptureBinding, ...]:
|
|
1238
|
+
with self._operation_lock:
|
|
1239
|
+
bindings: list[CaptureBinding] = []
|
|
1240
|
+
for point in points:
|
|
1241
|
+
binding = self._capture_source_bindings.get(
|
|
1242
|
+
(point.name.casefold(), point.line)
|
|
1243
|
+
)
|
|
1244
|
+
if binding is None or binding.point != point:
|
|
1245
|
+
raise ValueError(
|
|
1246
|
+
"capture point was not resolved by this capture source"
|
|
1247
|
+
)
|
|
1248
|
+
bindings.append(binding)
|
|
1249
|
+
resolver = self._capture_source_resolver
|
|
1250
|
+
if resolver is None:
|
|
1251
|
+
raise ValueError(
|
|
1252
|
+
"capture point was not resolved by this capture source"
|
|
1253
|
+
)
|
|
1254
|
+
result = tuple(bindings)
|
|
1255
|
+
resolver.verify(result)
|
|
1256
|
+
return result
|
|
1257
|
+
|
|
1258
|
+
def prepare_main_for_capture(
|
|
1259
|
+
self, source: str, *, source_unit: SourceUnitRef
|
|
1260
|
+
) -> object:
|
|
1261
|
+
with self._operation_lock:
|
|
1262
|
+
if self._closed:
|
|
1263
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1264
|
+
return self.runtime_api.prepare_main_for_capture(
|
|
1265
|
+
source,
|
|
1266
|
+
source_unit=source_unit,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
def activate_prepared_main_for_capture(self, prepared: object) -> object:
|
|
1270
|
+
with self._operation_lock:
|
|
1271
|
+
if self._closed:
|
|
1272
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1273
|
+
return self.runtime_api.activate_prepared_main_for_capture(prepared)
|
|
1274
|
+
|
|
1275
|
+
def prepared_main_execution_provenance(
|
|
1276
|
+
self,
|
|
1277
|
+
prepared: object,
|
|
1278
|
+
) -> OperationExecutionProvenance:
|
|
1279
|
+
with self._operation_lock:
|
|
1280
|
+
if self._closed:
|
|
1281
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1282
|
+
return self.runtime_api.prepared_main_execution_provenance(
|
|
1283
|
+
prepared
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
def execute_prepared_main_for_capture(self, prepared: object) -> object:
|
|
1287
|
+
with self._operation_lock:
|
|
1288
|
+
if self._closed:
|
|
1289
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1290
|
+
return self.runtime_api._attempt_prepared_main_for_capture(prepared)
|
|
1291
|
+
|
|
1292
|
+
def discard_prepared_main_for_capture(self, prepared: object) -> None:
|
|
1293
|
+
with self._operation_lock:
|
|
1294
|
+
if self._closed:
|
|
1295
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1296
|
+
self.runtime_api.discard_prepared_main_for_capture(prepared)
|
|
1297
|
+
|
|
1298
|
+
def prepare_capture_hypothesis(
|
|
1299
|
+
self, source: str, capture: object
|
|
1300
|
+
) -> object:
|
|
1301
|
+
with self._operation_lock:
|
|
1302
|
+
if self._closed:
|
|
1303
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1304
|
+
self._require_capture_fence(capture)
|
|
1305
|
+
return self.runtime_api.prepare_capture_hypothesis(source)
|
|
1306
|
+
|
|
1307
|
+
def execute_prepared_capture_hypothesis(
|
|
1308
|
+
self, prepared: object, capture: object
|
|
1309
|
+
) -> RuntimeReply:
|
|
1310
|
+
with self._operation_lock:
|
|
1311
|
+
if self._closed:
|
|
1312
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1313
|
+
self._require_capture_fence(capture)
|
|
1314
|
+
return self.runtime_api.execute_prepared_capture_hypothesis(prepared)
|
|
1315
|
+
|
|
1316
|
+
def prepared_capture_hypothesis_provenance(
|
|
1317
|
+
self,
|
|
1318
|
+
prepared: object,
|
|
1319
|
+
) -> OperationExecutionProvenance:
|
|
1320
|
+
with self._operation_lock:
|
|
1321
|
+
if self._closed:
|
|
1322
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1323
|
+
return self.runtime_api.prepared_capture_hypothesis_provenance(
|
|
1324
|
+
prepared
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
def quarantine_capture_inspection(self, capture: object) -> None:
|
|
1328
|
+
"""Revoke every inspection route when preparation ownership is uncertain."""
|
|
1329
|
+
with self._operation_lock:
|
|
1330
|
+
if self._closed:
|
|
1331
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1332
|
+
self._require_capture_fence(capture)
|
|
1333
|
+
# Revoke the public session ticket before touching the lower layer:
|
|
1334
|
+
# even a failing controller cleanup must leave later reads fenced.
|
|
1335
|
+
self._active_capture_ticket = None
|
|
1336
|
+
self.runtime_api.invalidate_capture_inspection()
|
|
1337
|
+
|
|
1338
|
+
def arm_capture_intent(self, intent: object) -> RuntimeCaptureArming:
|
|
1339
|
+
with self._operation_lock:
|
|
1340
|
+
previous_ticket = self._active_capture_ticket
|
|
1341
|
+
previous_points = self._active_capture_points
|
|
1342
|
+
previous_locations = tuple(getattr(self, "_active_capture_locations", ()))
|
|
1343
|
+
previous_file_points = tuple(getattr(self, "_file_capture_points", ()))
|
|
1344
|
+
bindings = self.verify_capture_points(intent.points)
|
|
1345
|
+
locations = [binding.location for binding in bindings]
|
|
1346
|
+
# This public evidence is intentionally planned before live RDBG
|
|
1347
|
+
# state changes and never includes the opaque ticket itself.
|
|
1348
|
+
self.artifacts.append_jsonl(
|
|
1349
|
+
"capture-tickets.jsonl",
|
|
1350
|
+
{
|
|
1351
|
+
"capture_ticket_planned": True,
|
|
1352
|
+
"capture_intent_id": intent.capture_intent_id,
|
|
1353
|
+
"capture_generation": intent.capture_generation,
|
|
1354
|
+
"source_revision": intent.source_revision,
|
|
1355
|
+
"source_sha256": intent.source_sha256,
|
|
1356
|
+
},
|
|
1357
|
+
)
|
|
1358
|
+
try:
|
|
1359
|
+
if previous_ticket is not None:
|
|
1360
|
+
self.runtime_api.configure_continuation_capture_points(tuple(locations))
|
|
1361
|
+
else:
|
|
1362
|
+
self.runtime_api.configure_capture_points(tuple(locations))
|
|
1363
|
+
ticket = self.runtime_api.prepare_capture_ticket()
|
|
1364
|
+
except BaseException:
|
|
1365
|
+
try:
|
|
1366
|
+
if previous_ticket is not None:
|
|
1367
|
+
self.runtime_api.configure_continuation_capture_points(previous_locations)
|
|
1368
|
+
else:
|
|
1369
|
+
self.runtime_api.configure_capture_points(())
|
|
1370
|
+
finally:
|
|
1371
|
+
self._active_capture_points = previous_points
|
|
1372
|
+
self._active_capture_ticket = previous_ticket
|
|
1373
|
+
self._file_capture_points = previous_file_points
|
|
1374
|
+
raise
|
|
1375
|
+
self._active_capture_points = tuple(intent.points)
|
|
1376
|
+
self._active_capture_locations = tuple(locations)
|
|
1377
|
+
self._file_capture_points = ()
|
|
1378
|
+
self._active_capture_ticket = _ActiveCaptureTicket(
|
|
1379
|
+
ticket.ticket_id,
|
|
1380
|
+
intent.capture_intent_id,
|
|
1381
|
+
intent.operation_id,
|
|
1382
|
+
intent.capture_generation,
|
|
1383
|
+
intent.source_revision,
|
|
1384
|
+
intent.source_sha256,
|
|
1385
|
+
ticket.expected_stop_sequence,
|
|
1386
|
+
)
|
|
1387
|
+
return RuntimeCaptureArming(
|
|
1388
|
+
ticket.ticket_id,
|
|
1389
|
+
ticket.expected_operation_id,
|
|
1390
|
+
ticket.expected_stop_sequence,
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
def prepare_capture_successor(
|
|
1394
|
+
self,
|
|
1395
|
+
intent: object | None,
|
|
1396
|
+
*,
|
|
1397
|
+
attempt: object,
|
|
1398
|
+
) -> _SessionContinuationAdmission:
|
|
1399
|
+
"""Transact session ticket/points with API/controller successor state."""
|
|
1400
|
+
with self._operation_lock:
|
|
1401
|
+
if self._closed:
|
|
1402
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1403
|
+
previous_ticket = self._active_capture_ticket
|
|
1404
|
+
if previous_ticket is None:
|
|
1405
|
+
raise ProtocolError("successor admission requires an active capture")
|
|
1406
|
+
if attempt.capture_generation != previous_ticket.capture_generation:
|
|
1407
|
+
raise ValueError("continuation attempt capture_generation changed")
|
|
1408
|
+
previous_points = self._active_capture_points
|
|
1409
|
+
previous_locations = self._active_capture_locations
|
|
1410
|
+
locations: tuple[object, ...]
|
|
1411
|
+
if intent is None:
|
|
1412
|
+
locations = ()
|
|
1413
|
+
else:
|
|
1414
|
+
if (
|
|
1415
|
+
intent.operation_id != attempt.request_operation_id
|
|
1416
|
+
or intent.source_revision != previous_ticket.source_revision
|
|
1417
|
+
or intent.source_sha256 != previous_ticket.source_sha256
|
|
1418
|
+
or intent.capture_generation <= previous_ticket.capture_generation
|
|
1419
|
+
):
|
|
1420
|
+
raise ValueError("capture successor evidence changed")
|
|
1421
|
+
resolved_locations: list[object] = []
|
|
1422
|
+
for point in intent.points:
|
|
1423
|
+
location = self._capture_locations.get(
|
|
1424
|
+
(point.name.casefold(), point.line)
|
|
1425
|
+
)
|
|
1426
|
+
if location is None:
|
|
1427
|
+
raise ValueError(
|
|
1428
|
+
"capture intent was not resolved by this session"
|
|
1429
|
+
)
|
|
1430
|
+
resolved_locations.append(location)
|
|
1431
|
+
locations = tuple(resolved_locations)
|
|
1432
|
+
api_admission = self.runtime_api.begin_continuation_admission(
|
|
1433
|
+
ContinuationAttemptSpec(
|
|
1434
|
+
attempt.attempt_id,
|
|
1435
|
+
attempt.capture_generation,
|
|
1436
|
+
attempt.request_operation_id,
|
|
1437
|
+
attempt.dirty_roots,
|
|
1438
|
+
),
|
|
1439
|
+
locations, # type: ignore[arg-type]
|
|
1440
|
+
)
|
|
1441
|
+
try:
|
|
1442
|
+
ticket = getattr(api_admission, "ticket", None)
|
|
1443
|
+
arming: RuntimeCaptureArming | None = None
|
|
1444
|
+
next_points: tuple[object, ...] = ()
|
|
1445
|
+
next_locations: tuple[object, ...] = ()
|
|
1446
|
+
next_ticket: _ActiveCaptureTicket | None = None
|
|
1447
|
+
if intent is not None:
|
|
1448
|
+
if ticket is None:
|
|
1449
|
+
raise ProtocolError(
|
|
1450
|
+
"successor capture ticket is unavailable"
|
|
1451
|
+
)
|
|
1452
|
+
arming = RuntimeCaptureArming(
|
|
1453
|
+
ticket.ticket_id,
|
|
1454
|
+
ticket.expected_operation_id,
|
|
1455
|
+
ticket.expected_stop_sequence,
|
|
1456
|
+
)
|
|
1457
|
+
next_points = tuple(intent.points)
|
|
1458
|
+
next_locations = locations
|
|
1459
|
+
next_ticket = _ActiveCaptureTicket(
|
|
1460
|
+
ticket.ticket_id,
|
|
1461
|
+
intent.capture_intent_id,
|
|
1462
|
+
intent.operation_id,
|
|
1463
|
+
intent.capture_generation,
|
|
1464
|
+
intent.source_revision,
|
|
1465
|
+
intent.source_sha256,
|
|
1466
|
+
ticket.expected_stop_sequence,
|
|
1467
|
+
)
|
|
1468
|
+
self._active_capture_points = next_points
|
|
1469
|
+
self._active_capture_locations = next_locations
|
|
1470
|
+
self._active_capture_ticket = next_ticket
|
|
1471
|
+
return _SessionContinuationAdmission(
|
|
1472
|
+
self,
|
|
1473
|
+
api_admission,
|
|
1474
|
+
active_ticket=previous_ticket,
|
|
1475
|
+
active_points=previous_points,
|
|
1476
|
+
active_locations=previous_locations,
|
|
1477
|
+
arming=arming,
|
|
1478
|
+
)
|
|
1479
|
+
except BaseException:
|
|
1480
|
+
try:
|
|
1481
|
+
getattr(api_admission, "rollback")()
|
|
1482
|
+
except BaseException:
|
|
1483
|
+
self._active_capture_ticket = None
|
|
1484
|
+
self._active_capture_points = ()
|
|
1485
|
+
self._active_capture_locations = ()
|
|
1486
|
+
raise
|
|
1487
|
+
self._active_capture_ticket = previous_ticket
|
|
1488
|
+
self._active_capture_points = previous_points
|
|
1489
|
+
self._active_capture_locations = previous_locations
|
|
1490
|
+
raise
|
|
1491
|
+
|
|
1492
|
+
def continuation_admission_is_uncertain(self) -> bool:
|
|
1493
|
+
"""Classify a failed lower-layer admission without exposing internals."""
|
|
1494
|
+
return self.runtime_api.continuation_admission_is_uncertain()
|
|
1495
|
+
|
|
1496
|
+
def capture_location(
|
|
1497
|
+
self, location: object, *, ticket_id: str | None, intent: object
|
|
1498
|
+
) -> object | None:
|
|
1499
|
+
with self._operation_lock:
|
|
1500
|
+
active = getattr(self, "_active_capture_ticket", None)
|
|
1501
|
+
if (
|
|
1502
|
+
active is None
|
|
1503
|
+
or ticket_id != active.ticket_id
|
|
1504
|
+
or intent.capture_intent_id != active.capture_intent_id
|
|
1505
|
+
or intent.capture_generation != active.capture_generation
|
|
1506
|
+
or intent.source_revision != active.source_revision
|
|
1507
|
+
or intent.source_sha256 != active.source_sha256
|
|
1508
|
+
):
|
|
1509
|
+
return None
|
|
1510
|
+
for point in getattr(self, "_active_capture_points", ()):
|
|
1511
|
+
candidate = getattr(self, "_capture_locations", {}).get(
|
|
1512
|
+
(point.name.casefold(), point.line)
|
|
1513
|
+
)
|
|
1514
|
+
if candidate == location:
|
|
1515
|
+
# Arming is bound to the submitted MAIN revision, while a
|
|
1516
|
+
# paused frame is fenced by the exact source identity that
|
|
1517
|
+
# produced the observed stop. Publish that transition at
|
|
1518
|
+
# the same correlation boundary that proves the point.
|
|
1519
|
+
self._active_capture_ticket = replace(
|
|
1520
|
+
active,
|
|
1521
|
+
source_revision=point.source_revision,
|
|
1522
|
+
source_sha256=point.source_sha256,
|
|
1523
|
+
)
|
|
1524
|
+
return point
|
|
1525
|
+
return None
|
|
1526
|
+
|
|
1527
|
+
def disarm_capture_intent(self, *, policy: str) -> None:
|
|
1528
|
+
if policy not in {"terminal_main", "terminal_no_stop"}:
|
|
1529
|
+
raise ValueError("capture disarm policy is unsupported")
|
|
1530
|
+
with self._operation_lock:
|
|
1531
|
+
active = self._active_capture_ticket
|
|
1532
|
+
self.runtime_api.configure_capture_points(())
|
|
1533
|
+
self._file_capture_points = ()
|
|
1534
|
+
self._active_capture_points = ()
|
|
1535
|
+
self._active_capture_ticket = None
|
|
1536
|
+
if active is not None:
|
|
1537
|
+
self._notify_capture_ended(active)
|
|
1538
|
+
|
|
1539
|
+
def resume_capture(
|
|
1540
|
+
self,
|
|
1541
|
+
*,
|
|
1542
|
+
dirty_roots: tuple[str, ...] = (),
|
|
1543
|
+
continuation_attempt_id: str | None = None,
|
|
1544
|
+
) -> RuntimeReply:
|
|
1545
|
+
with self._operation_lock:
|
|
1546
|
+
active = self._active_capture_ticket
|
|
1547
|
+
try:
|
|
1548
|
+
reply = self.runtime_api.resume_capture(
|
|
1549
|
+
dirty_roots=dirty_roots,
|
|
1550
|
+
continuation_attempt_id=continuation_attempt_id,
|
|
1551
|
+
)
|
|
1552
|
+
except BaseException:
|
|
1553
|
+
# A non-CAPTURED runtime is not inspectable even when transport
|
|
1554
|
+
# outcome is unknown; expire the service fence before it can
|
|
1555
|
+
# issue another metadata request.
|
|
1556
|
+
if active is not None and self.runtime_api.status().state is not OperationState.CAPTURED:
|
|
1557
|
+
self._active_capture_ticket = None
|
|
1558
|
+
self._notify_capture_ended(active)
|
|
1559
|
+
raise
|
|
1560
|
+
if (
|
|
1561
|
+
active is not None
|
|
1562
|
+
and getattr(reply, "capture_ticket", None)
|
|
1563
|
+
== getattr(active, "ticket_id", None)
|
|
1564
|
+
and getattr(reply, "state", None) is OperationState.CAPTURED
|
|
1565
|
+
):
|
|
1566
|
+
return reply
|
|
1567
|
+
if active is not None:
|
|
1568
|
+
self._active_capture_ticket = None
|
|
1569
|
+
self._notify_capture_ended(active)
|
|
1570
|
+
return reply
|
|
1571
|
+
|
|
1572
|
+
def resume_debug_stop(
|
|
1573
|
+
self,
|
|
1574
|
+
*,
|
|
1575
|
+
timeout_s: float | None = None,
|
|
1576
|
+
) -> RuntimeReply:
|
|
1577
|
+
with self._operation_lock:
|
|
1578
|
+
active = self._active_capture_ticket
|
|
1579
|
+
reply = self.runtime_api.resume_debug_stop(timeout_s=timeout_s)
|
|
1580
|
+
if active is None or reply.state in {
|
|
1581
|
+
OperationState.CAPTURED,
|
|
1582
|
+
OperationState.CAPTURE_DEBUG_STOPPED,
|
|
1583
|
+
OperationState.DEBUG_STOPPED,
|
|
1584
|
+
}:
|
|
1585
|
+
return reply
|
|
1586
|
+
self._active_capture_ticket = None
|
|
1587
|
+
self._notify_capture_ended(active)
|
|
1588
|
+
return reply
|
|
1589
|
+
|
|
1590
|
+
def rearm_capture_successor(self, locations: tuple[object, ...]) -> None:
|
|
1591
|
+
with self._operation_lock:
|
|
1592
|
+
if self._active_capture_ticket is None:
|
|
1593
|
+
raise ProtocolError("successor rearm requires an active capture")
|
|
1594
|
+
self.runtime_api.configure_continuation_capture_points(locations) # type: ignore[arg-type]
|
|
1595
|
+
self._active_capture_points = ()
|
|
1596
|
+
self._active_capture_locations = ()
|
|
1597
|
+
self._active_capture_ticket = None
|
|
1598
|
+
|
|
1599
|
+
def continuation_attempt_evidence(
|
|
1600
|
+
self, attempt_id: str
|
|
1601
|
+
) -> ContinuationAttemptEvidence:
|
|
1602
|
+
with self._operation_lock:
|
|
1603
|
+
return self.runtime_api.continuation_attempt_evidence(attempt_id)
|
|
1604
|
+
|
|
1605
|
+
def add_capture_resume_listener(
|
|
1606
|
+
self, listener: Callable[[RuntimeCaptureFence], None]
|
|
1607
|
+
) -> None:
|
|
1608
|
+
if not callable(listener):
|
|
1609
|
+
raise TypeError("capture resume listener must be callable")
|
|
1610
|
+
if not hasattr(self, "_capture_resume_listeners"):
|
|
1611
|
+
self._capture_resume_listeners = []
|
|
1612
|
+
self._capture_resume_listeners.append(listener)
|
|
1613
|
+
|
|
1614
|
+
def _notify_capture_ended(self, active: _ActiveCaptureTicket) -> None:
|
|
1615
|
+
fence = RuntimeCaptureFence(
|
|
1616
|
+
active.capture_intent_id,
|
|
1617
|
+
active.operation_id,
|
|
1618
|
+
active.source_revision,
|
|
1619
|
+
active.source_sha256,
|
|
1620
|
+
active.capture_generation,
|
|
1621
|
+
active.stop_sequence,
|
|
1622
|
+
)
|
|
1623
|
+
for listener in tuple(getattr(self, "_capture_resume_listeners", ())):
|
|
1624
|
+
listener(fence)
|
|
1625
|
+
|
|
1626
|
+
def _require_capture_fence(self, capture: object) -> None:
|
|
1627
|
+
active = self._active_capture_ticket
|
|
1628
|
+
if active is None or (
|
|
1629
|
+
active.capture_intent_id != capture.capture_intent_id
|
|
1630
|
+
or active.operation_id != capture.operation_id
|
|
1631
|
+
or active.capture_generation != capture.capture_generation
|
|
1632
|
+
or active.source_revision != capture.source_revision
|
|
1633
|
+
or active.source_sha256 != capture.source_sha256
|
|
1634
|
+
or active.stop_sequence != capture.stop_sequence
|
|
1635
|
+
):
|
|
1636
|
+
raise ProtocolError("capture frame is stale or unavailable")
|
|
1637
|
+
|
|
1638
|
+
def frame_variables(self, capture: object, *, filters: Mapping[str, object], cursor: int, limit: int, timeout_s: float | None = None) -> Mapping[str, object]:
|
|
1639
|
+
with self._operation_lock:
|
|
1640
|
+
self._require_capture_fence(capture)
|
|
1641
|
+
return self.runtime_api.capture_frame_variables(
|
|
1642
|
+
filters=filters, cursor=cursor, limit=limit, timeout_s=timeout_s
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
def capture_stack(self, capture: object, *, cursor: int, limit: int, timeout_s: float | None = None) -> Mapping[str, object]:
|
|
1646
|
+
with self._operation_lock:
|
|
1647
|
+
self._require_capture_fence(capture)
|
|
1648
|
+
return self.runtime_api.capture_stack(
|
|
1649
|
+
cursor=cursor, limit=limit, timeout_s=timeout_s
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
def capture_frame(self, capture: object, *, level: int, cursor: int, limit: int, name: str | None = None, timeout_s: float | None = None) -> Mapping[str, object]:
|
|
1653
|
+
with self._operation_lock:
|
|
1654
|
+
self._require_capture_fence(capture)
|
|
1655
|
+
return self.runtime_api.capture_frame(
|
|
1656
|
+
level=level, cursor=cursor, limit=limit, name=name,
|
|
1657
|
+
timeout_s=timeout_s,
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
def resolve_manager_origin(self, capture: object, origin: object, *, timeout_s: float | None = None) -> Mapping[str, object]:
|
|
1661
|
+
with self._operation_lock:
|
|
1662
|
+
self._require_capture_fence(capture)
|
|
1663
|
+
return self.runtime_api.resolve_capture_manager_origin(
|
|
1664
|
+
origin, timeout_s=timeout_s
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
def temporary_tables(self, capture: object, manager_handle: str, *, names: tuple[str, ...] | None, cursor: int, limit: int, selection: object | None, timeout_s: float | None = None) -> Mapping[str, object]:
|
|
1668
|
+
with self._operation_lock:
|
|
1669
|
+
self._require_capture_fence(capture)
|
|
1670
|
+
return self.runtime_api.capture_temporary_tables(
|
|
1671
|
+
manager_handle, names=names, cursor=cursor, limit=limit,
|
|
1672
|
+
selection=selection, timeout_s=timeout_s,
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
def load_worker_modules(
|
|
1676
|
+
self,
|
|
1677
|
+
units: tuple[WorkerModuleUnit, ...],
|
|
1678
|
+
*,
|
|
1679
|
+
breakpoint_policy: WorkerBreakpointReloadPolicy = (
|
|
1680
|
+
WorkerBreakpointReloadPolicy.STRICT
|
|
1681
|
+
),
|
|
1682
|
+
profiler: PhaseRecorder | None = None,
|
|
1683
|
+
) -> WorkerGenerationHandle:
|
|
1684
|
+
with self._operation_lock:
|
|
1685
|
+
def reload_modules() -> WorkerGenerationHandle:
|
|
1686
|
+
if type(units) is not tuple or not units:
|
|
1687
|
+
raise ProtocolError(
|
|
1688
|
+
"Worker module units must be a non-empty tuple"
|
|
1689
|
+
)
|
|
1690
|
+
if any(not isinstance(unit, WorkerModuleUnit) for unit in units):
|
|
1691
|
+
raise ProtocolError(
|
|
1692
|
+
"Worker module catalog binding does not match"
|
|
1693
|
+
)
|
|
1694
|
+
names = tuple(unit.logical_name.casefold() for unit in units)
|
|
1695
|
+
if len(names) != len(set(names)):
|
|
1696
|
+
raise ProtocolError("Worker module names must be unique")
|
|
1697
|
+
catalog = self._require_common_module_catalog()
|
|
1698
|
+
generation = self.runtime_api.load_worker_modules(
|
|
1699
|
+
units,
|
|
1700
|
+
common_modules=catalog,
|
|
1701
|
+
breakpoint_policy=breakpoint_policy,
|
|
1702
|
+
profiler=profiler,
|
|
1703
|
+
)
|
|
1704
|
+
self._active_worker_file_units.clear()
|
|
1705
|
+
return generation
|
|
1706
|
+
|
|
1707
|
+
return (
|
|
1708
|
+
reload_modules()
|
|
1709
|
+
if profiler is None
|
|
1710
|
+
else profiler.measure(
|
|
1711
|
+
"end_to_end",
|
|
1712
|
+
reload_modules,
|
|
1713
|
+
item_count=lambda _result: len(units),
|
|
1714
|
+
)
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
def load_worker_module(self, path: Path | str) -> WorkerGenerationHandle:
|
|
1718
|
+
"""Publish a saved Designer or EDT common module by its BSL file path."""
|
|
1719
|
+
with self._operation_lock:
|
|
1720
|
+
name, source = self._require_common_module_catalog().read_worker_module_source(
|
|
1721
|
+
path
|
|
1722
|
+
)
|
|
1723
|
+
key = name.casefold()
|
|
1724
|
+
revision = self._worker_file_revisions.get(key, 0) + 1
|
|
1725
|
+
reference = SourceUnitRef(
|
|
1726
|
+
SourceUnitKind.MODULE,
|
|
1727
|
+
name,
|
|
1728
|
+
revision,
|
|
1729
|
+
source_sha256(source),
|
|
1730
|
+
)
|
|
1731
|
+
unit = WorkerModuleUnit(
|
|
1732
|
+
name,
|
|
1733
|
+
"module",
|
|
1734
|
+
revision,
|
|
1735
|
+
mapped_visible_source(source, reference),
|
|
1736
|
+
)
|
|
1737
|
+
generation = self.load_worker_modules((unit,))
|
|
1738
|
+
self._worker_file_revisions[key] = revision
|
|
1739
|
+
self._active_worker_file_units[key] = reference
|
|
1740
|
+
return generation
|
|
1741
|
+
|
|
1742
|
+
def add_worker_breakpoint(
|
|
1743
|
+
self,
|
|
1744
|
+
path: str,
|
|
1745
|
+
line: int,
|
|
1746
|
+
) -> WorkerBreakpointStatus:
|
|
1747
|
+
"""Set a one-based source line breakpoint in a file-loaded Worker module."""
|
|
1748
|
+
if not isinstance(path, str) or not path:
|
|
1749
|
+
raise ValueError("Worker breakpoint path must be a non-empty string")
|
|
1750
|
+
if type(line) is not int or line < 1:
|
|
1751
|
+
raise ValueError("Worker breakpoint line must be positive")
|
|
1752
|
+
with self._operation_lock:
|
|
1753
|
+
name, source = self._require_common_module_catalog().read_worker_module_source(
|
|
1754
|
+
path
|
|
1755
|
+
)
|
|
1756
|
+
source_unit = self._active_worker_file_units.get(name.casefold())
|
|
1757
|
+
if source_unit is None:
|
|
1758
|
+
raise ProtocolError("Worker module must be loaded before adding a breakpoint")
|
|
1759
|
+
if source_unit.source_sha256 != source_sha256(source):
|
|
1760
|
+
raise ProtocolError("Worker module source changed; reload it before adding a breakpoint")
|
|
1761
|
+
return self.runtime_api.add_worker_breakpoint(
|
|
1762
|
+
source_unit,
|
|
1763
|
+
name.casefold(),
|
|
1764
|
+
line,
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
def remove_worker_breakpoint(self, breakpoint_id: UUID) -> None:
|
|
1768
|
+
with self._operation_lock:
|
|
1769
|
+
self.runtime_api.remove_worker_breakpoint(breakpoint_id)
|
|
1770
|
+
|
|
1771
|
+
def set_worker_breakpoint_enabled(
|
|
1772
|
+
self,
|
|
1773
|
+
breakpoint_id: UUID,
|
|
1774
|
+
enabled: bool,
|
|
1775
|
+
) -> WorkerBreakpointStatus:
|
|
1776
|
+
with self._operation_lock:
|
|
1777
|
+
return self.runtime_api.set_worker_breakpoint_enabled(
|
|
1778
|
+
breakpoint_id,
|
|
1779
|
+
enabled,
|
|
1780
|
+
)
|
|
1781
|
+
|
|
1782
|
+
def worker_breakpoint_status(
|
|
1783
|
+
self,
|
|
1784
|
+
breakpoint_id: UUID,
|
|
1785
|
+
) -> WorkerBreakpointStatus:
|
|
1786
|
+
with self._operation_lock:
|
|
1787
|
+
return self.runtime_api.worker_breakpoint_status(breakpoint_id)
|
|
1788
|
+
|
|
1789
|
+
def list_worker_breakpoints(self) -> tuple[WorkerBreakpointStatus, ...]:
|
|
1790
|
+
with self._operation_lock:
|
|
1791
|
+
return self.runtime_api.list_worker_breakpoints()
|
|
1792
|
+
|
|
1793
|
+
def last_worker_breakpoint_reload_report(
|
|
1794
|
+
self,
|
|
1795
|
+
) -> WorkerBreakpointReloadReport | None:
|
|
1796
|
+
with self._operation_lock:
|
|
1797
|
+
return self.runtime_api.last_worker_breakpoint_reload_report()
|
|
1798
|
+
|
|
1799
|
+
def _require_common_module_catalog(self) -> SessionCommonModuleCatalog:
|
|
1800
|
+
catalog = self._common_module_catalog
|
|
1801
|
+
if catalog is None:
|
|
1802
|
+
raise ProtocolError("Worker common-module source root is not configured")
|
|
1803
|
+
return catalog
|
|
1804
|
+
|
|
1805
|
+
def release_worker_generation(self, handle: WorkerGenerationHandle) -> None:
|
|
1806
|
+
"""Release the active API generation; superseded handles are already stale."""
|
|
1807
|
+
with self._operation_lock:
|
|
1808
|
+
self.runtime_api.release_worker_generation(handle)
|
|
1809
|
+
self._active_worker_file_units.clear()
|
|
1810
|
+
|
|
1811
|
+
def configure_capture_points(self, locations: tuple[ModuleLocation, ...]) -> None:
|
|
1812
|
+
"""Configure already-resolved debugger locations for an interactive client."""
|
|
1813
|
+
with self._operation_lock:
|
|
1814
|
+
self.runtime_api.configure_capture_points(locations)
|
|
1815
|
+
self._file_capture_points = tuple(locations)
|
|
1816
|
+
|
|
1817
|
+
def add_capture_point(self, path: str, line: int) -> ModuleLocation:
|
|
1818
|
+
"""Arm a common-module breakpoint using only a source path and line."""
|
|
1819
|
+
if not isinstance(path, str) or not path:
|
|
1820
|
+
raise ValueError("capture point path must be a non-empty string")
|
|
1821
|
+
if type(line) is not int or line < 1:
|
|
1822
|
+
raise ValueError("capture point line must be positive")
|
|
1823
|
+
with self._operation_lock:
|
|
1824
|
+
catalog = self._common_module_catalog
|
|
1825
|
+
if catalog is None:
|
|
1826
|
+
raise ProtocolError("capture source_root is not configured")
|
|
1827
|
+
name, _source = catalog.read_worker_module_source(path)
|
|
1828
|
+
location = CommonModuleCaptureResolver(
|
|
1829
|
+
"Notebook", catalog.source_root
|
|
1830
|
+
).resolve_module_line(name, line)
|
|
1831
|
+
locations = (*self._file_capture_points, location)
|
|
1832
|
+
self.runtime_api.configure_capture_points(locations)
|
|
1833
|
+
self._file_capture_points = locations
|
|
1834
|
+
return location
|
|
1835
|
+
|
|
1836
|
+
def clear_capture_points(self) -> None:
|
|
1837
|
+
self.configure_capture_points(())
|
|
1838
|
+
|
|
1839
|
+
def to_df(
|
|
1840
|
+
self,
|
|
1841
|
+
handle: str,
|
|
1842
|
+
*,
|
|
1843
|
+
refs: str | ReferenceMode = ReferenceMode.PRESENTATION,
|
|
1844
|
+
ref_columns: dict[str, str | ReferenceMode] | None = None,
|
|
1845
|
+
uuid_suffix: str = "__uuid",
|
|
1846
|
+
chunk_size: int | None = None,
|
|
1847
|
+
profiler: PhaseRecorder | None = None,
|
|
1848
|
+
) -> pd.DataFrame:
|
|
1849
|
+
with self._operation_lock:
|
|
1850
|
+
self._require_public_value_handle(handle)
|
|
1851
|
+
return self.runtime_api.materialize_table(
|
|
1852
|
+
handle,
|
|
1853
|
+
refs=refs,
|
|
1854
|
+
ref_columns=ref_columns,
|
|
1855
|
+
uuid_suffix=uuid_suffix,
|
|
1856
|
+
chunk_size=chunk_size or self.config.chunk_size,
|
|
1857
|
+
profiler=profiler,
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
def project_to_df(
|
|
1861
|
+
self,
|
|
1862
|
+
handle: str,
|
|
1863
|
+
selection: dict[str, object],
|
|
1864
|
+
**options: object,
|
|
1865
|
+
) -> pd.DataFrame:
|
|
1866
|
+
"""Materialize a bounded table projection for a frontend value proxy."""
|
|
1867
|
+
with self._operation_lock:
|
|
1868
|
+
self._require_public_value_handle(handle)
|
|
1869
|
+
return self.runtime_api.project_to_df(handle, selection, **options)
|
|
1870
|
+
|
|
1871
|
+
def materialize(
|
|
1872
|
+
self,
|
|
1873
|
+
handle: str,
|
|
1874
|
+
*,
|
|
1875
|
+
refs: str | ReferenceMode = ReferenceMode.PRESENTATION,
|
|
1876
|
+
ref_columns: dict[str, str | ReferenceMode] | None = None,
|
|
1877
|
+
uuid_suffix: str = "__uuid",
|
|
1878
|
+
chunk_size: int | None = None,
|
|
1879
|
+
max_depth: int = 32,
|
|
1880
|
+
max_items: int = 100_000,
|
|
1881
|
+
max_bytes: int = 64 * 1024 * 1024,
|
|
1882
|
+
timeout_s: float | None = None,
|
|
1883
|
+
profiler: PhaseRecorder | None = None,
|
|
1884
|
+
) -> object:
|
|
1885
|
+
with self._operation_lock:
|
|
1886
|
+
self._require_public_value_handle(handle)
|
|
1887
|
+
options: dict[str, object] = {
|
|
1888
|
+
"refs": refs,
|
|
1889
|
+
"ref_columns": ref_columns,
|
|
1890
|
+
"uuid_suffix": uuid_suffix,
|
|
1891
|
+
"chunk_size": chunk_size or self.config.chunk_size,
|
|
1892
|
+
"max_depth": max_depth,
|
|
1893
|
+
"max_items": max_items,
|
|
1894
|
+
"max_bytes": max_bytes,
|
|
1895
|
+
"profiler": profiler,
|
|
1896
|
+
}
|
|
1897
|
+
if timeout_s is not None:
|
|
1898
|
+
options["timeout_s"] = timeout_s
|
|
1899
|
+
return self.runtime_api.materialize_value(
|
|
1900
|
+
handle,
|
|
1901
|
+
**options,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
def materialize_value(self, handle: str, **options: object) -> object:
|
|
1905
|
+
"""Expose recursive materialization under the frontend proxy contract."""
|
|
1906
|
+
return self.materialize(handle, **options)
|
|
1907
|
+
|
|
1908
|
+
def project_value(
|
|
1909
|
+
self,
|
|
1910
|
+
handle: str,
|
|
1911
|
+
selection: dict[str, object],
|
|
1912
|
+
**options: object,
|
|
1913
|
+
) -> object:
|
|
1914
|
+
"""Materialize a bounded recursive projection for a frontend proxy."""
|
|
1915
|
+
with self._operation_lock:
|
|
1916
|
+
self._require_public_value_handle(handle)
|
|
1917
|
+
return self.runtime_api.project_value(handle, selection, **options)
|
|
1918
|
+
|
|
1919
|
+
def materialization_kind(
|
|
1920
|
+
self, handle: str, *, timeout_s: float | None = None
|
|
1921
|
+
) -> str:
|
|
1922
|
+
with self._operation_lock:
|
|
1923
|
+
self._require_public_value_handle(handle)
|
|
1924
|
+
if timeout_s is None:
|
|
1925
|
+
return self.runtime_api.materialization_kind(handle)
|
|
1926
|
+
return self.runtime_api.materialization_kind(handle, timeout_s=timeout_s)
|
|
1927
|
+
|
|
1928
|
+
def materialize_value_payload(self, handle: str, **options: object) -> bytes:
|
|
1929
|
+
with self._operation_lock:
|
|
1930
|
+
self._require_public_value_handle(handle)
|
|
1931
|
+
return self.runtime_api.materialize_value_payload(handle, **options)
|
|
1932
|
+
|
|
1933
|
+
def materialize_table_payload(self, handle: str, **options: object) -> bytes:
|
|
1934
|
+
with self._operation_lock:
|
|
1935
|
+
self._require_public_value_handle(handle)
|
|
1936
|
+
return self.runtime_api.materialize_table_payload(handle, **options)
|
|
1937
|
+
|
|
1938
|
+
def project_value_payload(self, handle: str, selection, **options: object): # type: ignore[no-untyped-def]
|
|
1939
|
+
with self._operation_lock:
|
|
1940
|
+
self._require_public_value_handle(handle)
|
|
1941
|
+
return self.runtime_api.project_value_payload(
|
|
1942
|
+
handle,
|
|
1943
|
+
kind=selection.kind.value,
|
|
1944
|
+
offset=selection.offset,
|
|
1945
|
+
limit=selection.limit,
|
|
1946
|
+
columns=selection.columns,
|
|
1947
|
+
names=selection.names,
|
|
1948
|
+
**options,
|
|
1949
|
+
)
|
|
1950
|
+
|
|
1951
|
+
def require_public_value_handle(self, handle: str) -> None:
|
|
1952
|
+
with self._operation_lock:
|
|
1953
|
+
self._require_public_value_handle(handle)
|
|
1954
|
+
|
|
1955
|
+
def require_public_value_handles(self, handles: tuple[str, ...]) -> None:
|
|
1956
|
+
with self._operation_lock:
|
|
1957
|
+
self.runtime_api.require_public_value_handles(handles)
|
|
1958
|
+
|
|
1959
|
+
def _require_public_value_handle(self, handle: object) -> None:
|
|
1960
|
+
if not isinstance(handle, str):
|
|
1961
|
+
return
|
|
1962
|
+
normalized = handle.casefold()
|
|
1963
|
+
if normalized.startswith(
|
|
1964
|
+
"Контекст.RuntimeWorkerActiveGeneration".casefold()
|
|
1965
|
+
) or normalized.startswith("__OnecPinnedWorkerGeneration".casefold()):
|
|
1966
|
+
raise ProtocolError("Worker generation objects are not public values")
|
|
1967
|
+
self.runtime_api.require_public_value_handle(handle)
|
|
1968
|
+
|
|
1969
|
+
def status(self) -> RuntimeStatus:
|
|
1970
|
+
with self._operation_lock:
|
|
1971
|
+
return self.runtime_api.status()
|
|
1972
|
+
|
|
1973
|
+
def namespace_snapshot(self) -> RuntimeNamespaceSnapshot:
|
|
1974
|
+
with self._operation_lock:
|
|
1975
|
+
if self._closed:
|
|
1976
|
+
raise ProtocolError("ZUP demo runtime session is closed")
|
|
1977
|
+
return self.runtime_api.namespace_snapshot()
|
|
1978
|
+
|
|
1979
|
+
def completion_fields(
|
|
1980
|
+
self, handle: str, *, table_row: bool = False, timeout_s: float = 1.0
|
|
1981
|
+
) -> tuple[str, ...]:
|
|
1982
|
+
"""Return live field names without waiting behind a running cell."""
|
|
1983
|
+
if not self._operation_lock.acquire(blocking=False):
|
|
1984
|
+
raise ProtocolError("Runtime is already executing another request")
|
|
1985
|
+
try:
|
|
1986
|
+
if self._closed:
|
|
1987
|
+
raise ProtocolError("Runtime session is closed")
|
|
1988
|
+
return self.runtime_api.completion_fields(
|
|
1989
|
+
handle, table_row=table_row, timeout_s=timeout_s
|
|
1990
|
+
)
|
|
1991
|
+
finally:
|
|
1992
|
+
self._operation_lock.release()
|
|
1993
|
+
|
|
1994
|
+
def close(self) -> None:
|
|
1995
|
+
self._close(shutdown=False)
|
|
1996
|
+
|
|
1997
|
+
def close_for_kernel_shutdown(self) -> None:
|
|
1998
|
+
"""Release the owned server session before local Worker bookkeeping.
|
|
1999
|
+
|
|
2000
|
+
A Jupyter kernel has a short external shutdown deadline. Its Python
|
|
2001
|
+
objects disappear with the process, but the 1C server session does not.
|
|
2002
|
+
"""
|
|
2003
|
+
self._close(shutdown=self.config.runtime.is_server_infobase)
|
|
2004
|
+
|
|
2005
|
+
def _close(self, *, shutdown: bool) -> None:
|
|
2006
|
+
with self._close_lock:
|
|
2007
|
+
if self._closed:
|
|
2008
|
+
return
|
|
2009
|
+
self._heartbeat_stop.set()
|
|
2010
|
+
if current_thread() is not self._heartbeat_thread:
|
|
2011
|
+
self._heartbeat_thread.join(timeout=2.0)
|
|
2012
|
+
errors: list[BaseException] = []
|
|
2013
|
+
with self._operation_lock:
|
|
2014
|
+
if not shutdown and not self._runtime_api_closed:
|
|
2015
|
+
close_runtime_api = getattr(self.runtime_api, "close", None)
|
|
2016
|
+
if not callable(close_runtime_api):
|
|
2017
|
+
self._runtime_api_closed = True
|
|
2018
|
+
else:
|
|
2019
|
+
try:
|
|
2020
|
+
close_runtime_api()
|
|
2021
|
+
except BaseException as error:
|
|
2022
|
+
errors.append(error)
|
|
2023
|
+
else:
|
|
2024
|
+
self._runtime_api_closed = True
|
|
2025
|
+
if self.config.runtime.is_server_infobase:
|
|
2026
|
+
if not self._server_session_terminated:
|
|
2027
|
+
try:
|
|
2028
|
+
self._native_client_termination_requested = self._rdbg.terminate_bound_server_session()
|
|
2029
|
+
except RdbgDebugUiNotRegistered:
|
|
2030
|
+
# The UI is gone; fall back to closing our owned client.
|
|
2031
|
+
self._server_session_terminated = True
|
|
2032
|
+
except BaseException as error:
|
|
2033
|
+
errors.append(error)
|
|
2034
|
+
else:
|
|
2035
|
+
self._server_session_terminated = True
|
|
2036
|
+
# Server termination needs the owned client connection alive.
|
|
2037
|
+
# The cluster debugger belongs to the service, not this session.
|
|
2038
|
+
if self._server_session_terminated and not self._processes_closed:
|
|
2039
|
+
try:
|
|
2040
|
+
if self._native_client_termination_requested:
|
|
2041
|
+
self._processes.close(graceful_client_timeout_s=3.0)
|
|
2042
|
+
else:
|
|
2043
|
+
self._processes.close()
|
|
2044
|
+
except BaseException as error:
|
|
2045
|
+
errors.append(error)
|
|
2046
|
+
else:
|
|
2047
|
+
self._processes_closed = True
|
|
2048
|
+
if self._processes_closed and not self._debug_ui_detached:
|
|
2049
|
+
try:
|
|
2050
|
+
self._rdbg.detach()
|
|
2051
|
+
except BaseException as error:
|
|
2052
|
+
errors.append(error)
|
|
2053
|
+
else:
|
|
2054
|
+
self._debug_ui_detached = True
|
|
2055
|
+
if not self._transport_closed and (
|
|
2056
|
+
not self.config.runtime.is_server_infobase
|
|
2057
|
+
or self._debug_ui_detached
|
|
2058
|
+
):
|
|
2059
|
+
try:
|
|
2060
|
+
self._transport.close()
|
|
2061
|
+
except BaseException as error:
|
|
2062
|
+
errors.append(error)
|
|
2063
|
+
else:
|
|
2064
|
+
self._transport_closed = True
|
|
2065
|
+
if not self.config.runtime.is_server_infobase and not self._processes_closed:
|
|
2066
|
+
try:
|
|
2067
|
+
self._processes.close()
|
|
2068
|
+
except BaseException as error:
|
|
2069
|
+
errors.append(error)
|
|
2070
|
+
else:
|
|
2071
|
+
self._processes_closed = True
|
|
2072
|
+
if (
|
|
2073
|
+
shutdown
|
|
2074
|
+
and self._server_session_terminated
|
|
2075
|
+
and self._processes_closed
|
|
2076
|
+
and self._debug_ui_detached
|
|
2077
|
+
and self._transport_closed
|
|
2078
|
+
):
|
|
2079
|
+
# Local Worker generations die with this kernel. The
|
|
2080
|
+
# authenticated server target and client have been closed.
|
|
2081
|
+
self._runtime_api_closed = True
|
|
2082
|
+
if (
|
|
2083
|
+
self._runtime_api_closed
|
|
2084
|
+
and self._debug_ui_detached
|
|
2085
|
+
and self._transport_closed
|
|
2086
|
+
and self._processes_closed
|
|
2087
|
+
):
|
|
2088
|
+
self._closed = True
|
|
2089
|
+
if errors:
|
|
2090
|
+
raise ProtocolError(
|
|
2091
|
+
"ZUP demo cleanup failed: "
|
|
2092
|
+
+ ", ".join(type(error).__name__ for error in errors)
|
|
2093
|
+
) from errors[0]
|
|
2094
|
+
|
|
2095
|
+
def owned_process_snapshot(self) -> tuple[dict[str, object], ...]:
|
|
2096
|
+
"""Return exact identities needed for crash-safe owner reconciliation."""
|
|
2097
|
+
identities: list[dict[str, object]] = []
|
|
2098
|
+
for role, owned in (
|
|
2099
|
+
("dbgs", self._processes.debug_server),
|
|
2100
|
+
("onec", self._processes.debuggee),
|
|
2101
|
+
):
|
|
2102
|
+
if owned is None:
|
|
2103
|
+
continue
|
|
2104
|
+
process = psutil.Process(owned.pid)
|
|
2105
|
+
identities.append(
|
|
2106
|
+
{
|
|
2107
|
+
"role": role,
|
|
2108
|
+
"pid": owned.pid,
|
|
2109
|
+
"create_time": process.create_time(),
|
|
2110
|
+
"executable": str(Path(process.exe()).resolve()),
|
|
2111
|
+
}
|
|
2112
|
+
)
|
|
2113
|
+
expected_roles = {"onec"} if self.config.runtime.is_server_infobase else {"dbgs", "onec"}
|
|
2114
|
+
if {item["role"] for item in identities} != expected_roles:
|
|
2115
|
+
raise ProtocolError("ZUP runtime process ownership is incomplete")
|
|
2116
|
+
return tuple(identities)
|
|
2117
|
+
|
|
2118
|
+
def __enter__(self) -> "RuntimeSession":
|
|
2119
|
+
return self
|
|
2120
|
+
|
|
2121
|
+
def __exit__(self, *_args: object) -> None:
|
|
2122
|
+
self.close()
|
|
2123
|
+
|