stim 1.14.0 → 1.16.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/README.md +2 -0
- package/dist/activity-BxwdD81S.mjs +609 -0
- package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
- package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
- package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
- package/dist/android-cas-8U4DPqeR.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
- package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/budget-DjrURfQn.mjs +2700 -0
- package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
- package/dist/build-slots-rUk77xqh.mjs +132 -0
- package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
- package/dist/cli-BkYZ_6hy.mjs +51 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-BTY686R6.mjs +1368 -0
- package/dist/collector-run.mjs +9 -195
- package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
- package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
- package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
- package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
- package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CroAIjjj.mjs +43 -0
- package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
- package/dist/device-capacity-BHwxNBlu.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +964 -0
- package/dist/device-ios-BadmeL2V.mjs +381 -0
- package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
- package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
- package/dist/device-pool-CUaEWMMm.mjs +125 -0
- package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
- package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
- package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
- package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
- package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
- package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
- package/dist/hosted-client-a-WmqE2L.mjs +197 -0
- package/dist/hosted-ios-CIWelIqf.mjs +220 -0
- package/dist/hosted-logs-D5g041Q3.mjs +187 -0
- package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
- package/dist/idle-DYVymG92.mjs +1099 -0
- package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
- package/dist/in-use-DvBw0Kxo.mjs +130 -0
- package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
- package/dist/ios-state-DS5UpYTH.mjs +48 -0
- package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
- package/dist/logs-CHEVK8zD.mjs +371 -0
- package/dist/machines-BIBUdhpj.mjs +265 -0
- package/dist/macos-CSYEAFmF.mjs +473 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
- package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
- package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
- package/dist/native-run-NaBrRFtQ.mjs +147 -0
- package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +122 -0
- package/dist/offload-worker.mjs +606 -0
- package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
- package/dist/ownership-hCkPzGmr.mjs +571 -0
- package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
- package/dist/prebuild-D2fsca_Z.mjs +213 -0
- package/dist/preview-DhVbvPAZ.mjs +318 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-BIXOzVnr.mjs +37 -0
- package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
- package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
- package/dist/run-f4vI6EnK.mjs +119 -0
- package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
- package/dist/server-expo-CVqO2c35.mjs +2 -0
- package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
- package/dist/settings-oTSkkNrM.mjs +1439 -0
- package/dist/settings.schema.json +845 -97
- package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
- package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
- package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CX8Ig-U2.mjs +2 -0
- package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
- package/dist/state-3DwSQIBK.mjs +41 -0
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
- package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
- package/dist/stats-CEZ_i5je.mjs +77 -0
- package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
- package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/stop-B0Yxtmiy.mjs +48 -0
- package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
- package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
- package/dist/stop-cause-CidxIqup.mjs +97 -0
- package/dist/supervisor-run.d.mts +43 -11
- package/dist/supervisor-run.mjs +363 -24
- package/dist/support-CAP8qDUF.mjs +316 -0
- package/dist/support-D4aYJniO.mjs +201 -0
- package/dist/teardown-CL6vDs2Y.mjs +727 -0
- package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
- package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
- package/dist/web-run.mjs +310 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
- package/dist/workspaces-BV4pdHFz.mjs +306 -0
- package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
- package/dist/worktree-jnG8Iph5.mjs +2 -0
- package/package.json +10 -5
- package/shim/bundle-response.cjs +28 -12
- package/dist/activity-CYVDcrt3.mjs +0 -356
- package/dist/budget-CGDkhvv_.mjs +0 -1745
- package/dist/build-progress-DxKpwnaS.mjs +0 -247
- package/dist/idle-DXtx3RbX.mjs +0 -191
- package/dist/in-use-DlqBDKqY.mjs +0 -249
- package/dist/logs-Civ4gbBH.mjs +0 -486
- package/dist/merge-state-ytFQCeh4.mjs +0 -262
- package/dist/server-expo-DIHDhvGg.mjs +0 -2
- package/dist/settings-D61iNOsB.mjs +0 -629
- package/dist/start-Cetfvy8l.mjs +0 -2
- package/dist/stats-CFpFGlJV.mjs +0 -62
- package/dist/stats-CX1Z8Tca.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
- package/dist/worktree-BVrHsYZG.mjs +0 -752
- package/dist/worktree-CHWWvm5v.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -0,0 +1,964 @@
|
|
|
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";
|
|
3
|
+
import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
4
|
+
import { c as writeWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-B8-aJ1_5.mjs";
|
|
5
|
+
import { t as forgetCreatedDevice } from "./created-devices-D5owjN4C.mjs";
|
|
6
|
+
import { A as listAvds, D as hostSystemImageArch, E as getAvdNameForSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, _ as avdStorageRoots, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, h as avdPathExists, j as listInstalledSystemImages, k as listAvdDeviceProfiles, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-B_NumVyQ.mjs";
|
|
7
|
+
import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-BaInn3sW.mjs";
|
|
8
|
+
import { i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-CL6vDs2Y.mjs";
|
|
9
|
+
import { a as requiredMacosRecord, r as macosProcess } from "./state-3DwSQIBK.mjs";
|
|
10
|
+
import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
|
|
11
|
+
import { n as parseLogStreamLine } from "./ios-Cr1h5gnO.mjs";
|
|
12
|
+
import { _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, x as launchIosApp } from "./app-install-CKWdbXIX.mjs";
|
|
13
|
+
import { h as findAapt } from "./support-CAP8qDUF.mjs";
|
|
14
|
+
import { n as stopMacosAppHeld } from "./stop-B0Yxtmiy.mjs";
|
|
15
|
+
import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
+
import { chmodSync, closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, symlinkSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, isAbsolute, join, posix, relative, sep } from "node:path";
|
|
19
|
+
import { LOG_ROTATE_BYTES, configDir, withDirLock } from "@stim-cli/core";
|
|
20
|
+
import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedIosLogsDir, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedIosLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
|
|
21
|
+
import { availableParallelism, freemem, homedir, loadavg } from "node:os";
|
|
22
|
+
//#region src/device-host/ios-logs.ts
|
|
23
|
+
function logDate(ms) {
|
|
24
|
+
return new Date(ms).toISOString().replace("T", " ").replace(".000Z", "+0000");
|
|
25
|
+
}
|
|
26
|
+
/** Captures a bounded unified-log window only from this home's exact ledger-owned simulator. */
|
|
27
|
+
function collectHostedIosLogs(home, session, attempt, since, final = false) {
|
|
28
|
+
const device = readHostedDevice(home);
|
|
29
|
+
assertHostedDeviceLedger(home, device.udid);
|
|
30
|
+
if (readHostedAppMetadata(session, attempt, home).state !== "installed") return false;
|
|
31
|
+
const directory = hostedIosLogsDir(home);
|
|
32
|
+
const previous = readHostedIosLogsCheckpoint(home);
|
|
33
|
+
const beginning = Math.floor(since / 1e3) * 1e3;
|
|
34
|
+
let newest;
|
|
35
|
+
if (!previous) {
|
|
36
|
+
for (const entry of readNdjsonGenerations(join(directory, "device.ndjson"))) if (typeof entry.ts === "number" && (newest === void 0 || entry.ts > newest)) newest = entry.ts;
|
|
37
|
+
}
|
|
38
|
+
const checkpoint = previous?.until ?? Math.max(beginning, Math.floor((newest ?? beginning) / 1e3) * 1e3);
|
|
39
|
+
let from = Math.max(beginning, previous ? checkpoint - 5e3 : checkpoint);
|
|
40
|
+
const now = Math.floor(Date.now() / 1e3) * 1e3;
|
|
41
|
+
if (now < checkpoint) return false;
|
|
42
|
+
const backlogMs = Math.max(1e3, now - checkpoint);
|
|
43
|
+
let windowMs = !final && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
|
|
44
|
+
let until = Math.min(now, checkpoint + windowMs);
|
|
45
|
+
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
|
+
let output;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const remaining = deadline - Date.now();
|
|
60
|
+
if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
|
|
61
|
+
try {
|
|
62
|
+
output = exec.runFile("xcrun", [
|
|
63
|
+
"simctl",
|
|
64
|
+
"spawn",
|
|
65
|
+
device.udid,
|
|
66
|
+
"log",
|
|
67
|
+
"show",
|
|
68
|
+
"--style",
|
|
69
|
+
"ndjson",
|
|
70
|
+
"--predicate",
|
|
71
|
+
`processImagePath ENDSWITH "/App.app/${executable}"`,
|
|
72
|
+
"--info",
|
|
73
|
+
"--start",
|
|
74
|
+
logDate(from),
|
|
75
|
+
"--end",
|
|
76
|
+
logDate(end)
|
|
77
|
+
], {
|
|
78
|
+
timeoutMs: Math.min(remaining, 4e3),
|
|
79
|
+
killSignal: "SIGKILL"
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const span = final ? until - from : until - checkpoint;
|
|
84
|
+
if (span <= 1e3) throw error;
|
|
85
|
+
windowMs = Math.max(1e3, Math.floor(span / 4 / 1e3) * 1e3);
|
|
86
|
+
if (final) from = Math.max(beginning, now - windowMs);
|
|
87
|
+
else until = Math.min(now, checkpoint + windowMs);
|
|
88
|
+
end = until === now ? until + 1e3 : until;
|
|
89
|
+
if (previous) {
|
|
90
|
+
mkdirSync(directory, {
|
|
91
|
+
recursive: true,
|
|
92
|
+
mode: 448
|
|
93
|
+
});
|
|
94
|
+
withDirLock(join(directory, "query.lock"), () => {
|
|
95
|
+
const temporary = join(directory, "checkpoint.json.tmp");
|
|
96
|
+
writeFileSync(temporary, JSON.stringify({
|
|
97
|
+
until: checkpoint,
|
|
98
|
+
boundary: previous.boundary,
|
|
99
|
+
windowMs
|
|
100
|
+
}), { mode: 384 });
|
|
101
|
+
renameSync(temporary, join(directory, "checkpoint.json"));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const seen = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const digest of previous?.boundary ?? []) seen.set(digest, (seen.get(digest) ?? 0) + 1);
|
|
108
|
+
const boundary = [];
|
|
109
|
+
const records = [];
|
|
110
|
+
if (final && from > checkpoint) records.push({
|
|
111
|
+
ts: from,
|
|
112
|
+
src: "device",
|
|
113
|
+
platform: "ios",
|
|
114
|
+
level: "warn",
|
|
115
|
+
msg: `Hosted iOS native log gap: dropped interval [${new Date(checkpoint).toISOString()}, ${new Date(from).toISOString()}) to collect the final tail within its budget.`
|
|
116
|
+
});
|
|
117
|
+
for (const line of output.split("\n")) {
|
|
118
|
+
const parsed = parseLogStreamLine(line);
|
|
119
|
+
if (!parsed || typeof parsed.ts !== "number" || parsed.ts < from || parsed.ts > end) continue;
|
|
120
|
+
const entry = JSON.parse(line);
|
|
121
|
+
const digest = createHash("sha256").update(JSON.stringify(Object.entries(entry).toSorted(([a], [b]) => a.localeCompare(b)))).digest("hex");
|
|
122
|
+
if (parsed.ts >= until - 5e3) boundary.push(digest);
|
|
123
|
+
if (newest !== void 0 && parsed.ts <= newest) continue;
|
|
124
|
+
const count = seen.get(digest) ?? 0;
|
|
125
|
+
if (count) {
|
|
126
|
+
seen.set(digest, count - 1);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
records.push({
|
|
130
|
+
...parsed,
|
|
131
|
+
platform: "ios"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
mkdirSync(directory, {
|
|
135
|
+
recursive: true,
|
|
136
|
+
mode: 448
|
|
137
|
+
});
|
|
138
|
+
withDirLock(join(directory, "query.lock"), () => {
|
|
139
|
+
const writer = createNdjsonWriter(join(directory, "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
|
|
140
|
+
try {
|
|
141
|
+
for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted iOS native logs.");
|
|
142
|
+
} finally {
|
|
143
|
+
writer.close();
|
|
144
|
+
}
|
|
145
|
+
const temporary = join(directory, "checkpoint.json.tmp");
|
|
146
|
+
try {
|
|
147
|
+
writeFileSync(temporary, JSON.stringify({
|
|
148
|
+
until,
|
|
149
|
+
boundary,
|
|
150
|
+
...until < now ? { windowMs } : {}
|
|
151
|
+
}), { mode: 384 });
|
|
152
|
+
renameSync(temporary, join(directory, "checkpoint.json"));
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(temporary, { force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return until < now;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/device-host/app.ts
|
|
161
|
+
function inside(root, path) {
|
|
162
|
+
const diff = relative(root, path);
|
|
163
|
+
return diff !== "" && diff !== ".." && !diff.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(diff);
|
|
164
|
+
}
|
|
165
|
+
function removeHostedAppData(home) {
|
|
166
|
+
const area = realpathSync(join(home, ".."));
|
|
167
|
+
const assertInside = (path) => {
|
|
168
|
+
if (!inside(area, realpathSync(path))) throw new Error("Hosted app data resolves outside its private area.");
|
|
169
|
+
};
|
|
170
|
+
const remove = (path) => {
|
|
171
|
+
if (!lstatSync(path, { throwIfNoEntry: false })) return;
|
|
172
|
+
assertInside(path);
|
|
173
|
+
rmSync(path, {
|
|
174
|
+
recursive: true,
|
|
175
|
+
force: true
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
assertInside(home);
|
|
179
|
+
const apps = join(area, "apps");
|
|
180
|
+
if (existsSync(apps)) {
|
|
181
|
+
assertInside(apps);
|
|
182
|
+
for (const attempt of readdirSync(apps)) {
|
|
183
|
+
const directory = join(apps, attempt);
|
|
184
|
+
if (!lstatSync(directory).isDirectory()) continue;
|
|
185
|
+
assertInside(directory);
|
|
186
|
+
remove(join(directory, "App.app"));
|
|
187
|
+
remove(join(directory, "App.apk"));
|
|
188
|
+
remove(join(directory, "App.apk.tmp"));
|
|
189
|
+
remove(join(directory, "blobs"));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
remove(join(area, "blobs"));
|
|
193
|
+
}
|
|
194
|
+
async function digest$1(path) {
|
|
195
|
+
const hash = createHash("sha256");
|
|
196
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
197
|
+
return hash.digest("hex");
|
|
198
|
+
}
|
|
199
|
+
function version(value) {
|
|
200
|
+
if (!/^\d+(?:\.\d+){0,2}$/.test(value)) throw new Error("The app or simulator OS version is unknown.");
|
|
201
|
+
return value.split(".").map(Number);
|
|
202
|
+
}
|
|
203
|
+
function newer(value, supported) {
|
|
204
|
+
const required = version(value);
|
|
205
|
+
const installed = version(supported);
|
|
206
|
+
for (let index = 0; index < 3; index++) {
|
|
207
|
+
const difference = (required[index] ?? 0) - (installed[index] ?? 0);
|
|
208
|
+
if (difference) return difference > 0;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
/** Materializes only digest-verified manifest content with links contained inside the app bundle. */
|
|
213
|
+
async function materializeHostedApp(home, session, attempt) {
|
|
214
|
+
const record = readHostedApp(session, attempt, home);
|
|
215
|
+
const area = hostedAppArea(session, attempt, home);
|
|
216
|
+
const blobs = hostedAppBlobs(session, home);
|
|
217
|
+
const app = join(area, "App.app");
|
|
218
|
+
if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
|
|
219
|
+
const manifest = join(blobs, record.manifest.sha256);
|
|
220
|
+
if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
|
|
221
|
+
rmSync(app, {
|
|
222
|
+
recursive: true,
|
|
223
|
+
force: true
|
|
224
|
+
});
|
|
225
|
+
mkdirSync(app, { mode: 448 });
|
|
226
|
+
for (const file of record.files) {
|
|
227
|
+
const blob = join(blobs, file.sha256);
|
|
228
|
+
const stat = lstatSync(blob);
|
|
229
|
+
if (!stat.isFile() || stat.size !== file.size || await digest$1(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
|
|
230
|
+
if (file.kind === "link") continue;
|
|
231
|
+
const target = join(app, file.path);
|
|
232
|
+
mkdirSync(dirname(target), {
|
|
233
|
+
recursive: true,
|
|
234
|
+
mode: 448
|
|
235
|
+
});
|
|
236
|
+
copyFileSync(blob, target);
|
|
237
|
+
chmodSync(target, file.kind === "exec" ? 493 : 420);
|
|
238
|
+
}
|
|
239
|
+
for (const file of record.files.filter((each) => each.kind === "link")) {
|
|
240
|
+
const bytes = readFileSync(join(blobs, file.sha256));
|
|
241
|
+
const target = bytes.toString("utf8");
|
|
242
|
+
const destination = posix.normalize(posix.join(posix.dirname(file.path), target));
|
|
243
|
+
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}`);
|
|
244
|
+
const link = join(app, file.path);
|
|
245
|
+
mkdirSync(dirname(link), {
|
|
246
|
+
recursive: true,
|
|
247
|
+
mode: 448
|
|
248
|
+
});
|
|
249
|
+
symlinkSync(target, link);
|
|
250
|
+
}
|
|
251
|
+
const root = realpathSync(app);
|
|
252
|
+
for (const file of record.files) {
|
|
253
|
+
const path = realpathSync(join(app, file.path));
|
|
254
|
+
if (!inside(root, path)) throw new Error(`The app entry resolves outside its bundle: ${file.path}`);
|
|
255
|
+
if (file.kind !== "link" && await digest$1(path) !== file.sha256) throw new Error("App entries alias different content.");
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
app,
|
|
259
|
+
root,
|
|
260
|
+
record
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function installHostedApp(home, session, attempt, device, metroPort) {
|
|
264
|
+
const { app, root, record } = await materializeHostedApp(home, session, attempt);
|
|
265
|
+
const exec = getExecutor();
|
|
266
|
+
const plist = (key) => exec.runFile("/usr/libexec/PlistBuddy", [
|
|
267
|
+
"-c",
|
|
268
|
+
`Print :${key}`,
|
|
269
|
+
join(app, "Info.plist")
|
|
270
|
+
], {
|
|
271
|
+
timeoutMs: 1e4,
|
|
272
|
+
killSignal: "SIGKILL"
|
|
273
|
+
});
|
|
274
|
+
if (plist("CFBundleIdentifier") !== record.bundleId) throw new Error("The app bundle identity differs from the offered identity.");
|
|
275
|
+
if (plist("DTPlatformName") !== "iphonesimulator" || !plist("CFBundleSupportedPlatforms").includes("iPhoneSimulator")) throw new Error("The app is not an iOS simulator bundle.");
|
|
276
|
+
const executable = plist("CFBundleExecutable");
|
|
277
|
+
if (!executable || /[\\/\0\r\n]/.test(executable) || executable === "." || executable === "..") throw new Error("The app executable is not a bundle-root file.");
|
|
278
|
+
const binary = realpathSync(join(app, executable));
|
|
279
|
+
if (!inside(root, binary)) throw new Error("The app executable resolves outside its bundle.");
|
|
280
|
+
if (!exec.runFile("xcrun", [
|
|
281
|
+
"lipo",
|
|
282
|
+
"-archs",
|
|
283
|
+
binary
|
|
284
|
+
], {
|
|
285
|
+
timeoutMs: 1e4,
|
|
286
|
+
killSignal: "SIGKILL"
|
|
287
|
+
}).split(/\s+/).includes(device.architecture)) throw new Error("The app has no executable slice for the hosted simulator architecture.");
|
|
288
|
+
const build = exec.runFile("xcrun", [
|
|
289
|
+
"vtool",
|
|
290
|
+
"-arch",
|
|
291
|
+
device.architecture,
|
|
292
|
+
"-show-build",
|
|
293
|
+
binary
|
|
294
|
+
], {
|
|
295
|
+
timeoutMs: 1e4,
|
|
296
|
+
killSignal: "SIGKILL"
|
|
297
|
+
});
|
|
298
|
+
const minimum = /^\s*minos\s+(\S+)\s*$/m.exec(build)?.[1];
|
|
299
|
+
if (!/^\s*platform\s+IOSSIMULATOR\s*$/m.test(build) || !minimum || newer(minimum, device.runtime) || newer(plist("MinimumOSVersion"), device.runtime)) throw new Error("The executable platform or minimum OS is incompatible with the hosted simulator.");
|
|
300
|
+
assertHostedDeviceLedger(home, device.udid);
|
|
301
|
+
if (readHostedDevice(home).udid !== device.udid) throw new Error("The hosted device identity changed.");
|
|
302
|
+
const installed = installIosApp({
|
|
303
|
+
udid: device.udid,
|
|
304
|
+
appPath: app,
|
|
305
|
+
bundleId: record.bundleId,
|
|
306
|
+
devClientScheme: record.devClientScheme,
|
|
307
|
+
proveInstalled: false
|
|
308
|
+
});
|
|
309
|
+
if (!installed.ok) throw new Error(installed.reason ?? "Hosted app installation was not established.");
|
|
310
|
+
assertHostedDeviceLedger(home, device.udid);
|
|
311
|
+
if (readHostedDevice(home).udid !== device.udid) throw new Error("The hosted device identity changed.");
|
|
312
|
+
const launched = launchIosApp({
|
|
313
|
+
udid: device.udid,
|
|
314
|
+
bundleId: record.bundleId,
|
|
315
|
+
metroPort: record.mode === "development" ? metroPort ?? null : null,
|
|
316
|
+
devClientScheme: record.devClientScheme
|
|
317
|
+
});
|
|
318
|
+
if (!launched.ok) throw new Error(launched.reason ?? "Hosted app launch was not established.");
|
|
319
|
+
if (record.mode === "development") return "unverified";
|
|
320
|
+
for (let tries = 0; tries < 10; tries++) {
|
|
321
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
322
|
+
if (iosAppProcess(device.udid, record.bundleId)) return true;
|
|
323
|
+
}
|
|
324
|
+
return "unverified";
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/device-host/worker.ts
|
|
328
|
+
function inventory() {
|
|
329
|
+
return listAllIosSims({ includeUnavailable: true });
|
|
330
|
+
}
|
|
331
|
+
function selectHostedIosDevice(selectors) {
|
|
332
|
+
inventory();
|
|
333
|
+
if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
|
|
334
|
+
const selected = resolveIosCreation(selectors);
|
|
335
|
+
if (!selected.deviceType || !selected.runtime) throw new Error("The installed simulator type or runtime could not be established.");
|
|
336
|
+
if (process.arch !== "arm64" && process.arch !== "x64") throw new Error("Unsupported simulator host architecture.");
|
|
337
|
+
return {
|
|
338
|
+
...selected,
|
|
339
|
+
deviceType: selected.deviceType,
|
|
340
|
+
runtime: selected.runtime,
|
|
341
|
+
architecture: process.arch === "arm64" ? "arm64" : "x86_64"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Called only in the private server-owned worker home; native effects use Stim's existing ownership and teardown. */
|
|
345
|
+
async function runHostedDevice(mode, selectors, app) {
|
|
346
|
+
const home = process.env.STIM_HOME;
|
|
347
|
+
if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
|
|
348
|
+
let device = null;
|
|
349
|
+
let creationStarted = false;
|
|
350
|
+
try {
|
|
351
|
+
if (mode === "prepare") {
|
|
352
|
+
creationStarted = existsSync(join(home, "hosted-device.json")) || existsSync(join(home, "created-devices.json"));
|
|
353
|
+
if (creationStarted) throw new Error("This hosted worker home already has a device record. Attach or stop its existing session.");
|
|
354
|
+
const selected = selectHostedIosDevice(selectors);
|
|
355
|
+
creationStarted = true;
|
|
356
|
+
const created = createOwnedIosSim("hosted", {}, selected);
|
|
357
|
+
device = {
|
|
358
|
+
...selected,
|
|
359
|
+
udid: created.udid,
|
|
360
|
+
name: created.name
|
|
361
|
+
};
|
|
362
|
+
withDirLock(join(home, "hosted-device.lock"), () => {
|
|
363
|
+
const temporary = join(home, "hosted-device.json.tmp");
|
|
364
|
+
writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
|
|
365
|
+
renameSync(temporary, join(home, "hosted-device.json"));
|
|
366
|
+
});
|
|
367
|
+
assertHostedDeviceLedger(home, device.udid);
|
|
368
|
+
await bootIosSim(device.udid, { openViewer: false });
|
|
369
|
+
if (inventory().find((sim) => sim.udid === device.udid)?.state !== "Booted") throw new Error("Hosted simulator boot could not be verified.");
|
|
370
|
+
return {
|
|
371
|
+
state: "ready",
|
|
372
|
+
device
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
device = readHostedDevice(home);
|
|
376
|
+
const ledger = assertHostedDeviceLedger(home, device.udid, "ios", { allowEmpty: mode === "stop" });
|
|
377
|
+
const current = inventory().find((sim) => sim.udid === device.udid);
|
|
378
|
+
if (mode === "install") {
|
|
379
|
+
if (!app || current?.state !== "Booted") throw new Error("Hosted app installation requires its booted owned simulator.");
|
|
380
|
+
const launched = await installHostedApp(home, app.session, app.attempt, device, app.metroPort);
|
|
381
|
+
return {
|
|
382
|
+
state: "installed",
|
|
383
|
+
device,
|
|
384
|
+
launched
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (ledger === "empty") {
|
|
388
|
+
if (current) throw new Error("The hosted simulator remains without ledger ownership; it was kept.");
|
|
389
|
+
removeHostedAppData(home);
|
|
390
|
+
return {
|
|
391
|
+
state: "stopped",
|
|
392
|
+
device
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const outcome = teardownOwnedIosSim(device.udid, { del: true });
|
|
396
|
+
if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted simulator teardown was not established.");
|
|
397
|
+
const after = inventory().find((sim) => sim.udid === device.udid);
|
|
398
|
+
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.`);
|
|
399
|
+
forgetCreatedDevice("ios", device.udid);
|
|
400
|
+
removeHostedAppData(home);
|
|
401
|
+
return {
|
|
402
|
+
state: "stopped",
|
|
403
|
+
device
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
408
|
+
device,
|
|
409
|
+
notice: error.message
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
//#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
|
+
//#region src/device-host/android.ts
|
|
485
|
+
function portIsOccupied(port) {
|
|
486
|
+
const devices = listAdbDevices({ timeoutMs: 5e3 });
|
|
487
|
+
return [...devices.emulators, ...devices.unhealthy].some((device) => device.consolePort === port);
|
|
488
|
+
}
|
|
489
|
+
function selectHostedAndroidDevice(request) {
|
|
490
|
+
if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
|
|
491
|
+
const image = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage: request.systemImage });
|
|
492
|
+
if (!image || image.arch !== hostSystemImageArch()) throw new Error("No compatible installed Android image.");
|
|
493
|
+
const profile = request.deviceProfile ?? DEFAULT_AVD_DEVICE_PROFILE;
|
|
494
|
+
if (!listAvdDeviceProfiles().includes(profile)) throw new Error("The Android device profile is not installed.");
|
|
495
|
+
return {
|
|
496
|
+
systemImage: image.pkg,
|
|
497
|
+
deviceProfile: profile,
|
|
498
|
+
architecture: image.arch
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/** Runs only in the private worker home; a caller cannot select or tear down an existing AVD. */
|
|
502
|
+
async function runHostedAndroidDevice(mode, request, app) {
|
|
503
|
+
const home = process.env.STIM_HOME;
|
|
504
|
+
if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
|
|
505
|
+
let device = null;
|
|
506
|
+
let creationStarted = false;
|
|
507
|
+
try {
|
|
508
|
+
if (mode === "install") {
|
|
509
|
+
if (!app) throw new Error("Hosted Android installation needs its admitted app attempt.");
|
|
510
|
+
device = readHostedDevice(home, "android");
|
|
511
|
+
const launched = await installHostedAndroidApp(home, request.session, app.attempt, device);
|
|
512
|
+
return {
|
|
513
|
+
state: "installed",
|
|
514
|
+
device,
|
|
515
|
+
launched
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (mode === "prepare") {
|
|
519
|
+
creationStarted = existsSync(join(home, "hosted-device.json")) || existsSync(join(home, "created-devices.json"));
|
|
520
|
+
if (creationStarted) throw new Error("Attach or stop the existing hosted Android session.");
|
|
521
|
+
if (!/^[a-f0-9-]{36}$/.test(request.session) || !hostedConsolePort(request.consolePort)) throw new Error("Hosted Android prepare needs its server-selected session and console port.");
|
|
522
|
+
const choice = selectHostedAndroidDevice(request);
|
|
523
|
+
const avdName = ownedAvdName(`hosted-${request.session}`);
|
|
524
|
+
if (listAvds({ timeoutMs: 5e3 }).includes(avdName) || portIsOccupied(request.consolePort)) throw new Error("The hosted AVD name or selected console port is already in use.");
|
|
525
|
+
const selected = {
|
|
526
|
+
avdName,
|
|
527
|
+
serial: `emulator-${request.consolePort}`,
|
|
528
|
+
consolePort: request.consolePort,
|
|
529
|
+
...choice
|
|
530
|
+
};
|
|
531
|
+
creationStarted = true;
|
|
532
|
+
await createOwnedAvd(`hosted-${request.session}`, {
|
|
533
|
+
systemImage: choice.systemImage,
|
|
534
|
+
deviceProfile: choice.deviceProfile,
|
|
535
|
+
spawn: (file, args, options) => {
|
|
536
|
+
assertHostedDeviceLedger(home, avdName, "android");
|
|
537
|
+
withDirLock(join(home, "hosted-device.lock"), () => {
|
|
538
|
+
const temporary = join(home, "hosted-device.json.tmp");
|
|
539
|
+
writeFileSync(temporary, JSON.stringify(selected), { mode: 384 });
|
|
540
|
+
renameSync(temporary, join(home, "hosted-device.json"));
|
|
541
|
+
});
|
|
542
|
+
device = selected;
|
|
543
|
+
return getExecutor().spawn(file, args, {
|
|
544
|
+
...options,
|
|
545
|
+
detached: false
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
assertHostedDeviceLedger(home, avdName, "android");
|
|
550
|
+
if (portIsOccupied(selected.consolePort)) throw new Error("The selected Android console port became occupied.");
|
|
551
|
+
bootAndroidEmulator(avdName, selected.consolePort, {
|
|
552
|
+
openViewer: false,
|
|
553
|
+
logFile: join(home, "emulator.log")
|
|
554
|
+
});
|
|
555
|
+
if (!(await waitForBoot(selected.serial, 24e4, { commandTimeoutMs: 5e3 })).ok || getAvdNameForSerial(selected.serial) !== avdName || androidDeviceAbi(selected.serial) !== selected.architecture) throw new Error("Hosted Android boot could not be verified for its exact owned AVD.");
|
|
556
|
+
return {
|
|
557
|
+
state: "ready",
|
|
558
|
+
device: selected
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
device = readHostedDevice(home, "android");
|
|
562
|
+
const ledger = assertHostedDeviceLedger(home, device.avdName, "android", { allowEmpty: true });
|
|
563
|
+
if (ledger === "listed") {
|
|
564
|
+
withConfigLock$1(() => {
|
|
565
|
+
if (!loadConfig$1()) ensureConfig();
|
|
566
|
+
});
|
|
567
|
+
const outcome = teardownOwnedAvd(device.avdName, { del: true });
|
|
568
|
+
if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted Android deletion was not established.");
|
|
569
|
+
}
|
|
570
|
+
const name = device.avdName;
|
|
571
|
+
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.");
|
|
572
|
+
if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
|
|
573
|
+
if (ledger === "listed") forgetCreatedDevice("android", device.avdName);
|
|
574
|
+
removeHostedAppData(home);
|
|
575
|
+
return {
|
|
576
|
+
state: "stopped",
|
|
577
|
+
device
|
|
578
|
+
};
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return {
|
|
581
|
+
state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
|
|
582
|
+
device,
|
|
583
|
+
notice: error.message
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/device-host/macos.ts
|
|
589
|
+
function selectHostedMacos() {
|
|
590
|
+
if (readHostMemoryPressure(getExecutor()) !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
|
|
591
|
+
const macosVersion = getExecutor().runFile("sw_vers", ["-productVersion"], {
|
|
592
|
+
timeoutMs: 1e4,
|
|
593
|
+
killSignal: "SIGKILL"
|
|
594
|
+
});
|
|
595
|
+
if (!/^\d+(\.\d+){0,2}$/.test(macosVersion)) throw new Error("The host macOS version is unknown.");
|
|
596
|
+
return {
|
|
597
|
+
architecture: process.arch === "arm64" ? "arm64" : "x86_64",
|
|
598
|
+
macosVersion
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function writeRecord(home, file, value) {
|
|
602
|
+
withDirLock(join(home, "hosted-device.lock"), () => {
|
|
603
|
+
const temporary = join(home, `${file}.tmp`);
|
|
604
|
+
writeFileSync(temporary, JSON.stringify(value), { mode: 384 });
|
|
605
|
+
renameSync(temporary, join(home, file));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function preferenceIdentity(home, appSlot) {
|
|
609
|
+
const file = join(home, "hosted-macos-app.json");
|
|
610
|
+
if (!existsSync(file)) return null;
|
|
611
|
+
const stored = JSON.parse(readFileSync(file, "utf8"));
|
|
612
|
+
if (!isJsonObject(stored) || typeof stored.bundleId !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,248}$/.test(stored.bundleId) || stored.bundleId.startsWith("com.apple.") || !stored.bundleId.endsWith(`.hosted${appSlot}`)) throw new Error("The hosted macOS preference identity is malformed.");
|
|
613
|
+
return stored.bundleId;
|
|
614
|
+
}
|
|
615
|
+
function removePreferences(bundleId) {
|
|
616
|
+
const exec = getExecutor();
|
|
617
|
+
const options = {
|
|
618
|
+
timeoutMs: 1e4,
|
|
619
|
+
killSignal: "SIGKILL"
|
|
620
|
+
};
|
|
621
|
+
exec.runFileQuiet("defaults", ["delete", bundleId], options);
|
|
622
|
+
const file = join(homedir(), "Library", "Preferences", `${bundleId}.plist`);
|
|
623
|
+
rmSync(file, { force: true });
|
|
624
|
+
if (existsSync(file)) throw new Error("The hosted macOS preferences plist still exists after deletion.");
|
|
625
|
+
try {
|
|
626
|
+
exec.runFile("defaults", ["read", bundleId], options);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (/not found|does not exist/i.test(error.message)) return;
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
throw new Error("The hosted macOS preference domain is still readable after deletion.");
|
|
632
|
+
}
|
|
633
|
+
function assertInside(root, path) {
|
|
634
|
+
const diff = relative(root, realpathSync(path));
|
|
635
|
+
if (!diff || diff === ".." || diff.startsWith(`..${sep}`) || isAbsolute(diff)) throw new Error("Hosted macOS app data resolves outside its private area.");
|
|
636
|
+
}
|
|
637
|
+
function removeAppData(root, path) {
|
|
638
|
+
try {
|
|
639
|
+
lstatSync(path);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error.code === "ENOENT") return;
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
assertInside(root, path);
|
|
645
|
+
rmSync(path, {
|
|
646
|
+
recursive: true,
|
|
647
|
+
force: true
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
async function runHostedMacosApp(mode, request, app) {
|
|
651
|
+
const home = process.env.STIM_HOME;
|
|
652
|
+
let device = null;
|
|
653
|
+
let recorded = false;
|
|
654
|
+
try {
|
|
655
|
+
if (!home) throw new Error("Hosted workers require their isolated STIM_HOME.");
|
|
656
|
+
if (!hostedMacosAppSlot(request.appSlot)) throw new Error("Hosted macOS workers require a reserved app slot.");
|
|
657
|
+
if (mode === "prepare") {
|
|
658
|
+
recorded = existsSync(join(home, "hosted-device.json"));
|
|
659
|
+
if (recorded) throw new Error("Attach or stop the existing hosted macOS session.");
|
|
660
|
+
const selected = {
|
|
661
|
+
...selectHostedMacos(),
|
|
662
|
+
appSlot: request.appSlot
|
|
663
|
+
};
|
|
664
|
+
assertHostedDeviceLedger(home, `macos-${request.appSlot}`, "macos");
|
|
665
|
+
writeRecord(home, "hosted-device.json", selected);
|
|
666
|
+
recorded = true;
|
|
667
|
+
device = selected;
|
|
668
|
+
return {
|
|
669
|
+
state: "ready",
|
|
670
|
+
device
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
device = readHostedDevice(home, "macos");
|
|
674
|
+
if (device.appSlot !== request.appSlot) throw new Error("The hosted macOS app slot changed.");
|
|
675
|
+
assertHostedDeviceLedger(home, `macos-${device.appSlot}`, "macos");
|
|
676
|
+
const runRoot = join(realpathSync(home), "macos-app");
|
|
677
|
+
if (mode === "stop") {
|
|
678
|
+
if (requiredMacosRecord(runRoot)) await stopMacosAppHeld(runRoot);
|
|
679
|
+
const bundleId = preferenceIdentity(home, device.appSlot);
|
|
680
|
+
if (bundleId) removePreferences(bundleId);
|
|
681
|
+
const area = realpathSync(join(home, ".."));
|
|
682
|
+
assertInside(area, home);
|
|
683
|
+
removeAppData(realpathSync(home), join(home, "app-home"));
|
|
684
|
+
const apps = join(home, "..", "apps");
|
|
685
|
+
if (existsSync(apps)) {
|
|
686
|
+
assertInside(area, apps);
|
|
687
|
+
for (const attempt of readdirSync(apps)) {
|
|
688
|
+
const directory = join(apps, attempt);
|
|
689
|
+
assertInside(area, directory);
|
|
690
|
+
removeAppData(area, join(directory, "App.app"));
|
|
691
|
+
removeAppData(area, join(directory, "blobs"));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
removeAppData(area, join(home, "..", "blobs"));
|
|
695
|
+
return {
|
|
696
|
+
state: "stopped",
|
|
697
|
+
device
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (!app) throw new Error("Hosted macOS installation needs its admitted app attempt.");
|
|
701
|
+
const { app: bundle, root, record } = await materializeHostedApp(home, request.session, app.attempt);
|
|
702
|
+
const exec = getExecutor();
|
|
703
|
+
const options = {
|
|
704
|
+
timeoutMs: 1e4,
|
|
705
|
+
killSignal: "SIGKILL"
|
|
706
|
+
};
|
|
707
|
+
const plistPath = join(bundle, "Contents", "Info.plist");
|
|
708
|
+
const plist = JSON.parse(exec.runFile("plutil", [
|
|
709
|
+
"-convert",
|
|
710
|
+
"json",
|
|
711
|
+
"-o",
|
|
712
|
+
"-",
|
|
713
|
+
plistPath
|
|
714
|
+
], options));
|
|
715
|
+
if (!isJsonObject(plist) || plist.CFBundleIdentifier !== record.bundleId) throw new Error("The app bundle identity differs from the offered identity.");
|
|
716
|
+
const product = plist.CFBundleExecutable;
|
|
717
|
+
if (typeof product !== "string" || !product || /[\\/\0\r\n]/.test(product) || product === "." || product === "..") throw new Error("The app executable must be a single path component.");
|
|
718
|
+
if (plist.CFBundleURLTypes !== void 0 || plist.SUFeedURL !== void 0) throw new Error("Hosted macOS apps cannot register shared URL schemes or an update feed.");
|
|
719
|
+
if (plist.LSMinimumSystemVersion !== void 0 && (typeof plist.LSMinimumSystemVersion !== "string" || newer(plist.LSMinimumSystemVersion, device.macosVersion))) throw new Error("The app minimum OS is incompatible with the hosted Mac.");
|
|
720
|
+
const executable = realpathSync(join(bundle, "Contents", "MacOS", product));
|
|
721
|
+
const diff = relative(root, executable);
|
|
722
|
+
if (!diff || diff === ".." || diff.startsWith(`..${sep}`) || isAbsolute(diff)) throw new Error("The app executable resolves outside its bundle.");
|
|
723
|
+
if (!exec.runFile("xcrun", [
|
|
724
|
+
"lipo",
|
|
725
|
+
"-archs",
|
|
726
|
+
executable
|
|
727
|
+
], options).split(/\s+/).includes(device.architecture)) throw new Error("The app has no executable slice for the hosted Mac architecture.");
|
|
728
|
+
const build = exec.runFile("xcrun", [
|
|
729
|
+
"vtool",
|
|
730
|
+
"-arch",
|
|
731
|
+
device.architecture,
|
|
732
|
+
"-show-build",
|
|
733
|
+
executable
|
|
734
|
+
], options);
|
|
735
|
+
const minimum = /^\s*minos\s+(\S+)\s*$/m.exec(build)?.[1];
|
|
736
|
+
if (!/^\s*platform\s+MACOS\s*$/m.test(build) || !minimum || newer(minimum, device.macosVersion)) throw new Error("The executable platform or minimum OS is incompatible with the hosted Mac.");
|
|
737
|
+
const bundleId = hostedMacosBundleId(record.bundleId, device.appSlot);
|
|
738
|
+
exec.runFile("/usr/libexec/PlistBuddy", [
|
|
739
|
+
"-c",
|
|
740
|
+
`Set :CFBundleIdentifier ${bundleId}`,
|
|
741
|
+
plistPath
|
|
742
|
+
], options);
|
|
743
|
+
const frameworks = join(bundle, "Contents", "Frameworks");
|
|
744
|
+
if (existsSync(frameworks)) {
|
|
745
|
+
for (const entry of readdirSync(frameworks)) if (entry.endsWith(".framework")) exec.runFile("codesign", [
|
|
746
|
+
"--force",
|
|
747
|
+
"--sign",
|
|
748
|
+
"-",
|
|
749
|
+
join(frameworks, entry)
|
|
750
|
+
], options);
|
|
751
|
+
}
|
|
752
|
+
exec.runFile("codesign", [
|
|
753
|
+
"--force",
|
|
754
|
+
"--sign",
|
|
755
|
+
"-",
|
|
756
|
+
bundle
|
|
757
|
+
], options);
|
|
758
|
+
if (requiredMacosRecord(runRoot)) await stopMacosAppHeld(runRoot);
|
|
759
|
+
const previousId = preferenceIdentity(home, device.appSlot);
|
|
760
|
+
if (previousId && previousId !== bundleId) removePreferences(previousId);
|
|
761
|
+
mkdirSync(runRoot, {
|
|
762
|
+
recursive: true,
|
|
763
|
+
mode: 448
|
|
764
|
+
});
|
|
765
|
+
const canonicalRunRoot = realpathSync(runRoot);
|
|
766
|
+
ensureWorkspaceStorage(canonicalRunRoot);
|
|
767
|
+
const launch = {
|
|
768
|
+
product,
|
|
769
|
+
bundle: root,
|
|
770
|
+
bundleId,
|
|
771
|
+
executable,
|
|
772
|
+
arguments: record.arguments ?? [],
|
|
773
|
+
launchId: randomUUID(),
|
|
774
|
+
supervisor: macosProcess(process.pid),
|
|
775
|
+
build: {
|
|
776
|
+
state: "ok",
|
|
777
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
writeRecord(home, "hosted-macos-app.json", { bundleId });
|
|
781
|
+
writeWorkspaceState(canonicalRunRoot, { macos: launch });
|
|
782
|
+
const isolated = join(home, "app-home");
|
|
783
|
+
const temporary = join(isolated, "tmp");
|
|
784
|
+
mkdirSync(isolated, {
|
|
785
|
+
recursive: true,
|
|
786
|
+
mode: 448
|
|
787
|
+
});
|
|
788
|
+
mkdirSync(temporary, {
|
|
789
|
+
mode: 448,
|
|
790
|
+
recursive: true
|
|
791
|
+
});
|
|
792
|
+
const fd = openSync(join(home, "macos-supervisor.log"), "a", 384);
|
|
793
|
+
let child;
|
|
794
|
+
let spawnError;
|
|
795
|
+
const env = {};
|
|
796
|
+
for (const key of [
|
|
797
|
+
"PATH",
|
|
798
|
+
"LANG",
|
|
799
|
+
"LC_ALL",
|
|
800
|
+
"LC_CTYPE",
|
|
801
|
+
"USER",
|
|
802
|
+
"LOGNAME",
|
|
803
|
+
"SHELL",
|
|
804
|
+
"TERM"
|
|
805
|
+
]) if (process.env[key] !== void 0) env[key] = process.env[key];
|
|
806
|
+
Object.assign(env, {
|
|
807
|
+
STIM_HOME: home,
|
|
808
|
+
HOME: isolated,
|
|
809
|
+
CFFIXED_USER_HOME: isolated,
|
|
810
|
+
TMPDIR: temporary
|
|
811
|
+
});
|
|
812
|
+
try {
|
|
813
|
+
child = exec.spawn(process.execPath, [
|
|
814
|
+
spawnEntry("macos-run"),
|
|
815
|
+
canonicalRunRoot,
|
|
816
|
+
launch.launchId
|
|
817
|
+
], {
|
|
818
|
+
detached: true,
|
|
819
|
+
stdio: [
|
|
820
|
+
"ignore",
|
|
821
|
+
fd,
|
|
822
|
+
fd
|
|
823
|
+
],
|
|
824
|
+
cwd: canonicalRunRoot,
|
|
825
|
+
env
|
|
826
|
+
});
|
|
827
|
+
child.once("error", (error) => {
|
|
828
|
+
spawnError = error;
|
|
829
|
+
});
|
|
830
|
+
child.unref();
|
|
831
|
+
} finally {
|
|
832
|
+
closeSync(fd);
|
|
833
|
+
}
|
|
834
|
+
const deadline = Date.now() + 2e4;
|
|
835
|
+
while (Date.now() < deadline) {
|
|
836
|
+
if (spawnError) throw spawnError;
|
|
837
|
+
const current = readMacosRecord(canonicalRunRoot);
|
|
838
|
+
if (current?.launchId === launch.launchId && current.app && current.supervisor && (0, process_identity_exports.inspectProcessIdentity)(current.app) === "same" && (0, process_identity_exports.inspectProcessIdentity)(current.supervisor) === "same") return {
|
|
839
|
+
state: "installed",
|
|
840
|
+
device,
|
|
841
|
+
launched: true,
|
|
842
|
+
pid: current.app.pid
|
|
843
|
+
};
|
|
844
|
+
if (child.exitCode !== null || child.signalCode !== null) break;
|
|
845
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
846
|
+
}
|
|
847
|
+
throw new Error("The hosted macOS app did not register. See macos-supervisor.log in the worker home.");
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return {
|
|
850
|
+
state: mode === "prepare" && !recorded && !device && !(home && existsSync(join(home, "hosted-device.json"))) ? "stopped" : "unknown",
|
|
851
|
+
device,
|
|
852
|
+
notice: error.message
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/device-host/offer.ts
|
|
858
|
+
function inspectHostedDevice(request) {
|
|
859
|
+
const cpus = availableParallelism();
|
|
860
|
+
let workerDiskFreeBytes = null;
|
|
861
|
+
try {
|
|
862
|
+
const disk = statfsSync(configDir());
|
|
863
|
+
workerDiskFreeBytes = disk.bavail * disk.bsize;
|
|
864
|
+
} catch {}
|
|
865
|
+
const resources = {
|
|
866
|
+
cpus,
|
|
867
|
+
loadPerCore: loadavg()[1] / cpus,
|
|
868
|
+
memoryFreeBytes: freemem(),
|
|
869
|
+
memoryPressure: readHostMemoryPressure(),
|
|
870
|
+
workerDiskFreeBytes
|
|
871
|
+
};
|
|
872
|
+
try {
|
|
873
|
+
if (process.platform !== "darwin") throw new Error("Hosted device sessions require a Mac.");
|
|
874
|
+
if (resources.memoryPressure !== "normal") throw new Error("Host memory pressure is unknown or elevated.");
|
|
875
|
+
return request.platform === "macos" ? {
|
|
876
|
+
platform: "macos",
|
|
877
|
+
choice: selectHostedMacos(),
|
|
878
|
+
resources,
|
|
879
|
+
declined: null
|
|
880
|
+
} : request.platform === "ios" ? {
|
|
881
|
+
platform: "ios",
|
|
882
|
+
choice: selectHostedIosDevice(request),
|
|
883
|
+
resources,
|
|
884
|
+
declined: null
|
|
885
|
+
} : {
|
|
886
|
+
platform: "android",
|
|
887
|
+
choice: selectHostedAndroidDevice(request),
|
|
888
|
+
resources,
|
|
889
|
+
declined: null
|
|
890
|
+
};
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return {
|
|
893
|
+
platform: request.platform,
|
|
894
|
+
choice: null,
|
|
895
|
+
resources,
|
|
896
|
+
declined: error.message.slice(0, 4e3)
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/device-host/run.ts
|
|
902
|
+
async function main() {
|
|
903
|
+
const chunks = [];
|
|
904
|
+
let bytes = 0;
|
|
905
|
+
for await (const chunk of process.stdin) {
|
|
906
|
+
bytes += chunk.length;
|
|
907
|
+
if (bytes > 8192) throw new Error("Hosted worker request is too large.");
|
|
908
|
+
chunks.push(chunk);
|
|
909
|
+
}
|
|
910
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
911
|
+
if (!isJsonObject(input) || ![
|
|
912
|
+
"prepare",
|
|
913
|
+
"stop",
|
|
914
|
+
"install",
|
|
915
|
+
"offer",
|
|
916
|
+
"logs"
|
|
917
|
+
].includes(String(input.mode))) throw new Error("Invalid hosted worker request.");
|
|
918
|
+
if (input.mode === "offer") {
|
|
919
|
+
const request = parseHostedOfferRequest(input);
|
|
920
|
+
if (!request) throw new Error("Invalid hosted offer selectors.");
|
|
921
|
+
process.stdout.write(`${JSON.stringify(inspectHostedDevice(request))}\n`);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (process.platform !== "darwin") {
|
|
925
|
+
process.stdout.write(`${JSON.stringify({
|
|
926
|
+
state: input.mode === "prepare" ? "stopped" : "unknown",
|
|
927
|
+
device: null,
|
|
928
|
+
notice: "Hosted device sessions require a Mac."
|
|
929
|
+
})}\n`);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if ((input.mode === "install" || input.mode === "logs") && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
|
|
933
|
+
if (input.metroPort !== void 0 && (typeof input.metroPort !== "number" || !Number.isInteger(input.metroPort) || input.metroPort < 1 || input.metroPort > 65535)) throw new Error("Invalid hosted Metro port.");
|
|
934
|
+
if (input.platform === "macos" && !hostedMacosAppSlot(input.appSlot)) throw new Error("Invalid hosted macOS app slot.");
|
|
935
|
+
if (input.mode === "logs") {
|
|
936
|
+
if (input.platform !== "ios" || !process.env.STIM_HOME || typeof input.since !== "number" || !Number.isFinite(input.since)) throw new Error("Invalid hosted iOS log request.");
|
|
937
|
+
const more = collectHostedIosLogs(process.env.STIM_HOME, input.session, input.attempt, input.since, input.final === true);
|
|
938
|
+
process.stdout.write(`${JSON.stringify({ more })}\n`);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const result = input.platform === "macos" ? await runHostedMacosApp(input.mode, {
|
|
942
|
+
session: typeof input.session === "string" ? input.session : "",
|
|
943
|
+
appSlot: input.appSlot
|
|
944
|
+
}, input.mode === "install" ? { attempt: input.attempt } : void 0) : input.platform === "android" ? await runHostedAndroidDevice(input.mode, {
|
|
945
|
+
session: typeof input.session === "string" ? input.session : "",
|
|
946
|
+
consolePort: input.consolePort,
|
|
947
|
+
...typeof input.systemImage === "string" ? { systemImage: input.systemImage } : {},
|
|
948
|
+
...typeof input.deviceProfile === "string" ? { deviceProfile: input.deviceProfile } : {}
|
|
949
|
+
}, input.mode === "install" ? { attempt: input.attempt } : void 0) : await runHostedDevice(input.mode, {
|
|
950
|
+
...typeof input.deviceType === "string" ? { deviceType: input.deviceType } : {},
|
|
951
|
+
...typeof input.runtime === "string" ? { runtime: input.runtime } : {}
|
|
952
|
+
}, input.mode === "install" ? {
|
|
953
|
+
session: input.session,
|
|
954
|
+
attempt: input.attempt,
|
|
955
|
+
...typeof input.metroPort === "number" ? { metroPort: input.metroPort } : {}
|
|
956
|
+
} : void 0);
|
|
957
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
958
|
+
}
|
|
959
|
+
main().catch((error) => {
|
|
960
|
+
process.stderr.write(`${String(error.message ?? error)}\n`);
|
|
961
|
+
process.exitCode = 1;
|
|
962
|
+
});
|
|
963
|
+
//#endregion
|
|
964
|
+
export {};
|