stim 1.16.0 → 1.17.1

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 (108) hide show
  1. package/dist/{activity-BxwdD81S.mjs → activity-IRvJY_ru.mjs} +23 -3
  2. package/dist/{agent-device-usage-output-Dpih1Tfa.mjs → agent-device-usage-output-BK41vnzN.mjs} +3 -3
  3. package/dist/{android-Ift4kVvt.mjs → android-B6rDWrTm.mjs} +681 -319
  4. package/dist/{android-B_NumVyQ.mjs → android-B_YNZy61.mjs} +7 -43
  5. package/dist/{android-r2rBL8kV.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-8U4DPqeR.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CKWdbXIX.mjs → app-install-VPkI5WEg.mjs} +6 -5
  8. package/dist/{budget-DjrURfQn.mjs → budget-CUDz870h.mjs} +62 -25
  9. package/dist/{build-plan-DjeSb2Xl.mjs → build-plan-BJSXzsCL.mjs} +345 -86
  10. package/dist/{idle-DYVymG92.mjs → build-progress-D0WkjCPd.mjs} +170 -233
  11. package/dist/{build-slots-rUk77xqh.mjs → build-slots-41v_d2x9.mjs} +35 -25
  12. package/dist/{cli-BkYZ_6hy.mjs → cli-BLQqDGlX.mjs} +17 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/{client-BTY686R6.mjs → client-CxNotugx.mjs} +313 -44
  15. package/dist/collector-run.mjs +3 -3
  16. package/dist/{command-output-DnJMdEY-.mjs → command-output-CDAIFD-8.mjs} +3 -3
  17. package/dist/{created-devices-D5owjN4C.mjs → created-devices-CpvzBL25.mjs} +1 -1
  18. package/dist/{dependency-state-B6cMXZab.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  19. package/dist/{deps-urbmfXig.mjs → deps-CVxlsRFn.mjs} +1 -1
  20. package/dist/{dev-client-CroAIjjj.mjs → dev-client-B-XC4uRH.mjs} +2 -2
  21. package/dist/{device-DZQjF7H5.mjs → device-C8IOoJPv.mjs} +7 -7
  22. package/dist/device-capacity-ZC4XSZ4R.mjs +580 -0
  23. package/dist/device-host-worker.mjs +436 -154
  24. package/dist/{device-ios-BadmeL2V.mjs → device-ios-C2Um-XUR.mjs} +75 -44
  25. package/dist/{device-lease-D-hLqx1b.mjs → device-lease-DpoS36_u.mjs} +2 -2
  26. package/dist/{device-lease-run-CqjShqWW.mjs → device-lease-run-B8YltIH_.mjs} +3 -3
  27. package/dist/{device-pool-CUaEWMMm.mjs → device-pool-3o7s5lo4.mjs} +2 -2
  28. package/dist/{device-remote-D5WVWqOq.mjs → device-remote-hFSkpfQu.mjs} +12 -12
  29. package/dist/{doctor-Cimf-gGB.mjs → doctor-BF_435OC.mjs} +40 -58
  30. package/dist/{error-diagnostics-B9L4Ae0x.mjs → error-diagnostics-C-NMnhrk.mjs} +7 -7
  31. package/dist/{gc-Bqy4bjEu.mjs → gc-BsWKa_-d.mjs} +313 -30
  32. package/dist/{gradle-BsO1MdsH.mjs → gradle-bsBRy6i9.mjs} +3 -3
  33. package/dist/{guide-B_uyKTxW.mjs → guide-DC3n6-NH.mjs} +1776 -655
  34. package/dist/{guide-status-CjEZMAD4.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  35. package/dist/hosted-android-DgU31wY3.mjs +32 -0
  36. package/dist/{hosted-client-a-WmqE2L.mjs → hosted-client-CdSoH-G6.mjs} +5 -6
  37. package/dist/hosted-ios-BTaVj4Jb.mjs +15 -0
  38. package/dist/{hosted-logs-D5g041Q3.mjs → hosted-logs-SxfBvSvc.mjs} +16 -16
  39. package/dist/{hosted-macos-BSZyIb3y.mjs → hosted-macos-CT9E3meL.mjs} +4 -4
  40. package/dist/{hosted-ios-CIWelIqf.mjs → hosted-native-CZLOrMrN.mjs} +77 -42
  41. package/dist/idle-BXG__1BA.mjs +176 -0
  42. package/dist/{idle-shutdown-CTsDnAde.mjs → idle-shutdown-IGyNV8Cr.mjs} +25 -17
  43. package/dist/{in-use-DvBw0Kxo.mjs → in-use-CvGOUn-x.mjs} +11 -11
  44. package/dist/{ios-aq3QoGI3.mjs → ios-C7JuT1bI.mjs} +173 -91
  45. package/dist/{ios-BaInn3sW.mjs → ios-DSfnPfse.mjs} +4 -4
  46. package/dist/{ios-device-FfCN_C8p.mjs → ios-device-pAN4t54-.mjs} +1 -1
  47. package/dist/ios-state-8L3lgum6.mjs +89 -0
  48. package/dist/{launch-verify-2DBRizjo.mjs → launch-verify-lTnWZikR.mjs} +2 -2
  49. package/dist/{logs-CHEVK8zD.mjs → logs-DNxKcok9.mjs} +36 -23
  50. package/dist/{macos-CSYEAFmF.mjs → macos-BEs8cwKx.mjs} +75 -34
  51. package/dist/macos-run.mjs +1 -1
  52. package/dist/maintenance-run.mjs +2 -2
  53. package/dist/{metro-BeK1ZPz1.mjs → metro-g8NqiJp-.mjs} +1 -1
  54. package/dist/{metro-gateway-BJ-rxQAB.mjs → metro-gateway-CHqK0p-a.mjs} +22 -3
  55. package/dist/{named-ports-Bk_YXEmE.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  56. package/dist/{native-run-NaBrRFtQ.mjs → native-run-D1Q56rAD.mjs} +17 -6
  57. package/dist/{native-runtime-DBzRnEKS.mjs → native-runtime-YhaSxP4v.mjs} +6 -6
  58. package/dist/offload-worker.mjs +9 -9
  59. package/dist/{teardown-CL6vDs2Y.mjs → ownership-BMLqyBgJ.mjs} +603 -26
  60. package/dist/{ownership-DiA16AsN.mjs → ownership-DyRQru0q.mjs} +2 -2
  61. package/dist/{ports-R2k4Knz3.mjs → ports-4nYqxPWC.mjs} +2 -2
  62. package/dist/{prebuild-D2fsca_Z.mjs → prebuild-D7RUMkwN.mjs} +3 -3
  63. package/dist/{preview-DhVbvPAZ.mjs → preview-C_JjkEy4.mjs} +7 -7
  64. package/dist/{project-Du91Ilvc.mjs → project-D47Rh0ly.mjs} +7 -2
  65. package/dist/{recordings-BIXOzVnr.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  66. package/dist/{reload-rM9jV1fz.mjs → reload-byOjE3P4.mjs} +62 -35
  67. package/dist/{remote-cache-C8-V13XN.mjs → remote-cache-Ci4DQYYj.mjs} +3 -3
  68. package/dist/{run-f4vI6EnK.mjs → run-CkE9y0nc.mjs} +1 -1
  69. package/dist/{server-bare-CpPGmnmO.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  70. package/dist/{server-expo-MC0rHlOC.mjs → server-expo-CHTyXD0G.mjs} +2 -2
  71. package/dist/server-expo-CeakhI8P.mjs +2 -0
  72. package/dist/{settings-oTSkkNrM.mjs → settings-Cfd-0r2d.mjs} +10 -9
  73. package/dist/{settings-ZkDzkzvB.mjs → settings-SR9OmW9h.mjs} +3 -3
  74. package/dist/settings.schema.json +123 -76
  75. package/dist/{simslim-B8-7AoTI.mjs → simslim-BWNrc3M3.mjs} +3 -3
  76. package/dist/{slot-launch-B8VrHxn1.mjs → slot-launch-DmjBmpQV.mjs} +8 -8
  77. package/dist/{start-CX8Ig-U2.mjs → start-BEP-XmEA.mjs} +1 -1
  78. package/dist/{start-ClDfwXa4.mjs → start-BHIersSm.mjs} +11 -11
  79. package/dist/{state-DcW6vOd_.mjs → state-BKa8wNN5.mjs} +1 -1
  80. package/dist/{state-3DwSQIBK.mjs → state-DyaNTgfR.mjs} +1 -1
  81. package/dist/{state-C1jB7Q8q.mjs → state-PeQiJuIu.mjs} +2 -2
  82. package/dist/{stats-CEZ_i5je.mjs → stats-Bq4fJOdB.mjs} +7 -4
  83. package/dist/{status-Cs7VLg9D.mjs → status-CqeWbIWW.mjs} +128 -38
  84. package/dist/{status-BuvBFeII.mjs → status-CzS4bPO9.mjs} +4 -4
  85. package/dist/{stop-Bffozsac.mjs → stop-Cj01je8D.mjs} +48 -27
  86. package/dist/{stop-B0Yxtmiy.mjs → stop-D03prQnW.mjs} +4 -4
  87. package/dist/{stop-Dt_dM16A.mjs → stop-DOxwkH4j.mjs} +2 -2
  88. package/dist/{stop-cause-CidxIqup.mjs → stop-cause-rhmGfwvt.mjs} +1 -1
  89. package/dist/supervisor-run.d.mts +7 -4
  90. package/dist/supervisor-run.mjs +26 -28
  91. package/dist/{support-D4aYJniO.mjs → support-BE-tbOP9.mjs} +45 -20
  92. package/dist/{support-CAP8qDUF.mjs → support-WvD__tSV.mjs} +7 -7
  93. package/dist/{toolchain-DkUflduf.mjs → toolchain-CdrxgUeA.mjs} +8 -8
  94. package/dist/{warm-progress-CpGv3Wb6.mjs → warm-progress-DF4JRvYI.mjs} +3 -3
  95. package/dist/watchman-APYjXSQa.mjs +48 -0
  96. package/dist/{web-DWUaMp8P.mjs → web-DddUdJzr.mjs} +12 -13
  97. package/dist/web-run.mjs +4 -4
  98. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  99. package/dist/{workspace-state-B8-aJ1_5.mjs → workspace-state-DqcCbpE7.mjs} +2 -2
  100. package/dist/{workspaces-BV4pdHFz.mjs → workspaces-Bcib97-6.mjs} +3 -3
  101. package/dist/{worktree-K-xj9Db0.mjs → worktree-AK_m-d_m.mjs} +141 -40
  102. package/dist/{worktree-jnG8Iph5.mjs → worktree-CsAjSmEo.mjs} +1 -1
  103. package/package.json +5 -5
  104. package/dist/device-capacity-BHwxNBlu.mjs +0 -136
  105. package/dist/ios-state-DS5UpYTH.mjs +0 -48
  106. package/dist/machines-BIBUdhpj.mjs +0 -265
  107. package/dist/ownership-hCkPzGmr.mjs +0 -571
  108. package/dist/server-expo-CVqO2c35.mjs +0 -2
@@ -1,35 +1,134 @@
1
1
  import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
2
  import { D as withConfigLock$1, _ as loadConfig$1, c as ensureConfig } from "./config-D0DbpZtM.mjs";
3
3
  import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
- import { c as writeWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-B8-aJ1_5.mjs";
5
- import { t as forgetCreatedDevice } from "./created-devices-D5owjN4C.mjs";
6
- import { A as listAvds, D as hostSystemImageArch, E as getAvdNameForSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, _ as avdStorageRoots, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, h as avdPathExists, j as listInstalledSystemImages, k as listAvdDeviceProfiles, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-B_NumVyQ.mjs";
7
- import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-BaInn3sW.mjs";
8
- import { i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-CL6vDs2Y.mjs";
9
- import { a as requiredMacosRecord, r as macosProcess } from "./state-3DwSQIBK.mjs";
4
+ import { c as writeWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-DqcCbpE7.mjs";
5
+ import { t as forgetCreatedDevice } from "./created-devices-CpvzBL25.mjs";
6
+ import { A as listAvds, B as ownedAvdSystemImage, D as hostSystemImageArch, E as getAvdNameForSerial, J as resolveOwnedAvdSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, _ as avdStorageRoots, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, f as assertOwnedAvdStopped, h as avdPathExists, j as listInstalledSystemImages, k as listAvdDeviceProfiles, q as resetAdoptedAvd, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-B_YNZy61.mjs";
7
+ import { C as resolveIosCreation, D as uninstallIosApp, S as resetIosPrivacy, i as createOwnedIosSim, j as readHostMemoryPressure, m as listUserApps, n as bootIosSim, u as listAllIosSims, x as resetIosKeychain } from "./ios-DSfnPfse.mjs";
8
+ import { g as teardownOwnedIosSim, m as teardownOwnedAvd } from "./ownership-BMLqyBgJ.mjs";
9
+ import { a as requiredMacosRecord, r as macosProcess } from "./state-DyaNTgfR.mjs";
10
10
  import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
11
11
  import { n as parseLogStreamLine } from "./ios-Cr1h5gnO.mjs";
12
- import { _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, x as launchIosApp } from "./app-install-CKWdbXIX.mjs";
13
- import { h as findAapt } from "./support-CAP8qDUF.mjs";
14
- import { n as stopMacosAppHeld } from "./stop-B0Yxtmiy.mjs";
12
+ import { n as parseLogcatLine, r as parsePidof, t as androidClockOffset } from "./android-DE4v59Mb.mjs";
13
+ import { i as countLiveOwnedDevices } from "./device-capacity-ZC4XSZ4R.mjs";
14
+ import { C as reverseMetroPorts, _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, w as writeDebugHttpHost, x as launchIosApp, y as launchAndroidApp } from "./app-install-VPkI5WEg.mjs";
15
+ import { h as findAapt } from "./support-WvD__tSV.mjs";
16
+ import { n as stopMacosAppHeld } from "./stop-D03prQnW.mjs";
15
17
  import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
16
18
  import { createHash, randomUUID } from "node:crypto";
17
19
  import { chmodSync, closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, symlinkSync, writeFileSync } from "node:fs";
18
20
  import { dirname, isAbsolute, join, posix, relative, sep } from "node:path";
19
21
  import { LOG_ROTATE_BYTES, configDir, withDirLock } from "@stim-cli/core";
20
- import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedIosLogsDir, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedIosLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
22
+ import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedMacosAppSlot, hostedMacosBundleId, hostedNativeLogsDir, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedNativeLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
21
23
  import { availableParallelism, freemem, homedir, loadavg } from "node:os";
22
- //#region src/device-host/ios-logs.ts
23
- function logDate(ms) {
24
- return new Date(ms).toISOString().replace("T", " ").replace(".000Z", "+0000");
24
+ //#region src/device-host/android-app.ts
25
+ async function digest$1(path) {
26
+ const hash = createHash("sha256");
27
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
28
+ return hash.digest("hex");
25
29
  }
26
- /** Captures a bounded unified-log window only from this home's exact ledger-owned simulator. */
27
- function collectHostedIosLogs(home, session, attempt, since, final = false) {
28
- const device = readHostedDevice(home);
29
- assertHostedDeviceLedger(home, device.udid);
30
- if (readHostedAppMetadata(session, attempt, home).state !== "installed") return false;
31
- const directory = hostedIosLogsDir(home);
32
- const previous = readHostedIosLogsCheckpoint(home);
30
+ /** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
31
+ async function installHostedAndroidApp(home, session, attempt, device, metro) {
32
+ const record = readHostedApp(session, attempt, home);
33
+ const area = hostedAppArea(session, attempt, home);
34
+ if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
35
+ const entry = record.files[0];
36
+ if (record.files.length !== 1 || !entry || entry.path !== "App.apk" || entry.kind !== "file") throw new Error("Hosted Android delivery requires a single file entry named App.apk.");
37
+ const manifest = join(hostedAppBlobs(session, home), record.manifest.sha256);
38
+ if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
39
+ const blob = join(hostedAppBlobs(session, home), entry.sha256);
40
+ const stat = lstatSync(blob);
41
+ if (!stat.isFile() || stat.size !== entry.size || await digest$1(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
42
+ const temporary = join(area, "App.apk.tmp");
43
+ const apk = join(area, "App.apk");
44
+ copyFileSync(blob, temporary);
45
+ renameSync(temporary, apk);
46
+ const tool = findAapt();
47
+ if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
48
+ const badging = getExecutor().runFile(tool.path, [
49
+ "dump",
50
+ "badging",
51
+ apk
52
+ ], {
53
+ timeoutMs: 1e4,
54
+ killSignal: "SIGKILL"
55
+ });
56
+ const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
57
+ const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
58
+ const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
59
+ if (identity !== record.bundleId || !minimum || !api || Number(minimum) > Number(api)) throw new Error("The APK package identity or minimum SDK is incompatible with the hosted emulator.");
60
+ const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
61
+ if (nativeCode !== void 0 && !nativeCode.split(/\s+/).includes(`'${device.architecture}'`)) throw new Error("The APK has no native library slice for the hosted emulator architecture.");
62
+ assertHostedAndroidTarget(home, device);
63
+ const sdkExecutor = hostedAndroidExecutor();
64
+ const installed = installAndroidApp({
65
+ serial: device.serial,
66
+ apkPath: apk,
67
+ packageName: record.bundleId
68
+ }, { exec: sdkExecutor });
69
+ if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
70
+ assertHostedAndroidTarget(home, device);
71
+ if (record.mode === "development" && !metro) throw new Error("Hosted Android development launch requires its Metro bridge.");
72
+ const launched = record.mode === "development" && metro ? launchAndroidApp({
73
+ serial: device.serial,
74
+ packageName: record.bundleId,
75
+ metroPort: metro.devicePort,
76
+ bridgePort: metro.bridgePort,
77
+ physical: true,
78
+ devClientScheme: record.devClientScheme
79
+ }, { exec: sdkExecutor }) : launchAndroidReleaseApp({
80
+ serial: device.serial,
81
+ packageName: record.bundleId
82
+ }, { exec: sdkExecutor });
83
+ if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
84
+ if (record.mode === "development") return "unverified";
85
+ for (let tries = 0; tries < 10; tries++) {
86
+ await new Promise((resolve) => setTimeout(resolve, 200));
87
+ if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
88
+ }
89
+ return "unverified";
90
+ }
91
+ function assertHostedAndroidSerial(home, device) {
92
+ assertHostedDeviceLedger(home, device.avdName, "android");
93
+ const current = readHostedDevice(home, "android");
94
+ if (current.avdName !== device.avdName || current.serial !== device.serial || current.systemImage !== device.systemImage || getAvdNameForSerial(device.serial, { timeoutMs: 2e3 }) !== device.avdName) throw new Error("The hosted Android device identity changed.");
95
+ }
96
+ function assertHostedAndroidTarget(home, device) {
97
+ assertHostedAndroidSerial(home, device);
98
+ if (androidDeviceAbi(device.serial) !== device.architecture) throw new Error("The hosted Android device identity or running ABI changed.");
99
+ }
100
+ function hostedAndroidExecutor() {
101
+ const executor = getExecutor();
102
+ const adb = androidToolPath("adb");
103
+ return {
104
+ ...executor,
105
+ runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
106
+ };
107
+ }
108
+ function restoreHostedAndroidMetro(home, session, attempt, device, metro) {
109
+ const record = readHostedApp(session, attempt, home);
110
+ if (record.state !== "installed" || record.mode !== "development") throw new Error("Metro restore requires an installed development app.");
111
+ assertHostedAndroidTarget(home, device);
112
+ const exec = hostedAndroidExecutor();
113
+ const reversed = reverseMetroPorts({
114
+ serial: device.serial,
115
+ metroPort: metro.bridgePort,
116
+ devicePorts: [metro.devicePort]
117
+ }, { exec });
118
+ if (reversed.failed) throw new Error(reversed.reason);
119
+ assertHostedAndroidTarget(home, device);
120
+ writeDebugHttpHost({
121
+ serial: device.serial,
122
+ packageName: record.bundleId,
123
+ metroPort: metro.devicePort,
124
+ physical: true
125
+ }, { exec });
126
+ }
127
+ //#endregion
128
+ //#region src/device-host/native-logs.ts
129
+ function collectHostedNativeLogs({ home, platform, since, final, query, parse, tailOnly = false, deadline = Date.now() + 1e4, identity = {} }) {
130
+ const directory = hostedNativeLogsDir(home, platform);
131
+ const previous = readHostedNativeLogsCheckpoint(home, platform);
33
132
  const beginning = Math.floor(since / 1e3) * 1e3;
34
133
  let newest;
35
134
  if (!previous) {
@@ -40,50 +139,21 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
40
139
  const now = Math.floor(Date.now() / 1e3) * 1e3;
41
140
  if (now < checkpoint) return false;
42
141
  const backlogMs = Math.max(1e3, now - checkpoint);
43
- let windowMs = !final && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
142
+ let windowMs = !final && !tailOnly && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
44
143
  let until = Math.min(now, checkpoint + windowMs);
45
144
  let end = until === now ? until + 1e3 : until;
46
- const deadline = Date.now() + 1e4;
47
- const exec = getExecutor();
48
- const executable = exec.runFile("/usr/libexec/PlistBuddy", [
49
- "-c",
50
- "Print :CFBundleExecutable",
51
- join(hostedAppArea(session, attempt, home), "App.app", "Info.plist")
52
- ], {
53
- timeoutMs: 2e3,
54
- killSignal: "SIGKILL"
55
- });
56
- if (!executable || /["\\/\0\r\n]/.test(executable)) throw new Error("The app executable cannot form a log predicate.");
57
145
  let output;
58
146
  for (;;) {
59
147
  const remaining = deadline - Date.now();
60
148
  if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
61
149
  try {
62
- output = exec.runFile("xcrun", [
63
- "simctl",
64
- "spawn",
65
- device.udid,
66
- "log",
67
- "show",
68
- "--style",
69
- "ndjson",
70
- "--predicate",
71
- `processImagePath ENDSWITH "/App.app/${executable}"`,
72
- "--info",
73
- "--start",
74
- logDate(from),
75
- "--end",
76
- logDate(end)
77
- ], {
78
- timeoutMs: Math.min(remaining, 4e3),
79
- killSignal: "SIGKILL"
80
- });
150
+ output = query(from, end, Math.min(remaining, 4e3));
81
151
  break;
82
152
  } catch (error) {
83
- const span = final ? until - from : until - checkpoint;
153
+ const span = final || tailOnly ? until - from : until - checkpoint;
84
154
  if (span <= 1e3) throw error;
85
155
  windowMs = Math.max(1e3, Math.floor(span / 4 / 1e3) * 1e3);
86
- if (final) from = Math.max(beginning, now - windowMs);
156
+ if (final || tailOnly) from = Math.max(beginning, now - windowMs);
87
157
  else until = Math.min(now, checkpoint + windowMs);
88
158
  end = until === now ? until + 1e3 : until;
89
159
  if (previous) {
@@ -94,8 +164,8 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
94
164
  withDirLock(join(directory, "query.lock"), () => {
95
165
  const temporary = join(directory, "checkpoint.json.tmp");
96
166
  writeFileSync(temporary, JSON.stringify({
167
+ ...previous,
97
168
  until: checkpoint,
98
- boundary: previous.boundary,
99
169
  windowMs
100
170
  }), { mode: 384 });
101
171
  renameSync(temporary, join(directory, "checkpoint.json"));
@@ -107,18 +177,18 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
107
177
  for (const digest of previous?.boundary ?? []) seen.set(digest, (seen.get(digest) ?? 0) + 1);
108
178
  const boundary = [];
109
179
  const records = [];
110
- if (final && from > checkpoint) records.push({
180
+ if (from > checkpoint) records.push({
111
181
  ts: from,
112
182
  src: "device",
113
- platform: "ios",
183
+ platform,
114
184
  level: "warn",
115
- msg: `Hosted iOS native log gap: dropped interval [${new Date(checkpoint).toISOString()}, ${new Date(from).toISOString()}) to collect the final tail within its budget.`
185
+ msg: `Hosted ${platform === "ios" ? "iOS" : "Android"} native log gap: dropped interval [${new Date(checkpoint).toISOString()}, ${new Date(from).toISOString()}) to collect the tail within its budget.`
116
186
  });
117
187
  for (const line of output.split("\n")) {
118
- const parsed = parseLogStreamLine(line);
119
- if (!parsed || typeof parsed.ts !== "number" || parsed.ts < from || parsed.ts > end) continue;
120
- const entry = JSON.parse(line);
121
- const digest = createHash("sha256").update(JSON.stringify(Object.entries(entry).toSorted(([a], [b]) => a.localeCompare(b)))).digest("hex");
188
+ const event = parse(line);
189
+ if (!event) continue;
190
+ const { record: parsed, digest } = event;
191
+ if (typeof parsed.ts !== "number" || parsed.ts < from || parsed.ts > end) continue;
122
192
  if (parsed.ts >= until - 5e3) boundary.push(digest);
123
193
  if (newest !== void 0 && parsed.ts <= newest) continue;
124
194
  const count = seen.get(digest) ?? 0;
@@ -128,7 +198,7 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
128
198
  }
129
199
  records.push({
130
200
  ...parsed,
131
- platform: "ios"
201
+ platform
132
202
  });
133
203
  }
134
204
  mkdirSync(directory, {
@@ -138,7 +208,7 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
138
208
  withDirLock(join(directory, "query.lock"), () => {
139
209
  const writer = createNdjsonWriter(join(directory, "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
140
210
  try {
141
- for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted iOS native logs.");
211
+ for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted native logs.");
142
212
  } finally {
143
213
  writer.close();
144
214
  }
@@ -147,6 +217,7 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
147
217
  writeFileSync(temporary, JSON.stringify({
148
218
  until,
149
219
  boundary,
220
+ ...identity,
150
221
  ...until < now ? { windowMs } : {}
151
222
  }), { mode: 384 });
152
223
  renameSync(temporary, join(directory, "checkpoint.json"));
@@ -157,12 +228,144 @@ function collectHostedIosLogs(home, session, attempt, since, final = false) {
157
228
  return until < now;
158
229
  }
159
230
  //#endregion
231
+ //#region src/device-host/android-logs.ts
232
+ /** Captures bounded logcat output from the installed app on this home's exact ledger-owned serial. */
233
+ function collectHostedAndroidLogs(home, session, attempt, since, final = false) {
234
+ const deadline = Date.now() + 1e4;
235
+ const device = readHostedDevice(home, "android");
236
+ assertHostedAndroidSerial(home, device);
237
+ const app = readHostedAppMetadata(session, attempt, home);
238
+ if (app.state !== "installed") return false;
239
+ const exec = getExecutor();
240
+ const adb = androidToolPath("adb");
241
+ const clockOffsetMs = androidClockOffset(device.serial, { exec: {
242
+ ...exec,
243
+ runFile: (file, args, options) => exec.runFile(file === "adb" ? adb : file, args, options)
244
+ } }) ?? 0;
245
+ const previous = readHostedNativeLogsCheckpoint(home, "android");
246
+ let current = null;
247
+ try {
248
+ current = parsePidof(exec.runFile(adb, [
249
+ "-s",
250
+ device.serial,
251
+ "shell",
252
+ "pidof",
253
+ app.bundleId
254
+ ], {
255
+ timeoutMs: 2e3,
256
+ killSignal: "SIGKILL"
257
+ }));
258
+ } catch (error) {
259
+ const failure = error;
260
+ if (failure.status !== 1 || failure.signal != null || failure.stdout !== "" || failure.stderr !== "") throw error;
261
+ }
262
+ const prior = previous?.appAttempt === attempt ? previous.pid : void 0;
263
+ const pids = new Set([current, prior].filter((pid) => typeof pid === "number"));
264
+ if (!pids.size) return false;
265
+ return collectHostedNativeLogs({
266
+ home,
267
+ platform: "android",
268
+ since,
269
+ final,
270
+ deadline,
271
+ tailOnly: true,
272
+ identity: {
273
+ appAttempt: attempt,
274
+ pid: current ?? void 0
275
+ },
276
+ query: (from, _end, timeoutMs) => [...pids].map((pid) => {
277
+ const remaining = deadline - Date.now();
278
+ if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
279
+ return exec.runFile(adb, [
280
+ "-s",
281
+ device.serial,
282
+ "logcat",
283
+ "-d",
284
+ "-v",
285
+ "epoch",
286
+ "-T",
287
+ ((from - clockOffsetMs) / 1e3).toFixed(3),
288
+ "--pid",
289
+ String(pid)
290
+ ], {
291
+ timeoutMs: Math.min(remaining, timeoutMs),
292
+ killSignal: "SIGKILL"
293
+ });
294
+ }).join("\n"),
295
+ parse: (line) => {
296
+ const record = parseLogcatLine(line, { clockOffsetMs });
297
+ if (!record || !pids.has(Number(/\((\d+)\)$/.exec(String(record.proc))?.[1]))) return null;
298
+ return {
299
+ record,
300
+ digest: createHash("sha256").update(line.trim()).digest("hex")
301
+ };
302
+ }
303
+ });
304
+ }
305
+ //#endregion
306
+ //#region src/device-host/ios-logs.ts
307
+ function logDate(ms) {
308
+ return new Date(ms).toISOString().replace("T", " ").replace(".000Z", "+0000");
309
+ }
310
+ /** Captures a bounded unified-log window only from this home's exact ledger-owned simulator. */
311
+ function collectHostedIosLogs(home, session, attempt, since, final = false) {
312
+ const device = readHostedDevice(home);
313
+ assertHostedDeviceLedger(home, device.udid);
314
+ if (readHostedAppMetadata(session, attempt, home).state !== "installed") return false;
315
+ const deadline = Date.now() + 1e4;
316
+ const exec = getExecutor();
317
+ const executable = exec.runFile("/usr/libexec/PlistBuddy", [
318
+ "-c",
319
+ "Print :CFBundleExecutable",
320
+ join(hostedAppArea(session, attempt, home), "App.app", "Info.plist")
321
+ ], {
322
+ timeoutMs: 2e3,
323
+ killSignal: "SIGKILL"
324
+ });
325
+ if (!executable || /["\\/\0\r\n]/.test(executable)) throw new Error("The app executable cannot form a log predicate.");
326
+ return collectHostedNativeLogs({
327
+ home,
328
+ platform: "ios",
329
+ deadline,
330
+ since,
331
+ final,
332
+ query: (from, end, timeoutMs) => exec.runFile("xcrun", [
333
+ "simctl",
334
+ "spawn",
335
+ device.udid,
336
+ "log",
337
+ "show",
338
+ "--style",
339
+ "ndjson",
340
+ "--predicate",
341
+ `processImagePath ENDSWITH "/App.app/${executable}"`,
342
+ "--info",
343
+ "--start",
344
+ logDate(from),
345
+ "--end",
346
+ logDate(end)
347
+ ], {
348
+ timeoutMs,
349
+ killSignal: "SIGKILL"
350
+ }),
351
+ parse: (line) => {
352
+ const record = parseLogStreamLine(line);
353
+ if (!record) return null;
354
+ const entry = JSON.parse(line);
355
+ return {
356
+ record,
357
+ digest: createHash("sha256").update(JSON.stringify(Object.entries(entry).toSorted(([a], [b]) => a.localeCompare(b)))).digest("hex")
358
+ };
359
+ }
360
+ });
361
+ }
362
+ //#endregion
160
363
  //#region src/device-host/app.ts
161
364
  function inside(root, path) {
162
365
  const diff = relative(root, path);
163
366
  return diff !== "" && diff !== ".." && !diff.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(diff);
164
367
  }
165
- function removeHostedAppData(home) {
368
+ function removeHostedAppData(home, { keepBlobs = false } = {}) {
166
369
  const area = realpathSync(join(home, ".."));
167
370
  const assertInside = (path) => {
168
371
  if (!inside(area, realpathSync(path))) throw new Error("Hosted app data resolves outside its private area.");
@@ -189,9 +392,9 @@ function removeHostedAppData(home) {
189
392
  remove(join(directory, "blobs"));
190
393
  }
191
394
  }
192
- remove(join(area, "blobs"));
395
+ if (!keepBlobs) remove(join(area, "blobs"));
193
396
  }
194
- async function digest$1(path) {
397
+ async function digest(path) {
195
398
  const hash = createHash("sha256");
196
399
  for await (const chunk of createReadStream(path)) hash.update(chunk);
197
400
  return hash.digest("hex");
@@ -217,7 +420,7 @@ async function materializeHostedApp(home, session, attempt) {
217
420
  const app = join(area, "App.app");
218
421
  if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
219
422
  const manifest = join(blobs, record.manifest.sha256);
220
- if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
423
+ if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
221
424
  rmSync(app, {
222
425
  recursive: true,
223
426
  force: true
@@ -226,7 +429,7 @@ async function materializeHostedApp(home, session, attempt) {
226
429
  for (const file of record.files) {
227
430
  const blob = join(blobs, file.sha256);
228
431
  const stat = lstatSync(blob);
229
- if (!stat.isFile() || stat.size !== file.size || await digest$1(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
432
+ if (!stat.isFile() || stat.size !== file.size || await digest(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
230
433
  if (file.kind === "link") continue;
231
434
  const target = join(app, file.path);
232
435
  mkdirSync(dirname(target), {
@@ -252,7 +455,7 @@ async function materializeHostedApp(home, session, attempt) {
252
455
  for (const file of record.files) {
253
456
  const path = realpathSync(join(app, file.path));
254
457
  if (!inside(root, path)) throw new Error(`The app entry resolves outside its bundle: ${file.path}`);
255
- if (file.kind !== "link" && await digest$1(path) !== file.sha256) throw new Error("App entries alias different content.");
458
+ if (file.kind !== "link" && await digest(path) !== file.sha256) throw new Error("App entries alias different content.");
256
459
  }
257
460
  return {
258
461
  app,
@@ -375,6 +578,55 @@ async function runHostedDevice(mode, selectors, app) {
375
578
  device = readHostedDevice(home);
376
579
  const ledger = assertHostedDeviceLedger(home, device.udid, "ios", { allowEmpty: mode === "stop" });
377
580
  const current = inventory().find((sim) => sim.udid === device.udid);
581
+ if (mode === "inspect" || mode === "adopt") {
582
+ if (!current || !current.available || !current.name.startsWith("stim-") || current.name !== device.name || current.state !== "Shutdown") throw new Error("The parked hosted simulator is missing, renamed or not Shutdown.");
583
+ let choice;
584
+ try {
585
+ choice = selectHostedIosDevice(selectors);
586
+ } catch (error) {
587
+ if (mode === "inspect") return {
588
+ state: "incompatible",
589
+ device,
590
+ notice: error.message
591
+ };
592
+ throw error;
593
+ }
594
+ if (choice.deviceTypeId !== device.deviceTypeId || choice.runtimeId !== device.runtimeId || choice.architecture !== device.architecture) {
595
+ if (mode === "inspect") return {
596
+ state: "incompatible",
597
+ device
598
+ };
599
+ throw new Error("The parked hosted simulator does not match the requested selectors.");
600
+ }
601
+ if (mode === "inspect") return {
602
+ state: "compatible",
603
+ device
604
+ };
605
+ await bootIosSim(device.udid, { openViewer: false });
606
+ if (inventory().find((sim) => sim.udid === device.udid)?.state !== "Booted") throw new Error("Hosted simulator boot could not be verified.");
607
+ resetIosPrivacy(device.udid);
608
+ resetIosKeychain(device.udid);
609
+ for (const bundleId of listUserApps(device.udid)) uninstallIosApp(device.udid, bundleId);
610
+ withDirLock(join(home, "hosted-device.lock"), () => {
611
+ const temporary = join(home, "hosted-device.json.tmp");
612
+ writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
613
+ renameSync(temporary, join(home, "hosted-device.json"));
614
+ });
615
+ removeHostedAppData(home, { keepBlobs: true });
616
+ return {
617
+ state: "ready",
618
+ device
619
+ };
620
+ }
621
+ if (mode === "park") {
622
+ const outcome = teardownOwnedIosSim(device.udid);
623
+ if (outcome.status !== "torn-down" || inventory().find((sim) => sim.udid === device.udid)?.state !== "Shutdown") throw new Error(outcome.reason ?? "Hosted simulator shutdown could not be verified.");
624
+ removeHostedAppData(home, { keepBlobs: true });
625
+ return {
626
+ state: "parked",
627
+ device
628
+ };
629
+ }
378
630
  if (mode === "install") {
379
631
  if (!app || current?.state !== "Booted") throw new Error("Hosted app installation requires its booted owned simulator.");
380
632
  const launched = await installHostedApp(home, app.session, app.attempt, device, app.metroPort);
@@ -404,83 +656,13 @@ async function runHostedDevice(mode, selectors, app) {
404
656
  };
405
657
  } catch (error) {
406
658
  return {
407
- state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
659
+ state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
408
660
  device,
409
661
  notice: error.message
410
662
  };
411
663
  }
412
664
  }
413
665
  //#endregion
414
- //#region src/device-host/android-app.ts
415
- async function digest(path) {
416
- const hash = createHash("sha256");
417
- for await (const chunk of createReadStream(path)) hash.update(chunk);
418
- return hash.digest("hex");
419
- }
420
- /** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
421
- async function installHostedAndroidApp(home, session, attempt, device) {
422
- const record = readHostedApp(session, attempt, home);
423
- const area = hostedAppArea(session, attempt, home);
424
- if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
425
- const entry = record.files[0];
426
- if (record.files.length !== 1 || !entry || entry.path !== "App.apk" || entry.kind !== "file") throw new Error("Hosted Android delivery requires a single file entry named App.apk.");
427
- const manifest = join(hostedAppBlobs(session, home), record.manifest.sha256);
428
- if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
429
- const blob = join(hostedAppBlobs(session, home), entry.sha256);
430
- const stat = lstatSync(blob);
431
- if (!stat.isFile() || stat.size !== entry.size || await digest(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
432
- const temporary = join(area, "App.apk.tmp");
433
- const apk = join(area, "App.apk");
434
- copyFileSync(blob, temporary);
435
- renameSync(temporary, apk);
436
- const tool = findAapt();
437
- if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
438
- const badging = getExecutor().runFile(tool.path, [
439
- "dump",
440
- "badging",
441
- apk
442
- ], {
443
- timeoutMs: 1e4,
444
- killSignal: "SIGKILL"
445
- });
446
- const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
447
- const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
448
- const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
449
- if (identity !== record.bundleId || !minimum || !api || Number(minimum) > Number(api)) throw new Error("The APK package identity or minimum SDK is incompatible with the hosted emulator.");
450
- const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
451
- if (nativeCode !== void 0 && !nativeCode.split(/\s+/).includes(`'${device.architecture}'`)) throw new Error("The APK has no native library slice for the hosted emulator architecture.");
452
- const assertTarget = () => {
453
- assertHostedDeviceLedger(home, device.avdName, "android");
454
- const current = readHostedDevice(home, "android");
455
- if (current.avdName !== device.avdName || current.serial !== device.serial || current.systemImage !== device.systemImage || getAvdNameForSerial(device.serial) !== device.avdName || androidDeviceAbi(device.serial) !== device.architecture) throw new Error("The hosted Android device identity or running ABI changed.");
456
- };
457
- assertTarget();
458
- const executor = getExecutor();
459
- const adb = androidToolPath("adb");
460
- const sdkExecutor = {
461
- ...executor,
462
- runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
463
- };
464
- const installed = installAndroidApp({
465
- serial: device.serial,
466
- apkPath: apk,
467
- packageName: record.bundleId
468
- }, { exec: sdkExecutor });
469
- if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
470
- assertTarget();
471
- const launched = launchAndroidReleaseApp({
472
- serial: device.serial,
473
- packageName: record.bundleId
474
- }, { exec: sdkExecutor });
475
- if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
476
- if (record.mode === "development") return "unverified";
477
- for (let tries = 0; tries < 10; tries++) {
478
- await new Promise((resolve) => setTimeout(resolve, 200));
479
- if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
480
- }
481
- return "unverified";
482
- }
483
- //#endregion
484
666
  //#region src/device-host/android.ts
485
667
  function portIsOccupied(port) {
486
668
  const devices = listAdbDevices({ timeoutMs: 5e3 });
@@ -505,10 +687,23 @@ async function runHostedAndroidDevice(mode, request, app) {
505
687
  let device = null;
506
688
  let creationStarted = false;
507
689
  try {
508
- if (mode === "install") {
509
- if (!app) throw new Error("Hosted Android installation needs its admitted app attempt.");
690
+ if (mode === "install" || mode === "reverse") {
691
+ if (!app) throw new Error("Hosted Android native work needs its admitted app attempt.");
510
692
  device = readHostedDevice(home, "android");
511
- const launched = await installHostedAndroidApp(home, request.session, app.attempt, device);
693
+ if (!hostedConsolePort(request.consolePort) || device.consolePort !== request.consolePort) throw new Error("The hosted Android device identity differs from its session console port.");
694
+ const metro = app.metroPort && app.clientMetroPort ? {
695
+ bridgePort: app.metroPort,
696
+ devicePort: app.clientMetroPort
697
+ } : void 0;
698
+ if (mode === "reverse") {
699
+ if (!metro) throw new Error("Hosted Android Metro restore needs its bridge and device ports.");
700
+ restoreHostedAndroidMetro(home, request.session, app.attempt, device, metro);
701
+ return {
702
+ state: "ready",
703
+ device
704
+ };
705
+ }
706
+ const launched = await installHostedAndroidApp(home, request.session, app.attempt, device, metro);
512
707
  return {
513
708
  state: "installed",
514
709
  device,
@@ -559,7 +754,69 @@ async function runHostedAndroidDevice(mode, request, app) {
559
754
  };
560
755
  }
561
756
  device = readHostedDevice(home, "android");
562
- const ledger = assertHostedDeviceLedger(home, device.avdName, "android", { allowEmpty: true });
757
+ const ledger = assertHostedDeviceLedger(home, device.avdName, "android", { allowEmpty: mode === "stop" });
758
+ if (mode === "inspect" || mode === "adopt") {
759
+ if (device.avdName !== ownedAvdName(`hosted-${request.session}`) || !listAvds({ timeoutMs: 5e3 }).includes(device.avdName)) throw new Error("The parked hosted AVD is missing or names another session.");
760
+ if (!resolveOwnedAvdSerial(device.avdName, { timeoutMs: 5e3 }).notRunning) throw new Error("The parked hosted AVD is missing, not owned or running.");
761
+ assertOwnedAvdStopped(device.avdName);
762
+ if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The parked hosted emulator is still running.");
763
+ if (ownedAvdSystemImage(device.avdName) !== device.systemImage) throw new Error("The parked hosted AVD system image no longer matches its device record.");
764
+ let choice;
765
+ try {
766
+ choice = selectHostedAndroidDevice(request);
767
+ } catch (error) {
768
+ if (mode === "inspect") return {
769
+ state: "incompatible",
770
+ device,
771
+ notice: error.message
772
+ };
773
+ throw error;
774
+ }
775
+ if (choice.systemImage !== device.systemImage || choice.deviceProfile !== device.deviceProfile || choice.architecture !== device.architecture) {
776
+ if (mode === "inspect") return {
777
+ state: "incompatible",
778
+ device
779
+ };
780
+ throw new Error("The parked hosted emulator does not match the requested selectors.");
781
+ }
782
+ if (mode === "inspect") return {
783
+ state: "compatible",
784
+ device
785
+ };
786
+ if (!hostedConsolePort(request.consolePort) || portIsOccupied(request.consolePort)) throw new Error("The selected Android console port is invalid or occupied.");
787
+ device = {
788
+ ...device,
789
+ consolePort: request.consolePort,
790
+ serial: `emulator-${request.consolePort}`
791
+ };
792
+ withDirLock(join(home, "hosted-device.lock"), () => {
793
+ const temporary = join(home, "hosted-device.json.tmp");
794
+ writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
795
+ renameSync(temporary, join(home, "hosted-device.json"));
796
+ });
797
+ bootAndroidEmulator(device.avdName, device.consolePort, {
798
+ openViewer: false,
799
+ logFile: join(home, "emulator.log")
800
+ });
801
+ if (!(await waitForBoot(device.serial, 24e4, { commandTimeoutMs: 5e3 })).ok || getAvdNameForSerial(device.serial) !== device.avdName || androidDeviceAbi(device.serial) !== device.architecture) throw new Error("Hosted Android boot could not be verified for its exact owned AVD.");
802
+ await resetAdoptedAvd(device.avdName, device.serial, "");
803
+ removeHostedAppData(home, { keepBlobs: true });
804
+ return {
805
+ state: "ready",
806
+ device
807
+ };
808
+ }
809
+ if (mode === "park") {
810
+ const outcome = teardownOwnedAvd(device.avdName, {});
811
+ if (outcome.status !== "torn-down") throw new Error(outcome.reason ?? "Hosted Android shutdown was not established.");
812
+ assertOwnedAvdStopped(device.avdName);
813
+ if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
814
+ removeHostedAppData(home, { keepBlobs: true });
815
+ return {
816
+ state: "parked",
817
+ device
818
+ };
819
+ }
563
820
  if (ledger === "listed") {
564
821
  withConfigLock$1(() => {
565
822
  if (!loadConfig$1()) ensureConfig();
@@ -578,7 +835,7 @@ async function runHostedAndroidDevice(mode, request, app) {
578
835
  };
579
836
  } catch (error) {
580
837
  return {
581
- state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
838
+ state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
582
839
  device,
583
840
  notice: error.message
584
841
  };
@@ -855,7 +1112,7 @@ async function runHostedMacosApp(mode, request, app) {
855
1112
  }
856
1113
  //#endregion
857
1114
  //#region src/device-host/offer.ts
858
- function inspectHostedDevice(request) {
1115
+ function inspectHostedDevice(request, options) {
859
1116
  const cpus = availableParallelism();
860
1117
  let workerDiskFreeBytes = null;
861
1118
  try {
@@ -867,7 +1124,8 @@ function inspectHostedDevice(request) {
867
1124
  loadPerCore: loadavg()[1] / cpus,
868
1125
  memoryFreeBytes: freemem(),
869
1126
  memoryPressure: readHostMemoryPressure(),
870
- workerDiskFreeBytes
1127
+ workerDiskFreeBytes,
1128
+ ...options ? { localDevices: countLiveOwnedDevices({}, options) } : {}
871
1129
  };
872
1130
  try {
873
1131
  if (process.platform !== "darwin") throw new Error("Hosted device sessions require a Mac.");
@@ -913,12 +1171,26 @@ async function main() {
913
1171
  "stop",
914
1172
  "install",
915
1173
  "offer",
916
- "logs"
1174
+ "count",
1175
+ "logs",
1176
+ "reverse",
1177
+ "park",
1178
+ "inspect",
1179
+ "adopt"
917
1180
  ].includes(String(input.mode))) throw new Error("Invalid hosted worker request.");
1181
+ let exclude;
1182
+ if (input.exclude !== void 0) {
1183
+ if (!Array.isArray(input.exclude) || input.exclude.some((device) => !isJsonObject(device) || device.platform !== "ios" && device.platform !== "android" || device.key !== void 0 && typeof device.key !== "string")) throw new Error("Invalid hosted device exclusions.");
1184
+ exclude = input.exclude;
1185
+ }
1186
+ if (input.mode === "count") {
1187
+ process.stdout.write(`${JSON.stringify(countLiveOwnedDevices({}, { exclude }))}\n`);
1188
+ return;
1189
+ }
918
1190
  if (input.mode === "offer") {
919
1191
  const request = parseHostedOfferRequest(input);
920
1192
  if (!request) throw new Error("Invalid hosted offer selectors.");
921
- process.stdout.write(`${JSON.stringify(inspectHostedDevice(request))}\n`);
1193
+ process.stdout.write(`${JSON.stringify(inspectHostedDevice(request, exclude ? { exclude } : void 0))}\n`);
922
1194
  return;
923
1195
  }
924
1196
  if (process.platform !== "darwin") {
@@ -929,12 +1201,18 @@ async function main() {
929
1201
  })}\n`);
930
1202
  return;
931
1203
  }
932
- if ((input.mode === "install" || input.mode === "logs") && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
933
- if (input.metroPort !== void 0 && (typeof input.metroPort !== "number" || !Number.isInteger(input.metroPort) || input.metroPort < 1 || input.metroPort > 65535)) throw new Error("Invalid hosted Metro port.");
1204
+ if ((input.mode === "install" || input.mode === "logs" || input.mode === "reverse") && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
1205
+ for (const port of [input.metroPort, input.clientMetroPort]) if (port !== void 0 && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("Invalid hosted Metro port.");
1206
+ if (input.mode === "reverse" && input.platform !== "android") throw new Error("Metro reverse requires Android.");
1207
+ if (input.platform === "macos" && [
1208
+ "park",
1209
+ "inspect",
1210
+ "adopt"
1211
+ ].includes(String(input.mode))) throw new Error("Hosted parking requires iOS or Android.");
934
1212
  if (input.platform === "macos" && !hostedMacosAppSlot(input.appSlot)) throw new Error("Invalid hosted macOS app slot.");
935
1213
  if (input.mode === "logs") {
936
- if (input.platform !== "ios" || !process.env.STIM_HOME || typeof input.since !== "number" || !Number.isFinite(input.since)) throw new Error("Invalid hosted iOS log request.");
937
- const more = collectHostedIosLogs(process.env.STIM_HOME, input.session, input.attempt, input.since, input.final === true);
1214
+ if (input.platform !== "ios" && input.platform !== "android" || !process.env.STIM_HOME || typeof input.since !== "number" || !Number.isFinite(input.since)) throw new Error("Invalid hosted native log request.");
1215
+ const more = (input.platform === "android" ? collectHostedAndroidLogs : collectHostedIosLogs)(process.env.STIM_HOME, input.session, input.attempt, input.since, input.final === true);
938
1216
  process.stdout.write(`${JSON.stringify({ more })}\n`);
939
1217
  return;
940
1218
  }
@@ -946,7 +1224,11 @@ async function main() {
946
1224
  consolePort: input.consolePort,
947
1225
  ...typeof input.systemImage === "string" ? { systemImage: input.systemImage } : {},
948
1226
  ...typeof input.deviceProfile === "string" ? { deviceProfile: input.deviceProfile } : {}
949
- }, input.mode === "install" ? { attempt: input.attempt } : void 0) : await runHostedDevice(input.mode, {
1227
+ }, input.mode === "install" || input.mode === "reverse" ? {
1228
+ attempt: input.attempt,
1229
+ metroPort: input.metroPort,
1230
+ clientMetroPort: input.clientMetroPort
1231
+ } : void 0) : await runHostedDevice(input.mode, {
950
1232
  ...typeof input.deviceType === "string" ? { deviceType: input.deviceType } : {},
951
1233
  ...typeof input.runtime === "string" ? { runtime: input.runtime } : {}
952
1234
  }, input.mode === "install" ? {