androidctl 0.1.0__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 (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
@@ -0,0 +1,548 @@
1
+ """Runtime lifecycle, normalization, and progress-lane coordination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable, Iterator
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING
10
+
11
+ from androidctld.artifacts.models import ScreenArtifacts
12
+ from androidctld.device.types import (
13
+ BootstrapResult,
14
+ ConnectionConfig,
15
+ RuntimeTransport,
16
+ )
17
+ from androidctld.errors import DaemonError, DaemonErrorCode
18
+ from androidctld.protocol import RuntimeStatus
19
+ from androidctld.refs.models import RefRegistry
20
+ from androidctld.runtime.lifecycle import RuntimeLifecycleLease, capture_lifecycle_lease
21
+ from androidctld.runtime.models import ScreenState, WorkspaceRuntime
22
+ from androidctld.runtime.screen_state import get_authoritative_current_basis
23
+ from androidctld.runtime.store import RuntimeStore
24
+ from androidctld.runtime_policy import (
25
+ QUERY_PROGRESS_POLL_SECONDS,
26
+ QUERY_PROGRESS_WAIT_SECONDS,
27
+ )
28
+ from androidctld.semantics.compiler import CompiledScreen
29
+ from androidctld.semantics.public_models import PublicScreen
30
+ from androidctld.snapshots.models import RawSnapshot
31
+
32
+ if TYPE_CHECKING:
33
+ from androidctld.artifacts.writer import StagedArtifactWrite
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class _ScreenRefreshRestoreState:
38
+ screen_sequence: int
39
+ current_screen_id: str | None
40
+ status: RuntimeStatus
41
+ latest_snapshot: RawSnapshot | None
42
+ previous_snapshot: RawSnapshot | None
43
+ screen_state: ScreenState | None
44
+ ref_registry: RefRegistry
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ScreenshotArtifactAttachment:
49
+ current_screen: PublicScreen | None
50
+ artifacts: ScreenArtifacts
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ScreenRefreshUpdate:
55
+ sequence: int
56
+ snapshot: RawSnapshot
57
+ public_screen: PublicScreen
58
+ compiled_screen: CompiledScreen
59
+ artifacts: ScreenArtifacts
60
+ ref_registry: RefRegistry
61
+ staged_artifacts: StagedArtifactWrite
62
+
63
+
64
+ class RuntimeKernel:
65
+ def __init__(
66
+ self,
67
+ runtime_store: RuntimeStore,
68
+ *,
69
+ sleep_fn: Callable[[float], None] = time.sleep,
70
+ time_fn: Callable[[], float] = time.monotonic,
71
+ ) -> None:
72
+ self._runtime_store = runtime_store
73
+ self._sleep_fn = sleep_fn
74
+ self._time_fn = time_fn
75
+
76
+ def ensure_runtime(self) -> WorkspaceRuntime:
77
+ runtime = self._runtime_store.get_runtime()
78
+ normalize_stale_ready_runtime(
79
+ runtime,
80
+ persist=self.commit_runtime,
81
+ )
82
+ return runtime
83
+
84
+ def capture_lifecycle_lease(
85
+ self,
86
+ runtime: WorkspaceRuntime,
87
+ ) -> RuntimeLifecycleLease:
88
+ with runtime.lock:
89
+ return capture_lifecycle_lease(runtime)
90
+
91
+ @contextmanager
92
+ def committed_runtime_mutation(
93
+ self,
94
+ runtime: WorkspaceRuntime,
95
+ *,
96
+ persist: Callable[[WorkspaceRuntime], None] | None = None,
97
+ rollback: Callable[[WorkspaceRuntime], None] | None = None,
98
+ ) -> Iterator[WorkspaceRuntime]:
99
+ with runtime.lock:
100
+ try:
101
+ yield runtime
102
+ (persist or self.commit_runtime)(runtime)
103
+ except Exception:
104
+ if rollback is not None:
105
+ rollback(runtime)
106
+ raise
107
+
108
+ def commit_runtime(self, runtime: WorkspaceRuntime) -> None:
109
+ runtime.artifact_root.mkdir(parents=True, exist_ok=True)
110
+ self._runtime_store.persist_runtime(runtime)
111
+
112
+ def commit_screen_refresh(
113
+ self,
114
+ runtime: WorkspaceRuntime,
115
+ *,
116
+ update: ScreenRefreshUpdate,
117
+ pre_commit: Callable[[WorkspaceRuntime], None] | None = None,
118
+ ) -> None:
119
+ restore_state: _ScreenRefreshRestoreState | None = None
120
+
121
+ def rollback_refresh(active_runtime: WorkspaceRuntime) -> None:
122
+ if restore_state is None:
123
+ update.staged_artifacts.discard()
124
+ return
125
+ active_runtime.screen_sequence = restore_state.screen_sequence
126
+ active_runtime.current_screen_id = restore_state.current_screen_id
127
+ active_runtime.status = restore_state.status
128
+ active_runtime.latest_snapshot = restore_state.latest_snapshot
129
+ active_runtime.previous_snapshot = restore_state.previous_snapshot
130
+ active_runtime.screen_state = restore_state.screen_state
131
+ active_runtime.ref_registry = restore_state.ref_registry
132
+ update.staged_artifacts.rollback()
133
+ update.staged_artifacts.discard()
134
+
135
+ with self.committed_runtime_mutation(
136
+ runtime,
137
+ rollback=rollback_refresh,
138
+ ):
139
+ if pre_commit is not None:
140
+ pre_commit(runtime)
141
+ restore_state = _capture_screen_refresh_restore_state(runtime)
142
+ update.staged_artifacts.commit()
143
+ _commit_screen_refresh_locked(
144
+ runtime,
145
+ sequence=update.sequence,
146
+ snapshot=update.snapshot,
147
+ public_screen=update.public_screen,
148
+ compiled_screen=update.compiled_screen,
149
+ artifacts=update.artifacts,
150
+ ref_registry=update.ref_registry,
151
+ previous_snapshot=restore_state.latest_snapshot,
152
+ )
153
+ update.staged_artifacts.discard()
154
+
155
+ def attach_screenshot_artifact(
156
+ self,
157
+ runtime: WorkspaceRuntime,
158
+ lease: RuntimeLifecycleLease,
159
+ *,
160
+ screenshot_png: str,
161
+ ) -> ScreenshotArtifactAttachment | None:
162
+ with runtime.lock:
163
+ if not lease.is_current(runtime):
164
+ return None
165
+ screen_state = runtime.screen_state
166
+ artifacts = (
167
+ ScreenArtifacts(screen_json=None)
168
+ if screen_state is None or screen_state.artifacts is None
169
+ else screen_state.artifacts
170
+ ).with_screenshot(screenshot_png)
171
+ if screen_state is None:
172
+ runtime.screen_state = ScreenState(
173
+ public_screen=None,
174
+ compiled_screen=None,
175
+ artifacts=artifacts,
176
+ )
177
+ current_screen = None
178
+ else:
179
+ screen_state.artifacts = artifacts
180
+ current_screen = screen_state.public_screen
181
+ return ScreenshotArtifactAttachment(
182
+ current_screen=current_screen,
183
+ artifacts=artifacts,
184
+ )
185
+
186
+ def normalize_stale_ready_runtime(
187
+ self,
188
+ runtime: WorkspaceRuntime,
189
+ *,
190
+ persist: Callable[[WorkspaceRuntime], None] | None = None,
191
+ ) -> bool:
192
+ return normalize_stale_ready_runtime(
193
+ runtime,
194
+ persist=persist or self.commit_runtime,
195
+ )
196
+
197
+ def begin_connect(
198
+ self,
199
+ runtime: WorkspaceRuntime,
200
+ lease: RuntimeLifecycleLease,
201
+ *,
202
+ transport: RuntimeTransport,
203
+ ) -> bool:
204
+ with runtime.lock:
205
+ if not lease.is_current(runtime):
206
+ return False
207
+ _clear_runtime_state_locked(runtime)
208
+ runtime.transport = transport
209
+ runtime.status = RuntimeStatus.BOOTSTRAPPING
210
+ self.commit_runtime(runtime)
211
+ return True
212
+
213
+ def activate_connect(
214
+ self,
215
+ runtime: WorkspaceRuntime,
216
+ lease: RuntimeLifecycleLease,
217
+ *,
218
+ bootstrap_result: BootstrapResult,
219
+ device_token: str,
220
+ ) -> bool:
221
+ close_transport = None
222
+ with runtime.lock:
223
+ if not lease.is_current(runtime):
224
+ close_transport = bootstrap_result.transport
225
+ else:
226
+ runtime.connection = bootstrap_result.connection
227
+ runtime.transport = bootstrap_result.transport
228
+ runtime.device_token = device_token
229
+ runtime.device_capabilities = bootstrap_result.meta.capabilities
230
+ runtime.status = RuntimeStatus.CONNECTED
231
+ self.commit_runtime(runtime)
232
+ return True
233
+ if close_transport is not None:
234
+ close_transport.close()
235
+ return False
236
+
237
+ def rebootstrap_transport(
238
+ self,
239
+ runtime: WorkspaceRuntime,
240
+ *,
241
+ bootstrap: Callable[[ConnectionConfig], BootstrapResult],
242
+ lease: RuntimeLifecycleLease | None = None,
243
+ ) -> RuntimeTransport:
244
+ with runtime.lock:
245
+ if lease is not None and not lease.is_current(runtime):
246
+ raise _runtime_not_connected_error(runtime)
247
+ if runtime.transport is not None:
248
+ return runtime.transport
249
+ if runtime.connection is None or not runtime.device_token:
250
+ raise _runtime_not_connected_error(runtime)
251
+ active_lease = lease or capture_lifecycle_lease(runtime)
252
+ connection_config = runtime.connection.to_connection_config(
253
+ runtime.device_token
254
+ )
255
+
256
+ bootstrap_result = bootstrap(connection_config)
257
+ transport = self.commit_transport_rebootstrap(
258
+ runtime,
259
+ active_lease,
260
+ bootstrap_result=bootstrap_result,
261
+ )
262
+ if transport is None:
263
+ raise _runtime_not_connected_error(runtime)
264
+ return transport
265
+
266
+ def commit_transport_rebootstrap(
267
+ self,
268
+ runtime: WorkspaceRuntime,
269
+ lease: RuntimeLifecycleLease,
270
+ *,
271
+ bootstrap_result: BootstrapResult,
272
+ ) -> RuntimeTransport | None:
273
+ close_transport = None
274
+ result_transport = None
275
+ with runtime.lock:
276
+ if not lease.is_current(runtime):
277
+ close_transport = bootstrap_result.transport
278
+ elif runtime.transport is not None:
279
+ close_transport = bootstrap_result.transport
280
+ result_transport = runtime.transport
281
+ else:
282
+ runtime.connection = bootstrap_result.connection
283
+ runtime.transport = bootstrap_result.transport
284
+ runtime.device_capabilities = bootstrap_result.meta.capabilities
285
+ self.commit_runtime(runtime)
286
+ result_transport = bootstrap_result.transport
287
+ if close_transport is not None:
288
+ close_transport.close()
289
+ return result_transport
290
+
291
+ def fail_connect(
292
+ self,
293
+ runtime: WorkspaceRuntime,
294
+ lease: RuntimeLifecycleLease,
295
+ ) -> bool:
296
+ with runtime.lock:
297
+ if not lease.is_current(runtime):
298
+ return False
299
+ _clear_runtime_state_locked(runtime)
300
+ runtime.status = RuntimeStatus.BROKEN
301
+ self.commit_runtime(runtime)
302
+ return True
303
+
304
+ def invalidate_device_credentials(
305
+ self,
306
+ runtime: WorkspaceRuntime,
307
+ lease: RuntimeLifecycleLease | None = None,
308
+ ) -> bool:
309
+ with runtime.lock:
310
+ if lease is not None and not lease.is_current(runtime):
311
+ return False
312
+ _clear_runtime_state_locked(runtime)
313
+ runtime.status = RuntimeStatus.BROKEN
314
+ self.commit_runtime(runtime)
315
+ return True
316
+
317
+ def drop_current_screen_authority(
318
+ self,
319
+ runtime: WorkspaceRuntime,
320
+ lease: RuntimeLifecycleLease,
321
+ *,
322
+ discard_transport: bool = False,
323
+ ) -> bool:
324
+ with runtime.lock:
325
+ if not lease.is_current(runtime):
326
+ return False
327
+ _drop_current_screen_authority_locked(
328
+ runtime,
329
+ discard_transport=discard_transport,
330
+ )
331
+ self.commit_runtime(runtime)
332
+ return True
333
+
334
+ def acquire_progress_lane(
335
+ self,
336
+ runtime: WorkspaceRuntime,
337
+ *,
338
+ occupant_kind: str,
339
+ ) -> None:
340
+ busy_error: DaemonError | None = None
341
+ with runtime.lock:
342
+ if _try_acquire_progress_lane_locked(
343
+ runtime,
344
+ occupant_kind=occupant_kind,
345
+ ):
346
+ return
347
+ busy_error = _runtime_busy_error_locked(runtime)
348
+ if busy_error is None:
349
+ raise RuntimeError("runtime busy error was not available")
350
+ raise busy_error
351
+
352
+ def acquire_query_lane(self, runtime: WorkspaceRuntime) -> None:
353
+ deadline = self._time_fn() + QUERY_PROGRESS_WAIT_SECONDS
354
+ while True:
355
+ busy_error: DaemonError | None = None
356
+ with runtime.lock:
357
+ if _try_acquire_progress_lane_locked(
358
+ runtime,
359
+ occupant_kind="query",
360
+ ):
361
+ return
362
+ if self._time_fn() >= deadline:
363
+ busy_error = _runtime_busy_error_locked(runtime)
364
+ if busy_error is not None:
365
+ raise busy_error
366
+ self._sleep_fn(QUERY_PROGRESS_POLL_SECONDS)
367
+
368
+ def release_progress_lane(self, runtime: WorkspaceRuntime) -> None:
369
+ with runtime.lock:
370
+ runtime.progress_occupant_kind = None
371
+ runtime.progress_lock.release()
372
+
373
+ def close_runtime(self, runtime: WorkspaceRuntime) -> None:
374
+ with runtime.lock:
375
+ _clear_runtime_state_locked(runtime)
376
+ runtime.lifecycle_revision += 1
377
+ runtime.status = RuntimeStatus.CLOSED
378
+ self.commit_runtime(runtime)
379
+
380
+ def invalidate_runtime(
381
+ self,
382
+ runtime: WorkspaceRuntime,
383
+ lease: RuntimeLifecycleLease | None = None,
384
+ ) -> bool:
385
+ with runtime.lock:
386
+ if lease is not None and not lease.is_current(runtime):
387
+ return False
388
+ _clear_runtime_state_locked(runtime)
389
+ runtime.lifecycle_revision += 1
390
+ return True
391
+
392
+
393
+ def has_live_public_screen(runtime: WorkspaceRuntime) -> bool:
394
+ with runtime.lock:
395
+ if (
396
+ runtime.transport is None
397
+ or runtime.connection is None
398
+ or runtime.device_token is None
399
+ ):
400
+ return False
401
+ return get_authoritative_current_basis(runtime) is not None
402
+
403
+
404
+ def _capture_screen_refresh_restore_state(
405
+ runtime: WorkspaceRuntime,
406
+ ) -> _ScreenRefreshRestoreState:
407
+ return _ScreenRefreshRestoreState(
408
+ screen_sequence=runtime.screen_sequence,
409
+ current_screen_id=runtime.current_screen_id,
410
+ status=runtime.status,
411
+ latest_snapshot=runtime.latest_snapshot,
412
+ previous_snapshot=runtime.previous_snapshot,
413
+ screen_state=runtime.screen_state,
414
+ ref_registry=runtime.ref_registry,
415
+ )
416
+
417
+
418
+ def _commit_screen_refresh_locked(
419
+ runtime: WorkspaceRuntime,
420
+ *,
421
+ sequence: int,
422
+ snapshot: RawSnapshot,
423
+ public_screen: PublicScreen,
424
+ compiled_screen: CompiledScreen,
425
+ artifacts: ScreenArtifacts,
426
+ ref_registry: RefRegistry,
427
+ previous_snapshot: RawSnapshot | None,
428
+ ) -> None:
429
+ runtime.previous_snapshot = previous_snapshot
430
+ runtime.latest_snapshot = snapshot
431
+ runtime.screen_sequence = sequence
432
+ runtime.current_screen_id = public_screen.screen_id
433
+ runtime.status = RuntimeStatus.READY
434
+ runtime.screen_state = ScreenState(
435
+ public_screen=public_screen,
436
+ compiled_screen=compiled_screen,
437
+ artifacts=artifacts,
438
+ )
439
+ runtime.ref_registry = ref_registry
440
+
441
+
442
+ def normalize_stale_ready_runtime(
443
+ runtime: WorkspaceRuntime,
444
+ *,
445
+ persist: Callable[[WorkspaceRuntime], None] | None = None,
446
+ ) -> bool:
447
+ with runtime.lock:
448
+ if runtime.status is not RuntimeStatus.READY:
449
+ return False
450
+ if has_live_public_screen(runtime):
451
+ return False
452
+ runtime.latest_snapshot = None
453
+ runtime.previous_snapshot = None
454
+ runtime.current_screen_id = None
455
+ runtime.screen_state = None
456
+ runtime.ref_registry = RefRegistry()
457
+ if (
458
+ runtime.transport is not None
459
+ and runtime.connection is not None
460
+ and runtime.device_token is not None
461
+ ):
462
+ runtime.status = RuntimeStatus.CONNECTED
463
+ else:
464
+ release_transport(runtime)
465
+ runtime.connection = None
466
+ runtime.device_token = None
467
+ runtime.device_capabilities = None
468
+ runtime.status = RuntimeStatus.BROKEN
469
+ if persist is not None:
470
+ persist(runtime)
471
+ return True
472
+
473
+
474
+ def _drop_current_screen_authority_locked(
475
+ runtime: WorkspaceRuntime,
476
+ *,
477
+ discard_transport: bool = False,
478
+ ) -> None:
479
+ runtime.latest_snapshot = None
480
+ runtime.previous_snapshot = None
481
+ runtime.current_screen_id = None
482
+ runtime.screen_state = None
483
+ runtime.ref_registry = RefRegistry()
484
+ if runtime.connection is not None and runtime.device_token is not None:
485
+ if discard_transport:
486
+ release_transport(runtime)
487
+ runtime.device_capabilities = None
488
+ runtime.status = RuntimeStatus.CONNECTED
489
+ return
490
+ release_transport(runtime)
491
+ runtime.connection = None
492
+ runtime.device_token = None
493
+ runtime.device_capabilities = None
494
+ runtime.status = RuntimeStatus.BROKEN
495
+
496
+
497
+ def release_transport(runtime: WorkspaceRuntime) -> None:
498
+ transport = runtime.transport
499
+ runtime.transport = None
500
+ if transport is not None:
501
+ transport.close()
502
+
503
+
504
+ def _clear_runtime_state_locked(runtime: WorkspaceRuntime) -> None:
505
+ release_transport(runtime)
506
+ runtime.connection = None
507
+ runtime.device_token = None
508
+ runtime.device_capabilities = None
509
+ runtime.latest_snapshot = None
510
+ runtime.previous_snapshot = None
511
+ runtime.current_screen_id = None
512
+ runtime.screen_state = None
513
+ runtime.ref_registry = RefRegistry()
514
+
515
+
516
+ def _try_acquire_progress_lane_locked(
517
+ runtime: WorkspaceRuntime,
518
+ *,
519
+ occupant_kind: str,
520
+ ) -> bool:
521
+ if not runtime.progress_lock.acquire(blocking=False):
522
+ return False
523
+ runtime.progress_occupant_kind = occupant_kind
524
+ return True
525
+
526
+
527
+ def _runtime_busy_error_locked(runtime: WorkspaceRuntime) -> DaemonError:
528
+ details = {
529
+ "reason": "runtime_progress_busy",
530
+ "workspaceRoot": runtime.workspace_root.as_posix(),
531
+ }
532
+ return DaemonError(
533
+ code=DaemonErrorCode.RUNTIME_BUSY,
534
+ message="runtime already has an in-flight progress command",
535
+ retryable=True,
536
+ details=details,
537
+ http_status=200,
538
+ )
539
+
540
+
541
+ def _runtime_not_connected_error(runtime: WorkspaceRuntime) -> DaemonError:
542
+ return DaemonError(
543
+ code=DaemonErrorCode.RUNTIME_NOT_CONNECTED,
544
+ message="runtime is not connected to a device",
545
+ retryable=False,
546
+ details={"workspaceRoot": runtime.workspace_root.as_posix()},
547
+ http_status=200,
548
+ )
@@ -0,0 +1,19 @@
1
+ """Runtime lifecycle coordination helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from androidctld.runtime.models import WorkspaceRuntime
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RuntimeLifecycleLease:
12
+ revision: int
13
+
14
+ def is_current(self, runtime: WorkspaceRuntime) -> bool:
15
+ return self.revision == runtime.lifecycle_revision
16
+
17
+
18
+ def capture_lifecycle_lease(runtime: WorkspaceRuntime) -> RuntimeLifecycleLease:
19
+ return RuntimeLifecycleLease(revision=runtime.lifecycle_revision)
@@ -0,0 +1,48 @@
1
+ """Workspace-scoped runtime models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ from androidctld.artifacts.models import ScreenArtifacts
10
+ from androidctld.device.types import (
11
+ ConnectionSpec,
12
+ DeviceCapabilities,
13
+ RuntimeTransport,
14
+ )
15
+ from androidctld.protocol import RuntimeStatus
16
+ from androidctld.refs.models import RefRegistry
17
+ from androidctld.semantics.compiler import CompiledScreen
18
+ from androidctld.semantics.public_models import PublicScreen
19
+ from androidctld.snapshots.models import RawSnapshot
20
+
21
+
22
+ @dataclass
23
+ class ScreenState:
24
+ public_screen: PublicScreen | None
25
+ compiled_screen: CompiledScreen | None = None
26
+ artifacts: ScreenArtifacts | None = None
27
+
28
+
29
+ @dataclass
30
+ class WorkspaceRuntime:
31
+ workspace_root: Path
32
+ artifact_root: Path
33
+ runtime_path: Path
34
+ status: RuntimeStatus = RuntimeStatus.NEW
35
+ screen_sequence: int = 0
36
+ current_screen_id: str | None = None
37
+ connection: ConnectionSpec | None = field(default=None, repr=False)
38
+ device_token: str | None = field(default=None, repr=False)
39
+ device_capabilities: DeviceCapabilities | None = field(default=None, repr=False)
40
+ transport: RuntimeTransport | None = field(default=None, repr=False)
41
+ latest_snapshot: RawSnapshot | None = field(default=None, repr=False)
42
+ previous_snapshot: RawSnapshot | None = field(default=None, repr=False)
43
+ screen_state: ScreenState | None = field(default=None, repr=False)
44
+ ref_registry: RefRegistry = field(default_factory=RefRegistry, repr=False)
45
+ lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
46
+ progress_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
47
+ progress_occupant_kind: str | None = field(default=None, repr=False)
48
+ lifecycle_revision: int = field(default=0, repr=False)