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.
Files changed (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. 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()