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.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- 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)
|