stim 1.16.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-BxwdD81S.mjs → activity-IRvJY_ru.mjs} +23 -3
- package/dist/{agent-device-usage-output-Dpih1Tfa.mjs → agent-device-usage-output-BK41vnzN.mjs} +3 -3
- package/dist/{android-Ift4kVvt.mjs → android--GorydsK.mjs} +679 -317
- package/dist/{android-B_NumVyQ.mjs → android-B_YNZy61.mjs} +7 -43
- package/dist/{android-r2rBL8kV.mjs → android-DE4v59Mb.mjs} +12 -3
- package/dist/{android-cas-8U4DPqeR.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
- package/dist/{app-install-CKWdbXIX.mjs → app-install-VPkI5WEg.mjs} +6 -5
- package/dist/{budget-DjrURfQn.mjs → budget-DM984mnd.mjs} +34 -18
- package/dist/{build-plan-DjeSb2Xl.mjs → build-plan-CmQCoyT-.mjs} +345 -86
- package/dist/{build-slots-rUk77xqh.mjs → build-slots-41v_d2x9.mjs} +35 -25
- package/dist/{cli-BkYZ_6hy.mjs → cli-Daxug5-K.mjs} +17 -17
- package/dist/cli.mjs +1 -1
- package/dist/collector-run.mjs +3 -3
- package/dist/{command-output-DnJMdEY-.mjs → command-output-CDAIFD-8.mjs} +3 -3
- package/dist/{created-devices-D5owjN4C.mjs → created-devices-CpvzBL25.mjs} +1 -1
- package/dist/{dependency-state-B6cMXZab.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
- package/dist/{deps-urbmfXig.mjs → deps-DNWaQs4V.mjs} +1 -1
- package/dist/{dev-client-CroAIjjj.mjs → dev-client-B-XC4uRH.mjs} +2 -2
- package/dist/{device-DZQjF7H5.mjs → device-C8IOoJPv.mjs} +7 -7
- package/dist/device-capacity-D0GWHnTM.mjs +580 -0
- package/dist/device-host-worker.mjs +436 -154
- package/dist/{device-ios-BadmeL2V.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
- package/dist/{device-lease-D-hLqx1b.mjs → device-lease-DpoS36_u.mjs} +2 -2
- package/dist/{device-lease-run-CqjShqWW.mjs → device-lease-run-B8YltIH_.mjs} +3 -3
- package/dist/{device-pool-CUaEWMMm.mjs → device-pool-3o7s5lo4.mjs} +2 -2
- package/dist/{device-remote-D5WVWqOq.mjs → device-remote-hFSkpfQu.mjs} +12 -12
- package/dist/{doctor-Cimf-gGB.mjs → doctor-Cnk9d7Hm.mjs} +32 -51
- package/dist/{error-diagnostics-B9L4Ae0x.mjs → error-diagnostics-C-NMnhrk.mjs} +7 -7
- package/dist/{gc-Bqy4bjEu.mjs → gc-DxCL2A46.mjs} +308 -26
- package/dist/{gradle-BsO1MdsH.mjs → gradle-D7-L2Th3.mjs} +3 -3
- package/dist/{guide-B_uyKTxW.mjs → guide-DjrTQGik.mjs} +1662 -565
- package/dist/{guide-status-CjEZMAD4.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
- package/dist/hosted-android-DwLS4Mse.mjs +32 -0
- package/dist/{hosted-client-a-WmqE2L.mjs → hosted-client-wk1W_CNW.mjs} +2 -3
- package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
- package/dist/{hosted-logs-D5g041Q3.mjs → hosted-logs-DqYmWtXc.mjs} +16 -16
- package/dist/{hosted-macos-BSZyIb3y.mjs → hosted-macos-dlHXEeF5.mjs} +4 -4
- package/dist/{hosted-ios-CIWelIqf.mjs → hosted-native-C9xJ9rpA.mjs} +77 -42
- package/dist/{idle-DYVymG92.mjs → idle-B2m-Ovod.mjs} +122 -58
- package/dist/{idle-shutdown-CTsDnAde.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
- package/dist/{in-use-DvBw0Kxo.mjs → in-use-CvGOUn-x.mjs} +11 -11
- package/dist/{ios-aq3QoGI3.mjs → ios-DE6duQoz.mjs} +170 -88
- package/dist/{ios-BaInn3sW.mjs → ios-DSfnPfse.mjs} +4 -4
- package/dist/{ios-device-FfCN_C8p.mjs → ios-device-pAN4t54-.mjs} +1 -1
- package/dist/ios-state-8L3lgum6.mjs +89 -0
- package/dist/{launch-verify-2DBRizjo.mjs → launch-verify-lTnWZikR.mjs} +2 -2
- package/dist/{logs-CHEVK8zD.mjs → logs-DUoKlDoS.mjs} +36 -23
- package/dist/{client-BTY686R6.mjs → machines-C2XiNwh9.mjs} +278 -20
- package/dist/{macos-CSYEAFmF.mjs → macos-32po3kUq.mjs} +14 -14
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +2 -2
- package/dist/{metro-BeK1ZPz1.mjs → metro-g8NqiJp-.mjs} +1 -1
- package/dist/{metro-gateway-BJ-rxQAB.mjs → metro-gateway-DmslUM30.mjs} +22 -3
- package/dist/{named-ports-Bk_YXEmE.mjs → named-ports-C8m8EQs2.mjs} +2 -2
- package/dist/{native-run-NaBrRFtQ.mjs → native-run-D1Q56rAD.mjs} +17 -6
- package/dist/{native-runtime-DBzRnEKS.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
- package/dist/offload-worker.mjs +8 -8
- package/dist/{teardown-CL6vDs2Y.mjs → ownership-BMLqyBgJ.mjs} +603 -26
- package/dist/{ownership-DiA16AsN.mjs → ownership-DyRQru0q.mjs} +2 -2
- package/dist/{ports-R2k4Knz3.mjs → ports-4nYqxPWC.mjs} +2 -2
- package/dist/{prebuild-D2fsca_Z.mjs → prebuild-D7RUMkwN.mjs} +3 -3
- package/dist/{preview-DhVbvPAZ.mjs → preview-CxPRIEht.mjs} +7 -7
- package/dist/{project-Du91Ilvc.mjs → project-D47Rh0ly.mjs} +7 -2
- package/dist/{recordings-BIXOzVnr.mjs → recordings-BNgsD1Tr.mjs} +1 -1
- package/dist/{reload-rM9jV1fz.mjs → reload-qZei6iWn.mjs} +62 -35
- package/dist/{remote-cache-C8-V13XN.mjs → remote-cache-Ci4DQYYj.mjs} +3 -3
- package/dist/{run-f4vI6EnK.mjs → run-CkE9y0nc.mjs} +1 -1
- package/dist/{server-bare-CpPGmnmO.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
- package/dist/{server-expo-MC0rHlOC.mjs → server-expo-CHTyXD0G.mjs} +2 -2
- package/dist/server-expo-CeakhI8P.mjs +2 -0
- package/dist/{settings-oTSkkNrM.mjs → settings-Cfd-0r2d.mjs} +10 -9
- package/dist/{settings-ZkDzkzvB.mjs → settings-SR9OmW9h.mjs} +3 -3
- package/dist/settings.schema.json +42 -13
- package/dist/{simslim-B8-7AoTI.mjs → simslim-BWNrc3M3.mjs} +3 -3
- package/dist/{slot-launch-B8VrHxn1.mjs → slot-launch-DmjBmpQV.mjs} +8 -8
- package/dist/{start-CX8Ig-U2.mjs → start-BopZcr4n.mjs} +1 -1
- package/dist/{start-ClDfwXa4.mjs → start-uee1wWjY.mjs} +11 -11
- package/dist/{state-DcW6vOd_.mjs → state-BKa8wNN5.mjs} +1 -1
- package/dist/{state-3DwSQIBK.mjs → state-DyaNTgfR.mjs} +1 -1
- package/dist/{state-C1jB7Q8q.mjs → state-PeQiJuIu.mjs} +2 -2
- package/dist/{stats-CEZ_i5je.mjs → stats-Bq4fJOdB.mjs} +7 -4
- package/dist/{status-BuvBFeII.mjs → status-CzS4bPO9.mjs} +4 -4
- package/dist/{status-Cs7VLg9D.mjs → status-dXX2JX1C.mjs} +125 -35
- package/dist/{stop-Dt_dM16A.mjs → stop-BCwvvs7F.mjs} +2 -2
- package/dist/{stop-Bffozsac.mjs → stop-BjizrQwv.mjs} +48 -27
- package/dist/{stop-B0Yxtmiy.mjs → stop-Cr1d8DSE.mjs} +4 -4
- package/dist/{stop-cause-CidxIqup.mjs → stop-cause-rhmGfwvt.mjs} +1 -1
- package/dist/supervisor-run.d.mts +7 -4
- package/dist/supervisor-run.mjs +26 -28
- package/dist/{support-D4aYJniO.mjs → support-BJPgYCu-.mjs} +44 -19
- package/dist/{support-CAP8qDUF.mjs → support-F9-sr0Oi.mjs} +6 -6
- package/dist/{toolchain-DkUflduf.mjs → toolchain-CNIBTIOv.mjs} +7 -7
- package/dist/{warm-progress-CpGv3Wb6.mjs → warm-progress-DF4JRvYI.mjs} +3 -3
- package/dist/watchman-APYjXSQa.mjs +48 -0
- package/dist/{web-DWUaMp8P.mjs → web-exQtGkoc.mjs} +12 -13
- package/dist/web-run.mjs +4 -4
- package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
- package/dist/{workspace-state-B8-aJ1_5.mjs → workspace-state-DqcCbpE7.mjs} +2 -2
- package/dist/{workspaces-BV4pdHFz.mjs → workspaces-Bcib97-6.mjs} +3 -3
- package/dist/{worktree-jnG8Iph5.mjs → worktree-Dc98cE6_.mjs} +1 -1
- package/dist/{worktree-K-xj9Db0.mjs → worktree-ZpZR3S1k.mjs} +141 -40
- package/package.json +5 -5
- package/dist/device-capacity-BHwxNBlu.mjs +0 -136
- package/dist/ios-state-DS5UpYTH.mjs +0 -48
- package/dist/machines-BIBUdhpj.mjs +0 -265
- package/dist/ownership-hCkPzGmr.mjs +0 -571
- 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-
|
|
5
|
-
import { t as forgetCreatedDevice } from "./created-devices-
|
|
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-
|
|
7
|
-
import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-
|
|
8
|
-
import {
|
|
9
|
-
import { a as requiredMacosRecord, r as macosProcess } from "./state-
|
|
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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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";
|
|
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,
|
|
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/
|
|
23
|
-
function
|
|
24
|
-
|
|
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
|
-
/**
|
|
27
|
-
function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
const
|
|
32
|
-
|
|
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 =
|
|
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 (
|
|
180
|
+
if (from > checkpoint) records.push({
|
|
111
181
|
ts: from,
|
|
112
182
|
src: "device",
|
|
113
|
-
platform
|
|
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
|
|
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
|
|
119
|
-
if (!
|
|
120
|
-
const
|
|
121
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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"
|
|
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" ? {
|