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,913 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from time import monotonic, sleep
|
|
8
|
+
from uuid import UUID, uuid4
|
|
9
|
+
|
|
10
|
+
from onec_runtime.errors import (
|
|
11
|
+
CommandTimeout,
|
|
12
|
+
ProtocolError,
|
|
13
|
+
RdbgDebugUiNotRegistered,
|
|
14
|
+
TransportRecoveryError,
|
|
15
|
+
TargetLost,
|
|
16
|
+
UnexpectedStop,
|
|
17
|
+
)
|
|
18
|
+
from onec_runtime.performance_profile import PhaseRecorder
|
|
19
|
+
from onec_runtime.rdbg.models import (
|
|
20
|
+
DebugTarget,
|
|
21
|
+
EvaluationResult,
|
|
22
|
+
LocalVariablesResult,
|
|
23
|
+
ModuleLocation,
|
|
24
|
+
ModifyResult,
|
|
25
|
+
PendingEvaluation,
|
|
26
|
+
StopEvent,
|
|
27
|
+
TargetId,
|
|
28
|
+
)
|
|
29
|
+
from onec_runtime.rdbg.transport import RdbgTransport
|
|
30
|
+
from onec_runtime.rdbg.xml_codec import (
|
|
31
|
+
build_attach_request,
|
|
32
|
+
build_attach_target_request,
|
|
33
|
+
build_auto_attach_request,
|
|
34
|
+
build_breakpoints_request,
|
|
35
|
+
build_call_stack_request,
|
|
36
|
+
build_collection_eval_request,
|
|
37
|
+
build_clear_break_request,
|
|
38
|
+
build_detach_request,
|
|
39
|
+
build_eval_request,
|
|
40
|
+
build_get_targets_request,
|
|
41
|
+
build_init_settings_request,
|
|
42
|
+
build_local_variables_request,
|
|
43
|
+
build_modify_request,
|
|
44
|
+
build_step_request,
|
|
45
|
+
build_terminate_request,
|
|
46
|
+
parse_eval_result,
|
|
47
|
+
parse_eval_response,
|
|
48
|
+
parse_call_stack,
|
|
49
|
+
parse_local_variables_result,
|
|
50
|
+
parse_modify_result,
|
|
51
|
+
parse_ping_document,
|
|
52
|
+
parse_ping_evaluations_from_document,
|
|
53
|
+
parse_ping_events_from_document,
|
|
54
|
+
parse_ping_local_variables_from_document,
|
|
55
|
+
parse_ping_target_events_from_document,
|
|
56
|
+
parse_targets,
|
|
57
|
+
validate_command_acknowledgement,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SessionState(Enum):
|
|
62
|
+
DETACHED = "detached"
|
|
63
|
+
ATTACHED = "attached"
|
|
64
|
+
READY = "ready"
|
|
65
|
+
EXECUTING = "executing"
|
|
66
|
+
FAILED = "failed"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class _PendingEvaluationState:
|
|
71
|
+
capability: PendingEvaluation
|
|
72
|
+
suspended_stop: StopEvent | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RdbgSession:
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
transport: RdbgTransport,
|
|
79
|
+
expected_location: ModuleLocation,
|
|
80
|
+
*,
|
|
81
|
+
alias: str = "DefAlias",
|
|
82
|
+
ui_id: UUID | None = None,
|
|
83
|
+
break_on_next: bool = False,
|
|
84
|
+
server_target_type: str = "ServerEmulation",
|
|
85
|
+
) -> None:
|
|
86
|
+
if server_target_type not in {"ServerEmulation", "Server"}:
|
|
87
|
+
raise ValueError("Unsupported server target type")
|
|
88
|
+
self.transport = transport
|
|
89
|
+
self.expected_location = expected_location
|
|
90
|
+
self.alias = alias
|
|
91
|
+
self.ui_id = ui_id or uuid4()
|
|
92
|
+
self.break_on_next = break_on_next
|
|
93
|
+
self.server_target_type = server_target_type
|
|
94
|
+
self._preexisting_target_ids: set[UUID] = set()
|
|
95
|
+
self._managed_client_id: UUID | None = None
|
|
96
|
+
self._bound_client_target: TargetId | None = None
|
|
97
|
+
self.state = SessionState.DETACHED
|
|
98
|
+
self.target: DebugTarget | None = None
|
|
99
|
+
self.attached_targets: dict[UUID, DebugTarget] = {}
|
|
100
|
+
self._breakpoint_installed = False
|
|
101
|
+
self._breakpoint_locations: tuple[ModuleLocation, ...] = ()
|
|
102
|
+
self._pending_evaluations: dict[UUID, EvaluationResult] = {}
|
|
103
|
+
self._pending_local_variables: dict[UUID, LocalVariablesResult] = {}
|
|
104
|
+
self._event_queue: deque[StopEvent | EvaluationResult] = deque()
|
|
105
|
+
self._evaluation_owner = object()
|
|
106
|
+
self._pending_evaluation_states: dict[int, _PendingEvaluationState] = {}
|
|
107
|
+
self.profiler: PhaseRecorder | None = None
|
|
108
|
+
|
|
109
|
+
def _profile(self, phase: str, operation, **metadata): # type: ignore[no-untyped-def]
|
|
110
|
+
if self.profiler is None:
|
|
111
|
+
return operation()
|
|
112
|
+
return self.profiler.measure(phase, operation, **metadata)
|
|
113
|
+
|
|
114
|
+
def _require(self, *states: SessionState) -> None:
|
|
115
|
+
if self.state not in states:
|
|
116
|
+
expected = ", ".join(state.value for state in states)
|
|
117
|
+
raise ProtocolError(
|
|
118
|
+
f"RDBG operation requires state {expected}; current state is {self.state.value}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def initialize(self) -> None:
|
|
122
|
+
self._require(SessionState.DETACHED)
|
|
123
|
+
self.transport.request(
|
|
124
|
+
"attachDebugUI", build_attach_request(self.alias, self.ui_id)
|
|
125
|
+
)
|
|
126
|
+
# Registration already succeeded; later initialization failures must
|
|
127
|
+
# still allow cleanup of our UI on an externally owned debugger.
|
|
128
|
+
self.state = SessionState.ATTACHED
|
|
129
|
+
if self.server_target_type == "Server":
|
|
130
|
+
self._preexisting_target_ids = {
|
|
131
|
+
target.target_id.id for target in self.list_targets()
|
|
132
|
+
}
|
|
133
|
+
self.transport.request(
|
|
134
|
+
"initSettings",
|
|
135
|
+
build_init_settings_request(
|
|
136
|
+
self.alias, self.ui_id, break_on_next=self.break_on_next
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
self.transport.request(
|
|
140
|
+
"setAutoAttachSettings",
|
|
141
|
+
build_auto_attach_request(
|
|
142
|
+
self.alias, self.ui_id, target_types=self._auto_attach_target_types()
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _auto_attach_target_types(self) -> tuple[str, ...]:
|
|
147
|
+
if self.server_target_type == "Server" and self._bound_client_target is None:
|
|
148
|
+
return ("ManagedClient",)
|
|
149
|
+
return self.server_target_type, "ManagedClient"
|
|
150
|
+
|
|
151
|
+
def bind_server_session(self, *, launch_token: str) -> None:
|
|
152
|
+
"""Allow server subjects only for the managed client we stopped in."""
|
|
153
|
+
self._require(SessionState.READY)
|
|
154
|
+
if (
|
|
155
|
+
self.target is None
|
|
156
|
+
or self.target.target_type != "ManagedClient"
|
|
157
|
+
or self.target.target_id.seance_id is None
|
|
158
|
+
):
|
|
159
|
+
raise ProtocolError("Server bootstrap requires a managed client session identity")
|
|
160
|
+
if self._bound_client_target is not None and self._bound_client_target != self.target.target_id:
|
|
161
|
+
raise ProtocolError("Server bootstrap cannot change its managed client session")
|
|
162
|
+
# A fresh subject is not necessarily ours. Read the launch argument
|
|
163
|
+
# before binding a server session or changing any extension guard.
|
|
164
|
+
if not launch_token:
|
|
165
|
+
raise ProtocolError("Server bootstrap requires a launch identity")
|
|
166
|
+
launch = self.evaluate("ПараметрЗапуска")
|
|
167
|
+
if (
|
|
168
|
+
launch.error_occurred
|
|
169
|
+
or launch.type_name != "Строка"
|
|
170
|
+
or launch.presentation not in (launch_token, '"' + launch_token + '"')
|
|
171
|
+
):
|
|
172
|
+
raise ProtocolError("Managed client launch identity does not match the owned process")
|
|
173
|
+
self._bound_client_target = self.target.target_id
|
|
174
|
+
self.transport.request(
|
|
175
|
+
"setAutoAttachSettings",
|
|
176
|
+
build_auto_attach_request(
|
|
177
|
+
self.alias, self.ui_id, target_types=self._auto_attach_target_types()
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
# A cluster registers its server subject before the managed client.
|
|
181
|
+
# Its earlier targetStarted event was deliberately ignored until we
|
|
182
|
+
# could authenticate the client and correlate the session identity.
|
|
183
|
+
self._attach_discovered_targets(self.list_targets())
|
|
184
|
+
|
|
185
|
+
def _attachable_targets(self, targets: list[DebugTarget]) -> list[DebugTarget]:
|
|
186
|
+
if self.server_target_type != "Server":
|
|
187
|
+
return targets
|
|
188
|
+
candidates = [
|
|
189
|
+
target for target in targets
|
|
190
|
+
if target.target_id.infobase_alias.casefold() == self.alias.casefold()
|
|
191
|
+
and target.target_id.id not in self._preexisting_target_ids
|
|
192
|
+
and target.target_type in self._auto_attach_target_types()
|
|
193
|
+
]
|
|
194
|
+
client = self._bound_client_target
|
|
195
|
+
if client is not None:
|
|
196
|
+
return [
|
|
197
|
+
target for target in candidates
|
|
198
|
+
if target.target_id.seance_id == client.seance_id
|
|
199
|
+
and (
|
|
200
|
+
client.infobase_instance_id is None
|
|
201
|
+
or target.target_id.infobase_instance_id is None
|
|
202
|
+
or target.target_id.infobase_instance_id == client.infobase_instance_id
|
|
203
|
+
)
|
|
204
|
+
and (target.target_type != "ManagedClient" or target.target_id.id == client.id)
|
|
205
|
+
]
|
|
206
|
+
client_ids = {target.target_id.id for target in candidates}
|
|
207
|
+
if self._managed_client_id is not None:
|
|
208
|
+
client_ids.add(self._managed_client_id)
|
|
209
|
+
if len(client_ids) > 1:
|
|
210
|
+
raise ProtocolError("Multiple new managed client targets make bootstrap ambiguous")
|
|
211
|
+
if client_ids:
|
|
212
|
+
self._managed_client_id = next(iter(client_ids))
|
|
213
|
+
return candidates
|
|
214
|
+
|
|
215
|
+
def list_targets(self) -> list[DebugTarget]:
|
|
216
|
+
payload = self.transport.request(
|
|
217
|
+
"getDbgAllTargetStates", build_get_targets_request(self.alias, self.ui_id)
|
|
218
|
+
)
|
|
219
|
+
return parse_targets(payload)
|
|
220
|
+
|
|
221
|
+
def terminate_bound_server_session(self) -> bool:
|
|
222
|
+
"""Stop our server calls, then client; return whether native client exit was requested."""
|
|
223
|
+
if self.server_target_type != "Server" or self._bound_client_target is None:
|
|
224
|
+
return False
|
|
225
|
+
payload = self.transport.request(
|
|
226
|
+
"getDbgAllTargetStates", build_get_targets_request(self.alias, self.ui_id),
|
|
227
|
+
timeout_s=10.0,
|
|
228
|
+
)
|
|
229
|
+
targets = tuple(
|
|
230
|
+
target.target_id for target in self._attachable_targets(parse_targets(payload))
|
|
231
|
+
if target.target_type == "Server"
|
|
232
|
+
)
|
|
233
|
+
if targets:
|
|
234
|
+
response = self.transport.request(
|
|
235
|
+
"terminateDbgTarget",
|
|
236
|
+
build_terminate_request(self.alias, self.ui_id, targets, payload),
|
|
237
|
+
timeout_s=10.0,
|
|
238
|
+
)
|
|
239
|
+
validate_command_acknowledgement(response, command="terminateDbgTarget")
|
|
240
|
+
|
|
241
|
+
# Hard-killing a managed client leaves a stale subject in the shared
|
|
242
|
+
# debugger. Let 1C remove its own subject after the server calls stop.
|
|
243
|
+
# Rediscover the full identity: the server termination may change it.
|
|
244
|
+
payload = self.transport.request(
|
|
245
|
+
"getDbgAllTargetStates", build_get_targets_request(self.alias, self.ui_id),
|
|
246
|
+
timeout_s=10.0,
|
|
247
|
+
)
|
|
248
|
+
client = self._bound_client_target
|
|
249
|
+
if not any(target.target_id.id == client.id for target in parse_targets(payload)):
|
|
250
|
+
return False
|
|
251
|
+
response = self.transport.request(
|
|
252
|
+
"terminateDbgTarget",
|
|
253
|
+
build_terminate_request(
|
|
254
|
+
self.alias, self.ui_id, (client,), payload, target_type="ManagedClient",
|
|
255
|
+
),
|
|
256
|
+
timeout_s=10.0,
|
|
257
|
+
)
|
|
258
|
+
validate_command_acknowledgement(response, command="terminateDbgTarget")
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
def discover_server_emulation_target(self, *, timeout_s: float = 30.0) -> DebugTarget:
|
|
262
|
+
self._require(SessionState.ATTACHED)
|
|
263
|
+
deadline = monotonic() + timeout_s
|
|
264
|
+
while monotonic() < deadline:
|
|
265
|
+
matches = [
|
|
266
|
+
target
|
|
267
|
+
for target in self.list_targets()
|
|
268
|
+
if target.target_type == "ServerEmulation"
|
|
269
|
+
and target.target_id.infobase_alias == self.alias
|
|
270
|
+
]
|
|
271
|
+
if len(matches) == 1:
|
|
272
|
+
self.target = matches[0]
|
|
273
|
+
return matches[0]
|
|
274
|
+
if len(matches) > 1:
|
|
275
|
+
raise ProtocolError(
|
|
276
|
+
f"Expected one ServerEmulation target, found {len(matches)}"
|
|
277
|
+
)
|
|
278
|
+
sleep(0.1)
|
|
279
|
+
raise CommandTimeout("Timed out waiting for the ServerEmulation target")
|
|
280
|
+
|
|
281
|
+
def attach_target(self, target: DebugTarget) -> None:
|
|
282
|
+
self._require(SessionState.ATTACHED)
|
|
283
|
+
self.transport.request(
|
|
284
|
+
"clearBreakOnNextStatement",
|
|
285
|
+
build_clear_break_request(self.alias, self.ui_id),
|
|
286
|
+
)
|
|
287
|
+
self.transport.request(
|
|
288
|
+
"attachDetachDbgTargets",
|
|
289
|
+
build_attach_target_request(
|
|
290
|
+
self.alias, self.ui_id, target.target_id, attach=True
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
self.target = target
|
|
294
|
+
self.attached_targets[target.target_id.id] = target
|
|
295
|
+
|
|
296
|
+
def set_service_breakpoint(self) -> None:
|
|
297
|
+
self.set_breakpoints((self.expected_location,))
|
|
298
|
+
|
|
299
|
+
def verify_registration(self) -> None:
|
|
300
|
+
"""Probe this UI before starting the client whose first stop it owns."""
|
|
301
|
+
self._require(SessionState.ATTACHED)
|
|
302
|
+
self.transport.request(
|
|
303
|
+
"pingDebugUIParams",
|
|
304
|
+
b"",
|
|
305
|
+
timeout_s=0.2,
|
|
306
|
+
dbgui=str(self.ui_id),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def set_breakpoints(self, locations: tuple[ModuleLocation, ...]) -> None:
|
|
310
|
+
self._require(SessionState.ATTACHED, SessionState.READY)
|
|
311
|
+
if not locations:
|
|
312
|
+
raise ValueError("At least one breakpoint location is required")
|
|
313
|
+
response = self.transport.request(
|
|
314
|
+
"setBreakpoints",
|
|
315
|
+
build_breakpoints_request(self.alias, self.ui_id, locations),
|
|
316
|
+
)
|
|
317
|
+
validate_command_acknowledgement(response, command="setBreakpoints")
|
|
318
|
+
self._breakpoint_installed = True
|
|
319
|
+
self._breakpoint_locations = tuple(locations)
|
|
320
|
+
|
|
321
|
+
def _poll(
|
|
322
|
+
self,
|
|
323
|
+
timeout_s: float,
|
|
324
|
+
*,
|
|
325
|
+
profile_page_start: int | None = None,
|
|
326
|
+
profile_result_id: str = "",
|
|
327
|
+
) -> tuple[list[StopEvent], list[EvaluationResult]]:
|
|
328
|
+
metadata = {
|
|
329
|
+
"page_start": profile_page_start,
|
|
330
|
+
"result_id": profile_result_id,
|
|
331
|
+
}
|
|
332
|
+
payload = self._profile(
|
|
333
|
+
"rdbg.ping.request",
|
|
334
|
+
lambda: self.transport.request(
|
|
335
|
+
"pingDebugUIParams",
|
|
336
|
+
b"",
|
|
337
|
+
timeout_s=timeout_s,
|
|
338
|
+
dbgui=str(self.ui_id),
|
|
339
|
+
),
|
|
340
|
+
output_bytes=len,
|
|
341
|
+
**metadata,
|
|
342
|
+
)
|
|
343
|
+
if not payload.strip():
|
|
344
|
+
return [], []
|
|
345
|
+
document = self._profile(
|
|
346
|
+
"rdbg.ping.parse_xml",
|
|
347
|
+
lambda: parse_ping_document(payload),
|
|
348
|
+
input_bytes=len(payload),
|
|
349
|
+
**metadata,
|
|
350
|
+
)
|
|
351
|
+
targets = self._profile(
|
|
352
|
+
"rdbg.ping.extract_targets",
|
|
353
|
+
lambda: parse_ping_target_events_from_document(document),
|
|
354
|
+
item_count=len,
|
|
355
|
+
**metadata,
|
|
356
|
+
)
|
|
357
|
+
self._attach_discovered_targets(targets)
|
|
358
|
+
stops = self._profile(
|
|
359
|
+
"rdbg.ping.extract_stops",
|
|
360
|
+
lambda: parse_ping_events_from_document(document),
|
|
361
|
+
item_count=len,
|
|
362
|
+
**metadata,
|
|
363
|
+
)
|
|
364
|
+
local_variables = self._profile(
|
|
365
|
+
"rdbg.ping.extract_local_variables",
|
|
366
|
+
lambda: parse_ping_local_variables_from_document(document),
|
|
367
|
+
item_count=len,
|
|
368
|
+
**metadata,
|
|
369
|
+
)
|
|
370
|
+
for result in local_variables:
|
|
371
|
+
self._pending_local_variables[result.result_id] = result
|
|
372
|
+
evaluations = self._profile(
|
|
373
|
+
"rdbg.ping.extract_evaluations",
|
|
374
|
+
lambda: parse_ping_evaluations_from_document(document),
|
|
375
|
+
item_count=len,
|
|
376
|
+
**metadata,
|
|
377
|
+
)
|
|
378
|
+
for evaluation in evaluations:
|
|
379
|
+
self._pending_evaluations[evaluation.result_id] = evaluation
|
|
380
|
+
return stops, evaluations
|
|
381
|
+
|
|
382
|
+
def _attach_discovered_targets(self, targets: list[DebugTarget]) -> None:
|
|
383
|
+
for target in self._attachable_targets(targets):
|
|
384
|
+
if target.target_id.id in self.attached_targets:
|
|
385
|
+
continue
|
|
386
|
+
if not self.break_on_next:
|
|
387
|
+
self.transport.request(
|
|
388
|
+
"clearBreakOnNextStatement",
|
|
389
|
+
build_clear_break_request(self.alias, self.ui_id),
|
|
390
|
+
)
|
|
391
|
+
self.transport.request(
|
|
392
|
+
"attachDetachDbgTargets",
|
|
393
|
+
build_attach_target_request(
|
|
394
|
+
self.alias, self.ui_id, target.target_id, attach=True
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
self.attached_targets[target.target_id.id] = target
|
|
398
|
+
if self._breakpoint_installed:
|
|
399
|
+
self.transport.request(
|
|
400
|
+
"setBreakpoints",
|
|
401
|
+
build_breakpoints_request(
|
|
402
|
+
self.alias, self.ui_id, self._breakpoint_locations
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def _ingest_poll_events(
|
|
407
|
+
self,
|
|
408
|
+
stops: list[StopEvent],
|
|
409
|
+
evaluations: list[EvaluationResult],
|
|
410
|
+
) -> None:
|
|
411
|
+
self._event_queue.extend(stops)
|
|
412
|
+
pending_result_ids = {
|
|
413
|
+
state.capability.result_id
|
|
414
|
+
for state in self._pending_evaluation_states.values()
|
|
415
|
+
}
|
|
416
|
+
self._event_queue.extend(
|
|
417
|
+
result
|
|
418
|
+
for result in evaluations
|
|
419
|
+
if result.result_id in pending_result_ids
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def _pop_queued_stop(self) -> StopEvent | None:
|
|
423
|
+
queued = list(self._event_queue)
|
|
424
|
+
for index, event in enumerate(queued):
|
|
425
|
+
if isinstance(event, StopEvent):
|
|
426
|
+
del queued[index]
|
|
427
|
+
self._event_queue = deque(queued)
|
|
428
|
+
return event
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
def _admit_stop(self, stop: StopEvent) -> StopEvent:
|
|
432
|
+
stopped_target = self.attached_targets.get(stop.target_id.id)
|
|
433
|
+
if stopped_target is None or stopped_target.target_id != stop.target_id:
|
|
434
|
+
raise UnexpectedStop(
|
|
435
|
+
f"Stop belongs to unattached target {stop.target_id.id}"
|
|
436
|
+
)
|
|
437
|
+
self.target = stopped_target
|
|
438
|
+
self.state = SessionState.READY
|
|
439
|
+
return stop
|
|
440
|
+
|
|
441
|
+
def wait_for_service_stop(self, *, timeout_s: float = 60.0) -> StopEvent:
|
|
442
|
+
return self.wait_for_stop((self.expected_location,), timeout_s=timeout_s)
|
|
443
|
+
|
|
444
|
+
def wait_for_stop(
|
|
445
|
+
self,
|
|
446
|
+
allowed_locations: tuple[ModuleLocation, ...],
|
|
447
|
+
*,
|
|
448
|
+
timeout_s: float = 60.0,
|
|
449
|
+
) -> StopEvent:
|
|
450
|
+
if not allowed_locations:
|
|
451
|
+
raise ValueError("At least one allowed stop location is required")
|
|
452
|
+
allowed = frozenset(allowed_locations)
|
|
453
|
+
stop = self.wait_for_any_stop(timeout_s=timeout_s)
|
|
454
|
+
if stop.location not in allowed:
|
|
455
|
+
raise UnexpectedStop(
|
|
456
|
+
f"Stop location {stop.location!r} does not match any allowed "
|
|
457
|
+
f"location: {allowed_locations!r}"
|
|
458
|
+
)
|
|
459
|
+
return stop
|
|
460
|
+
|
|
461
|
+
def wait_for_any_stop(
|
|
462
|
+
self,
|
|
463
|
+
*,
|
|
464
|
+
timeout_s: float = 60.0,
|
|
465
|
+
on_poll: Callable[[], None] | None = None,
|
|
466
|
+
) -> StopEvent:
|
|
467
|
+
self._require(SessionState.ATTACHED, SessionState.EXECUTING)
|
|
468
|
+
deadline = monotonic() + timeout_s
|
|
469
|
+
while monotonic() < deadline:
|
|
470
|
+
if on_poll is not None:
|
|
471
|
+
on_poll()
|
|
472
|
+
queued = self._pop_queued_stop()
|
|
473
|
+
if queued is not None:
|
|
474
|
+
return self._admit_stop(queued)
|
|
475
|
+
remaining = max(0.1, min(6.0, deadline - monotonic()))
|
|
476
|
+
polled = self._poll(remaining)
|
|
477
|
+
self._ingest_poll_events(*polled)
|
|
478
|
+
sleep(0.05)
|
|
479
|
+
self.state = SessionState.FAILED
|
|
480
|
+
raise CommandTimeout("Timed out waiting for a runtime stop")
|
|
481
|
+
|
|
482
|
+
def read_current_stack(self, *, timeout_s: float = 5.0) -> StopEvent:
|
|
483
|
+
self._require(SessionState.ATTACHED, SessionState.READY)
|
|
484
|
+
if self.target is None:
|
|
485
|
+
raise TargetLost("No target has been selected")
|
|
486
|
+
payload = self.transport.request(
|
|
487
|
+
"getCallStack",
|
|
488
|
+
build_call_stack_request(self.alias, self.ui_id, self.target.target_id),
|
|
489
|
+
timeout_s=timeout_s,
|
|
490
|
+
)
|
|
491
|
+
if not payload.strip():
|
|
492
|
+
raise TransportRecoveryError(
|
|
493
|
+
"RDBG getCallStack returned no frame evidence"
|
|
494
|
+
)
|
|
495
|
+
frames = parse_call_stack(payload, self.target.target_id)
|
|
496
|
+
if not frames:
|
|
497
|
+
raise TransportRecoveryError("RDBG getCallStack returned an empty stack")
|
|
498
|
+
locations = tuple(frame.location for frame in frames)
|
|
499
|
+
self.state = SessionState.READY
|
|
500
|
+
return StopEvent(
|
|
501
|
+
self.target.target_id,
|
|
502
|
+
locations[0],
|
|
503
|
+
"recoveredCallStack",
|
|
504
|
+
stack=locations,
|
|
505
|
+
stack_frames=tuple(frames),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def evaluate(
|
|
509
|
+
self,
|
|
510
|
+
expression: str,
|
|
511
|
+
*,
|
|
512
|
+
timeout_s: float = 30.0,
|
|
513
|
+
max_text_size: int = 307_200,
|
|
514
|
+
stack_level: int = 0,
|
|
515
|
+
) -> EvaluationResult:
|
|
516
|
+
pending = self.start_evaluation(
|
|
517
|
+
expression,
|
|
518
|
+
max_text_size=max_text_size,
|
|
519
|
+
stack_level=stack_level,
|
|
520
|
+
)
|
|
521
|
+
event = self.wait_evaluation_event(pending, timeout_s=timeout_s)
|
|
522
|
+
if isinstance(event, StopEvent):
|
|
523
|
+
raise UnexpectedStop(
|
|
524
|
+
"Expression evaluation stopped at a user breakpoint"
|
|
525
|
+
)
|
|
526
|
+
return event
|
|
527
|
+
|
|
528
|
+
def start_evaluation(
|
|
529
|
+
self,
|
|
530
|
+
expression: str,
|
|
531
|
+
*,
|
|
532
|
+
max_text_size: int = 307_200,
|
|
533
|
+
stack_level: int = 0,
|
|
534
|
+
) -> PendingEvaluation:
|
|
535
|
+
self._require(SessionState.READY)
|
|
536
|
+
if self.target is None:
|
|
537
|
+
raise TargetLost("No target has been selected")
|
|
538
|
+
if type(expression) is not str or not expression:
|
|
539
|
+
raise ValueError("evaluation expression must be non-empty")
|
|
540
|
+
if type(max_text_size) is not int or max_text_size <= 0:
|
|
541
|
+
raise ValueError("max_text_size must be positive")
|
|
542
|
+
if type(stack_level) is not int or stack_level < 0:
|
|
543
|
+
raise ValueError("stack_level must be non-negative")
|
|
544
|
+
if self._pending_evaluation_states:
|
|
545
|
+
raise ProtocolError("Another expression evaluation is already pending")
|
|
546
|
+
result_id = uuid4()
|
|
547
|
+
pending = PendingEvaluation(
|
|
548
|
+
self.target.target_id,
|
|
549
|
+
result_id,
|
|
550
|
+
self._evaluation_owner,
|
|
551
|
+
)
|
|
552
|
+
self._pending_evaluation_states[id(pending)] = _PendingEvaluationState(
|
|
553
|
+
pending
|
|
554
|
+
)
|
|
555
|
+
response = self.transport.request(
|
|
556
|
+
"evalExpr",
|
|
557
|
+
build_eval_request(
|
|
558
|
+
self.alias,
|
|
559
|
+
self.ui_id,
|
|
560
|
+
self.target.target_id,
|
|
561
|
+
expression,
|
|
562
|
+
result_id,
|
|
563
|
+
max_text_size=max_text_size,
|
|
564
|
+
stack_level=stack_level,
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
if response.strip():
|
|
568
|
+
try:
|
|
569
|
+
result = parse_eval_response(response)
|
|
570
|
+
except ProtocolError:
|
|
571
|
+
self._pending_evaluation_states.pop(id(pending), None)
|
|
572
|
+
raise
|
|
573
|
+
if result is not None and result.result_id != result_id:
|
|
574
|
+
self._pending_evaluation_states.pop(id(pending), None)
|
|
575
|
+
raise ProtocolError("RDBG evaluation result ID mismatch")
|
|
576
|
+
if result is not None:
|
|
577
|
+
self._event_queue.append(result)
|
|
578
|
+
return pending
|
|
579
|
+
|
|
580
|
+
def wait_evaluation_event(
|
|
581
|
+
self,
|
|
582
|
+
pending: PendingEvaluation,
|
|
583
|
+
*,
|
|
584
|
+
timeout_s: float,
|
|
585
|
+
) -> EvaluationResult | StopEvent:
|
|
586
|
+
state = self._require_pending_evaluation(pending)
|
|
587
|
+
if state.suspended_stop is not None:
|
|
588
|
+
raise ProtocolError("Pending evaluation stop must be continued first")
|
|
589
|
+
deadline = monotonic() + timeout_s
|
|
590
|
+
while monotonic() < deadline:
|
|
591
|
+
event = self._pop_pending_evaluation_event(pending)
|
|
592
|
+
if event is not None:
|
|
593
|
+
if isinstance(event, StopEvent):
|
|
594
|
+
if event.target_id != pending.target_id:
|
|
595
|
+
raise UnexpectedStop(
|
|
596
|
+
"Evaluation stop belongs to a different target"
|
|
597
|
+
)
|
|
598
|
+
self._admit_stop(event)
|
|
599
|
+
state.suspended_stop = event
|
|
600
|
+
return event
|
|
601
|
+
self._pending_evaluations.pop(event.result_id, None)
|
|
602
|
+
del self._pending_evaluation_states[id(pending)]
|
|
603
|
+
self.state = SessionState.READY
|
|
604
|
+
return event
|
|
605
|
+
polled = self._poll(max(0.1, min(6.0, deadline - monotonic())))
|
|
606
|
+
self._ingest_poll_events(*polled)
|
|
607
|
+
raise CommandTimeout(
|
|
608
|
+
f"Timed out waiting for expression result {pending.result_id}"
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def continue_evaluation(
|
|
612
|
+
self,
|
|
613
|
+
pending: PendingEvaluation,
|
|
614
|
+
stop: StopEvent,
|
|
615
|
+
) -> None:
|
|
616
|
+
state = self._require_pending_evaluation(pending)
|
|
617
|
+
if state.suspended_stop is not stop:
|
|
618
|
+
raise ProtocolError("Pending evaluation stop is stale or foreign")
|
|
619
|
+
if stop.target_id != pending.target_id or self.target is None:
|
|
620
|
+
raise ProtocolError("Pending evaluation target changed")
|
|
621
|
+
self.continue_()
|
|
622
|
+
state.suspended_stop = None
|
|
623
|
+
|
|
624
|
+
def _require_pending_evaluation(
|
|
625
|
+
self,
|
|
626
|
+
pending: PendingEvaluation,
|
|
627
|
+
) -> _PendingEvaluationState:
|
|
628
|
+
if type(pending) is not PendingEvaluation:
|
|
629
|
+
raise TypeError("pending evaluation capability is required")
|
|
630
|
+
state = self._pending_evaluation_states.get(id(pending))
|
|
631
|
+
if (
|
|
632
|
+
state is None
|
|
633
|
+
or state.capability is not pending
|
|
634
|
+
or pending.owner is not self._evaluation_owner
|
|
635
|
+
or self.target is None
|
|
636
|
+
or pending.target_id != self.target.target_id
|
|
637
|
+
):
|
|
638
|
+
raise ProtocolError("Pending evaluation is stale or foreign")
|
|
639
|
+
return state
|
|
640
|
+
|
|
641
|
+
def _pop_pending_evaluation_event(
|
|
642
|
+
self,
|
|
643
|
+
pending: PendingEvaluation,
|
|
644
|
+
) -> EvaluationResult | StopEvent | None:
|
|
645
|
+
if not self._event_queue:
|
|
646
|
+
return None
|
|
647
|
+
event = self._event_queue[0]
|
|
648
|
+
if isinstance(event, StopEvent):
|
|
649
|
+
return self._event_queue.popleft()
|
|
650
|
+
if event.result_id != pending.result_id:
|
|
651
|
+
raise ProtocolError("Evaluation event correlation mismatch")
|
|
652
|
+
return self._event_queue.popleft()
|
|
653
|
+
|
|
654
|
+
def evaluate_collection(
|
|
655
|
+
self,
|
|
656
|
+
expression: str,
|
|
657
|
+
*,
|
|
658
|
+
start_index: int,
|
|
659
|
+
page_size: int = 2400,
|
|
660
|
+
timeout_s: float = 30.0,
|
|
661
|
+
max_text_size: int = 4096,
|
|
662
|
+
stack_level: int = 0,
|
|
663
|
+
) -> EvaluationResult:
|
|
664
|
+
self._require(SessionState.READY)
|
|
665
|
+
if self.target is None:
|
|
666
|
+
raise TargetLost("No target has been selected")
|
|
667
|
+
if stack_level < 0:
|
|
668
|
+
raise ValueError("stack_level must be non-negative")
|
|
669
|
+
deadline = monotonic() + timeout_s
|
|
670
|
+
result_id = uuid4()
|
|
671
|
+
|
|
672
|
+
def remaining_timeout() -> float:
|
|
673
|
+
remaining = deadline - monotonic()
|
|
674
|
+
if remaining <= 0:
|
|
675
|
+
raise CommandTimeout(
|
|
676
|
+
f"Timed out waiting for collection result {result_id}"
|
|
677
|
+
)
|
|
678
|
+
return remaining
|
|
679
|
+
|
|
680
|
+
request = build_collection_eval_request(
|
|
681
|
+
self.alias,
|
|
682
|
+
self.ui_id,
|
|
683
|
+
self.target.target_id,
|
|
684
|
+
expression,
|
|
685
|
+
result_id,
|
|
686
|
+
start_index=start_index,
|
|
687
|
+
page_size=page_size,
|
|
688
|
+
max_text_size=max_text_size,
|
|
689
|
+
stack_level=stack_level,
|
|
690
|
+
)
|
|
691
|
+
metadata = {"page_start": start_index, "result_id": str(result_id)}
|
|
692
|
+
response = self._profile(
|
|
693
|
+
"rdbg.collection.eval_request",
|
|
694
|
+
lambda: self.transport.request(
|
|
695
|
+
"evalExpr",
|
|
696
|
+
request,
|
|
697
|
+
timeout_s=remaining_timeout(),
|
|
698
|
+
),
|
|
699
|
+
input_bytes=len(request),
|
|
700
|
+
output_bytes=len,
|
|
701
|
+
**metadata,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
def parse_direct() -> EvaluationResult | None:
|
|
705
|
+
remaining_timeout()
|
|
706
|
+
if not response.strip():
|
|
707
|
+
return None
|
|
708
|
+
try:
|
|
709
|
+
candidate = parse_eval_result(response)
|
|
710
|
+
if candidate.result_id == result_id:
|
|
711
|
+
return candidate
|
|
712
|
+
except ProtocolError:
|
|
713
|
+
pass
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
result = self._profile(
|
|
717
|
+
"rdbg.collection.direct_parse",
|
|
718
|
+
parse_direct,
|
|
719
|
+
input_bytes=len(response),
|
|
720
|
+
item_count=lambda candidate: int(candidate is not None),
|
|
721
|
+
**metadata,
|
|
722
|
+
)
|
|
723
|
+
while result is None:
|
|
724
|
+
remaining_timeout()
|
|
725
|
+
result = self._profile(
|
|
726
|
+
"rdbg.collection.pending_lookup",
|
|
727
|
+
lambda: self._pending_evaluations.pop(result_id, None),
|
|
728
|
+
item_count=lambda candidate: int(candidate is not None),
|
|
729
|
+
**metadata,
|
|
730
|
+
)
|
|
731
|
+
if result is None:
|
|
732
|
+
polled = self._poll(
|
|
733
|
+
min(6.0, remaining_timeout()),
|
|
734
|
+
profile_page_start=start_index,
|
|
735
|
+
profile_result_id=str(result_id),
|
|
736
|
+
)
|
|
737
|
+
self._ingest_poll_events(*polled)
|
|
738
|
+
remaining_timeout()
|
|
739
|
+
return self._profile(
|
|
740
|
+
"rdbg.collection.reindex_rows",
|
|
741
|
+
lambda: replace(
|
|
742
|
+
result,
|
|
743
|
+
collection_rows=tuple(
|
|
744
|
+
replace(row, index=start_index + offset)
|
|
745
|
+
for offset, row in enumerate(result.collection_rows)
|
|
746
|
+
),
|
|
747
|
+
),
|
|
748
|
+
item_count=lambda candidate: len(candidate.collection_rows),
|
|
749
|
+
**metadata,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
def local_variables(
|
|
753
|
+
self,
|
|
754
|
+
stack_level: int = 0,
|
|
755
|
+
*,
|
|
756
|
+
timeout_s: float = 30.0,
|
|
757
|
+
max_text_size: int = 307_200,
|
|
758
|
+
retry_delays_s: tuple[float, ...] = (0.05, 0.10, 0.15),
|
|
759
|
+
) -> LocalVariablesResult:
|
|
760
|
+
self._require(SessionState.READY)
|
|
761
|
+
if self.target is None:
|
|
762
|
+
raise TargetLost("No target has been selected")
|
|
763
|
+
if stack_level < 0:
|
|
764
|
+
raise ValueError("stack_level must be non-negative")
|
|
765
|
+
|
|
766
|
+
deadline = monotonic() + timeout_s
|
|
767
|
+
|
|
768
|
+
def request_once() -> LocalVariablesResult:
|
|
769
|
+
remaining = deadline - monotonic()
|
|
770
|
+
if remaining <= 0:
|
|
771
|
+
raise CommandTimeout("Timed out waiting for local variables")
|
|
772
|
+
result_id = uuid4()
|
|
773
|
+
response = self.transport.request(
|
|
774
|
+
"evalLocalVariables",
|
|
775
|
+
build_local_variables_request(
|
|
776
|
+
self.alias,
|
|
777
|
+
self.ui_id,
|
|
778
|
+
self.target.target_id,
|
|
779
|
+
stack_level,
|
|
780
|
+
result_id,
|
|
781
|
+
max_text_size=max_text_size,
|
|
782
|
+
),
|
|
783
|
+
timeout_s=remaining,
|
|
784
|
+
)
|
|
785
|
+
if response.strip():
|
|
786
|
+
try:
|
|
787
|
+
direct = parse_local_variables_result(response)
|
|
788
|
+
if direct.result_id == result_id:
|
|
789
|
+
return direct
|
|
790
|
+
except ProtocolError:
|
|
791
|
+
pass
|
|
792
|
+
while monotonic() < deadline:
|
|
793
|
+
pending = self._pending_local_variables.pop(result_id, None)
|
|
794
|
+
if pending is not None:
|
|
795
|
+
return pending
|
|
796
|
+
polled = self._poll(max(0.1, min(6.0, deadline - monotonic())))
|
|
797
|
+
self._ingest_poll_events(*polled)
|
|
798
|
+
raise CommandTimeout(
|
|
799
|
+
f"Timed out waiting for local variables result {result_id}"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
result = request_once()
|
|
803
|
+
if result.variables or result.error_occurred:
|
|
804
|
+
return result
|
|
805
|
+
for delay_s in retry_delays_s:
|
|
806
|
+
if monotonic() + delay_s >= deadline:
|
|
807
|
+
break
|
|
808
|
+
sleep(delay_s)
|
|
809
|
+
result = request_once()
|
|
810
|
+
if result.variables or result.error_occurred:
|
|
811
|
+
return result
|
|
812
|
+
return result
|
|
813
|
+
|
|
814
|
+
def modify(self, variable: str, value_expression: str) -> ModifyResult:
|
|
815
|
+
self._require(SessionState.READY)
|
|
816
|
+
if self.target is None:
|
|
817
|
+
raise TargetLost("No target has been selected")
|
|
818
|
+
result_id = uuid4()
|
|
819
|
+
response = self.transport.request(
|
|
820
|
+
"modifyValue",
|
|
821
|
+
build_modify_request(
|
|
822
|
+
self.alias,
|
|
823
|
+
self.ui_id,
|
|
824
|
+
self.target.target_id,
|
|
825
|
+
variable,
|
|
826
|
+
value_expression,
|
|
827
|
+
result_id,
|
|
828
|
+
),
|
|
829
|
+
)
|
|
830
|
+
if not response.strip():
|
|
831
|
+
raise ProtocolError("RDBG modifyValue returned an empty response")
|
|
832
|
+
result = parse_modify_result(response)
|
|
833
|
+
if result.result_id != result_id:
|
|
834
|
+
raise ProtocolError(
|
|
835
|
+
"RDBG modifyValue result ID mismatch: "
|
|
836
|
+
f"expected {result_id}, got {result.result_id}"
|
|
837
|
+
)
|
|
838
|
+
return result
|
|
839
|
+
|
|
840
|
+
def continue_(self) -> None:
|
|
841
|
+
self._require(SessionState.READY)
|
|
842
|
+
if self.target is None:
|
|
843
|
+
raise TargetLost("No target has been selected")
|
|
844
|
+
self.transport.request(
|
|
845
|
+
"step", build_step_request(self.alias, self.ui_id, self.target.target_id)
|
|
846
|
+
)
|
|
847
|
+
self.state = SessionState.EXECUTING
|
|
848
|
+
|
|
849
|
+
def heartbeat(self) -> dict[str, object]:
|
|
850
|
+
self._require(SessionState.READY)
|
|
851
|
+
if self.target is None:
|
|
852
|
+
raise TargetLost("No target has been selected")
|
|
853
|
+
rtt_ms = self.transport.test_server()
|
|
854
|
+
# The platform expires the registered Debug UI unless its dedicated
|
|
855
|
+
# long-poll endpoint is called. Server and target probes alone do not
|
|
856
|
+
# renew that lease. Bound idle empty-response waiting while RuntimeSession
|
|
857
|
+
# holds its shared operation lock; returned events are still ingested.
|
|
858
|
+
self._ingest_poll_events(*self._poll(0.1))
|
|
859
|
+
matches = [
|
|
860
|
+
target for target in self.list_targets() if target.target_id.id == self.target.target_id.id
|
|
861
|
+
]
|
|
862
|
+
if len(matches) != 1:
|
|
863
|
+
raise TargetLost("Selected target disappeared during heartbeat")
|
|
864
|
+
return {"rtt_ms": rtt_ms, "target_state": matches[0].state}
|
|
865
|
+
|
|
866
|
+
def invalidate(self) -> None:
|
|
867
|
+
self.state = SessionState.FAILED
|
|
868
|
+
self.target = None
|
|
869
|
+
self.attached_targets.clear()
|
|
870
|
+
self._pending_evaluations.clear()
|
|
871
|
+
self._pending_local_variables.clear()
|
|
872
|
+
self._event_queue.clear()
|
|
873
|
+
self._pending_evaluation_states.clear()
|
|
874
|
+
|
|
875
|
+
def detach(self) -> None:
|
|
876
|
+
if self.state is SessionState.DETACHED:
|
|
877
|
+
return
|
|
878
|
+
target_errors: list[BaseException] = []
|
|
879
|
+
for target in self.attached_targets.values():
|
|
880
|
+
try:
|
|
881
|
+
self.transport.request(
|
|
882
|
+
"attachDetachDbgTargets",
|
|
883
|
+
build_attach_target_request(
|
|
884
|
+
self.alias, self.ui_id, target.target_id, attach=False
|
|
885
|
+
),
|
|
886
|
+
timeout_s=10.0,
|
|
887
|
+
)
|
|
888
|
+
except BaseException as error:
|
|
889
|
+
target_errors.append(error)
|
|
890
|
+
try:
|
|
891
|
+
self.transport.request(
|
|
892
|
+
"detachDebugUI",
|
|
893
|
+
build_detach_request(self.alias, self.ui_id),
|
|
894
|
+
timeout_s=10.0,
|
|
895
|
+
)
|
|
896
|
+
except RdbgDebugUiNotRegistered:
|
|
897
|
+
# A lost registration is already detached on the shared server.
|
|
898
|
+
pass
|
|
899
|
+
except BaseException as error:
|
|
900
|
+
if target_errors:
|
|
901
|
+
errors = (*target_errors, error)
|
|
902
|
+
raise ProtocolError(
|
|
903
|
+
"RDBG detach failed: "
|
|
904
|
+
+ ", ".join(type(item).__name__ for item in errors)
|
|
905
|
+
) from error
|
|
906
|
+
raise
|
|
907
|
+
self.state = SessionState.DETACHED
|
|
908
|
+
self.target = None
|
|
909
|
+
self.attached_targets.clear()
|
|
910
|
+
self._pending_evaluations.clear()
|
|
911
|
+
self._pending_local_variables.clear()
|
|
912
|
+
self._event_queue.clear()
|
|
913
|
+
self._pending_evaluation_states.clear()
|