stim 1.15.0 → 1.17.0
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.
- package/dist/{activity-B2-kb7k0.mjs → activity-IRvJY_ru.mjs} +49 -16
- package/dist/agent-device-usage-output-BK41vnzN.mjs +411 -0
- package/dist/{android-Cu_L4RpL.mjs → android--GorydsK.mjs} +692 -321
- package/dist/{android-Db2eEMaf.mjs → android-B_YNZy61.mjs} +25 -60
- package/dist/{android-CYmq0jNi.mjs → android-DE4v59Mb.mjs} +12 -3
- package/dist/{android-cas-PnKfAdB2.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
- package/dist/{app-install-CAAplXAt.mjs → app-install-VPkI5WEg.mjs} +52 -8
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/{budget-CMiqghig.mjs → budget-DM984mnd.mjs} +372 -366
- package/dist/{build-plan-aeGMFX7e.mjs → build-plan-CmQCoyT-.mjs} +346 -88
- package/dist/{build-slots-Df1ApSuH.mjs → build-slots-41v_d2x9.mjs} +36 -21
- package/dist/{cli-0cX5s4NM.mjs → cli-Daxug5-K.mjs} +23 -17
- package/dist/cli.mjs +1 -1
- package/dist/collector-run.mjs +5 -192
- package/dist/{command-output-CliHHn7a.mjs → command-output-CDAIFD-8.mjs} +3 -3
- package/dist/{created-devices-BwVb3liH.mjs → created-devices-CpvzBL25.mjs} +1 -1
- package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
- package/dist/{deps-BFlKbOIp.mjs → deps-DNWaQs4V.mjs} +2 -2
- package/dist/{dev-client-CjzLoNs0.mjs → dev-client-B-XC4uRH.mjs} +3 -40
- package/dist/{device-BZ1IyS5A.mjs → device-C8IOoJPv.mjs} +7 -6
- package/dist/device-capacity-D0GWHnTM.mjs +580 -0
- package/dist/device-host-worker.mjs +587 -110
- package/dist/{device-ios-DMQBUPus.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
- package/dist/{device-lease-W5Pi1q38.mjs → device-lease-DpoS36_u.mjs} +2 -2
- package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-B8YltIH_.mjs} +5 -125
- package/dist/device-pool-3o7s5lo4.mjs +125 -0
- package/dist/{device-remote-9ulPsdhY.mjs → device-remote-hFSkpfQu.mjs} +13 -13
- package/dist/{doctor-BcE5xsBn.mjs → doctor-Cnk9d7Hm.mjs} +86 -59
- package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-C-NMnhrk.mjs} +11 -15
- package/dist/{gc-BuL1Genr.mjs → gc-DxCL2A46.mjs} +478 -33
- package/dist/{gradle-CmdOBq1d.mjs → gradle-D7-L2Th3.mjs} +4 -4
- package/dist/{guide-cCSGRFd3.mjs → guide-DjrTQGik.mjs} +2198 -566
- package/dist/{guide-status-CIoWK6mG.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
- package/dist/hosted-android-DwLS4Mse.mjs +32 -0
- package/dist/hosted-client-wk1W_CNW.mjs +196 -0
- package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
- package/dist/hosted-logs-DqYmWtXc.mjs +187 -0
- package/dist/hosted-macos-dlHXEeF5.mjs +135 -0
- package/dist/hosted-native-C9xJ9rpA.mjs +255 -0
- package/dist/{build-progress-C2Jv1AtA.mjs → idle-B2m-Ovod.mjs} +300 -9
- package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
- package/dist/{in-use-CndPMilN.mjs → in-use-CvGOUn-x.mjs} +30 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-Chob8ny7.mjs → ios-DE6duQoz.mjs} +819 -460
- package/dist/{ios-3CtCRhwr.mjs → ios-DSfnPfse.mjs} +4 -4
- package/dist/{ios-device-DERmMmk-.mjs → ios-device-pAN4t54-.mjs} +1 -1
- package/dist/ios-state-8L3lgum6.mjs +89 -0
- package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-lTnWZikR.mjs} +3 -3
- package/dist/logs-DUoKlDoS.mjs +384 -0
- package/dist/{client-C2IS36Qf.mjs → machines-C2XiNwh9.mjs} +330 -37
- package/dist/{macos-BwtbUtTN.mjs → macos-32po3kUq.mjs} +23 -18
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-Di8EN2Tb.mjs → metro-g8NqiJp-.mjs} +1 -1
- package/dist/metro-gateway-DmslUM30.mjs +103 -0
- package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-C8m8EQs2.mjs} +2 -2
- package/dist/{native-run-DFDoIRbY.mjs → native-run-D1Q56rAD.mjs} +17 -6
- package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +1 -0
- package/dist/offload-worker.mjs +13 -8
- package/dist/{teardown-BYuQafiA.mjs → ownership-BMLqyBgJ.mjs} +639 -53
- package/dist/{ownership-BkYxUtuj.mjs → ownership-DyRQru0q.mjs} +2 -2
- package/dist/{ports-DddwttKM.mjs → ports-4nYqxPWC.mjs} +2 -2
- package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D7RUMkwN.mjs} +4 -4
- package/dist/preview-CxPRIEht.mjs +318 -0
- package/dist/{project-CV3tDFHl.mjs → project-D47Rh0ly.mjs} +72 -3
- package/dist/{recordings-GEXp4iBT.mjs → recordings-BNgsD1Tr.mjs} +1 -1
- package/dist/{reload-ChPiwvot.mjs → reload-qZei6iWn.mjs} +77 -13
- package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-Ci4DQYYj.mjs} +4 -4
- package/dist/{run-B-xqRrsg.mjs → run-CkE9y0nc.mjs} +2 -2
- package/dist/{server-bare-X7PUgizL.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
- package/dist/{server-expo-DhtX6bom.mjs → server-expo-CHTyXD0G.mjs} +4 -4
- package/dist/server-expo-CeakhI8P.mjs +2 -0
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/settings-Cfd-0r2d.mjs +1440 -0
- package/dist/{settings-DbL6uhH8.mjs → settings-SR9OmW9h.mjs} +10 -4
- package/dist/settings.schema.json +472 -50
- package/dist/{simslim-Ciluxlq4.mjs → simslim-BWNrc3M3.mjs} +3 -3
- package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-DmjBmpQV.mjs} +10 -7
- package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
- package/dist/{start-CPO4wRg7.mjs → start-BopZcr4n.mjs} +1 -1
- package/dist/{start-CUD9nasA.mjs → start-uee1wWjY.mjs} +21 -17
- package/dist/{state-CaeaHbDA.mjs → state-BKa8wNN5.mjs} +1 -1
- package/dist/{state-BxCgeTQu.mjs → state-DyaNTgfR.mjs} +2 -2
- package/dist/{state-DFm77wRn.mjs → state-PeQiJuIu.mjs} +5 -5
- package/dist/{stats-DBLLFmKd.mjs → stats-Bq4fJOdB.mjs} +19 -5
- package/dist/{status-l84Gbvp2.mjs → status-CzS4bPO9.mjs} +5 -3
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/{status-DYXxNOT0.mjs → status-dXX2JX1C.mjs} +244 -50
- package/dist/{stop-CceYh1yG.mjs → stop-BCwvvs7F.mjs} +2 -2
- package/dist/{stop-Bad61z_2.mjs → stop-BjizrQwv.mjs} +83 -21
- package/dist/{stop-Bvf1MflS.mjs → stop-Cr1d8DSE.mjs} +4 -4
- package/dist/stop-cause-rhmGfwvt.mjs +97 -0
- package/dist/supervisor-run.d.mts +7 -4
- package/dist/supervisor-run.mjs +156 -27
- package/dist/{support-WLOzVKUv.mjs → support-BJPgYCu-.mjs} +61 -11
- package/dist/{support-CDQbDkL5.mjs → support-F9-sr0Oi.mjs} +6 -5
- package/dist/{toolchain-DmeYjXD9.mjs → toolchain-CNIBTIOv.mjs} +11 -11
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-DF4JRvYI.mjs} +274 -5
- package/dist/watchman-APYjXSQa.mjs +48 -0
- package/dist/{web-k6w8-HnE.mjs → web-exQtGkoc.mjs} +14 -16
- package/dist/web-run.mjs +5 -5
- package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
- package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-DqcCbpE7.mjs} +34 -34
- package/dist/workspaces-Bcib97-6.mjs +306 -0
- package/dist/{worktree-BT_uULFx.mjs → worktree-Dc98cE6_.mjs} +1 -1
- package/dist/{worktree-qXKzADSC.mjs → worktree-ZpZR3S1k.mjs} +601 -33
- package/package.json +5 -5
- package/dist/device-capacity-DMuKC7nx.mjs +0 -136
- package/dist/hosted-macos-DwowNU7W.mjs +0 -447
- package/dist/logs-h9h6MUvE.mjs +0 -568
- package/dist/machine-usage-D_YYMzfR.mjs +0 -283
- package/dist/machines-CYtOCqcj.mjs +0 -259
- package/dist/merge-state-BnxLNQiM.mjs +0 -272
- package/dist/server-expo-KJeD109I.mjs +0 -2
- package/dist/settings-CduVcoxj.mjs +0 -680
- package/dist/stop-cause-CZ_Mr-v9.mjs +0 -665
- package/dist/watchman-Bx0wceoF.mjs +0 -238
- package/dist/worktree-C0c5Udwa.mjs +0 -753
|
@@ -1,26 +1,400 @@
|
|
|
1
1
|
import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
2
|
+
import { D as withConfigLock$1, _ as loadConfig$1, c as ensureConfig } from "./config-D0DbpZtM.mjs";
|
|
2
3
|
import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
3
|
-
import { c as writeWorkspaceState,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { n as
|
|
11
|
-
import { t as
|
|
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
|
+
import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
|
|
11
|
+
import { n as parseLogStreamLine } from "./ios-Cr1h5gnO.mjs";
|
|
12
|
+
import { n as parseLogcatLine, r as parsePidof, t as androidClockOffset } from "./android-DE4v59Mb.mjs";
|
|
13
|
+
import { i as countLiveOwnedDevices } from "./device-capacity-D0GWHnTM.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-F9-sr0Oi.mjs";
|
|
16
|
+
import { n as stopMacosAppHeld } from "./stop-Cr1d8DSE.mjs";
|
|
17
|
+
import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
|
|
12
18
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
19
|
import { chmodSync, closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, symlinkSync, writeFileSync } from "node:fs";
|
|
14
20
|
import { dirname, isAbsolute, join, posix, relative, sep } from "node:path";
|
|
15
|
-
import { configDir, withDirLock } from "@stim-cli/core";
|
|
16
|
-
import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedConsolePort, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedDevice, readMacosRecord } from "@stim-cli/core/state";
|
|
21
|
+
import { LOG_ROTATE_BYTES, configDir, withDirLock } from "@stim-cli/core";
|
|
22
|
+
import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedMacosAppSlot, hostedMacosBundleId, hostedNativeLogsDir, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedNativeLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
|
|
17
23
|
import { availableParallelism, freemem, homedir, loadavg } from "node:os";
|
|
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");
|
|
29
|
+
}
|
|
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);
|
|
132
|
+
const beginning = Math.floor(since / 1e3) * 1e3;
|
|
133
|
+
let newest;
|
|
134
|
+
if (!previous) {
|
|
135
|
+
for (const entry of readNdjsonGenerations(join(directory, "device.ndjson"))) if (typeof entry.ts === "number" && (newest === void 0 || entry.ts > newest)) newest = entry.ts;
|
|
136
|
+
}
|
|
137
|
+
const checkpoint = previous?.until ?? Math.max(beginning, Math.floor((newest ?? beginning) / 1e3) * 1e3);
|
|
138
|
+
let from = Math.max(beginning, previous ? checkpoint - 5e3 : checkpoint);
|
|
139
|
+
const now = Math.floor(Date.now() / 1e3) * 1e3;
|
|
140
|
+
if (now < checkpoint) return false;
|
|
141
|
+
const backlogMs = Math.max(1e3, now - checkpoint);
|
|
142
|
+
let windowMs = !final && !tailOnly && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
|
|
143
|
+
let until = Math.min(now, checkpoint + windowMs);
|
|
144
|
+
let end = until === now ? until + 1e3 : until;
|
|
145
|
+
let output;
|
|
146
|
+
for (;;) {
|
|
147
|
+
const remaining = deadline - Date.now();
|
|
148
|
+
if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
|
|
149
|
+
try {
|
|
150
|
+
output = query(from, end, Math.min(remaining, 4e3));
|
|
151
|
+
break;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const span = final || tailOnly ? until - from : until - checkpoint;
|
|
154
|
+
if (span <= 1e3) throw error;
|
|
155
|
+
windowMs = Math.max(1e3, Math.floor(span / 4 / 1e3) * 1e3);
|
|
156
|
+
if (final || tailOnly) from = Math.max(beginning, now - windowMs);
|
|
157
|
+
else until = Math.min(now, checkpoint + windowMs);
|
|
158
|
+
end = until === now ? until + 1e3 : until;
|
|
159
|
+
if (previous) {
|
|
160
|
+
mkdirSync(directory, {
|
|
161
|
+
recursive: true,
|
|
162
|
+
mode: 448
|
|
163
|
+
});
|
|
164
|
+
withDirLock(join(directory, "query.lock"), () => {
|
|
165
|
+
const temporary = join(directory, "checkpoint.json.tmp");
|
|
166
|
+
writeFileSync(temporary, JSON.stringify({
|
|
167
|
+
...previous,
|
|
168
|
+
until: checkpoint,
|
|
169
|
+
windowMs
|
|
170
|
+
}), { mode: 384 });
|
|
171
|
+
renameSync(temporary, join(directory, "checkpoint.json"));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const seen = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const digest of previous?.boundary ?? []) seen.set(digest, (seen.get(digest) ?? 0) + 1);
|
|
178
|
+
const boundary = [];
|
|
179
|
+
const records = [];
|
|
180
|
+
if (from > checkpoint) records.push({
|
|
181
|
+
ts: from,
|
|
182
|
+
src: "device",
|
|
183
|
+
platform,
|
|
184
|
+
level: "warn",
|
|
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.`
|
|
186
|
+
});
|
|
187
|
+
for (const line of output.split("\n")) {
|
|
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;
|
|
192
|
+
if (parsed.ts >= until - 5e3) boundary.push(digest);
|
|
193
|
+
if (newest !== void 0 && parsed.ts <= newest) continue;
|
|
194
|
+
const count = seen.get(digest) ?? 0;
|
|
195
|
+
if (count) {
|
|
196
|
+
seen.set(digest, count - 1);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
records.push({
|
|
200
|
+
...parsed,
|
|
201
|
+
platform
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
mkdirSync(directory, {
|
|
205
|
+
recursive: true,
|
|
206
|
+
mode: 448
|
|
207
|
+
});
|
|
208
|
+
withDirLock(join(directory, "query.lock"), () => {
|
|
209
|
+
const writer = createNdjsonWriter(join(directory, "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
|
|
210
|
+
try {
|
|
211
|
+
for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted native logs.");
|
|
212
|
+
} finally {
|
|
213
|
+
writer.close();
|
|
214
|
+
}
|
|
215
|
+
const temporary = join(directory, "checkpoint.json.tmp");
|
|
216
|
+
try {
|
|
217
|
+
writeFileSync(temporary, JSON.stringify({
|
|
218
|
+
until,
|
|
219
|
+
boundary,
|
|
220
|
+
...identity,
|
|
221
|
+
...until < now ? { windowMs } : {}
|
|
222
|
+
}), { mode: 384 });
|
|
223
|
+
renameSync(temporary, join(directory, "checkpoint.json"));
|
|
224
|
+
} finally {
|
|
225
|
+
rmSync(temporary, { force: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return until < now;
|
|
229
|
+
}
|
|
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
|
|
18
363
|
//#region src/device-host/app.ts
|
|
19
364
|
function inside(root, path) {
|
|
20
365
|
const diff = relative(root, path);
|
|
21
366
|
return diff !== "" && diff !== ".." && !diff.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(diff);
|
|
22
367
|
}
|
|
23
|
-
|
|
368
|
+
function removeHostedAppData(home, { keepBlobs = false } = {}) {
|
|
369
|
+
const area = realpathSync(join(home, ".."));
|
|
370
|
+
const assertInside = (path) => {
|
|
371
|
+
if (!inside(area, realpathSync(path))) throw new Error("Hosted app data resolves outside its private area.");
|
|
372
|
+
};
|
|
373
|
+
const remove = (path) => {
|
|
374
|
+
if (!lstatSync(path, { throwIfNoEntry: false })) return;
|
|
375
|
+
assertInside(path);
|
|
376
|
+
rmSync(path, {
|
|
377
|
+
recursive: true,
|
|
378
|
+
force: true
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
assertInside(home);
|
|
382
|
+
const apps = join(area, "apps");
|
|
383
|
+
if (existsSync(apps)) {
|
|
384
|
+
assertInside(apps);
|
|
385
|
+
for (const attempt of readdirSync(apps)) {
|
|
386
|
+
const directory = join(apps, attempt);
|
|
387
|
+
if (!lstatSync(directory).isDirectory()) continue;
|
|
388
|
+
assertInside(directory);
|
|
389
|
+
remove(join(directory, "App.app"));
|
|
390
|
+
remove(join(directory, "App.apk"));
|
|
391
|
+
remove(join(directory, "App.apk.tmp"));
|
|
392
|
+
remove(join(directory, "blobs"));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (!keepBlobs) remove(join(area, "blobs"));
|
|
396
|
+
}
|
|
397
|
+
async function digest(path) {
|
|
24
398
|
const hash = createHash("sha256");
|
|
25
399
|
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
26
400
|
return hash.digest("hex");
|
|
@@ -42,19 +416,20 @@ function newer(value, supported) {
|
|
|
42
416
|
async function materializeHostedApp(home, session, attempt) {
|
|
43
417
|
const record = readHostedApp(session, attempt, home);
|
|
44
418
|
const area = hostedAppArea(session, attempt, home);
|
|
419
|
+
const blobs = hostedAppBlobs(session, home);
|
|
45
420
|
const app = join(area, "App.app");
|
|
46
421
|
if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
|
|
47
|
-
const manifest = join(
|
|
48
|
-
if (lstatSync(manifest).size !== record.manifest.size || await digest
|
|
422
|
+
const manifest = join(blobs, record.manifest.sha256);
|
|
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.");
|
|
49
424
|
rmSync(app, {
|
|
50
425
|
recursive: true,
|
|
51
426
|
force: true
|
|
52
427
|
});
|
|
53
428
|
mkdirSync(app, { mode: 448 });
|
|
54
429
|
for (const file of record.files) {
|
|
55
|
-
const blob = join(
|
|
430
|
+
const blob = join(blobs, file.sha256);
|
|
56
431
|
const stat = lstatSync(blob);
|
|
57
|
-
if (!stat.isFile() || stat.size !== file.size || await digest
|
|
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}`);
|
|
58
433
|
if (file.kind === "link") continue;
|
|
59
434
|
const target = join(app, file.path);
|
|
60
435
|
mkdirSync(dirname(target), {
|
|
@@ -65,7 +440,7 @@ async function materializeHostedApp(home, session, attempt) {
|
|
|
65
440
|
chmodSync(target, file.kind === "exec" ? 493 : 420);
|
|
66
441
|
}
|
|
67
442
|
for (const file of record.files.filter((each) => each.kind === "link")) {
|
|
68
|
-
const bytes = readFileSync(join(
|
|
443
|
+
const bytes = readFileSync(join(blobs, file.sha256));
|
|
69
444
|
const target = bytes.toString("utf8");
|
|
70
445
|
const destination = posix.normalize(posix.join(posix.dirname(file.path), target));
|
|
71
446
|
if (!target || !Buffer.from(target).equals(bytes) || /[\\\0\r\n]/.test(target) || posix.isAbsolute(target) || destination === "." || destination === ".." || destination.startsWith("../") || file.path.startsWith(`${destination}/`)) throw new Error(`The app link escapes its bundle or forms an ancestor cycle: ${file.path}`);
|
|
@@ -80,7 +455,7 @@ async function materializeHostedApp(home, session, attempt) {
|
|
|
80
455
|
for (const file of record.files) {
|
|
81
456
|
const path = realpathSync(join(app, file.path));
|
|
82
457
|
if (!inside(root, path)) throw new Error(`The app entry resolves outside its bundle: ${file.path}`);
|
|
83
|
-
if (file.kind !== "link" && await digest
|
|
458
|
+
if (file.kind !== "link" && await digest(path) !== file.sha256) throw new Error("App entries alias different content.");
|
|
84
459
|
}
|
|
85
460
|
return {
|
|
86
461
|
app,
|
|
@@ -201,8 +576,57 @@ async function runHostedDevice(mode, selectors, app) {
|
|
|
201
576
|
};
|
|
202
577
|
}
|
|
203
578
|
device = readHostedDevice(home);
|
|
204
|
-
assertHostedDeviceLedger(home, device.udid);
|
|
579
|
+
const ledger = assertHostedDeviceLedger(home, device.udid, "ios", { allowEmpty: mode === "stop" });
|
|
205
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
|
+
}
|
|
206
630
|
if (mode === "install") {
|
|
207
631
|
if (!app || current?.state !== "Booted") throw new Error("Hosted app installation requires its booted owned simulator.");
|
|
208
632
|
const launched = await installHostedApp(home, app.session, app.attempt, device, app.metroPort);
|
|
@@ -212,97 +636,33 @@ async function runHostedDevice(mode, selectors, app) {
|
|
|
212
636
|
launched
|
|
213
637
|
};
|
|
214
638
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
639
|
+
if (ledger === "empty") {
|
|
640
|
+
if (current) throw new Error("The hosted simulator remains without ledger ownership; it was kept.");
|
|
641
|
+
removeHostedAppData(home);
|
|
642
|
+
return {
|
|
643
|
+
state: "stopped",
|
|
644
|
+
device
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const outcome = teardownOwnedIosSim(device.udid, { del: true });
|
|
220
648
|
if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted simulator teardown was not established.");
|
|
221
649
|
const after = inventory().find((sim) => sim.udid === device.udid);
|
|
222
|
-
if (after
|
|
650
|
+
if (after) throw new Error(`Hosted simulator ${device.udid} is still listed (${after.state}); deletion could not be verified. If its runtime is unavailable, delete it with xcrun simctl delete and stop the session again.`);
|
|
651
|
+
forgetCreatedDevice("ios", device.udid);
|
|
652
|
+
removeHostedAppData(home);
|
|
223
653
|
return {
|
|
224
654
|
state: "stopped",
|
|
225
655
|
device
|
|
226
656
|
};
|
|
227
657
|
} catch (error) {
|
|
228
658
|
return {
|
|
229
|
-
state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
659
|
+
state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
230
660
|
device,
|
|
231
661
|
notice: error.message
|
|
232
662
|
};
|
|
233
663
|
}
|
|
234
664
|
}
|
|
235
665
|
//#endregion
|
|
236
|
-
//#region src/device-host/android-app.ts
|
|
237
|
-
async function digest(path) {
|
|
238
|
-
const hash = createHash("sha256");
|
|
239
|
-
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
240
|
-
return hash.digest("hex");
|
|
241
|
-
}
|
|
242
|
-
/** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
|
|
243
|
-
async function installHostedAndroidApp(home, session, attempt, device) {
|
|
244
|
-
const record = readHostedApp(session, attempt, home);
|
|
245
|
-
const area = hostedAppArea(session, attempt, home);
|
|
246
|
-
if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
|
|
247
|
-
const entry = record.files[0];
|
|
248
|
-
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.");
|
|
249
|
-
const manifest = join(area, "blobs", record.manifest.sha256);
|
|
250
|
-
if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
|
|
251
|
-
const blob = join(area, "blobs", entry.sha256);
|
|
252
|
-
const stat = lstatSync(blob);
|
|
253
|
-
if (!stat.isFile() || stat.size !== entry.size || await digest(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
|
|
254
|
-
const temporary = join(area, "App.apk.tmp");
|
|
255
|
-
const apk = join(area, "App.apk");
|
|
256
|
-
copyFileSync(blob, temporary);
|
|
257
|
-
renameSync(temporary, apk);
|
|
258
|
-
const tool = findAapt();
|
|
259
|
-
if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
|
|
260
|
-
const badging = getExecutor().runFile(tool.path, [
|
|
261
|
-
"dump",
|
|
262
|
-
"badging",
|
|
263
|
-
apk
|
|
264
|
-
], {
|
|
265
|
-
timeoutMs: 1e4,
|
|
266
|
-
killSignal: "SIGKILL"
|
|
267
|
-
});
|
|
268
|
-
const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
|
|
269
|
-
const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
|
|
270
|
-
const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
|
|
271
|
-
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.");
|
|
272
|
-
const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
|
|
273
|
-
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.");
|
|
274
|
-
const assertTarget = () => {
|
|
275
|
-
assertHostedDeviceLedger(home, device.avdName, "android");
|
|
276
|
-
const current = readHostedDevice(home, "android");
|
|
277
|
-
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.");
|
|
278
|
-
};
|
|
279
|
-
assertTarget();
|
|
280
|
-
const executor = getExecutor();
|
|
281
|
-
const adb = androidToolPath("adb");
|
|
282
|
-
const sdkExecutor = {
|
|
283
|
-
...executor,
|
|
284
|
-
runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
|
|
285
|
-
};
|
|
286
|
-
const installed = installAndroidApp({
|
|
287
|
-
serial: device.serial,
|
|
288
|
-
apkPath: apk,
|
|
289
|
-
packageName: record.bundleId
|
|
290
|
-
}, { exec: sdkExecutor });
|
|
291
|
-
if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
|
|
292
|
-
assertTarget();
|
|
293
|
-
const launched = launchAndroidReleaseApp({
|
|
294
|
-
serial: device.serial,
|
|
295
|
-
packageName: record.bundleId
|
|
296
|
-
}, { exec: sdkExecutor });
|
|
297
|
-
if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
|
|
298
|
-
if (record.mode === "development") return "unverified";
|
|
299
|
-
for (let tries = 0; tries < 10; tries++) {
|
|
300
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
301
|
-
if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
|
|
302
|
-
}
|
|
303
|
-
return "unverified";
|
|
304
|
-
}
|
|
305
|
-
//#endregion
|
|
306
666
|
//#region src/device-host/android.ts
|
|
307
667
|
function portIsOccupied(port) {
|
|
308
668
|
const devices = listAdbDevices({ timeoutMs: 5e3 });
|
|
@@ -327,10 +687,23 @@ async function runHostedAndroidDevice(mode, request, app) {
|
|
|
327
687
|
let device = null;
|
|
328
688
|
let creationStarted = false;
|
|
329
689
|
try {
|
|
330
|
-
if (mode === "install") {
|
|
331
|
-
if (!app) throw new Error("Hosted Android
|
|
690
|
+
if (mode === "install" || mode === "reverse") {
|
|
691
|
+
if (!app) throw new Error("Hosted Android native work needs its admitted app attempt.");
|
|
332
692
|
device = readHostedDevice(home, "android");
|
|
333
|
-
|
|
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);
|
|
334
707
|
return {
|
|
335
708
|
state: "installed",
|
|
336
709
|
device,
|
|
@@ -381,17 +754,88 @@ async function runHostedAndroidDevice(mode, request, app) {
|
|
|
381
754
|
};
|
|
382
755
|
}
|
|
383
756
|
device = readHostedDevice(home, "android");
|
|
384
|
-
assertHostedDeviceLedger(home, device.avdName, "android");
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
}
|
|
820
|
+
if (ledger === "listed") {
|
|
821
|
+
withConfigLock$1(() => {
|
|
822
|
+
if (!loadConfig$1()) ensureConfig();
|
|
823
|
+
});
|
|
824
|
+
const outcome = teardownOwnedAvd(device.avdName, { del: true });
|
|
825
|
+
if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted Android deletion was not established.");
|
|
826
|
+
}
|
|
827
|
+
const name = device.avdName;
|
|
828
|
+
if (listAvds({ timeoutMs: 5e3 }).includes(name) || avdStorageRoots().some((root) => avdPathExists(join(root, `${name}.ini`)) || avdPathExists(join(root, `${name}.avd`)))) throw new Error("The hosted AVD or its data still exists; deletion could not be verified.");
|
|
387
829
|
if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
|
|
830
|
+
if (ledger === "listed") forgetCreatedDevice("android", device.avdName);
|
|
831
|
+
removeHostedAppData(home);
|
|
388
832
|
return {
|
|
389
833
|
state: "stopped",
|
|
390
834
|
device
|
|
391
835
|
};
|
|
392
836
|
} catch (error) {
|
|
393
837
|
return {
|
|
394
|
-
state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
838
|
+
state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
395
839
|
device,
|
|
396
840
|
notice: error.message
|
|
397
841
|
};
|
|
@@ -504,6 +948,7 @@ async function runHostedMacosApp(mode, request, app) {
|
|
|
504
948
|
removeAppData(area, join(directory, "blobs"));
|
|
505
949
|
}
|
|
506
950
|
}
|
|
951
|
+
removeAppData(area, join(home, "..", "blobs"));
|
|
507
952
|
return {
|
|
508
953
|
state: "stopped",
|
|
509
954
|
device
|
|
@@ -667,7 +1112,7 @@ async function runHostedMacosApp(mode, request, app) {
|
|
|
667
1112
|
}
|
|
668
1113
|
//#endregion
|
|
669
1114
|
//#region src/device-host/offer.ts
|
|
670
|
-
function inspectHostedDevice(request) {
|
|
1115
|
+
function inspectHostedDevice(request, options) {
|
|
671
1116
|
const cpus = availableParallelism();
|
|
672
1117
|
let workerDiskFreeBytes = null;
|
|
673
1118
|
try {
|
|
@@ -679,7 +1124,8 @@ function inspectHostedDevice(request) {
|
|
|
679
1124
|
loadPerCore: loadavg()[1] / cpus,
|
|
680
1125
|
memoryFreeBytes: freemem(),
|
|
681
1126
|
memoryPressure: readHostMemoryPressure(),
|
|
682
|
-
workerDiskFreeBytes
|
|
1127
|
+
workerDiskFreeBytes,
|
|
1128
|
+
...options ? { localDevices: countLiveOwnedDevices({}, options) } : {}
|
|
683
1129
|
};
|
|
684
1130
|
try {
|
|
685
1131
|
if (process.platform !== "darwin") throw new Error("Hosted device sessions require a Mac.");
|
|
@@ -724,12 +1170,27 @@ async function main() {
|
|
|
724
1170
|
"prepare",
|
|
725
1171
|
"stop",
|
|
726
1172
|
"install",
|
|
727
|
-
"offer"
|
|
1173
|
+
"offer",
|
|
1174
|
+
"count",
|
|
1175
|
+
"logs",
|
|
1176
|
+
"reverse",
|
|
1177
|
+
"park",
|
|
1178
|
+
"inspect",
|
|
1179
|
+
"adopt"
|
|
728
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
|
+
}
|
|
729
1190
|
if (input.mode === "offer") {
|
|
730
1191
|
const request = parseHostedOfferRequest(input);
|
|
731
1192
|
if (!request) throw new Error("Invalid hosted offer selectors.");
|
|
732
|
-
process.stdout.write(`${JSON.stringify(inspectHostedDevice(request))}\n`);
|
|
1193
|
+
process.stdout.write(`${JSON.stringify(inspectHostedDevice(request, exclude ? { exclude } : void 0))}\n`);
|
|
733
1194
|
return;
|
|
734
1195
|
}
|
|
735
1196
|
if (process.platform !== "darwin") {
|
|
@@ -740,9 +1201,21 @@ async function main() {
|
|
|
740
1201
|
})}\n`);
|
|
741
1202
|
return;
|
|
742
1203
|
}
|
|
743
|
-
if (input.mode === "install" && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
|
|
744
|
-
|
|
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.");
|
|
745
1212
|
if (input.platform === "macos" && !hostedMacosAppSlot(input.appSlot)) throw new Error("Invalid hosted macOS app slot.");
|
|
1213
|
+
if (input.mode === "logs") {
|
|
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);
|
|
1216
|
+
process.stdout.write(`${JSON.stringify({ more })}\n`);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
746
1219
|
const result = input.platform === "macos" ? await runHostedMacosApp(input.mode, {
|
|
747
1220
|
session: typeof input.session === "string" ? input.session : "",
|
|
748
1221
|
appSlot: input.appSlot
|
|
@@ -751,7 +1224,11 @@ async function main() {
|
|
|
751
1224
|
consolePort: input.consolePort,
|
|
752
1225
|
...typeof input.systemImage === "string" ? { systemImage: input.systemImage } : {},
|
|
753
1226
|
...typeof input.deviceProfile === "string" ? { deviceProfile: input.deviceProfile } : {}
|
|
754
|
-
}, input.mode === "install"
|
|
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, {
|
|
755
1232
|
...typeof input.deviceType === "string" ? { deviceType: input.deviceType } : {},
|
|
756
1233
|
...typeof input.runtime === "string" ? { runtime: input.runtime } : {}
|
|
757
1234
|
}, input.mode === "install" ? {
|