onec-interactive-runtime-core 0.1.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
|
@@ -0,0 +1,2644 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from math import isfinite
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
10
|
+
from time import monotonic
|
|
11
|
+
from typing import Callable
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from uuid import UUID, uuid4
|
|
14
|
+
|
|
15
|
+
from onec_runtime.bsl import (
|
|
16
|
+
DiagnosticStage,
|
|
17
|
+
LoweringMode,
|
|
18
|
+
MappedSource,
|
|
19
|
+
NormalizedDiagnostic,
|
|
20
|
+
SemanticNotebookLowerer,
|
|
21
|
+
SourceArtifactKind,
|
|
22
|
+
SourceSpan,
|
|
23
|
+
SourceTransformBuilder,
|
|
24
|
+
SourceUnitKind,
|
|
25
|
+
SourceUnitRef,
|
|
26
|
+
VisibleSourceContext,
|
|
27
|
+
mapped_visible_source,
|
|
28
|
+
parse_platform_diagnostic,
|
|
29
|
+
remap_platform_diagnostic,
|
|
30
|
+
source_sha256,
|
|
31
|
+
)
|
|
32
|
+
from onec_runtime.bsl.parser_target import PythonParserTarget
|
|
33
|
+
from onec_runtime.capture import (
|
|
34
|
+
build_capture_transfer_call,
|
|
35
|
+
build_live_capture_begin_call,
|
|
36
|
+
build_live_capture_end_call,
|
|
37
|
+
build_live_capture_root_transfer_call,
|
|
38
|
+
build_live_current_capture_call,
|
|
39
|
+
build_temporary_storage_value_expression,
|
|
40
|
+
)
|
|
41
|
+
from onec_runtime.breakpoint_workspace import (
|
|
42
|
+
BreakpointWorkspaceController,
|
|
43
|
+
BreakpointWorkspaceOutcomeUnknown,
|
|
44
|
+
WorkspaceInstallReceipt,
|
|
45
|
+
WorkspaceSnapshot,
|
|
46
|
+
)
|
|
47
|
+
from onec_runtime.errors import BslExecutionError, ProtocolError, RdbgTransportError
|
|
48
|
+
from onec_runtime.experiment import bsl_string_literal
|
|
49
|
+
from onec_runtime.fault_injection import FaultPoint
|
|
50
|
+
from onec_runtime.rdbg.models import (
|
|
51
|
+
EvaluationResult,
|
|
52
|
+
FrameVariable,
|
|
53
|
+
ModuleLocation,
|
|
54
|
+
PendingEvaluation,
|
|
55
|
+
StackFrame,
|
|
56
|
+
StopEvent,
|
|
57
|
+
TargetId,
|
|
58
|
+
)
|
|
59
|
+
from onec_runtime.rdbg.reconnect import ReconnectedSession
|
|
60
|
+
from onec_runtime.rdbg.session import RdbgSession
|
|
61
|
+
from onec_runtime.recovery import (
|
|
62
|
+
RecoveryCheckpoint,
|
|
63
|
+
RecoveryOutcome,
|
|
64
|
+
RecoveryPhase,
|
|
65
|
+
RecoveryResult,
|
|
66
|
+
RootWriteRecord,
|
|
67
|
+
SideEffectStatus,
|
|
68
|
+
validate_paused_identity,
|
|
69
|
+
)
|
|
70
|
+
from onec_runtime.recovery_journal import RecoveryJournal
|
|
71
|
+
from onec_runtime.stop_routing import (
|
|
72
|
+
BreakpointRegistry,
|
|
73
|
+
ClassifiedStop,
|
|
74
|
+
StopReason,
|
|
75
|
+
classify_stop,
|
|
76
|
+
)
|
|
77
|
+
from onec_runtime.table_value import evaluation_to_python
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
MAX_CAPTURE_PROJECTION_POSITION = 10_000_000
|
|
81
|
+
MAX_CAPTURE_SCHEMA_COLUMNS = 100
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _safe_platform_diagnostic(
|
|
85
|
+
message: str,
|
|
86
|
+
executed_source: MappedSource,
|
|
87
|
+
*,
|
|
88
|
+
visible_source_context: VisibleSourceContext | None,
|
|
89
|
+
) -> NormalizedDiagnostic | None:
|
|
90
|
+
try:
|
|
91
|
+
return remap_platform_diagnostic(
|
|
92
|
+
parse_platform_diagnostic(message),
|
|
93
|
+
executed_source,
|
|
94
|
+
stage=DiagnosticStage.EXECUTION,
|
|
95
|
+
visible_source_context=visible_source_context,
|
|
96
|
+
)
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OperationState(Enum):
|
|
102
|
+
IDLE = "idle"
|
|
103
|
+
MAIN_PENDING = "main_pending"
|
|
104
|
+
CAPTURED = "captured"
|
|
105
|
+
EVALUATING_CAPTURE = "evaluating_capture"
|
|
106
|
+
CAPTURE_DEBUG_STOPPED = "capture_debug_stopped"
|
|
107
|
+
DEBUG_STOPPED = "debug_stopped"
|
|
108
|
+
FLUSHING = "flushing"
|
|
109
|
+
PARTIAL_WRITEBACK_FAILURE = "partial_writeback_failure"
|
|
110
|
+
BREAKPOINT_RESTORE_FAILURE = "breakpoint_restore_failure"
|
|
111
|
+
RESUMING = "resuming"
|
|
112
|
+
RECOVERING = "recovering"
|
|
113
|
+
LOST = "lost"
|
|
114
|
+
COMPLETED = "completed"
|
|
115
|
+
FAILED = "failed"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PartialWritebackError(ProtocolError):
|
|
119
|
+
"""At least one staged capture root could not be written to the frame."""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class BreakpointRestoreError(ProtocolError):
|
|
123
|
+
"""The full breakpoint workspace could not be restored while paused."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CompletionDecodeError(ProtocolError):
|
|
127
|
+
"""A MAIN completion scalar could not be decoded without exposing its value."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, phase: str, evaluation: EvaluationResult) -> None:
|
|
130
|
+
super().__init__(f"MAIN completion {phase} cannot be decoded")
|
|
131
|
+
self.phase = phase
|
|
132
|
+
self.evaluation_type = evaluation.type_name
|
|
133
|
+
self.exact_decimal_present = bool(evaluation.value_decimal)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class OperationHandle:
|
|
138
|
+
operation_id: int
|
|
139
|
+
visible_source: str = field(repr=False)
|
|
140
|
+
lowered_source: str = field(repr=False)
|
|
141
|
+
messages_intercepted: int = 0
|
|
142
|
+
message_collector_key: str = ""
|
|
143
|
+
executed_source: MappedSource | None = field(default=None, repr=False)
|
|
144
|
+
visible_source_context: VisibleSourceContext | None = field(
|
|
145
|
+
default=None,
|
|
146
|
+
repr=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True, slots=True)
|
|
151
|
+
class CapturedStop:
|
|
152
|
+
operation: OperationHandle
|
|
153
|
+
location: ModuleLocation
|
|
154
|
+
stop_sequence: int
|
|
155
|
+
variables: tuple[FrameVariable, ...]
|
|
156
|
+
observed_command_id: int | None = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True, slots=True)
|
|
160
|
+
class DebugStop:
|
|
161
|
+
operation: OperationHandle
|
|
162
|
+
stop: StopEvent
|
|
163
|
+
reason: StopReason
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class _PendingCaptureEvaluation:
|
|
168
|
+
pending: PendingEvaluation
|
|
169
|
+
operation: OperationHandle
|
|
170
|
+
visible_source: str = field(repr=False)
|
|
171
|
+
lowered_source: str = field(repr=False)
|
|
172
|
+
executed_source: MappedSource = field(repr=False)
|
|
173
|
+
visible_source_context: VisibleSourceContext | None = field(repr=False)
|
|
174
|
+
messages_intercepted: int
|
|
175
|
+
message_collector_key: str
|
|
176
|
+
cell_fields: Mapping[str, object] = field(repr=False)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True, slots=True)
|
|
180
|
+
class BreakpointWorkspaceEvent:
|
|
181
|
+
phase: str
|
|
182
|
+
locations: tuple[ModuleLocation, ...]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True, slots=True)
|
|
186
|
+
class ContinuationAttemptSpec:
|
|
187
|
+
"""Opaque identity binding one continuation to its exact ordered roots."""
|
|
188
|
+
|
|
189
|
+
attempt_id: str
|
|
190
|
+
capture_generation: int
|
|
191
|
+
request_operation_id: str
|
|
192
|
+
dirty_roots: tuple[str, ...]
|
|
193
|
+
|
|
194
|
+
def __post_init__(self) -> None:
|
|
195
|
+
if not isinstance(self.attempt_id, str) or not self.attempt_id or len(self.attempt_id) > 256:
|
|
196
|
+
raise ValueError("continuation attempt_id is invalid")
|
|
197
|
+
if type(self.capture_generation) is not int or self.capture_generation <= 0:
|
|
198
|
+
raise ValueError("continuation capture_generation must be positive")
|
|
199
|
+
if (
|
|
200
|
+
not isinstance(self.request_operation_id, str)
|
|
201
|
+
or not self.request_operation_id
|
|
202
|
+
or len(self.request_operation_id) > 256
|
|
203
|
+
):
|
|
204
|
+
raise ValueError("continuation request_operation_id is invalid")
|
|
205
|
+
roots = tuple(self.dirty_roots)
|
|
206
|
+
if len(roots) > 100:
|
|
207
|
+
raise ValueError("continuation dirty_roots exceeds 100")
|
|
208
|
+
if any(
|
|
209
|
+
not isinstance(root, str)
|
|
210
|
+
or not root
|
|
211
|
+
or len(root) > 256
|
|
212
|
+
or not root.isidentifier()
|
|
213
|
+
for root in roots
|
|
214
|
+
):
|
|
215
|
+
raise ValueError("continuation dirty_roots are invalid")
|
|
216
|
+
if len({root.casefold() for root in roots}) != len(roots):
|
|
217
|
+
raise ValueError("continuation dirty_roots must be unique")
|
|
218
|
+
object.__setattr__(self, "dirty_roots", roots)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True, slots=True)
|
|
222
|
+
class ContinuationAttemptEvidence:
|
|
223
|
+
root_statuses: tuple[tuple[str, str], ...]
|
|
224
|
+
continue_state: str
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(slots=True)
|
|
228
|
+
class _ContinuationAttemptState:
|
|
229
|
+
spec: ContinuationAttemptSpec
|
|
230
|
+
root_statuses: dict[str, str]
|
|
231
|
+
continue_state: str = "unattempted"
|
|
232
|
+
irreversible_roots: set[str] = field(default_factory=set)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class _ControllerContinuationAdmission:
|
|
236
|
+
"""Exact rollback fence for one paused successor preparation."""
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
controller: "PrototypeRuntimeController",
|
|
241
|
+
*,
|
|
242
|
+
registry: BreakpointRegistry,
|
|
243
|
+
capture_points: tuple[ModuleLocation, ...],
|
|
244
|
+
workspace: tuple[ModuleLocation, ...],
|
|
245
|
+
state: OperationState,
|
|
246
|
+
attempt_id: str,
|
|
247
|
+
) -> None:
|
|
248
|
+
self._controller = controller
|
|
249
|
+
self._registry = registry
|
|
250
|
+
self._capture_points = capture_points
|
|
251
|
+
self._workspace = workspace
|
|
252
|
+
self._state = state
|
|
253
|
+
self.attempt_id = attempt_id
|
|
254
|
+
self._closed = False
|
|
255
|
+
|
|
256
|
+
def commit(self) -> None:
|
|
257
|
+
self._closed = True
|
|
258
|
+
|
|
259
|
+
def rollback(self) -> None:
|
|
260
|
+
if self._closed:
|
|
261
|
+
return
|
|
262
|
+
attempt = self._controller._continuation_attempts[self.attempt_id]
|
|
263
|
+
unsafe = (
|
|
264
|
+
bool(attempt.irreversible_roots)
|
|
265
|
+
or attempt.continue_state != "unattempted"
|
|
266
|
+
or self._controller.state is OperationState.RECOVERING
|
|
267
|
+
)
|
|
268
|
+
if unsafe:
|
|
269
|
+
self.quarantine()
|
|
270
|
+
raise ProtocolError("continuation admission has irreversible evidence")
|
|
271
|
+
try:
|
|
272
|
+
self._controller._set_workspace(
|
|
273
|
+
"capture_successor_restore", self._workspace
|
|
274
|
+
)
|
|
275
|
+
except BaseException as error:
|
|
276
|
+
self.quarantine()
|
|
277
|
+
raise ProtocolError(
|
|
278
|
+
"capture successor breakpoint rollback is uncertain"
|
|
279
|
+
) from error
|
|
280
|
+
self._controller.registry = self._registry
|
|
281
|
+
self._controller.capture_points = self._capture_points
|
|
282
|
+
self._controller.state = self._state
|
|
283
|
+
self._closed = True
|
|
284
|
+
|
|
285
|
+
def quarantine(self) -> None:
|
|
286
|
+
self._controller._clear_capture_inspection()
|
|
287
|
+
self._controller.state = OperationState.RECOVERING
|
|
288
|
+
self._closed = True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@dataclass(frozen=True, slots=True)
|
|
292
|
+
class CaptureCellResult:
|
|
293
|
+
operation_id: int
|
|
294
|
+
visible_source: str = field(repr=False)
|
|
295
|
+
lowered_source: str = field(repr=False)
|
|
296
|
+
result: object
|
|
297
|
+
messages: tuple[str, ...] = ()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass(frozen=True, slots=True)
|
|
301
|
+
class MainCompletion:
|
|
302
|
+
operation: OperationHandle
|
|
303
|
+
result: object
|
|
304
|
+
error: str
|
|
305
|
+
succeeded: bool
|
|
306
|
+
messages: tuple[str, ...] = ()
|
|
307
|
+
diagnostic: NormalizedDiagnostic | None = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@lru_cache(maxsize=1)
|
|
311
|
+
def _semantic_parser_target() -> PythonParserTarget:
|
|
312
|
+
return PythonParserTarget.from_generated()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class PrototypeRuntimeController:
|
|
316
|
+
"""Single-writer in-process facade for the PrototypeProfile."""
|
|
317
|
+
|
|
318
|
+
def __init__(
|
|
319
|
+
self,
|
|
320
|
+
session: RdbgSession,
|
|
321
|
+
service_location: ModuleLocation,
|
|
322
|
+
*,
|
|
323
|
+
kernel_location: ModuleLocation | None = None,
|
|
324
|
+
command_timeout_s: float = 60.0,
|
|
325
|
+
runtime_generation: int = 1,
|
|
326
|
+
journal: RecoveryJournal | None = None,
|
|
327
|
+
fault_hook: Callable[[FaultPoint], None] | None = None,
|
|
328
|
+
on_generation_lost: Callable[[RecoveryCheckpoint], None] | None = None,
|
|
329
|
+
) -> None:
|
|
330
|
+
self.session = session
|
|
331
|
+
self.service_location = service_location
|
|
332
|
+
self.kernel_location = kernel_location or service_location
|
|
333
|
+
self.command_timeout_s = command_timeout_s
|
|
334
|
+
self.runtime_generation = runtime_generation
|
|
335
|
+
self.journal = journal or RecoveryJournal()
|
|
336
|
+
self.fault_hook = fault_hook
|
|
337
|
+
self.on_generation_lost = on_generation_lost
|
|
338
|
+
self.lowerer = SemanticNotebookLowerer(_semantic_parser_target().new_instance())
|
|
339
|
+
self.state = OperationState.IDLE
|
|
340
|
+
self.operation_id = 0
|
|
341
|
+
self.active_operation: OperationHandle | None = None
|
|
342
|
+
self.capture_points: tuple[ModuleLocation, ...] = ()
|
|
343
|
+
self.registry = BreakpointRegistry(service_location)
|
|
344
|
+
self.stop_sequence = 0
|
|
345
|
+
self.last_capture_location: ModuleLocation | None = None
|
|
346
|
+
self.capture_kernel_stack_level: int | None = None
|
|
347
|
+
self.capture_frame_stack_level: int | None = None
|
|
348
|
+
self._capture_frame_variables: tuple[FrameVariable, ...] = ()
|
|
349
|
+
self._capture_stack_frames: tuple[StackFrame, ...] = ()
|
|
350
|
+
self._capture_target_id: TargetId | None = None
|
|
351
|
+
self._capture_manager_paths: dict[str, str] = {}
|
|
352
|
+
self._capture_value_paths: dict[str, str] = {}
|
|
353
|
+
self._capture_metadata_handles: set[str] = set()
|
|
354
|
+
self.checkpoint_sequence = 0
|
|
355
|
+
self.recovery_checkpoint: RecoveryCheckpoint | None = None
|
|
356
|
+
self.continue_sent = False
|
|
357
|
+
self._generation_lost_notified = False
|
|
358
|
+
self.cell_sequence = 0
|
|
359
|
+
self.side_effect_sequence = 0
|
|
360
|
+
self.write_journal: list[RootWriteRecord] = []
|
|
361
|
+
self.stop_history: list[ClassifiedStop] = []
|
|
362
|
+
self.last_debug_stop: DebugStop | None = None
|
|
363
|
+
self.pending_capture_evaluation: _PendingCaptureEvaluation | None = None
|
|
364
|
+
self.breakpoint_workspaces: list[BreakpointWorkspaceEvent] = []
|
|
365
|
+
self._breakpoint_workspace = self.registry.full_locations
|
|
366
|
+
self.breakpoint_workspace_owner = BreakpointWorkspaceController(
|
|
367
|
+
session,
|
|
368
|
+
WorkspaceSnapshot(0, service_location, (), (), (), False),
|
|
369
|
+
)
|
|
370
|
+
self._continuation_attempts: dict[str, _ContinuationAttemptState] = {}
|
|
371
|
+
self._active_continuation_attempt_id: str | None = None
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _capture_identifier(value: object, *, name: str) -> str:
|
|
375
|
+
if (
|
|
376
|
+
not isinstance(value, str)
|
|
377
|
+
or not value
|
|
378
|
+
or len(value) > 256
|
|
379
|
+
or re.fullmatch(r"[^\W\d]\w*", value, re.UNICODE) is None
|
|
380
|
+
):
|
|
381
|
+
raise ProtocolError(f"capture {name} is invalid")
|
|
382
|
+
return value
|
|
383
|
+
|
|
384
|
+
def _require_capture_frame(self) -> int:
|
|
385
|
+
self._require_state(OperationState.CAPTURED)
|
|
386
|
+
if self.capture_frame_stack_level is None:
|
|
387
|
+
raise ProtocolError("capture frame is not initialized")
|
|
388
|
+
return self.capture_frame_stack_level
|
|
389
|
+
|
|
390
|
+
def _clear_capture_inspection(self) -> None:
|
|
391
|
+
self.capture_frame_stack_level = None
|
|
392
|
+
self._capture_frame_variables = ()
|
|
393
|
+
self._capture_stack_frames = ()
|
|
394
|
+
self._capture_target_id = None
|
|
395
|
+
self._capture_manager_paths.clear()
|
|
396
|
+
self._capture_value_paths.clear()
|
|
397
|
+
self._capture_metadata_handles.clear()
|
|
398
|
+
|
|
399
|
+
def invalidate_capture_inspection(self) -> None:
|
|
400
|
+
"""Revoke captured-frame handles without resuming the suspended target."""
|
|
401
|
+
self._clear_capture_inspection()
|
|
402
|
+
|
|
403
|
+
def _capture_command_deadline(self, timeout_s: float | None) -> float:
|
|
404
|
+
selected = self.command_timeout_s if timeout_s is None else timeout_s
|
|
405
|
+
for value in (selected, self.command_timeout_s):
|
|
406
|
+
if (
|
|
407
|
+
isinstance(value, bool)
|
|
408
|
+
or not isinstance(value, (int, float))
|
|
409
|
+
or not isfinite(float(value))
|
|
410
|
+
or float(value) <= 0
|
|
411
|
+
):
|
|
412
|
+
raise ProtocolError(
|
|
413
|
+
"capture inspection timeout must be finite and positive"
|
|
414
|
+
)
|
|
415
|
+
return monotonic() + min(float(selected), float(self.command_timeout_s))
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _capture_remaining_timeout(deadline: float) -> float:
|
|
419
|
+
remaining = deadline - monotonic()
|
|
420
|
+
if remaining <= 0:
|
|
421
|
+
raise ProtocolError("capture inspection deadline exceeded")
|
|
422
|
+
return remaining
|
|
423
|
+
|
|
424
|
+
def _record(self, stream: str, event: str, **fields: object) -> None:
|
|
425
|
+
self.journal.record(stream, event, **fields)
|
|
426
|
+
|
|
427
|
+
def _flush_journal(self) -> None:
|
|
428
|
+
self.journal.flush()
|
|
429
|
+
|
|
430
|
+
def _checkpoint(self, phase: RecoveryPhase) -> RecoveryCheckpoint:
|
|
431
|
+
if self.active_operation is None:
|
|
432
|
+
raise ProtocolError("Recovery checkpoint has no active operation")
|
|
433
|
+
target = self.session.target
|
|
434
|
+
if target is None:
|
|
435
|
+
raise ProtocolError("Recovery checkpoint has no selected target")
|
|
436
|
+
self.checkpoint_sequence += 1
|
|
437
|
+
checkpoint = RecoveryCheckpoint(
|
|
438
|
+
sequence=self.checkpoint_sequence,
|
|
439
|
+
runtime_generation=self.runtime_generation,
|
|
440
|
+
operation_id=self.active_operation.operation_id,
|
|
441
|
+
phase=phase,
|
|
442
|
+
target=target,
|
|
443
|
+
frame_location=self.last_capture_location,
|
|
444
|
+
stop_sequence=self.stop_sequence,
|
|
445
|
+
breakpoint_workspace=(
|
|
446
|
+
self.breakpoint_workspace_owner.confirmed_snapshot.effective_locations
|
|
447
|
+
),
|
|
448
|
+
write_journal=tuple(self.write_journal),
|
|
449
|
+
continue_sent=self.continue_sent,
|
|
450
|
+
)
|
|
451
|
+
self.recovery_checkpoint = checkpoint
|
|
452
|
+
self._record(
|
|
453
|
+
"recovery-checkpoints.jsonl",
|
|
454
|
+
"checkpoint",
|
|
455
|
+
checkpoint=checkpoint,
|
|
456
|
+
)
|
|
457
|
+
self._flush_journal()
|
|
458
|
+
return checkpoint
|
|
459
|
+
|
|
460
|
+
def _inject(self, point: FaultPoint, phase: RecoveryPhase) -> None:
|
|
461
|
+
checkpoint = self._checkpoint(phase)
|
|
462
|
+
if self.fault_hook is None:
|
|
463
|
+
return
|
|
464
|
+
try:
|
|
465
|
+
self.fault_hook(point)
|
|
466
|
+
except RdbgTransportError as error:
|
|
467
|
+
self.state = OperationState.RECOVERING
|
|
468
|
+
self._record(
|
|
469
|
+
"recovery-transitions.jsonl",
|
|
470
|
+
"transport_lost",
|
|
471
|
+
checkpoint_sequence=checkpoint.sequence,
|
|
472
|
+
phase=phase.value,
|
|
473
|
+
fault_point=point.value,
|
|
474
|
+
error=str(error),
|
|
475
|
+
)
|
|
476
|
+
self._flush_journal()
|
|
477
|
+
raise
|
|
478
|
+
|
|
479
|
+
def _set_root_status(
|
|
480
|
+
self,
|
|
481
|
+
root: str,
|
|
482
|
+
expression_sha256: str,
|
|
483
|
+
status: SideEffectStatus,
|
|
484
|
+
*,
|
|
485
|
+
result_id: UUID | None = None,
|
|
486
|
+
error: str = "",
|
|
487
|
+
) -> RootWriteRecord:
|
|
488
|
+
existing_index = next(
|
|
489
|
+
(
|
|
490
|
+
index
|
|
491
|
+
for index, entry in enumerate(self.write_journal)
|
|
492
|
+
if entry.root == root
|
|
493
|
+
),
|
|
494
|
+
None,
|
|
495
|
+
)
|
|
496
|
+
if existing_index is None:
|
|
497
|
+
self.side_effect_sequence += 1
|
|
498
|
+
sequence = self.side_effect_sequence
|
|
499
|
+
else:
|
|
500
|
+
sequence = self.write_journal[existing_index].sequence
|
|
501
|
+
record = RootWriteRecord(
|
|
502
|
+
sequence,
|
|
503
|
+
root,
|
|
504
|
+
expression_sha256,
|
|
505
|
+
status,
|
|
506
|
+
result_id,
|
|
507
|
+
error,
|
|
508
|
+
)
|
|
509
|
+
if existing_index is None:
|
|
510
|
+
self.write_journal.append(record)
|
|
511
|
+
else:
|
|
512
|
+
self.write_journal[existing_index] = record
|
|
513
|
+
return record
|
|
514
|
+
|
|
515
|
+
def _require_state(self, *states: OperationState) -> None:
|
|
516
|
+
if self.state not in states:
|
|
517
|
+
expected = ", ".join(state.value for state in states)
|
|
518
|
+
raise ProtocolError(
|
|
519
|
+
f"Runtime operation requires state {expected}; current state is "
|
|
520
|
+
f"{self.state.value}"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def _allowed_locations(self) -> tuple[ModuleLocation, ...]:
|
|
525
|
+
return self.registry.full_locations
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _checked_modify(result: object, variable: str) -> None:
|
|
529
|
+
if getattr(result, "error_occurred", False):
|
|
530
|
+
raise ProtocolError(
|
|
531
|
+
f"Failed to modify {variable}: {getattr(result, 'error_text', '')}"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
def _set_workspace(
|
|
535
|
+
self,
|
|
536
|
+
phase: str,
|
|
537
|
+
locations: tuple[ModuleLocation, ...],
|
|
538
|
+
) -> None:
|
|
539
|
+
if not locations or locations[0] != self.service_location:
|
|
540
|
+
raise ProtocolError("Breakpoint workspace must retain the service point")
|
|
541
|
+
if phase == "capture-evaluation":
|
|
542
|
+
captures = self.registry.captures
|
|
543
|
+
ordinary_users = self.registry.users
|
|
544
|
+
shielded = True
|
|
545
|
+
else:
|
|
546
|
+
ordinary_users = tuple(
|
|
547
|
+
location
|
|
548
|
+
for location in locations[1:]
|
|
549
|
+
if location in self.registry.users
|
|
550
|
+
)
|
|
551
|
+
captures = tuple(
|
|
552
|
+
location
|
|
553
|
+
for location in locations[1:]
|
|
554
|
+
if location not in ordinary_users
|
|
555
|
+
)
|
|
556
|
+
shielded = False
|
|
557
|
+
try:
|
|
558
|
+
confirmed = self.breakpoint_workspace_owner.confirmed_snapshot
|
|
559
|
+
desired = self.breakpoint_workspace_owner.prepare(
|
|
560
|
+
captures=captures,
|
|
561
|
+
ordinary_users=ordinary_users,
|
|
562
|
+
worker_slots=confirmed.worker_slots,
|
|
563
|
+
shielded=shielded,
|
|
564
|
+
)
|
|
565
|
+
self.breakpoint_workspace_owner.install(desired)
|
|
566
|
+
except BreakpointWorkspaceOutcomeUnknown:
|
|
567
|
+
self.state = OperationState.RECOVERING
|
|
568
|
+
raise
|
|
569
|
+
self._breakpoint_workspace = desired.effective_locations
|
|
570
|
+
self.breakpoint_workspaces.append(
|
|
571
|
+
BreakpointWorkspaceEvent(phase, desired.effective_locations)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def install_worker_workspace(
|
|
575
|
+
self,
|
|
576
|
+
snapshot: WorkspaceSnapshot,
|
|
577
|
+
) -> WorkspaceInstallReceipt:
|
|
578
|
+
try:
|
|
579
|
+
receipt = self.breakpoint_workspace_owner.install(snapshot)
|
|
580
|
+
except BreakpointWorkspaceOutcomeUnknown:
|
|
581
|
+
self.state = OperationState.RECOVERING
|
|
582
|
+
raise
|
|
583
|
+
self._breakpoint_workspace = snapshot.effective_locations
|
|
584
|
+
self.breakpoint_workspaces.append(
|
|
585
|
+
BreakpointWorkspaceEvent("worker", snapshot.effective_locations)
|
|
586
|
+
)
|
|
587
|
+
return receipt
|
|
588
|
+
|
|
589
|
+
def require_debug_workspace_ready(self) -> None:
|
|
590
|
+
try:
|
|
591
|
+
self.breakpoint_workspace_owner.require_confirmed()
|
|
592
|
+
except BreakpointWorkspaceOutcomeUnknown:
|
|
593
|
+
self.state = OperationState.RECOVERING
|
|
594
|
+
raise
|
|
595
|
+
|
|
596
|
+
def rearm_capture_successor(
|
|
597
|
+
self, capture_points: tuple[ModuleLocation, ...]
|
|
598
|
+
) -> None:
|
|
599
|
+
"""Replace only capture breakpoints while the business frame is paused.
|
|
600
|
+
|
|
601
|
+
The debugger registry and physical workspace are one transaction: a
|
|
602
|
+
failed install restores the exact prior registry/workspace before the
|
|
603
|
+
caller may decide that no Continue has been sent.
|
|
604
|
+
"""
|
|
605
|
+
self._require_state(OperationState.CAPTURED)
|
|
606
|
+
points = tuple(capture_points)
|
|
607
|
+
previous_registry = self.registry
|
|
608
|
+
previous_points = self.capture_points
|
|
609
|
+
candidate = BreakpointRegistry(
|
|
610
|
+
self.service_location, points, previous_registry.users
|
|
611
|
+
)
|
|
612
|
+
try:
|
|
613
|
+
self._set_workspace("capture_successor", candidate.full_locations)
|
|
614
|
+
except BaseException:
|
|
615
|
+
try:
|
|
616
|
+
self._set_workspace("capture_successor_restore", previous_registry.full_locations)
|
|
617
|
+
except BaseException as rollback_error:
|
|
618
|
+
self.state = OperationState.RECOVERING
|
|
619
|
+
raise ProtocolError("capture successor breakpoint rollback is uncertain") from rollback_error
|
|
620
|
+
raise
|
|
621
|
+
self.registry = candidate
|
|
622
|
+
self.capture_points = points
|
|
623
|
+
|
|
624
|
+
def begin_continuation_admission(
|
|
625
|
+
self,
|
|
626
|
+
attempt: ContinuationAttemptSpec,
|
|
627
|
+
capture_points: tuple[ModuleLocation, ...],
|
|
628
|
+
) -> _ControllerContinuationAdmission:
|
|
629
|
+
"""Begin one durable successor attempt before any root resolver runs."""
|
|
630
|
+
self._require_state(OperationState.CAPTURED)
|
|
631
|
+
if not isinstance(attempt, ContinuationAttemptSpec):
|
|
632
|
+
raise TypeError("attempt must be a ContinuationAttemptSpec")
|
|
633
|
+
if attempt.attempt_id in self._continuation_attempts:
|
|
634
|
+
raise ProtocolError("continuation attempt_id was already used")
|
|
635
|
+
snapshot = _ControllerContinuationAdmission(
|
|
636
|
+
self,
|
|
637
|
+
registry=self.registry,
|
|
638
|
+
capture_points=self.capture_points,
|
|
639
|
+
workspace=self._breakpoint_workspace,
|
|
640
|
+
state=self.state,
|
|
641
|
+
attempt_id=attempt.attempt_id,
|
|
642
|
+
)
|
|
643
|
+
state = _ContinuationAttemptState(
|
|
644
|
+
attempt,
|
|
645
|
+
{root: "unattempted" for root in attempt.dirty_roots},
|
|
646
|
+
)
|
|
647
|
+
self._continuation_attempts[attempt.attempt_id] = state
|
|
648
|
+
self._active_continuation_attempt_id = attempt.attempt_id
|
|
649
|
+
self._record(
|
|
650
|
+
"write-journal.jsonl",
|
|
651
|
+
"continuation_attempt_started",
|
|
652
|
+
attempt_id=attempt.attempt_id,
|
|
653
|
+
runtime_generation=self.runtime_generation,
|
|
654
|
+
operation_id=self.active_operation.operation_id if self.active_operation else 0,
|
|
655
|
+
capture_generation=attempt.capture_generation,
|
|
656
|
+
request_operation_id=attempt.request_operation_id,
|
|
657
|
+
dirty_roots=list(attempt.dirty_roots),
|
|
658
|
+
)
|
|
659
|
+
for root in attempt.dirty_roots:
|
|
660
|
+
self._record(
|
|
661
|
+
"write-journal.jsonl",
|
|
662
|
+
"root_write_planned",
|
|
663
|
+
attempt_id=attempt.attempt_id,
|
|
664
|
+
runtime_generation=self.runtime_generation,
|
|
665
|
+
operation_id=self.active_operation.operation_id if self.active_operation else 0,
|
|
666
|
+
root=root,
|
|
667
|
+
)
|
|
668
|
+
try:
|
|
669
|
+
self._flush_journal()
|
|
670
|
+
except BaseException:
|
|
671
|
+
snapshot.quarantine()
|
|
672
|
+
raise
|
|
673
|
+
points = tuple(capture_points)
|
|
674
|
+
candidate = BreakpointRegistry(
|
|
675
|
+
self.service_location, points, self.registry.users
|
|
676
|
+
)
|
|
677
|
+
try:
|
|
678
|
+
self._set_workspace(
|
|
679
|
+
"capture_successor", candidate.full_locations
|
|
680
|
+
)
|
|
681
|
+
self.registry = candidate
|
|
682
|
+
self.capture_points = points
|
|
683
|
+
except BaseException as error:
|
|
684
|
+
if isinstance(error, RdbgTransportError):
|
|
685
|
+
snapshot.quarantine()
|
|
686
|
+
raise
|
|
687
|
+
try:
|
|
688
|
+
snapshot.rollback()
|
|
689
|
+
except BaseException:
|
|
690
|
+
pass
|
|
691
|
+
raise
|
|
692
|
+
return snapshot
|
|
693
|
+
|
|
694
|
+
def continuation_attempt_evidence(
|
|
695
|
+
self, attempt_id: str
|
|
696
|
+
) -> ContinuationAttemptEvidence:
|
|
697
|
+
try:
|
|
698
|
+
attempt = self._continuation_attempts[attempt_id]
|
|
699
|
+
except KeyError as error:
|
|
700
|
+
raise ProtocolError("continuation attempt is unknown") from error
|
|
701
|
+
return ContinuationAttemptEvidence(
|
|
702
|
+
tuple(
|
|
703
|
+
(root, attempt.root_statuses[root])
|
|
704
|
+
for root in attempt.spec.dirty_roots
|
|
705
|
+
),
|
|
706
|
+
attempt.continue_state,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
def _continuation_attempt(
|
|
710
|
+
self,
|
|
711
|
+
dirty_roots: tuple[str, ...],
|
|
712
|
+
attempt_id: str | None,
|
|
713
|
+
) -> _ContinuationAttemptState:
|
|
714
|
+
if attempt_id is None:
|
|
715
|
+
spec = ContinuationAttemptSpec(
|
|
716
|
+
f"internal-{uuid4().hex}",
|
|
717
|
+
max(1, self.stop_sequence),
|
|
718
|
+
f"controller-{self.operation_id}",
|
|
719
|
+
dirty_roots,
|
|
720
|
+
)
|
|
721
|
+
state = _ContinuationAttemptState(
|
|
722
|
+
spec, {root: "unattempted" for root in dirty_roots}
|
|
723
|
+
)
|
|
724
|
+
self._continuation_attempts[spec.attempt_id] = state
|
|
725
|
+
self._active_continuation_attempt_id = spec.attempt_id
|
|
726
|
+
self._record(
|
|
727
|
+
"write-journal.jsonl",
|
|
728
|
+
"continuation_attempt_started",
|
|
729
|
+
attempt_id=spec.attempt_id,
|
|
730
|
+
runtime_generation=self.runtime_generation,
|
|
731
|
+
operation_id=self.operation_id,
|
|
732
|
+
capture_generation=spec.capture_generation,
|
|
733
|
+
request_operation_id=spec.request_operation_id,
|
|
734
|
+
dirty_roots=list(dirty_roots),
|
|
735
|
+
)
|
|
736
|
+
for root in dirty_roots:
|
|
737
|
+
self._record(
|
|
738
|
+
"write-journal.jsonl",
|
|
739
|
+
"root_write_planned",
|
|
740
|
+
attempt_id=spec.attempt_id,
|
|
741
|
+
runtime_generation=self.runtime_generation,
|
|
742
|
+
operation_id=self.operation_id,
|
|
743
|
+
root=root,
|
|
744
|
+
)
|
|
745
|
+
self._flush_journal()
|
|
746
|
+
return state
|
|
747
|
+
try:
|
|
748
|
+
state = self._continuation_attempts[attempt_id]
|
|
749
|
+
except KeyError as error:
|
|
750
|
+
raise ProtocolError("continuation attempt is unknown") from error
|
|
751
|
+
if state.spec.dirty_roots != dirty_roots:
|
|
752
|
+
raise ProtocolError("continuation attempt dirty_roots changed")
|
|
753
|
+
return state
|
|
754
|
+
|
|
755
|
+
def _mark_continuation_root(
|
|
756
|
+
self,
|
|
757
|
+
attempt: _ContinuationAttemptState,
|
|
758
|
+
root: str,
|
|
759
|
+
status: str,
|
|
760
|
+
*,
|
|
761
|
+
error: str = "",
|
|
762
|
+
) -> None:
|
|
763
|
+
attempt.root_statuses[root] = status
|
|
764
|
+
if status in {"sent", "succeeded", "outcome_unknown"}:
|
|
765
|
+
attempt.irreversible_roots.add(root)
|
|
766
|
+
self._record(
|
|
767
|
+
"write-journal.jsonl",
|
|
768
|
+
f"root_write_{status}",
|
|
769
|
+
attempt_id=attempt.spec.attempt_id,
|
|
770
|
+
runtime_generation=self.runtime_generation,
|
|
771
|
+
operation_id=self.operation_id,
|
|
772
|
+
root=root,
|
|
773
|
+
error=error,
|
|
774
|
+
)
|
|
775
|
+
self._flush_journal()
|
|
776
|
+
|
|
777
|
+
def _mark_later_roots_unattempted(
|
|
778
|
+
self, attempt: _ContinuationAttemptState, failed_root: str
|
|
779
|
+
) -> None:
|
|
780
|
+
start = attempt.spec.dirty_roots.index(failed_root) + 1
|
|
781
|
+
for root in attempt.spec.dirty_roots[start:]:
|
|
782
|
+
if attempt.root_statuses[root] == "unattempted":
|
|
783
|
+
self._record(
|
|
784
|
+
"write-journal.jsonl",
|
|
785
|
+
"root_write_unattempted",
|
|
786
|
+
attempt_id=attempt.spec.attempt_id,
|
|
787
|
+
runtime_generation=self.runtime_generation,
|
|
788
|
+
operation_id=self.operation_id,
|
|
789
|
+
root=root,
|
|
790
|
+
)
|
|
791
|
+
self._flush_journal()
|
|
792
|
+
|
|
793
|
+
def execute_main(
|
|
794
|
+
self,
|
|
795
|
+
source: str,
|
|
796
|
+
*,
|
|
797
|
+
capture_points: tuple[ModuleLocation, ...] = (),
|
|
798
|
+
user_breakpoints: tuple[ModuleLocation, ...] = (),
|
|
799
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
800
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
801
|
+
return self._execute_main(
|
|
802
|
+
source,
|
|
803
|
+
None,
|
|
804
|
+
capture_points=capture_points,
|
|
805
|
+
user_breakpoints=user_breakpoints,
|
|
806
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
def execute_lowered_main(
|
|
810
|
+
self,
|
|
811
|
+
visible_source: str,
|
|
812
|
+
lowered_source: str,
|
|
813
|
+
*,
|
|
814
|
+
messages_intercepted: int = 0,
|
|
815
|
+
message_collector_key: str = "",
|
|
816
|
+
capture_points: tuple[ModuleLocation, ...] = (),
|
|
817
|
+
user_breakpoints: tuple[ModuleLocation, ...] = (),
|
|
818
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
819
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
820
|
+
return self._execute_main(
|
|
821
|
+
visible_source,
|
|
822
|
+
lowered_source,
|
|
823
|
+
messages_intercepted=messages_intercepted,
|
|
824
|
+
message_collector_key=message_collector_key,
|
|
825
|
+
capture_points=capture_points,
|
|
826
|
+
user_breakpoints=user_breakpoints,
|
|
827
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
def execute_mapped_main(
|
|
831
|
+
self,
|
|
832
|
+
visible_source: str,
|
|
833
|
+
lowered_source: MappedSource,
|
|
834
|
+
*,
|
|
835
|
+
visible_source_context: VisibleSourceContext,
|
|
836
|
+
messages_intercepted: int = 0,
|
|
837
|
+
message_collector_key: str = "",
|
|
838
|
+
worker_messages: bool = False,
|
|
839
|
+
worker_globals: tuple[str, ...] = (),
|
|
840
|
+
capture_points: tuple[ModuleLocation, ...] = (),
|
|
841
|
+
user_breakpoints: tuple[ModuleLocation, ...] = (),
|
|
842
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
843
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
844
|
+
return self._execute_main(
|
|
845
|
+
visible_source,
|
|
846
|
+
lowered_source,
|
|
847
|
+
visible_source_context=visible_source_context,
|
|
848
|
+
messages_intercepted=messages_intercepted,
|
|
849
|
+
message_collector_key=message_collector_key,
|
|
850
|
+
worker_messages=worker_messages,
|
|
851
|
+
worker_globals=worker_globals,
|
|
852
|
+
capture_points=capture_points,
|
|
853
|
+
user_breakpoints=user_breakpoints,
|
|
854
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def execute_system_main(self, source: str) -> MainCompletion:
|
|
858
|
+
mapped = self._compatibility_mapped_source(source, "system-main")
|
|
859
|
+
result = self._execute_main(
|
|
860
|
+
source,
|
|
861
|
+
mapped,
|
|
862
|
+
visible_source_context=self._visible_context(mapped, source),
|
|
863
|
+
)
|
|
864
|
+
if not isinstance(result, MainCompletion):
|
|
865
|
+
raise ProtocolError("System MAIN stopped outside the service boundary")
|
|
866
|
+
return result
|
|
867
|
+
|
|
868
|
+
def _execute_main(
|
|
869
|
+
self,
|
|
870
|
+
visible_source: str,
|
|
871
|
+
lowered_source: str | MappedSource | None,
|
|
872
|
+
*,
|
|
873
|
+
visible_source_context: VisibleSourceContext | None = None,
|
|
874
|
+
messages_intercepted: int | None = None,
|
|
875
|
+
message_collector_key: str = "",
|
|
876
|
+
worker_messages: bool = False,
|
|
877
|
+
worker_globals: tuple[str, ...] = (),
|
|
878
|
+
capture_points: tuple[ModuleLocation, ...] = (),
|
|
879
|
+
user_breakpoints: tuple[ModuleLocation, ...] = (),
|
|
880
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
881
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
882
|
+
self._require_state(
|
|
883
|
+
OperationState.IDLE,
|
|
884
|
+
OperationState.COMPLETED,
|
|
885
|
+
OperationState.FAILED,
|
|
886
|
+
)
|
|
887
|
+
if lowered_source is None:
|
|
888
|
+
message_collector_key = self.message_collector_key(LoweringMode.MAIN)
|
|
889
|
+
lowering = self.lowerer.lower(
|
|
890
|
+
visible_source,
|
|
891
|
+
mode=LoweringMode.MAIN,
|
|
892
|
+
message_collector_key=message_collector_key,
|
|
893
|
+
)
|
|
894
|
+
mapped_lowered = lowering.mapped_source
|
|
895
|
+
messages_intercepted = lowering.messages_intercepted
|
|
896
|
+
elif isinstance(lowered_source, MappedSource):
|
|
897
|
+
mapped_lowered = lowered_source
|
|
898
|
+
if messages_intercepted is None:
|
|
899
|
+
messages_intercepted = 0
|
|
900
|
+
else:
|
|
901
|
+
mapped_lowered = self._compatibility_mapped_source(
|
|
902
|
+
lowered_source,
|
|
903
|
+
"lowered-main",
|
|
904
|
+
)
|
|
905
|
+
if messages_intercepted is None:
|
|
906
|
+
messages_intercepted = 0
|
|
907
|
+
messages_intercepted = max(messages_intercepted, int(worker_messages))
|
|
908
|
+
bound_source = self._with_worker_globals_mapped(
|
|
909
|
+
mapped_lowered, worker_globals
|
|
910
|
+
)
|
|
911
|
+
wrapped_source = self._with_message_collector_mapped(
|
|
912
|
+
bound_source,
|
|
913
|
+
messages_intercepted,
|
|
914
|
+
message_collector_key,
|
|
915
|
+
worker_messages=worker_messages,
|
|
916
|
+
)
|
|
917
|
+
executed_source = self._as_executed_source(
|
|
918
|
+
wrapped_source,
|
|
919
|
+
mode=LoweringMode.MAIN,
|
|
920
|
+
)
|
|
921
|
+
lowered_text = executed_source.text
|
|
922
|
+
if visible_source_context is None:
|
|
923
|
+
visible_source_context = self._visible_context(
|
|
924
|
+
executed_source,
|
|
925
|
+
visible_source,
|
|
926
|
+
)
|
|
927
|
+
self.operation_id += 1
|
|
928
|
+
operation = OperationHandle(
|
|
929
|
+
self.operation_id,
|
|
930
|
+
visible_source,
|
|
931
|
+
lowered_text,
|
|
932
|
+
messages_intercepted,
|
|
933
|
+
message_collector_key,
|
|
934
|
+
executed_source,
|
|
935
|
+
visible_source_context,
|
|
936
|
+
)
|
|
937
|
+
self.active_operation = operation
|
|
938
|
+
self._record(
|
|
939
|
+
"write-journal.jsonl",
|
|
940
|
+
"main_started",
|
|
941
|
+
runtime_generation=self.runtime_generation,
|
|
942
|
+
operation_id=operation.operation_id,
|
|
943
|
+
state_before=self.state.value,
|
|
944
|
+
visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
|
|
945
|
+
lowered_sha256=executed_source.artifact.source_sha256,
|
|
946
|
+
)
|
|
947
|
+
self._flush_journal()
|
|
948
|
+
try:
|
|
949
|
+
self.capture_points = tuple(capture_points)
|
|
950
|
+
self.registry = BreakpointRegistry(
|
|
951
|
+
self.service_location,
|
|
952
|
+
self.capture_points,
|
|
953
|
+
tuple(user_breakpoints),
|
|
954
|
+
)
|
|
955
|
+
self.stop_sequence = 0
|
|
956
|
+
self.last_capture_location = None
|
|
957
|
+
self.recovery_checkpoint = None
|
|
958
|
+
self.continue_sent = False
|
|
959
|
+
self._generation_lost_notified = False
|
|
960
|
+
self.write_journal.clear()
|
|
961
|
+
self.stop_history.clear()
|
|
962
|
+
self.last_debug_stop = None
|
|
963
|
+
self.breakpoint_workspaces.clear()
|
|
964
|
+
self._set_workspace("full", self._allowed_locations)
|
|
965
|
+
instruction = self.session.modify(
|
|
966
|
+
"ТекущаяИнструкция", bsl_string_literal(lowered_text)
|
|
967
|
+
)
|
|
968
|
+
self._checked_modify(instruction, "ТекущаяИнструкция")
|
|
969
|
+
command = self.session.modify(
|
|
970
|
+
"ИдентификаторКоманды", str(operation.operation_id)
|
|
971
|
+
)
|
|
972
|
+
self._checked_modify(command, "ИдентификаторКоманды")
|
|
973
|
+
self.state = OperationState.MAIN_PENDING
|
|
974
|
+
if on_transport_dispatch is not None:
|
|
975
|
+
on_transport_dispatch()
|
|
976
|
+
self.require_debug_workspace_ready()
|
|
977
|
+
self.session.continue_()
|
|
978
|
+
stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
|
|
979
|
+
return self._route_stop(stop)
|
|
980
|
+
except BaseException as error:
|
|
981
|
+
if self.state not in {
|
|
982
|
+
OperationState.CAPTURED,
|
|
983
|
+
OperationState.DEBUG_STOPPED,
|
|
984
|
+
OperationState.PARTIAL_WRITEBACK_FAILURE,
|
|
985
|
+
OperationState.BREAKPOINT_RESTORE_FAILURE,
|
|
986
|
+
OperationState.RECOVERING,
|
|
987
|
+
}:
|
|
988
|
+
self.state = OperationState.FAILED
|
|
989
|
+
failure_fields: dict[str, object] = {}
|
|
990
|
+
if isinstance(error, CompletionDecodeError):
|
|
991
|
+
failure_fields = {
|
|
992
|
+
"failure_phase": error.phase,
|
|
993
|
+
"evaluation_type": error.evaluation_type,
|
|
994
|
+
"exact_decimal_present": error.exact_decimal_present,
|
|
995
|
+
}
|
|
996
|
+
self._record(
|
|
997
|
+
"write-journal.jsonl",
|
|
998
|
+
"main_failed",
|
|
999
|
+
runtime_generation=self.runtime_generation,
|
|
1000
|
+
operation_id=operation.operation_id,
|
|
1001
|
+
state_after=self.state.value,
|
|
1002
|
+
visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
|
|
1003
|
+
lowered_sha256=executed_source.artifact.source_sha256,
|
|
1004
|
+
error_type=type(error).__name__,
|
|
1005
|
+
**failure_fields,
|
|
1006
|
+
)
|
|
1007
|
+
self._flush_journal()
|
|
1008
|
+
raise
|
|
1009
|
+
|
|
1010
|
+
def _route_stop(
|
|
1011
|
+
self,
|
|
1012
|
+
stop: StopEvent,
|
|
1013
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
1014
|
+
classified = classify_stop(
|
|
1015
|
+
stop,
|
|
1016
|
+
self.registry,
|
|
1017
|
+
worker_locations=(
|
|
1018
|
+
self.breakpoint_workspace_owner.confirmed_snapshot.worker_slots
|
|
1019
|
+
),
|
|
1020
|
+
)
|
|
1021
|
+
self.stop_history.append(classified)
|
|
1022
|
+
if classified.reason is StopReason.MAIN_SERVICE:
|
|
1023
|
+
return self._complete_main()
|
|
1024
|
+
if classified.reason is StopReason.CAPTURE:
|
|
1025
|
+
return self._begin_capture(stop)
|
|
1026
|
+
if self.active_operation is None:
|
|
1027
|
+
self.state = OperationState.FAILED
|
|
1028
|
+
raise ProtocolError("Debug stop has no active MAIN operation")
|
|
1029
|
+
debug_stop = DebugStop(self.active_operation, stop, classified.reason)
|
|
1030
|
+
self.last_debug_stop = debug_stop
|
|
1031
|
+
self.state = OperationState.DEBUG_STOPPED
|
|
1032
|
+
return debug_stop
|
|
1033
|
+
|
|
1034
|
+
def resume_debug_stop(
|
|
1035
|
+
self,
|
|
1036
|
+
*,
|
|
1037
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
1038
|
+
) -> MainCompletion | CapturedStop | CaptureCellResult | DebugStop:
|
|
1039
|
+
self._require_state(
|
|
1040
|
+
OperationState.DEBUG_STOPPED,
|
|
1041
|
+
OperationState.CAPTURE_DEBUG_STOPPED,
|
|
1042
|
+
)
|
|
1043
|
+
if (
|
|
1044
|
+
self.last_debug_stop is None
|
|
1045
|
+
or self.last_debug_stop.reason is not StopReason.USER_BREAKPOINT
|
|
1046
|
+
):
|
|
1047
|
+
raise ProtocolError("Only an explicit user breakpoint can be resumed")
|
|
1048
|
+
if self.state is OperationState.CAPTURE_DEBUG_STOPPED:
|
|
1049
|
+
capture = self.pending_capture_evaluation
|
|
1050
|
+
if capture is None:
|
|
1051
|
+
raise ProtocolError("CAPTURE debug stop has no pending evaluation")
|
|
1052
|
+
if on_transport_dispatch is not None:
|
|
1053
|
+
on_transport_dispatch()
|
|
1054
|
+
self.session.continue_evaluation(
|
|
1055
|
+
capture.pending,
|
|
1056
|
+
self.last_debug_stop.stop,
|
|
1057
|
+
)
|
|
1058
|
+
self.state = OperationState.EVALUATING_CAPTURE
|
|
1059
|
+
event = self.session.wait_evaluation_event(
|
|
1060
|
+
capture.pending,
|
|
1061
|
+
timeout_s=self.command_timeout_s,
|
|
1062
|
+
)
|
|
1063
|
+
return self._handle_capture_evaluation_event(capture, event)
|
|
1064
|
+
self.state = OperationState.MAIN_PENDING
|
|
1065
|
+
if on_transport_dispatch is not None:
|
|
1066
|
+
on_transport_dispatch()
|
|
1067
|
+
self.require_debug_workspace_ready()
|
|
1068
|
+
self.session.continue_()
|
|
1069
|
+
stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
|
|
1070
|
+
return self._route_stop(stop)
|
|
1071
|
+
|
|
1072
|
+
def _begin_capture(self, stop: StopEvent) -> CapturedStop:
|
|
1073
|
+
if self.active_operation is None:
|
|
1074
|
+
self.state = OperationState.FAILED
|
|
1075
|
+
raise ProtocolError("Capture stop has no active MAIN operation")
|
|
1076
|
+
local_result = self.session.local_variables(stack_level=0)
|
|
1077
|
+
if local_result.error_occurred:
|
|
1078
|
+
self.state = OperationState.FAILED
|
|
1079
|
+
raise ProtocolError(local_result.error_text)
|
|
1080
|
+
transfer = self.session.evaluate(
|
|
1081
|
+
build_capture_transfer_call(
|
|
1082
|
+
variable.name for variable in local_result.variables
|
|
1083
|
+
)
|
|
1084
|
+
)
|
|
1085
|
+
if transfer.error_occurred:
|
|
1086
|
+
self.state = OperationState.FAILED
|
|
1087
|
+
raise BslExecutionError(transfer.error_text)
|
|
1088
|
+
address = evaluation_to_python(transfer)
|
|
1089
|
+
if not isinstance(address, str) or not address:
|
|
1090
|
+
self.state = OperationState.FAILED
|
|
1091
|
+
raise ProtocolError("Capture temporary-storage address is invalid")
|
|
1092
|
+
stack_level = self._locate_kernel_context_frame(stop)
|
|
1093
|
+
command_evidence = self.session.evaluate(
|
|
1094
|
+
"ИдентификаторКоманды", stack_level=stack_level
|
|
1095
|
+
)
|
|
1096
|
+
if command_evidence.error_occurred:
|
|
1097
|
+
self.state = OperationState.FAILED
|
|
1098
|
+
raise BslExecutionError(command_evidence.error_text)
|
|
1099
|
+
observed_command_id = self._decode_main_completion_value(
|
|
1100
|
+
command_evidence,
|
|
1101
|
+
phase="capture_command",
|
|
1102
|
+
)
|
|
1103
|
+
if observed_command_id != self.active_operation.operation_id:
|
|
1104
|
+
self.state = OperationState.FAILED
|
|
1105
|
+
raise ProtocolError(
|
|
1106
|
+
f"Captured command {observed_command_id!r} does not match active "
|
|
1107
|
+
f"operation {self.active_operation.operation_id}"
|
|
1108
|
+
)
|
|
1109
|
+
begin = self.session.evaluate(
|
|
1110
|
+
build_live_capture_begin_call(address),
|
|
1111
|
+
stack_level=stack_level,
|
|
1112
|
+
)
|
|
1113
|
+
if begin.error_occurred:
|
|
1114
|
+
self.state = OperationState.FAILED
|
|
1115
|
+
raise BslExecutionError(begin.error_text)
|
|
1116
|
+
self.capture_kernel_stack_level = stack_level
|
|
1117
|
+
# RDBG level zero is the exact suspended business frame used to build
|
|
1118
|
+
# the capture context. The default metadata index reuses these locals;
|
|
1119
|
+
# other frames and named value details are read only on explicit request.
|
|
1120
|
+
self.capture_frame_stack_level = 0
|
|
1121
|
+
self._capture_frame_variables = tuple(local_result.variables)
|
|
1122
|
+
self._capture_stack_frames = tuple(stop.stack_frames)
|
|
1123
|
+
self._capture_target_id = stop.target_id
|
|
1124
|
+
self._capture_manager_paths.clear()
|
|
1125
|
+
self._capture_value_paths.clear()
|
|
1126
|
+
self._capture_metadata_handles.clear()
|
|
1127
|
+
self.stop_sequence += 1
|
|
1128
|
+
self.last_capture_location = stop.location
|
|
1129
|
+
self.state = OperationState.CAPTURED
|
|
1130
|
+
captured = CapturedStop(
|
|
1131
|
+
self.active_operation,
|
|
1132
|
+
stop.location,
|
|
1133
|
+
self.stop_sequence,
|
|
1134
|
+
local_result.variables,
|
|
1135
|
+
observed_command_id,
|
|
1136
|
+
)
|
|
1137
|
+
self._inject(
|
|
1138
|
+
FaultPoint.AFTER_CAPTURE_CHECKPOINT,
|
|
1139
|
+
RecoveryPhase.CAPTURED,
|
|
1140
|
+
)
|
|
1141
|
+
return captured
|
|
1142
|
+
|
|
1143
|
+
def _locate_kernel_context_frame(self, stop: StopEvent) -> int:
|
|
1144
|
+
required = {
|
|
1145
|
+
"контекст",
|
|
1146
|
+
"текущаяинструкция",
|
|
1147
|
+
"идентификаторкоманды",
|
|
1148
|
+
}
|
|
1149
|
+
stack = stop.stack
|
|
1150
|
+
frames = stop.stack_frames
|
|
1151
|
+
if not stack or not frames:
|
|
1152
|
+
self.state = OperationState.FAILED
|
|
1153
|
+
raise ProtocolError("Capture stop has no exact stack mapping for runtime kernel")
|
|
1154
|
+
if (
|
|
1155
|
+
len(frames) != len(stack)
|
|
1156
|
+
or tuple(frame.location for frame in frames) != stack
|
|
1157
|
+
or any(frame.level < 0 for frame in frames)
|
|
1158
|
+
or len({frame.level for frame in frames}) != len(frames)
|
|
1159
|
+
):
|
|
1160
|
+
self.state = OperationState.FAILED
|
|
1161
|
+
raise ProtocolError("Capture stop stack mapping is incoherent")
|
|
1162
|
+
for frame in frames:
|
|
1163
|
+
stack_level = frame.level
|
|
1164
|
+
if stack_level <= 0 or stack_level >= 8:
|
|
1165
|
+
continue
|
|
1166
|
+
frame_location = frame.location
|
|
1167
|
+
if not self._same_kernel_module(frame_location):
|
|
1168
|
+
continue
|
|
1169
|
+
local_result = self.session.local_variables(stack_level=stack_level)
|
|
1170
|
+
if local_result.error_occurred:
|
|
1171
|
+
continue
|
|
1172
|
+
names = {variable.name.casefold() for variable in local_result.variables}
|
|
1173
|
+
if not required.issubset(names):
|
|
1174
|
+
continue
|
|
1175
|
+
return stack_level
|
|
1176
|
+
self.state = OperationState.FAILED
|
|
1177
|
+
raise ProtocolError("Runtime kernel context frame was not found")
|
|
1178
|
+
|
|
1179
|
+
def _same_kernel_module(self, location: ModuleLocation) -> bool:
|
|
1180
|
+
kernel = self.kernel_location
|
|
1181
|
+
return (
|
|
1182
|
+
location.module_type == kernel.module_type
|
|
1183
|
+
and location.url == kernel.url
|
|
1184
|
+
and location.object_id == kernel.object_id
|
|
1185
|
+
and location.property_id == kernel.property_id
|
|
1186
|
+
and location.extension_name == kernel.extension_name
|
|
1187
|
+
and location.ext_id == kernel.ext_id
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
def _required_capture_kernel_stack_level(self) -> int:
|
|
1191
|
+
if self.capture_kernel_stack_level is None:
|
|
1192
|
+
raise ProtocolError("Capture kernel context frame is not initialized")
|
|
1193
|
+
return self.capture_kernel_stack_level
|
|
1194
|
+
|
|
1195
|
+
def execute_capture(
|
|
1196
|
+
self,
|
|
1197
|
+
source: str,
|
|
1198
|
+
*,
|
|
1199
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
1200
|
+
) -> CaptureCellResult | DebugStop:
|
|
1201
|
+
self._require_state(OperationState.CAPTURED)
|
|
1202
|
+
message_collector_key = self.message_collector_key(LoweringMode.CAPTURE)
|
|
1203
|
+
lowering = self.lowerer.lower(
|
|
1204
|
+
source,
|
|
1205
|
+
mode=LoweringMode.CAPTURE,
|
|
1206
|
+
message_collector_key=message_collector_key,
|
|
1207
|
+
)
|
|
1208
|
+
return self._execute_capture(
|
|
1209
|
+
source,
|
|
1210
|
+
lowering.mapped_source,
|
|
1211
|
+
visible_source_context=self._visible_context(
|
|
1212
|
+
lowering.mapped_source,
|
|
1213
|
+
source,
|
|
1214
|
+
),
|
|
1215
|
+
messages_intercepted=lowering.messages_intercepted,
|
|
1216
|
+
message_collector_key=message_collector_key,
|
|
1217
|
+
dirty_roots=lowering.dirty_roots,
|
|
1218
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
def execute_lowered_capture(
|
|
1222
|
+
self,
|
|
1223
|
+
visible_source: str,
|
|
1224
|
+
lowered_source: str,
|
|
1225
|
+
*,
|
|
1226
|
+
messages_intercepted: int = 0,
|
|
1227
|
+
message_collector_key: str = "",
|
|
1228
|
+
dirty_roots: tuple[str, ...] = (),
|
|
1229
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
1230
|
+
) -> CaptureCellResult | DebugStop:
|
|
1231
|
+
return self._execute_capture(
|
|
1232
|
+
visible_source,
|
|
1233
|
+
self._compatibility_mapped_source(
|
|
1234
|
+
lowered_source,
|
|
1235
|
+
"lowered-capture",
|
|
1236
|
+
),
|
|
1237
|
+
messages_intercepted=messages_intercepted,
|
|
1238
|
+
message_collector_key=message_collector_key,
|
|
1239
|
+
dirty_roots=dirty_roots,
|
|
1240
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
def execute_mapped_capture(
|
|
1244
|
+
self,
|
|
1245
|
+
visible_source: str,
|
|
1246
|
+
lowered_source: MappedSource,
|
|
1247
|
+
*,
|
|
1248
|
+
visible_source_context: VisibleSourceContext,
|
|
1249
|
+
messages_intercepted: int = 0,
|
|
1250
|
+
message_collector_key: str = "",
|
|
1251
|
+
worker_messages: bool = False,
|
|
1252
|
+
worker_globals: tuple[str, ...] = (),
|
|
1253
|
+
dirty_roots: tuple[str, ...] = (),
|
|
1254
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
1255
|
+
) -> CaptureCellResult | DebugStop:
|
|
1256
|
+
return self._execute_capture(
|
|
1257
|
+
visible_source,
|
|
1258
|
+
lowered_source,
|
|
1259
|
+
visible_source_context=visible_source_context,
|
|
1260
|
+
messages_intercepted=messages_intercepted,
|
|
1261
|
+
message_collector_key=message_collector_key,
|
|
1262
|
+
worker_messages=worker_messages,
|
|
1263
|
+
worker_globals=worker_globals,
|
|
1264
|
+
dirty_roots=dirty_roots,
|
|
1265
|
+
on_transport_dispatch=on_transport_dispatch,
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
def capture_frame_variables(
|
|
1269
|
+
self, *, filters: Mapping[str, object], cursor: int, limit: int,
|
|
1270
|
+
timeout_s: float | None = None,
|
|
1271
|
+
) -> Mapping[str, object]:
|
|
1272
|
+
"""Return a page of captured-frame metadata without reading values."""
|
|
1273
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
1274
|
+
self._require_capture_frame()
|
|
1275
|
+
if not isinstance(filters, Mapping) or set(filters) - {"name", "role", "type"}:
|
|
1276
|
+
raise ProtocolError("capture frame filters are invalid")
|
|
1277
|
+
if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
|
|
1278
|
+
raise ProtocolError("capture frame page is invalid")
|
|
1279
|
+
expected = {
|
|
1280
|
+
key: value.casefold()
|
|
1281
|
+
for key, value in filters.items()
|
|
1282
|
+
if isinstance(value, str) and value
|
|
1283
|
+
}
|
|
1284
|
+
if len(expected) != len(filters):
|
|
1285
|
+
raise ProtocolError("capture frame filters must be non-empty strings")
|
|
1286
|
+
items = tuple(
|
|
1287
|
+
variable
|
|
1288
|
+
for variable in self._capture_frame_variables
|
|
1289
|
+
if (
|
|
1290
|
+
("name" not in expected or expected["name"] in variable.name.casefold())
|
|
1291
|
+
and ("type" not in expected or expected["type"] in variable.type_name.casefold())
|
|
1292
|
+
and ("role" not in expected or expected["role"] == "local")
|
|
1293
|
+
)
|
|
1294
|
+
)
|
|
1295
|
+
if cursor > len(items):
|
|
1296
|
+
raise ProtocolError("capture frame cursor exceeds metadata")
|
|
1297
|
+
self._capture_remaining_timeout(deadline)
|
|
1298
|
+
page = items[cursor : cursor + limit]
|
|
1299
|
+
next_cursor = cursor + len(page)
|
|
1300
|
+
return {
|
|
1301
|
+
"items": tuple(
|
|
1302
|
+
{
|
|
1303
|
+
"name": variable.name,
|
|
1304
|
+
"type_name": variable.type_name,
|
|
1305
|
+
"role": "local",
|
|
1306
|
+
# Registry-private path into the captured Context structure.
|
|
1307
|
+
"handle": "Контекст.КонтекстОтладки." + variable.name,
|
|
1308
|
+
}
|
|
1309
|
+
for variable in page
|
|
1310
|
+
),
|
|
1311
|
+
"total": len(items),
|
|
1312
|
+
"next_cursor": next_cursor if next_cursor < len(items) else None,
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
def _capture_stack_frame_wire(self, frame: StackFrame) -> Mapping[str, object]:
|
|
1316
|
+
location = frame.location
|
|
1317
|
+
if self._same_kernel_module(location):
|
|
1318
|
+
# The caller needs the stack level, not the extension's private
|
|
1319
|
+
# RDBG URL, source line, or module identity.
|
|
1320
|
+
return {
|
|
1321
|
+
"level": frame.level,
|
|
1322
|
+
"runtime_kernel": True,
|
|
1323
|
+
"module_type": None,
|
|
1324
|
+
"object_id": None,
|
|
1325
|
+
"property_id": None,
|
|
1326
|
+
"line": None,
|
|
1327
|
+
"extension_name": None,
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
"level": frame.level,
|
|
1331
|
+
"runtime_kernel": False,
|
|
1332
|
+
"module_type": location.module_type,
|
|
1333
|
+
"object_id": str(location.object_id) if location.object_id is not None else None,
|
|
1334
|
+
"property_id": str(location.property_id) if location.property_id is not None else None,
|
|
1335
|
+
"line": location.line,
|
|
1336
|
+
"extension_name": location.extension_name,
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
def _require_capture_stack(self) -> tuple[StackFrame, ...]:
|
|
1340
|
+
self._require_capture_frame()
|
|
1341
|
+
if not self._capture_stack_frames or self._capture_target_id is None:
|
|
1342
|
+
raise ProtocolError("capture stack is unavailable")
|
|
1343
|
+
target = self.session.target
|
|
1344
|
+
if target is None or target.target_id != self._capture_target_id:
|
|
1345
|
+
raise ProtocolError("capture target has changed")
|
|
1346
|
+
return self._capture_stack_frames
|
|
1347
|
+
|
|
1348
|
+
def capture_stack(
|
|
1349
|
+
self, *, cursor: int, limit: int, timeout_s: float | None = None,
|
|
1350
|
+
) -> Mapping[str, object]:
|
|
1351
|
+
"""Page already-observed stack locations only when requested."""
|
|
1352
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
1353
|
+
if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
|
|
1354
|
+
raise ProtocolError("capture stack page is invalid")
|
|
1355
|
+
frames = self._require_capture_stack()
|
|
1356
|
+
if cursor > len(frames):
|
|
1357
|
+
raise ProtocolError("capture stack cursor exceeds frames")
|
|
1358
|
+
self._capture_remaining_timeout(deadline)
|
|
1359
|
+
page = frames[cursor : cursor + limit]
|
|
1360
|
+
next_cursor = cursor + len(page)
|
|
1361
|
+
return {
|
|
1362
|
+
"frames": tuple(self._capture_stack_frame_wire(frame) for frame in page),
|
|
1363
|
+
"total": len(frames),
|
|
1364
|
+
"next_cursor": next_cursor if next_cursor < len(frames) else None,
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
def capture_frame(
|
|
1368
|
+
self, *, level: int, cursor: int, limit: int,
|
|
1369
|
+
name: str | None = None, timeout_s: float | None = None,
|
|
1370
|
+
) -> Mapping[str, object]:
|
|
1371
|
+
"""Read one frame on demand; evaluate only an explicitly named local."""
|
|
1372
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
1373
|
+
if type(level) is not int or level < 0:
|
|
1374
|
+
raise ProtocolError("capture frame level is invalid")
|
|
1375
|
+
if type(cursor) is not int or cursor < 0 or type(limit) is not int or not 1 <= limit <= 100:
|
|
1376
|
+
raise ProtocolError("capture frame page is invalid")
|
|
1377
|
+
if name is not None:
|
|
1378
|
+
self._capture_identifier(name, name="variable name")
|
|
1379
|
+
if cursor != 0:
|
|
1380
|
+
raise ProtocolError("named capture variable cursor must be zero")
|
|
1381
|
+
frames = self._require_capture_stack()
|
|
1382
|
+
frame = next((item for item in frames if item.level == level), None)
|
|
1383
|
+
if frame is None:
|
|
1384
|
+
raise ProtocolError("capture frame level is unavailable")
|
|
1385
|
+
if self._same_kernel_module(frame.location):
|
|
1386
|
+
raise ProtocolError("runtime kernel frame variables are unavailable")
|
|
1387
|
+
if level == self.capture_frame_stack_level and name is None:
|
|
1388
|
+
variables = self._capture_frame_variables
|
|
1389
|
+
else:
|
|
1390
|
+
result = self.session.local_variables(
|
|
1391
|
+
stack_level=level,
|
|
1392
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
1393
|
+
max_text_size=512,
|
|
1394
|
+
)
|
|
1395
|
+
if result.error_occurred:
|
|
1396
|
+
raise ProtocolError("capture frame variables are unavailable")
|
|
1397
|
+
variables = result.variables
|
|
1398
|
+
if name is not None:
|
|
1399
|
+
variables = tuple(item for item in variables if item.name.casefold() == name.casefold())
|
|
1400
|
+
if not variables:
|
|
1401
|
+
raise ProtocolError("capture variable is unavailable")
|
|
1402
|
+
if cursor > len(variables):
|
|
1403
|
+
raise ProtocolError("capture frame cursor exceeds variables")
|
|
1404
|
+
self._capture_remaining_timeout(deadline)
|
|
1405
|
+
page = variables[cursor : cursor + limit]
|
|
1406
|
+
if name is None:
|
|
1407
|
+
items: tuple[Mapping[str, object], ...] = tuple(
|
|
1408
|
+
{"name": item.name, "type_name": item.type_name} for item in page
|
|
1409
|
+
)
|
|
1410
|
+
else:
|
|
1411
|
+
item = page[0]
|
|
1412
|
+
items = ({
|
|
1413
|
+
"name": item.name,
|
|
1414
|
+
"type_name": item.type_name,
|
|
1415
|
+
"presentation": item.presentation[:512],
|
|
1416
|
+
"collection_size": item.collection_size,
|
|
1417
|
+
},)
|
|
1418
|
+
next_cursor = cursor + len(page)
|
|
1419
|
+
return {
|
|
1420
|
+
"frame": self._capture_stack_frame_wire(frame),
|
|
1421
|
+
"variables": items,
|
|
1422
|
+
"total": len(variables),
|
|
1423
|
+
"next_cursor": next_cursor if next_cursor < len(variables) else None,
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
def resolve_capture_manager_origin(
|
|
1427
|
+
self, root: str, fields: tuple[str, ...], *,
|
|
1428
|
+
timeout_s: float | None = None,
|
|
1429
|
+
) -> Mapping[str, object]:
|
|
1430
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
1431
|
+
frame_stack_level = self._require_capture_frame()
|
|
1432
|
+
root = self._capture_identifier(root, name="manager root")
|
|
1433
|
+
if isinstance(fields, str) or not isinstance(fields, tuple):
|
|
1434
|
+
raise ProtocolError("capture manager fields are invalid")
|
|
1435
|
+
checked_fields = tuple(
|
|
1436
|
+
self._capture_identifier(field, name="manager field") for field in fields
|
|
1437
|
+
)
|
|
1438
|
+
frame_roots = {
|
|
1439
|
+
item.name.casefold(): item.name
|
|
1440
|
+
for item in self._capture_frame_variables
|
|
1441
|
+
}
|
|
1442
|
+
canonical_root = frame_roots.get(root.casefold())
|
|
1443
|
+
if canonical_root is None:
|
|
1444
|
+
raise ProtocolError("capture manager root is unavailable")
|
|
1445
|
+
physical_path = ".".join((canonical_root, *checked_fields))
|
|
1446
|
+
native_path = "Контекст.КонтекстОтладки." + physical_path
|
|
1447
|
+
existing = next(
|
|
1448
|
+
(
|
|
1449
|
+
key
|
|
1450
|
+
for key, value in self._capture_manager_paths.items()
|
|
1451
|
+
if value.casefold() == native_path.casefold()
|
|
1452
|
+
),
|
|
1453
|
+
None,
|
|
1454
|
+
)
|
|
1455
|
+
if existing is not None:
|
|
1456
|
+
self._capture_remaining_timeout(deadline)
|
|
1457
|
+
return {
|
|
1458
|
+
"key": existing,
|
|
1459
|
+
"handle": existing,
|
|
1460
|
+
"type_name": "МенеджерВременныхТаблиц",
|
|
1461
|
+
}
|
|
1462
|
+
expression = (
|
|
1463
|
+
f"ТипЗнч({physical_path}) = "
|
|
1464
|
+
'Тип("МенеджерВременныхТаблиц")'
|
|
1465
|
+
)
|
|
1466
|
+
try:
|
|
1467
|
+
proof = self.session.evaluate(
|
|
1468
|
+
expression,
|
|
1469
|
+
stack_level=frame_stack_level,
|
|
1470
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
1471
|
+
)
|
|
1472
|
+
except BaseException:
|
|
1473
|
+
raise ProtocolError("capture manager origin is unavailable") from None
|
|
1474
|
+
self._capture_remaining_timeout(deadline)
|
|
1475
|
+
if (
|
|
1476
|
+
proof.error_occurred
|
|
1477
|
+
or proof.type_name != "Булево"
|
|
1478
|
+
or evaluation_to_python(proof) is not True
|
|
1479
|
+
):
|
|
1480
|
+
raise ProtocolError("capture manager origin is unavailable")
|
|
1481
|
+
handle = existing or "capture_manager_" + uuid4().hex
|
|
1482
|
+
self._capture_manager_paths[handle] = native_path
|
|
1483
|
+
return {"key": handle, "handle": handle, "type_name": "МенеджерВременныхТаблиц"}
|
|
1484
|
+
|
|
1485
|
+
def capture_temporary_tables(
|
|
1486
|
+
self,
|
|
1487
|
+
manager_handle: str,
|
|
1488
|
+
*,
|
|
1489
|
+
names: tuple[str, ...] | None,
|
|
1490
|
+
cursor: int,
|
|
1491
|
+
limit: int,
|
|
1492
|
+
selection: Mapping[str, object] | None,
|
|
1493
|
+
timeout_s: float | None = None,
|
|
1494
|
+
) -> Mapping[str, object]:
|
|
1495
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
1496
|
+
self._require_capture_frame()
|
|
1497
|
+
if not isinstance(manager_handle, str) or manager_handle not in self._capture_manager_paths:
|
|
1498
|
+
raise ProtocolError("capture manager handle is stale or invalid")
|
|
1499
|
+
if names is None or isinstance(names, str) or len(names) != 1:
|
|
1500
|
+
raise ProtocolError("capture table inspection requires one named table")
|
|
1501
|
+
if type(cursor) is not int or cursor != 0 or type(limit) is not int or not 1 <= limit <= 100:
|
|
1502
|
+
raise ProtocolError("capture table page is invalid")
|
|
1503
|
+
name = self._capture_identifier(names[0], name="table name")
|
|
1504
|
+
offset, row_limit, columns = self._capture_table_selection(selection)
|
|
1505
|
+
manager_path = self._capture_manager_paths[manager_handle]
|
|
1506
|
+
if selection is None:
|
|
1507
|
+
schema = self._capture_temporary_table_schema(
|
|
1508
|
+
manager_path, name, deadline=deadline
|
|
1509
|
+
)
|
|
1510
|
+
handle = "capture_table_metadata_" + uuid4().hex
|
|
1511
|
+
self._capture_metadata_handles.add(handle)
|
|
1512
|
+
return {
|
|
1513
|
+
"items": ({"name": name, "schema": schema, "handle": handle},),
|
|
1514
|
+
"total": 1,
|
|
1515
|
+
"next_cursor": None,
|
|
1516
|
+
}
|
|
1517
|
+
context_key = "__onec_capture_table_" + uuid4().hex
|
|
1518
|
+
columns_expression = (
|
|
1519
|
+
"Новый Массив"
|
|
1520
|
+
if not columns
|
|
1521
|
+
else "СтрРазделить(" + bsl_string_literal(",".join(columns)) + ", \",\")"
|
|
1522
|
+
)
|
|
1523
|
+
expression = (
|
|
1524
|
+
"RuntimeKernelServer.СохранитьВременнуюТаблицуОтладки(Контекст, "
|
|
1525
|
+
+ manager_path
|
|
1526
|
+
+ ", " + bsl_string_literal(name)
|
|
1527
|
+
+ ", " + bsl_string_literal(context_key)
|
|
1528
|
+
+ f", {offset}, {row_limit}, " + columns_expression + ")"
|
|
1529
|
+
)
|
|
1530
|
+
result = self.session.evaluate(
|
|
1531
|
+
expression,
|
|
1532
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
1533
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
1534
|
+
)
|
|
1535
|
+
self._capture_remaining_timeout(deadline)
|
|
1536
|
+
if result.error_occurred:
|
|
1537
|
+
raise BslExecutionError(result.error_text)
|
|
1538
|
+
native_path = "Контекст." + context_key
|
|
1539
|
+
schema_result = self.inspect_declared_table_schema(
|
|
1540
|
+
native_path,
|
|
1541
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
1542
|
+
)
|
|
1543
|
+
self._capture_remaining_timeout(deadline)
|
|
1544
|
+
schema = self._capture_schema_names(schema_result.collection_rows)
|
|
1545
|
+
handle = "capture_table_" + uuid4().hex
|
|
1546
|
+
self._capture_value_paths[handle] = native_path
|
|
1547
|
+
return {
|
|
1548
|
+
"items": ({"name": name, "schema": schema, "handle": handle},),
|
|
1549
|
+
"total": 1,
|
|
1550
|
+
"next_cursor": None,
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
def _capture_temporary_table_schema(
|
|
1554
|
+
self, manager_path: str, name: str, *, deadline: float
|
|
1555
|
+
) -> tuple[str, ...]:
|
|
1556
|
+
expression = (
|
|
1557
|
+
"RuntimeKernelServer.ПолучитьСхемуВременнойТаблицыОтладки("
|
|
1558
|
+
+ manager_path
|
|
1559
|
+
+ ", "
|
|
1560
|
+
+ bsl_string_literal(name)
|
|
1561
|
+
+ ")"
|
|
1562
|
+
)
|
|
1563
|
+
rows: list[object] = []
|
|
1564
|
+
start_index = 0
|
|
1565
|
+
while len(rows) <= MAX_CAPTURE_SCHEMA_COLUMNS:
|
|
1566
|
+
page_size = min(64, MAX_CAPTURE_SCHEMA_COLUMNS + 1 - len(rows))
|
|
1567
|
+
result = self.session.evaluate_collection(
|
|
1568
|
+
expression,
|
|
1569
|
+
start_index=start_index,
|
|
1570
|
+
page_size=page_size,
|
|
1571
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
1572
|
+
max_text_size=4096,
|
|
1573
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
1574
|
+
)
|
|
1575
|
+
self._capture_remaining_timeout(deadline)
|
|
1576
|
+
if result.error_occurred:
|
|
1577
|
+
raise BslExecutionError(result.error_text)
|
|
1578
|
+
page = result.collection_rows
|
|
1579
|
+
rows.extend(page)
|
|
1580
|
+
if len(rows) > MAX_CAPTURE_SCHEMA_COLUMNS:
|
|
1581
|
+
raise ProtocolError("capture table schema exceeds bounded metadata")
|
|
1582
|
+
if result.collection_size is not None:
|
|
1583
|
+
if result.collection_size > MAX_CAPTURE_SCHEMA_COLUMNS:
|
|
1584
|
+
raise ProtocolError("capture table schema exceeds bounded metadata")
|
|
1585
|
+
if len(rows) >= result.collection_size:
|
|
1586
|
+
break
|
|
1587
|
+
if len(page) < page_size:
|
|
1588
|
+
break
|
|
1589
|
+
start_index += len(page)
|
|
1590
|
+
return self._capture_schema_names(rows)
|
|
1591
|
+
|
|
1592
|
+
@classmethod
|
|
1593
|
+
def _capture_table_selection(
|
|
1594
|
+
cls, selection: Mapping[str, object] | None
|
|
1595
|
+
) -> tuple[int, int, tuple[str, ...]]:
|
|
1596
|
+
if selection is None:
|
|
1597
|
+
return 0, 0, ()
|
|
1598
|
+
if not isinstance(selection, Mapping) or set(selection) != {"offset", "limit", "columns"}:
|
|
1599
|
+
raise ProtocolError("capture table selection is invalid")
|
|
1600
|
+
offset, limit, columns = selection["offset"], selection["limit"], selection["columns"]
|
|
1601
|
+
if (
|
|
1602
|
+
type(offset) is not int
|
|
1603
|
+
or offset < 0
|
|
1604
|
+
or offset > MAX_CAPTURE_PROJECTION_POSITION
|
|
1605
|
+
or type(limit) is not int
|
|
1606
|
+
or not 1 <= limit <= 100
|
|
1607
|
+
or offset + limit > MAX_CAPTURE_PROJECTION_POSITION
|
|
1608
|
+
):
|
|
1609
|
+
raise ProtocolError("capture table selection bounds are invalid")
|
|
1610
|
+
if isinstance(columns, str) or not isinstance(columns, Sequence) or len(columns) > 100:
|
|
1611
|
+
raise ProtocolError("capture table selection columns are invalid")
|
|
1612
|
+
return offset, limit, tuple(cls._capture_identifier(column, name="table column") for column in columns)
|
|
1613
|
+
|
|
1614
|
+
@staticmethod
|
|
1615
|
+
def _capture_schema_names(rows: Sequence[object]) -> tuple[str, ...]:
|
|
1616
|
+
names: list[str] = []
|
|
1617
|
+
for row in rows:
|
|
1618
|
+
cells = getattr(row, "cells", ())
|
|
1619
|
+
for cell in cells:
|
|
1620
|
+
if getattr(cell, "name", "") == "Имя":
|
|
1621
|
+
name = getattr(cell, "value_string", "") or getattr(cell, "presentation", "")
|
|
1622
|
+
if isinstance(name, str) and name:
|
|
1623
|
+
names.append(name.strip('"'))
|
|
1624
|
+
return tuple(names)
|
|
1625
|
+
|
|
1626
|
+
def capture_value_handle(self, handle: str) -> str:
|
|
1627
|
+
self._require_capture_frame()
|
|
1628
|
+
if handle in self._capture_manager_paths:
|
|
1629
|
+
return self._capture_manager_paths[handle]
|
|
1630
|
+
if handle in self._capture_metadata_handles:
|
|
1631
|
+
raise ProtocolError(
|
|
1632
|
+
"capture table inventory is metadata-only; request a bounded row selection"
|
|
1633
|
+
)
|
|
1634
|
+
try:
|
|
1635
|
+
return self._capture_value_paths[handle]
|
|
1636
|
+
except KeyError as error:
|
|
1637
|
+
raise ProtocolError("capture value handle is stale or invalid") from error
|
|
1638
|
+
|
|
1639
|
+
def is_capture_metadata_handle(self, handle: str) -> bool:
|
|
1640
|
+
"""Prove an opaque inventory handle belongs to the active frame."""
|
|
1641
|
+
self._require_capture_frame()
|
|
1642
|
+
return isinstance(handle, str) and handle in self._capture_metadata_handles
|
|
1643
|
+
|
|
1644
|
+
@classmethod
|
|
1645
|
+
def _with_message_collector(
|
|
1646
|
+
cls,
|
|
1647
|
+
source: str,
|
|
1648
|
+
messages_intercepted: int,
|
|
1649
|
+
message_collector_key: str,
|
|
1650
|
+
) -> str:
|
|
1651
|
+
mapped = cls._compatibility_mapped_source(source, "collector-compatibility")
|
|
1652
|
+
return cls._with_message_collector_mapped(
|
|
1653
|
+
mapped,
|
|
1654
|
+
messages_intercepted,
|
|
1655
|
+
message_collector_key,
|
|
1656
|
+
).text
|
|
1657
|
+
|
|
1658
|
+
@staticmethod
|
|
1659
|
+
def _with_worker_globals_mapped(
|
|
1660
|
+
source: MappedSource,
|
|
1661
|
+
names: tuple[str, ...],
|
|
1662
|
+
) -> MappedSource:
|
|
1663
|
+
"""Expose only referenced notebook values for one Worker invocation."""
|
|
1664
|
+
if not names:
|
|
1665
|
+
return source
|
|
1666
|
+
worker = "__OnecNotebookGlobalWorker"
|
|
1667
|
+
previous = "__OnecNotebookPreviousGlobals"
|
|
1668
|
+
globals_name = "__OnecNotebookBoundGlobals"
|
|
1669
|
+
initialization = (
|
|
1670
|
+
f'{worker} = Контекст.RuntimeWorkerActiveGeneration.Modules.Получить("Worker");\n'
|
|
1671
|
+
f"{previous} = {worker}.__OnecNotebookGlobals;\n"
|
|
1672
|
+
f"{globals_name} = Новый Структура;\n"
|
|
1673
|
+
)
|
|
1674
|
+
for name in names:
|
|
1675
|
+
initialization += (
|
|
1676
|
+
f"{globals_name}.Вставить({bsl_string_literal(name)}, "
|
|
1677
|
+
f"Контекст.{name});\n"
|
|
1678
|
+
)
|
|
1679
|
+
initialization += f"{worker}.__OnecNotebookGlobals = {globals_name};\n"
|
|
1680
|
+
restore = f"{worker}.__OnecNotebookGlobals = {previous};\n"
|
|
1681
|
+
start = SourceSpan(0, 0)
|
|
1682
|
+
end = SourceSpan(len(source.text), len(source.text))
|
|
1683
|
+
builder = SourceTransformBuilder(source)
|
|
1684
|
+
builder.synthetic(initialization, start, "worker_globals_initialize")
|
|
1685
|
+
builder.synthetic("Попытка\n", start, "worker_globals_try")
|
|
1686
|
+
builder.copy(SourceSpan(0, len(source.text)))
|
|
1687
|
+
builder.synthetic("\n" + restore, end, "worker_globals_finalize")
|
|
1688
|
+
builder.synthetic("Исключение\n", end, "worker_globals_exception")
|
|
1689
|
+
builder.synthetic(restore, end, "worker_globals_finalize")
|
|
1690
|
+
builder.synthetic("ВызватьИсключение;\n", end, "worker_globals_rethrow")
|
|
1691
|
+
builder.synthetic("КонецПопытки;", end, "worker_globals_end")
|
|
1692
|
+
return builder.build(
|
|
1693
|
+
SourceArtifactKind.COLLECTOR_WRAPPER,
|
|
1694
|
+
wrapper_semantic_version="notebook-globals-v1",
|
|
1695
|
+
mode=source.artifact.mode,
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
@staticmethod
|
|
1699
|
+
def _with_message_collector_mapped(
|
|
1700
|
+
source: MappedSource,
|
|
1701
|
+
messages_intercepted: int,
|
|
1702
|
+
message_collector_key: str,
|
|
1703
|
+
*,
|
|
1704
|
+
worker_messages: bool = False,
|
|
1705
|
+
) -> MappedSource:
|
|
1706
|
+
if not isinstance(source, MappedSource):
|
|
1707
|
+
raise ValueError("message collector source must be mapped")
|
|
1708
|
+
if not messages_intercepted:
|
|
1709
|
+
return source
|
|
1710
|
+
worker_object = "__OnecPinnedWorkerGenerationMessageObject"
|
|
1711
|
+
previous_sink = "__OnecPinnedWorkerGenerationPreviousMessageSink"
|
|
1712
|
+
finalize = (
|
|
1713
|
+
(
|
|
1714
|
+
f"{worker_object}.__OnecWorkerMessageSink = "
|
|
1715
|
+
f"{previous_sink};\n"
|
|
1716
|
+
if worker_messages else ""
|
|
1717
|
+
)
|
|
1718
|
+
+ 'Контекст.Вставить("__onec_cell_messages_result_key", '
|
|
1719
|
+
+ bsl_string_literal(message_collector_key)
|
|
1720
|
+
+ ");\n"
|
|
1721
|
+
+ 'Контекст.Вставить("__onec_cell_messages_result", Контекст.'
|
|
1722
|
+
+ message_collector_key
|
|
1723
|
+
+ ");\n"
|
|
1724
|
+
+ "Контекст.Удалить("
|
|
1725
|
+
+ bsl_string_literal(message_collector_key)
|
|
1726
|
+
+ ");"
|
|
1727
|
+
)
|
|
1728
|
+
start = SourceSpan(0, 0)
|
|
1729
|
+
end = SourceSpan(len(source.text), len(source.text))
|
|
1730
|
+
builder = SourceTransformBuilder(source)
|
|
1731
|
+
initialize = f'Контекст.Вставить("{message_collector_key}", Новый Массив);\n'
|
|
1732
|
+
if worker_messages:
|
|
1733
|
+
initialize += (
|
|
1734
|
+
f"{worker_object} = "
|
|
1735
|
+
'Контекст.RuntimeWorkerActiveGeneration.Modules.Получить("Worker");\n'
|
|
1736
|
+
f"{previous_sink} = "
|
|
1737
|
+
f"{worker_object}.__OnecWorkerMessageSink;\n"
|
|
1738
|
+
f"{worker_object}.__OnecWorkerMessageSink = "
|
|
1739
|
+
f"Контекст.{message_collector_key};\n"
|
|
1740
|
+
)
|
|
1741
|
+
builder.synthetic(
|
|
1742
|
+
initialize,
|
|
1743
|
+
start,
|
|
1744
|
+
"message_collector_initialize",
|
|
1745
|
+
)
|
|
1746
|
+
builder.synthetic("Попытка\n", start, "message_collector_try")
|
|
1747
|
+
builder.copy(SourceSpan(0, len(source.text)))
|
|
1748
|
+
builder.synthetic(
|
|
1749
|
+
"\n" + finalize + "\n",
|
|
1750
|
+
end,
|
|
1751
|
+
"message_collector_finalize",
|
|
1752
|
+
)
|
|
1753
|
+
builder.synthetic(
|
|
1754
|
+
"Исключение\n",
|
|
1755
|
+
end,
|
|
1756
|
+
"message_collector_exception",
|
|
1757
|
+
)
|
|
1758
|
+
builder.synthetic(
|
|
1759
|
+
finalize + "\n",
|
|
1760
|
+
end,
|
|
1761
|
+
"message_collector_finalize",
|
|
1762
|
+
)
|
|
1763
|
+
builder.synthetic(
|
|
1764
|
+
"ВызватьИсключение;\n",
|
|
1765
|
+
end,
|
|
1766
|
+
"message_collector_rethrow",
|
|
1767
|
+
)
|
|
1768
|
+
builder.synthetic("КонецПопытки;", end, "message_collector_end")
|
|
1769
|
+
return builder.build(
|
|
1770
|
+
SourceArtifactKind.COLLECTOR_WRAPPER,
|
|
1771
|
+
wrapper_semantic_version="message-collector-v1",
|
|
1772
|
+
mode=source.artifact.mode,
|
|
1773
|
+
)
|
|
1774
|
+
|
|
1775
|
+
@staticmethod
|
|
1776
|
+
def _as_executed_source(
|
|
1777
|
+
source: MappedSource,
|
|
1778
|
+
*,
|
|
1779
|
+
mode: LoweringMode,
|
|
1780
|
+
) -> MappedSource:
|
|
1781
|
+
builder = SourceTransformBuilder(source)
|
|
1782
|
+
builder.copy(SourceSpan(0, len(source.text)))
|
|
1783
|
+
return builder.build(
|
|
1784
|
+
SourceArtifactKind.EXECUTED_BSL,
|
|
1785
|
+
wrapper_semantic_version="runtime-execution-v1",
|
|
1786
|
+
mode=mode.value,
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
@staticmethod
|
|
1790
|
+
def _compatibility_mapped_source(source: str, purpose: str) -> MappedSource:
|
|
1791
|
+
digest = source_sha256(source)
|
|
1792
|
+
return mapped_visible_source(
|
|
1793
|
+
source,
|
|
1794
|
+
SourceUnitRef(
|
|
1795
|
+
SourceUnitKind.NOTEBOOK_CELL,
|
|
1796
|
+
f"runtime-{purpose}:{digest}",
|
|
1797
|
+
0,
|
|
1798
|
+
digest,
|
|
1799
|
+
),
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
@staticmethod
|
|
1803
|
+
def _visible_context(
|
|
1804
|
+
mapped: MappedSource,
|
|
1805
|
+
visible_source: str,
|
|
1806
|
+
) -> VisibleSourceContext | None:
|
|
1807
|
+
digest = source_sha256(visible_source)
|
|
1808
|
+
units = {
|
|
1809
|
+
reference
|
|
1810
|
+
for segment in mapped.source_map.segments
|
|
1811
|
+
for reference in (segment.origin_ref, segment.anchor_ref)
|
|
1812
|
+
if isinstance(reference, SourceUnitRef)
|
|
1813
|
+
and reference.source_sha256 == digest
|
|
1814
|
+
}
|
|
1815
|
+
if not units:
|
|
1816
|
+
return None
|
|
1817
|
+
return VisibleSourceContext({unit: visible_source for unit in units})
|
|
1818
|
+
|
|
1819
|
+
def message_collector_key(self, mode: LoweringMode) -> str:
|
|
1820
|
+
operation_id = (
|
|
1821
|
+
self.operation_id + 1
|
|
1822
|
+
if mode is LoweringMode.MAIN
|
|
1823
|
+
else self.active_operation.operation_id if self.active_operation else 0
|
|
1824
|
+
)
|
|
1825
|
+
cell_sequence = 0 if mode is LoweringMode.MAIN else self.cell_sequence + 1
|
|
1826
|
+
return (
|
|
1827
|
+
f"__onec_cell_messages_{self.runtime_generation}_{operation_id}_{cell_sequence}"
|
|
1828
|
+
)
|
|
1829
|
+
|
|
1830
|
+
def execute_system_capture(self, source: str) -> CaptureCellResult | DebugStop:
|
|
1831
|
+
"""Execute trusted runtime BSL verbatim in the current capture frame."""
|
|
1832
|
+
mapped = self._compatibility_mapped_source(source, "system-capture")
|
|
1833
|
+
return self._execute_capture(
|
|
1834
|
+
source,
|
|
1835
|
+
mapped,
|
|
1836
|
+
visible_source_context=self._visible_context(mapped, source),
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
def install_capture_worker_generation_pin(
|
|
1840
|
+
self,
|
|
1841
|
+
manifest_sha256: str,
|
|
1842
|
+
) -> None:
|
|
1843
|
+
"""Bind the host-fenced Worker root into the ephemeral CAPTURE slot."""
|
|
1844
|
+
self._require_state(OperationState.CAPTURED)
|
|
1845
|
+
if (
|
|
1846
|
+
not isinstance(manifest_sha256, str)
|
|
1847
|
+
or re.fullmatch(r"[0-9a-f]{64}", manifest_sha256) is None
|
|
1848
|
+
):
|
|
1849
|
+
raise ProtocolError("CAPTURE Worker manifest identity is invalid")
|
|
1850
|
+
result = self.session.evaluate(
|
|
1851
|
+
"RuntimeKernelServer.УстановитьПинПоколенияWorker(Контекст, "
|
|
1852
|
+
f"{bsl_string_literal(manifest_sha256)})",
|
|
1853
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
1854
|
+
)
|
|
1855
|
+
if result.error_occurred or evaluation_to_python(result) is not True:
|
|
1856
|
+
raise ProtocolError("CAPTURE Worker pin installation failed")
|
|
1857
|
+
|
|
1858
|
+
def clear_capture_worker_generation_pin(self) -> None:
|
|
1859
|
+
"""Remove only the reserved ephemeral CAPTURE Worker slot."""
|
|
1860
|
+
self._require_state(OperationState.CAPTURED)
|
|
1861
|
+
result = self.session.evaluate(
|
|
1862
|
+
"RuntimeKernelServer.ОчиститьПинПоколенияWorker(Контекст)",
|
|
1863
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
1864
|
+
)
|
|
1865
|
+
if result.error_occurred or evaluation_to_python(result) is not True:
|
|
1866
|
+
raise ProtocolError("CAPTURE Worker pin cleanup failed")
|
|
1867
|
+
|
|
1868
|
+
def _execute_capture(
|
|
1869
|
+
self,
|
|
1870
|
+
visible_source: str,
|
|
1871
|
+
lowered_source: str | MappedSource,
|
|
1872
|
+
*,
|
|
1873
|
+
visible_source_context: VisibleSourceContext | None = None,
|
|
1874
|
+
messages_intercepted: int = 0,
|
|
1875
|
+
message_collector_key: str = "",
|
|
1876
|
+
worker_messages: bool = False,
|
|
1877
|
+
worker_globals: tuple[str, ...] = (),
|
|
1878
|
+
dirty_roots: tuple[str, ...] = (),
|
|
1879
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
1880
|
+
) -> CaptureCellResult | DebugStop:
|
|
1881
|
+
self._require_state(OperationState.CAPTURED)
|
|
1882
|
+
if self.active_operation is None:
|
|
1883
|
+
raise ProtocolError("Capture cell has no active MAIN operation")
|
|
1884
|
+
mapped_lowered = (
|
|
1885
|
+
lowered_source
|
|
1886
|
+
if isinstance(lowered_source, MappedSource)
|
|
1887
|
+
else self._compatibility_mapped_source(lowered_source, "capture-cell")
|
|
1888
|
+
)
|
|
1889
|
+
messages_intercepted = max(messages_intercepted, int(worker_messages))
|
|
1890
|
+
bound_source = self._with_worker_globals_mapped(
|
|
1891
|
+
mapped_lowered, worker_globals
|
|
1892
|
+
)
|
|
1893
|
+
wrapped_source = self._with_message_collector_mapped(
|
|
1894
|
+
bound_source,
|
|
1895
|
+
messages_intercepted,
|
|
1896
|
+
message_collector_key,
|
|
1897
|
+
worker_messages=worker_messages,
|
|
1898
|
+
)
|
|
1899
|
+
executed_source = self._as_executed_source(
|
|
1900
|
+
wrapped_source,
|
|
1901
|
+
mode=LoweringMode.CAPTURE,
|
|
1902
|
+
)
|
|
1903
|
+
lowered_text = executed_source.text
|
|
1904
|
+
if visible_source_context is None:
|
|
1905
|
+
visible_source_context = self._visible_context(
|
|
1906
|
+
executed_source,
|
|
1907
|
+
visible_source,
|
|
1908
|
+
)
|
|
1909
|
+
self.cell_sequence += 1
|
|
1910
|
+
cell_fields = {
|
|
1911
|
+
"runtime_generation": self.runtime_generation,
|
|
1912
|
+
"operation_id": self.active_operation.operation_id,
|
|
1913
|
+
"capture_stop_sequence": self.stop_sequence,
|
|
1914
|
+
"cell_sequence": self.cell_sequence,
|
|
1915
|
+
}
|
|
1916
|
+
self._record(
|
|
1917
|
+
"write-journal.jsonl",
|
|
1918
|
+
"cell_started",
|
|
1919
|
+
**cell_fields,
|
|
1920
|
+
visible_sha256=sha256(visible_source.encode("utf-8")).hexdigest(),
|
|
1921
|
+
lowered_sha256=executed_source.artifact.source_sha256,
|
|
1922
|
+
state_before=OperationState.CAPTURED.value,
|
|
1923
|
+
dirty_roots=list(dirty_roots),
|
|
1924
|
+
immediate_object_mutation_possible=True,
|
|
1925
|
+
)
|
|
1926
|
+
try:
|
|
1927
|
+
self.state = OperationState.EVALUATING_CAPTURE
|
|
1928
|
+
try:
|
|
1929
|
+
self._set_workspace(
|
|
1930
|
+
"capture-evaluation",
|
|
1931
|
+
self.registry.evaluation_locations,
|
|
1932
|
+
)
|
|
1933
|
+
except BaseException as error:
|
|
1934
|
+
if not isinstance(error, BreakpointWorkspaceOutcomeUnknown):
|
|
1935
|
+
self.state = OperationState.CAPTURED
|
|
1936
|
+
raise
|
|
1937
|
+
if on_transport_dispatch is not None:
|
|
1938
|
+
on_transport_dispatch()
|
|
1939
|
+
pending = self.session.start_evaluation(
|
|
1940
|
+
build_live_current_capture_call(lowered_text),
|
|
1941
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
1942
|
+
)
|
|
1943
|
+
capture = _PendingCaptureEvaluation(
|
|
1944
|
+
pending,
|
|
1945
|
+
self.active_operation,
|
|
1946
|
+
visible_source,
|
|
1947
|
+
lowered_text,
|
|
1948
|
+
executed_source,
|
|
1949
|
+
visible_source_context,
|
|
1950
|
+
messages_intercepted,
|
|
1951
|
+
message_collector_key,
|
|
1952
|
+
cell_fields,
|
|
1953
|
+
)
|
|
1954
|
+
self.pending_capture_evaluation = capture
|
|
1955
|
+
event = self.session.wait_evaluation_event(
|
|
1956
|
+
pending,
|
|
1957
|
+
timeout_s=self.command_timeout_s,
|
|
1958
|
+
)
|
|
1959
|
+
except BaseException as error:
|
|
1960
|
+
self._record(
|
|
1961
|
+
"write-journal.jsonl",
|
|
1962
|
+
"cell_failed",
|
|
1963
|
+
**cell_fields,
|
|
1964
|
+
state_after=self.state.value,
|
|
1965
|
+
error_type=type(error).__name__,
|
|
1966
|
+
)
|
|
1967
|
+
self._flush_journal()
|
|
1968
|
+
raise
|
|
1969
|
+
return self._handle_capture_evaluation_event(capture, event)
|
|
1970
|
+
|
|
1971
|
+
def _handle_capture_evaluation_event(
|
|
1972
|
+
self,
|
|
1973
|
+
capture: _PendingCaptureEvaluation,
|
|
1974
|
+
event: EvaluationResult | StopEvent,
|
|
1975
|
+
) -> CaptureCellResult | DebugStop:
|
|
1976
|
+
if self.pending_capture_evaluation is not capture:
|
|
1977
|
+
raise ProtocolError("Pending CAPTURE evaluation identity changed")
|
|
1978
|
+
if isinstance(event, StopEvent):
|
|
1979
|
+
classified = classify_stop(
|
|
1980
|
+
event,
|
|
1981
|
+
self.registry,
|
|
1982
|
+
worker_locations=(
|
|
1983
|
+
self.breakpoint_workspace_owner.confirmed_snapshot.worker_slots
|
|
1984
|
+
),
|
|
1985
|
+
)
|
|
1986
|
+
self.stop_history.append(classified)
|
|
1987
|
+
debug_stop = DebugStop(capture.operation, event, classified.reason)
|
|
1988
|
+
self.last_debug_stop = debug_stop
|
|
1989
|
+
self.state = OperationState.CAPTURE_DEBUG_STOPPED
|
|
1990
|
+
return debug_stop
|
|
1991
|
+
try:
|
|
1992
|
+
try:
|
|
1993
|
+
self._set_workspace("full-restore", self.registry.full_locations)
|
|
1994
|
+
except BaseException as restore_error:
|
|
1995
|
+
self.state = OperationState.BREAKPOINT_RESTORE_FAILURE
|
|
1996
|
+
raise BreakpointRestoreError(
|
|
1997
|
+
"Failed to restore full breakpoint workspace"
|
|
1998
|
+
) from restore_error
|
|
1999
|
+
self.state = OperationState.CAPTURED
|
|
2000
|
+
self.pending_capture_evaluation = None
|
|
2001
|
+
self.last_debug_stop = None
|
|
2002
|
+
if event.error_occurred:
|
|
2003
|
+
messages = self._take_capture_cell_messages(
|
|
2004
|
+
capture.message_collector_key
|
|
2005
|
+
if capture.messages_intercepted
|
|
2006
|
+
else ""
|
|
2007
|
+
)
|
|
2008
|
+
diagnostic = _safe_platform_diagnostic(
|
|
2009
|
+
event.error_text,
|
|
2010
|
+
capture.executed_source,
|
|
2011
|
+
visible_source_context=capture.visible_source_context,
|
|
2012
|
+
)
|
|
2013
|
+
raise BslExecutionError(
|
|
2014
|
+
event.error_text,
|
|
2015
|
+
messages=messages,
|
|
2016
|
+
diagnostic=diagnostic,
|
|
2017
|
+
)
|
|
2018
|
+
value = evaluation_to_python(event)
|
|
2019
|
+
messages = self._take_capture_cell_messages(
|
|
2020
|
+
capture.message_collector_key
|
|
2021
|
+
if capture.messages_intercepted
|
|
2022
|
+
else ""
|
|
2023
|
+
)
|
|
2024
|
+
cell = CaptureCellResult(
|
|
2025
|
+
capture.operation.operation_id,
|
|
2026
|
+
capture.visible_source,
|
|
2027
|
+
capture.lowered_source,
|
|
2028
|
+
value,
|
|
2029
|
+
messages,
|
|
2030
|
+
)
|
|
2031
|
+
except BaseException as error:
|
|
2032
|
+
self._record(
|
|
2033
|
+
"write-journal.jsonl",
|
|
2034
|
+
"cell_failed",
|
|
2035
|
+
**capture.cell_fields,
|
|
2036
|
+
state_after=self.state.value,
|
|
2037
|
+
error_type=type(error).__name__,
|
|
2038
|
+
)
|
|
2039
|
+
self._flush_journal()
|
|
2040
|
+
raise
|
|
2041
|
+
self._record(
|
|
2042
|
+
"write-journal.jsonl",
|
|
2043
|
+
"cell_completed",
|
|
2044
|
+
**capture.cell_fields,
|
|
2045
|
+
state_after=self.state.value,
|
|
2046
|
+
)
|
|
2047
|
+
self._flush_journal()
|
|
2048
|
+
return cell
|
|
2049
|
+
|
|
2050
|
+
def resume(
|
|
2051
|
+
self,
|
|
2052
|
+
*,
|
|
2053
|
+
dirty_roots: tuple[str, ...] = (),
|
|
2054
|
+
continuation_attempt_id: str | None = None,
|
|
2055
|
+
on_transport_dispatch: Callable[[], None] | None = None,
|
|
2056
|
+
) -> MainCompletion | CapturedStop | DebugStop:
|
|
2057
|
+
self._require_state(OperationState.CAPTURED)
|
|
2058
|
+
dirty_roots = tuple(dirty_roots)
|
|
2059
|
+
attempt = self._continuation_attempt(
|
|
2060
|
+
dirty_roots, continuation_attempt_id
|
|
2061
|
+
)
|
|
2062
|
+
self.state = OperationState.FLUSHING
|
|
2063
|
+
for root in dirty_roots:
|
|
2064
|
+
try:
|
|
2065
|
+
transfer = self.session.evaluate(
|
|
2066
|
+
build_live_capture_root_transfer_call(root),
|
|
2067
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
2068
|
+
)
|
|
2069
|
+
except BaseException as error:
|
|
2070
|
+
self._mark_continuation_root(
|
|
2071
|
+
attempt, root, "failed", error=type(error).__name__
|
|
2072
|
+
)
|
|
2073
|
+
self._mark_later_roots_unattempted(attempt, root)
|
|
2074
|
+
self.state = (
|
|
2075
|
+
OperationState.RECOVERING
|
|
2076
|
+
if isinstance(error, RdbgTransportError)
|
|
2077
|
+
else OperationState.PARTIAL_WRITEBACK_FAILURE
|
|
2078
|
+
)
|
|
2079
|
+
raise
|
|
2080
|
+
if transfer.error_occurred:
|
|
2081
|
+
self._mark_continuation_root(
|
|
2082
|
+
attempt, root, "failed", error=transfer.error_text
|
|
2083
|
+
)
|
|
2084
|
+
self._mark_later_roots_unattempted(attempt, root)
|
|
2085
|
+
self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
|
|
2086
|
+
raise PartialWritebackError(
|
|
2087
|
+
f"Capture root {root} transfer failed: {transfer.error_text}"
|
|
2088
|
+
)
|
|
2089
|
+
address = evaluation_to_python(transfer)
|
|
2090
|
+
if not isinstance(address, str) or not address:
|
|
2091
|
+
self._mark_continuation_root(
|
|
2092
|
+
attempt,
|
|
2093
|
+
root,
|
|
2094
|
+
"failed",
|
|
2095
|
+
error="invalid temporary-storage address",
|
|
2096
|
+
)
|
|
2097
|
+
self._mark_later_roots_unattempted(attempt, root)
|
|
2098
|
+
self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
|
|
2099
|
+
raise PartialWritebackError(
|
|
2100
|
+
f"Capture root {root} temporary-storage address is invalid"
|
|
2101
|
+
)
|
|
2102
|
+
expression = build_temporary_storage_value_expression(address)
|
|
2103
|
+
expression_sha256 = sha256(expression.encode("utf-8")).hexdigest()
|
|
2104
|
+
self._set_root_status(root, expression_sha256, SideEffectStatus.PLANNED)
|
|
2105
|
+
self._set_root_status(root, expression_sha256, SideEffectStatus.SENT)
|
|
2106
|
+
self._mark_continuation_root(attempt, root, "sent")
|
|
2107
|
+
try:
|
|
2108
|
+
result = self.session.modify(root, expression)
|
|
2109
|
+
except RdbgTransportError as error:
|
|
2110
|
+
self._set_root_status(
|
|
2111
|
+
root,
|
|
2112
|
+
expression_sha256,
|
|
2113
|
+
SideEffectStatus.OUTCOME_UNKNOWN,
|
|
2114
|
+
error=str(error),
|
|
2115
|
+
)
|
|
2116
|
+
self._mark_continuation_root(
|
|
2117
|
+
attempt, root, "outcome_unknown", error=str(error)
|
|
2118
|
+
)
|
|
2119
|
+
self._mark_later_roots_unattempted(attempt, root)
|
|
2120
|
+
raise
|
|
2121
|
+
self._set_root_status(
|
|
2122
|
+
root,
|
|
2123
|
+
expression_sha256,
|
|
2124
|
+
SideEffectStatus.ACKNOWLEDGED,
|
|
2125
|
+
result_id=result.result_id,
|
|
2126
|
+
)
|
|
2127
|
+
self._record(
|
|
2128
|
+
"write-journal.jsonl",
|
|
2129
|
+
"root_write_acknowledged",
|
|
2130
|
+
runtime_generation=self.runtime_generation,
|
|
2131
|
+
operation_id=self.active_operation.operation_id,
|
|
2132
|
+
root=root,
|
|
2133
|
+
result_id=str(result.result_id),
|
|
2134
|
+
)
|
|
2135
|
+
if result.error_occurred:
|
|
2136
|
+
self._set_root_status(
|
|
2137
|
+
root,
|
|
2138
|
+
expression_sha256,
|
|
2139
|
+
SideEffectStatus.FAILED,
|
|
2140
|
+
result_id=result.result_id,
|
|
2141
|
+
error=result.error_text,
|
|
2142
|
+
)
|
|
2143
|
+
self._mark_continuation_root(
|
|
2144
|
+
attempt, root, "failed", error=result.error_text
|
|
2145
|
+
)
|
|
2146
|
+
self._mark_later_roots_unattempted(attempt, root)
|
|
2147
|
+
self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
|
|
2148
|
+
raise PartialWritebackError(
|
|
2149
|
+
f"Capture root {root} failed: {result.error_text}"
|
|
2150
|
+
)
|
|
2151
|
+
self._set_root_status(
|
|
2152
|
+
root,
|
|
2153
|
+
expression_sha256,
|
|
2154
|
+
SideEffectStatus.SUCCEEDED,
|
|
2155
|
+
result_id=result.result_id,
|
|
2156
|
+
)
|
|
2157
|
+
self._mark_continuation_root(attempt, root, "succeeded")
|
|
2158
|
+
if sum(entry.succeeded for entry in self.write_journal) == 1:
|
|
2159
|
+
self._inject(
|
|
2160
|
+
FaultPoint.AFTER_FIRST_ROOT_WRITE,
|
|
2161
|
+
RecoveryPhase.FLUSHING,
|
|
2162
|
+
)
|
|
2163
|
+
self._record(
|
|
2164
|
+
"write-journal.jsonl",
|
|
2165
|
+
"capture_cleanup_planned",
|
|
2166
|
+
runtime_generation=self.runtime_generation,
|
|
2167
|
+
operation_id=self.active_operation.operation_id,
|
|
2168
|
+
)
|
|
2169
|
+
self._flush_journal()
|
|
2170
|
+
try:
|
|
2171
|
+
close = self.session.evaluate(
|
|
2172
|
+
build_live_capture_end_call(),
|
|
2173
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
2174
|
+
)
|
|
2175
|
+
except RdbgTransportError as error:
|
|
2176
|
+
self._record(
|
|
2177
|
+
"write-journal.jsonl",
|
|
2178
|
+
"capture_cleanup_outcome_unknown",
|
|
2179
|
+
runtime_generation=self.runtime_generation,
|
|
2180
|
+
operation_id=self.active_operation.operation_id,
|
|
2181
|
+
error=str(error),
|
|
2182
|
+
)
|
|
2183
|
+
self._flush_journal()
|
|
2184
|
+
raise
|
|
2185
|
+
if close.error_occurred:
|
|
2186
|
+
self._record(
|
|
2187
|
+
"write-journal.jsonl",
|
|
2188
|
+
"capture_cleanup_failed",
|
|
2189
|
+
runtime_generation=self.runtime_generation,
|
|
2190
|
+
operation_id=self.active_operation.operation_id,
|
|
2191
|
+
error=close.error_text,
|
|
2192
|
+
)
|
|
2193
|
+
self._flush_journal()
|
|
2194
|
+
self.state = OperationState.PARTIAL_WRITEBACK_FAILURE
|
|
2195
|
+
raise PartialWritebackError(
|
|
2196
|
+
f"Capture cleanup failed after write-back: {close.error_text}"
|
|
2197
|
+
)
|
|
2198
|
+
self._record(
|
|
2199
|
+
"write-journal.jsonl",
|
|
2200
|
+
"capture_cleanup_completed",
|
|
2201
|
+
runtime_generation=self.runtime_generation,
|
|
2202
|
+
operation_id=self.active_operation.operation_id,
|
|
2203
|
+
)
|
|
2204
|
+
self._flush_journal()
|
|
2205
|
+
self.state = OperationState.RESUMING
|
|
2206
|
+
attempt.continue_state = "planned"
|
|
2207
|
+
self._record(
|
|
2208
|
+
"write-journal.jsonl",
|
|
2209
|
+
"continue_planned",
|
|
2210
|
+
attempt_id=attempt.spec.attempt_id,
|
|
2211
|
+
runtime_generation=self.runtime_generation,
|
|
2212
|
+
operation_id=self.active_operation.operation_id,
|
|
2213
|
+
)
|
|
2214
|
+
self._flush_journal()
|
|
2215
|
+
attempt.continue_state = "sent"
|
|
2216
|
+
self._record(
|
|
2217
|
+
"write-journal.jsonl",
|
|
2218
|
+
"continue_sent",
|
|
2219
|
+
attempt_id=attempt.spec.attempt_id,
|
|
2220
|
+
runtime_generation=self.runtime_generation,
|
|
2221
|
+
operation_id=self.active_operation.operation_id,
|
|
2222
|
+
)
|
|
2223
|
+
self._flush_journal()
|
|
2224
|
+
try:
|
|
2225
|
+
if on_transport_dispatch is not None:
|
|
2226
|
+
on_transport_dispatch()
|
|
2227
|
+
self.require_debug_workspace_ready()
|
|
2228
|
+
self.session.continue_()
|
|
2229
|
+
except RdbgTransportError as error:
|
|
2230
|
+
# The transport gives no pre-send acknowledgement. Treat this as
|
|
2231
|
+
# an ambiguous continuation: the paused frame may already be gone.
|
|
2232
|
+
self._clear_capture_inspection()
|
|
2233
|
+
self.state = OperationState.RECOVERING
|
|
2234
|
+
attempt.continue_state = "outcome_unknown"
|
|
2235
|
+
self._record(
|
|
2236
|
+
"write-journal.jsonl",
|
|
2237
|
+
"continue_outcome_unknown",
|
|
2238
|
+
attempt_id=attempt.spec.attempt_id,
|
|
2239
|
+
runtime_generation=self.runtime_generation,
|
|
2240
|
+
operation_id=self.active_operation.operation_id,
|
|
2241
|
+
error=str(error),
|
|
2242
|
+
)
|
|
2243
|
+
self._flush_journal()
|
|
2244
|
+
raise
|
|
2245
|
+
# The debugger has accepted Continue; no frame-backed resolver may
|
|
2246
|
+
# survive into the next stop, even if the subsequent wait is unknown.
|
|
2247
|
+
self._clear_capture_inspection()
|
|
2248
|
+
attempt.continue_state = "acknowledged"
|
|
2249
|
+
self._record(
|
|
2250
|
+
"write-journal.jsonl",
|
|
2251
|
+
"continue_acknowledged",
|
|
2252
|
+
attempt_id=attempt.spec.attempt_id,
|
|
2253
|
+
runtime_generation=self.runtime_generation,
|
|
2254
|
+
operation_id=self.active_operation.operation_id,
|
|
2255
|
+
)
|
|
2256
|
+
self.continue_sent = True
|
|
2257
|
+
self._flush_journal()
|
|
2258
|
+
self._inject(
|
|
2259
|
+
FaultPoint.AFTER_CONTINUE_ACK,
|
|
2260
|
+
RecoveryPhase.RESUMING,
|
|
2261
|
+
)
|
|
2262
|
+
stop = self.session.wait_for_any_stop(timeout_s=self.command_timeout_s)
|
|
2263
|
+
return self._route_stop(stop)
|
|
2264
|
+
|
|
2265
|
+
def _lose_generation(
|
|
2266
|
+
self,
|
|
2267
|
+
checkpoint: RecoveryCheckpoint,
|
|
2268
|
+
reason: str,
|
|
2269
|
+
) -> RecoveryResult:
|
|
2270
|
+
self.state = OperationState.LOST
|
|
2271
|
+
self._record(
|
|
2272
|
+
"recovery-transitions.jsonl",
|
|
2273
|
+
"lost",
|
|
2274
|
+
checkpoint_sequence=checkpoint.sequence,
|
|
2275
|
+
phase=checkpoint.phase.value,
|
|
2276
|
+
reason=reason,
|
|
2277
|
+
)
|
|
2278
|
+
self._flush_journal()
|
|
2279
|
+
self.session.invalidate()
|
|
2280
|
+
if not self._generation_lost_notified:
|
|
2281
|
+
self._generation_lost_notified = True
|
|
2282
|
+
if self.on_generation_lost is not None:
|
|
2283
|
+
self.on_generation_lost(checkpoint)
|
|
2284
|
+
return RecoveryResult(
|
|
2285
|
+
RecoveryOutcome.LOST,
|
|
2286
|
+
checkpoint.phase,
|
|
2287
|
+
checkpoint,
|
|
2288
|
+
reason,
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
@staticmethod
|
|
2292
|
+
def _validate_target_identity(
|
|
2293
|
+
checkpoint: RecoveryCheckpoint,
|
|
2294
|
+
reconnected: ReconnectedSession,
|
|
2295
|
+
) -> None:
|
|
2296
|
+
target = reconnected.session.target
|
|
2297
|
+
if target is None:
|
|
2298
|
+
raise ProtocolError("Recovered session has no selected target")
|
|
2299
|
+
if target.target_id != checkpoint.target.target_id:
|
|
2300
|
+
raise ProtocolError("Recovered target identity changed")
|
|
2301
|
+
if target.target_type != checkpoint.target.target_type:
|
|
2302
|
+
raise ProtocolError("Recovered target type changed")
|
|
2303
|
+
|
|
2304
|
+
def recover_transport(
|
|
2305
|
+
self,
|
|
2306
|
+
reconnector: Callable[[RecoveryCheckpoint, bool], ReconnectedSession],
|
|
2307
|
+
) -> RecoveryResult:
|
|
2308
|
+
self._require_state(OperationState.RECOVERING)
|
|
2309
|
+
checkpoint = self.recovery_checkpoint
|
|
2310
|
+
if checkpoint is None:
|
|
2311
|
+
raise ProtocolError("RECOVERING state has no checkpoint")
|
|
2312
|
+
self._record(
|
|
2313
|
+
"recovery-transitions.jsonl",
|
|
2314
|
+
"recovering",
|
|
2315
|
+
checkpoint_sequence=checkpoint.sequence,
|
|
2316
|
+
phase=checkpoint.phase.value,
|
|
2317
|
+
)
|
|
2318
|
+
self._flush_journal()
|
|
2319
|
+
if checkpoint.phase is RecoveryPhase.FLUSHING:
|
|
2320
|
+
return self._lose_generation(
|
|
2321
|
+
checkpoint,
|
|
2322
|
+
"partial write-back cannot be replayed",
|
|
2323
|
+
)
|
|
2324
|
+
|
|
2325
|
+
reconnected: ReconnectedSession | None = None
|
|
2326
|
+
try:
|
|
2327
|
+
reconnected = reconnector(
|
|
2328
|
+
checkpoint,
|
|
2329
|
+
checkpoint.phase is RecoveryPhase.RESUMING,
|
|
2330
|
+
)
|
|
2331
|
+
self._validate_target_identity(checkpoint, reconnected)
|
|
2332
|
+
if checkpoint.phase is RecoveryPhase.CAPTURED:
|
|
2333
|
+
if reconnected.evidence is None:
|
|
2334
|
+
raise ProtocolError("Paused recovery has no frame evidence")
|
|
2335
|
+
validate_paused_identity(checkpoint, reconnected.evidence)
|
|
2336
|
+
old_session = self.session
|
|
2337
|
+
self.session = reconnected.session
|
|
2338
|
+
self.breakpoint_workspace_owner._adopt_confirmed_session(
|
|
2339
|
+
reconnected.session,
|
|
2340
|
+
checkpoint.breakpoint_workspace,
|
|
2341
|
+
)
|
|
2342
|
+
old_session.invalidate()
|
|
2343
|
+
self.state = OperationState.CAPTURED
|
|
2344
|
+
self._record(
|
|
2345
|
+
"recovery-transitions.jsonl",
|
|
2346
|
+
"recovered",
|
|
2347
|
+
checkpoint_sequence=checkpoint.sequence,
|
|
2348
|
+
phase=checkpoint.phase.value,
|
|
2349
|
+
)
|
|
2350
|
+
self._flush_journal()
|
|
2351
|
+
return RecoveryResult(
|
|
2352
|
+
RecoveryOutcome.RECOVERED,
|
|
2353
|
+
checkpoint.phase,
|
|
2354
|
+
checkpoint,
|
|
2355
|
+
"exact paused identity recovered",
|
|
2356
|
+
)
|
|
2357
|
+
|
|
2358
|
+
stop = (
|
|
2359
|
+
reconnected.evidence.stop
|
|
2360
|
+
if reconnected.evidence is not None
|
|
2361
|
+
else reconnected.session.wait_for_any_stop(
|
|
2362
|
+
timeout_s=self.command_timeout_s
|
|
2363
|
+
)
|
|
2364
|
+
)
|
|
2365
|
+
if stop.target_id != checkpoint.target.target_id:
|
|
2366
|
+
raise ProtocolError("Recovered stop belongs to another target")
|
|
2367
|
+
old_session = self.session
|
|
2368
|
+
self.session = reconnected.session
|
|
2369
|
+
self.breakpoint_workspace_owner._adopt_confirmed_session(
|
|
2370
|
+
reconnected.session,
|
|
2371
|
+
checkpoint.breakpoint_workspace,
|
|
2372
|
+
)
|
|
2373
|
+
old_session.invalidate()
|
|
2374
|
+
self.state = OperationState.RESUMING
|
|
2375
|
+
continuation = self._route_stop(stop)
|
|
2376
|
+
self._record(
|
|
2377
|
+
"recovery-transitions.jsonl",
|
|
2378
|
+
"recovered",
|
|
2379
|
+
checkpoint_sequence=checkpoint.sequence,
|
|
2380
|
+
phase=checkpoint.phase.value,
|
|
2381
|
+
)
|
|
2382
|
+
self._flush_journal()
|
|
2383
|
+
return RecoveryResult(
|
|
2384
|
+
RecoveryOutcome.RECOVERED,
|
|
2385
|
+
checkpoint.phase,
|
|
2386
|
+
checkpoint,
|
|
2387
|
+
"continued operation observed without replay",
|
|
2388
|
+
continuation,
|
|
2389
|
+
)
|
|
2390
|
+
except Exception as error:
|
|
2391
|
+
if reconnected is not None and reconnected.session is not self.session:
|
|
2392
|
+
reconnected.session.invalidate()
|
|
2393
|
+
return self._lose_generation(checkpoint, str(error))
|
|
2394
|
+
|
|
2395
|
+
def _complete_main(self) -> MainCompletion:
|
|
2396
|
+
if self.active_operation is None:
|
|
2397
|
+
self.state = OperationState.FAILED
|
|
2398
|
+
raise ProtocolError("MAIN completion has no active operation")
|
|
2399
|
+
completed_result = self.session.evaluate("ЗавершеннаяКоманда")
|
|
2400
|
+
if completed_result.error_occurred:
|
|
2401
|
+
self.state = OperationState.FAILED
|
|
2402
|
+
raise BslExecutionError(completed_result.error_text)
|
|
2403
|
+
completed_id = self._decode_main_completion_value(
|
|
2404
|
+
completed_result,
|
|
2405
|
+
phase="completed_command",
|
|
2406
|
+
)
|
|
2407
|
+
if completed_id != self.active_operation.operation_id:
|
|
2408
|
+
self.state = OperationState.FAILED
|
|
2409
|
+
raise ProtocolError(
|
|
2410
|
+
f"Completed command {completed_id!r} does not match active "
|
|
2411
|
+
f"operation {self.active_operation.operation_id}"
|
|
2412
|
+
)
|
|
2413
|
+
result_evaluation = self.session.evaluate("Результат")
|
|
2414
|
+
error_evaluation = self.session.evaluate("Ошибка")
|
|
2415
|
+
if result_evaluation.error_occurred:
|
|
2416
|
+
self.state = OperationState.FAILED
|
|
2417
|
+
raise BslExecutionError(result_evaluation.error_text)
|
|
2418
|
+
if error_evaluation.error_occurred:
|
|
2419
|
+
self.state = OperationState.FAILED
|
|
2420
|
+
raise BslExecutionError(error_evaluation.error_text)
|
|
2421
|
+
error_value = self._decode_main_completion_value(
|
|
2422
|
+
error_evaluation,
|
|
2423
|
+
phase="error",
|
|
2424
|
+
)
|
|
2425
|
+
error = "" if error_value is None else str(error_value)
|
|
2426
|
+
diagnostic = None
|
|
2427
|
+
if error and self.active_operation.executed_source is not None:
|
|
2428
|
+
diagnostic = _safe_platform_diagnostic(
|
|
2429
|
+
error,
|
|
2430
|
+
self.active_operation.executed_source,
|
|
2431
|
+
visible_source_context=self.active_operation.visible_source_context,
|
|
2432
|
+
)
|
|
2433
|
+
self.state = OperationState.FAILED if error else OperationState.COMPLETED
|
|
2434
|
+
result_value = self._decode_main_completion_value(
|
|
2435
|
+
result_evaluation,
|
|
2436
|
+
phase="result",
|
|
2437
|
+
)
|
|
2438
|
+
messages = self._take_kernel_cell_messages(
|
|
2439
|
+
self.active_operation.message_collector_key
|
|
2440
|
+
if self.active_operation.messages_intercepted
|
|
2441
|
+
else ""
|
|
2442
|
+
)
|
|
2443
|
+
completion = MainCompletion(
|
|
2444
|
+
self.active_operation,
|
|
2445
|
+
result_value,
|
|
2446
|
+
error,
|
|
2447
|
+
not error,
|
|
2448
|
+
messages,
|
|
2449
|
+
diagnostic,
|
|
2450
|
+
)
|
|
2451
|
+
self._record(
|
|
2452
|
+
"write-journal.jsonl",
|
|
2453
|
+
"main_completed" if completion.succeeded else "main_failed",
|
|
2454
|
+
runtime_generation=self.runtime_generation,
|
|
2455
|
+
operation_id=self.active_operation.operation_id,
|
|
2456
|
+
state_after=self.state.value,
|
|
2457
|
+
visible_sha256=sha256(
|
|
2458
|
+
self.active_operation.visible_source.encode("utf-8")
|
|
2459
|
+
).hexdigest(),
|
|
2460
|
+
lowered_sha256=sha256(
|
|
2461
|
+
self.active_operation.lowered_source.encode("utf-8")
|
|
2462
|
+
).hexdigest(),
|
|
2463
|
+
)
|
|
2464
|
+
self._flush_journal()
|
|
2465
|
+
return completion
|
|
2466
|
+
|
|
2467
|
+
@staticmethod
|
|
2468
|
+
def _decode_main_completion_value(
|
|
2469
|
+
evaluation: EvaluationResult,
|
|
2470
|
+
*,
|
|
2471
|
+
phase: str,
|
|
2472
|
+
) -> object:
|
|
2473
|
+
try:
|
|
2474
|
+
return evaluation_to_python(evaluation)
|
|
2475
|
+
except Exception as error:
|
|
2476
|
+
raise CompletionDecodeError(phase, evaluation) from error
|
|
2477
|
+
|
|
2478
|
+
def _take_capture_cell_messages(
|
|
2479
|
+
self, message_collector_key: str
|
|
2480
|
+
) -> tuple[str, ...]:
|
|
2481
|
+
return self._take_context_cell_messages(
|
|
2482
|
+
message_collector_key,
|
|
2483
|
+
stack_level=self._required_capture_kernel_stack_level(),
|
|
2484
|
+
)
|
|
2485
|
+
|
|
2486
|
+
def _take_kernel_cell_messages(
|
|
2487
|
+
self, message_collector_key: str
|
|
2488
|
+
) -> tuple[str, ...]:
|
|
2489
|
+
return self._take_context_cell_messages(message_collector_key, stack_level=0)
|
|
2490
|
+
|
|
2491
|
+
def _take_context_cell_messages(
|
|
2492
|
+
self, message_collector_key: str, *, stack_level: int
|
|
2493
|
+
) -> tuple[str, ...]:
|
|
2494
|
+
if not message_collector_key:
|
|
2495
|
+
return ()
|
|
2496
|
+
result = self.session.evaluate(
|
|
2497
|
+
"RuntimeKernelServer.ЗабратьСообщенияЯчейкиИзКонтекста(Контекст, "
|
|
2498
|
+
+ bsl_string_literal(message_collector_key)
|
|
2499
|
+
+ ")",
|
|
2500
|
+
stack_level=stack_level,
|
|
2501
|
+
)
|
|
2502
|
+
if result.error_occurred:
|
|
2503
|
+
raise BslExecutionError(result.error_text)
|
|
2504
|
+
payload = evaluation_to_python(result)
|
|
2505
|
+
if not isinstance(payload, str):
|
|
2506
|
+
raise ProtocolError("Cell message payload is not JSON text")
|
|
2507
|
+
try:
|
|
2508
|
+
messages = json.loads(payload)
|
|
2509
|
+
except json.JSONDecodeError as error:
|
|
2510
|
+
raise ProtocolError("Cell message payload is invalid JSON") from error
|
|
2511
|
+
if not isinstance(messages, list) or any(
|
|
2512
|
+
not isinstance(message, str) for message in messages
|
|
2513
|
+
):
|
|
2514
|
+
raise ProtocolError("Cell message payload is not a text array")
|
|
2515
|
+
return tuple(messages)
|
|
2516
|
+
|
|
2517
|
+
def take_context_string(self, key: str, *, max_text_size: int) -> str:
|
|
2518
|
+
if not re.fullmatch(
|
|
2519
|
+
r"__(?:onec_compact_table|onec_value)_[0-9a-f]{32}", key
|
|
2520
|
+
):
|
|
2521
|
+
raise ProtocolError("materialization context key is invalid")
|
|
2522
|
+
if type(max_text_size) is not int or max_text_size <= 0:
|
|
2523
|
+
raise ProtocolError("compact table maximum text size is invalid")
|
|
2524
|
+
stack_level = (
|
|
2525
|
+
self._required_capture_kernel_stack_level()
|
|
2526
|
+
if self.state is OperationState.CAPTURED
|
|
2527
|
+
else 0
|
|
2528
|
+
)
|
|
2529
|
+
result = self.session.evaluate(
|
|
2530
|
+
"RuntimeKernelServer."
|
|
2531
|
+
"ЗабратьКомпактнуюМатериализациюИзКонтекста(Контекст, "
|
|
2532
|
+
+ bsl_string_literal(key)
|
|
2533
|
+
+ ")",
|
|
2534
|
+
timeout_s=self.command_timeout_s,
|
|
2535
|
+
max_text_size=max_text_size,
|
|
2536
|
+
stack_level=stack_level,
|
|
2537
|
+
)
|
|
2538
|
+
if result.error_occurred:
|
|
2539
|
+
raise BslExecutionError(result.error_text)
|
|
2540
|
+
value = evaluation_to_python(result)
|
|
2541
|
+
if not isinstance(value, str):
|
|
2542
|
+
raise ProtocolError("compact table payload is not a string")
|
|
2543
|
+
return value
|
|
2544
|
+
|
|
2545
|
+
def drop_context_value(self, key: str) -> None:
|
|
2546
|
+
if not re.fullmatch(
|
|
2547
|
+
r"__(?:onec_compact_table|onec_value|onec_projection)_[0-9a-f]{32}",
|
|
2548
|
+
key,
|
|
2549
|
+
):
|
|
2550
|
+
raise ProtocolError("materialization context key is invalid")
|
|
2551
|
+
stack_level = (
|
|
2552
|
+
self._required_capture_kernel_stack_level()
|
|
2553
|
+
if self.state is OperationState.CAPTURED
|
|
2554
|
+
else 0
|
|
2555
|
+
)
|
|
2556
|
+
result = self.session.evaluate(
|
|
2557
|
+
"RuntimeKernelServer."
|
|
2558
|
+
"УдалитьМатериализациюИзКонтекста(Контекст, "
|
|
2559
|
+
+ bsl_string_literal(key)
|
|
2560
|
+
+ ")",
|
|
2561
|
+
timeout_s=self.command_timeout_s,
|
|
2562
|
+
stack_level=stack_level,
|
|
2563
|
+
)
|
|
2564
|
+
if result.error_occurred:
|
|
2565
|
+
raise BslExecutionError(result.error_text)
|
|
2566
|
+
|
|
2567
|
+
def inspect_table_sample(
|
|
2568
|
+
self,
|
|
2569
|
+
handle: str,
|
|
2570
|
+
*,
|
|
2571
|
+
page_size: int = 16,
|
|
2572
|
+
) -> EvaluationResult:
|
|
2573
|
+
if not re.fullmatch(
|
|
2574
|
+
r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*)*", handle, re.UNICODE
|
|
2575
|
+
):
|
|
2576
|
+
raise ProtocolError(
|
|
2577
|
+
"table handle must be one direct or dotted persistent Context path"
|
|
2578
|
+
)
|
|
2579
|
+
if type(page_size) is not int or not 1 <= page_size <= 64:
|
|
2580
|
+
raise ProtocolError("table schema sample size is invalid")
|
|
2581
|
+
stack_level = (
|
|
2582
|
+
self._required_capture_kernel_stack_level()
|
|
2583
|
+
if self.state is OperationState.CAPTURED
|
|
2584
|
+
else 0
|
|
2585
|
+
)
|
|
2586
|
+
result = self.session.evaluate_collection(
|
|
2587
|
+
handle,
|
|
2588
|
+
start_index=0,
|
|
2589
|
+
page_size=page_size,
|
|
2590
|
+
timeout_s=self.command_timeout_s,
|
|
2591
|
+
max_text_size=4096,
|
|
2592
|
+
stack_level=stack_level,
|
|
2593
|
+
)
|
|
2594
|
+
if result.error_occurred:
|
|
2595
|
+
raise BslExecutionError(result.error_text)
|
|
2596
|
+
return result
|
|
2597
|
+
|
|
2598
|
+
def inspect_completion_fields(self, handle: str, *, table_row: bool) -> EvaluationResult:
|
|
2599
|
+
"""Inspect bounded column/key names, without serializing field values."""
|
|
2600
|
+
if (not isinstance(handle, str) or len(handle) > 512
|
|
2601
|
+
or not re.fullmatch(r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*){0,7}", handle)
|
|
2602
|
+
or type(table_row) is not bool):
|
|
2603
|
+
raise ProtocolError("Completion requires a direct or dotted Context path")
|
|
2604
|
+
stack_level = (
|
|
2605
|
+
self._required_capture_kernel_stack_level()
|
|
2606
|
+
if self.state is OperationState.CAPTURED else 0
|
|
2607
|
+
)
|
|
2608
|
+
result = self.session.evaluate_collection(
|
|
2609
|
+
"RuntimeValueTransferServer.ПолучитьИменаСвойствДляПодсказки("
|
|
2610
|
+
+ handle + (", Истина)" if table_row else ", Ложь)"),
|
|
2611
|
+
start_index=0, page_size=128, max_text_size=512,
|
|
2612
|
+
timeout_s=self.command_timeout_s, stack_level=stack_level,
|
|
2613
|
+
)
|
|
2614
|
+
if result.error_occurred:
|
|
2615
|
+
raise BslExecutionError(result.error_text)
|
|
2616
|
+
return result
|
|
2617
|
+
|
|
2618
|
+
def inspect_declared_table_schema(
|
|
2619
|
+
self, handle: str, *, timeout_s: float | None = None
|
|
2620
|
+
) -> EvaluationResult:
|
|
2621
|
+
deadline = self._capture_command_deadline(timeout_s)
|
|
2622
|
+
if not re.fullmatch(
|
|
2623
|
+
r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*)*", handle, re.UNICODE
|
|
2624
|
+
):
|
|
2625
|
+
raise ProtocolError(
|
|
2626
|
+
"table handle must be one direct or dotted persistent Context path"
|
|
2627
|
+
)
|
|
2628
|
+
stack_level = (
|
|
2629
|
+
self._required_capture_kernel_stack_level()
|
|
2630
|
+
if self.state is OperationState.CAPTURED
|
|
2631
|
+
else 0
|
|
2632
|
+
)
|
|
2633
|
+
result = self.session.evaluate_collection(
|
|
2634
|
+
"RuntimeTableTransferServer.ПолучитьКомпактнуюСхему(" + handle + ")",
|
|
2635
|
+
start_index=0,
|
|
2636
|
+
page_size=64,
|
|
2637
|
+
timeout_s=self._capture_remaining_timeout(deadline),
|
|
2638
|
+
max_text_size=4096,
|
|
2639
|
+
stack_level=stack_level,
|
|
2640
|
+
)
|
|
2641
|
+
self._capture_remaining_timeout(deadline)
|
|
2642
|
+
if result.error_occurred:
|
|
2643
|
+
raise BslExecutionError(result.error_text)
|
|
2644
|
+
return result
|