stim 1.12.0 → 1.13.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 +3 -1
- package/dist/{activity-CCZJs__P.mjs → activity-DfVKZTEB.mjs} +42 -25
- package/dist/{android-BQiKJYK0.mjs → android-BHfAu2mf.mjs} +1 -1
- package/dist/{android-B7X-8H7h.mjs → android-CD8pr8Q2.mjs} +62 -47
- package/dist/{android-cas-DzLPXiEn.mjs → android-cas-B9Kv9aIy.mjs} +9 -7
- package/dist/{android-75Sk2rIR.mjs → android-qx_31wJo.mjs} +395 -116
- package/dist/{app-install-CsZi0SAT.mjs → app-install-CDFY2nUL.mjs} +1 -1
- package/dist/{budget-DbzW5VTD.mjs → budget-E5o91NR1.mjs} +154 -37
- package/dist/{build-plan-CqntbR7b.mjs → build-plan-BTa93o_r.mjs} +21 -88
- package/dist/{build-progress-h60GPV27.mjs → build-progress-l7d-X-ZP.mjs} +75 -7
- package/dist/cdp-Dq_ypAlJ.mjs +115 -0
- package/dist/chrome-B5hWgQJS.mjs +61 -0
- package/dist/cli.mjs +16 -15
- package/dist/collector-run.mjs +14 -8
- package/dist/{command-output-BWCj5Vh-.mjs → command-output-BIL7rlX7.mjs} +1 -1
- package/dist/created-devices-Cd558h31.mjs +37 -0
- package/dist/{deps-CO-_7UOm.mjs → deps-2Wo81np9.mjs} +1 -1
- package/dist/{device-9r9xRYDa.mjs → device-Cf7vl-so.mjs} +6 -6
- package/dist/{device-lease-B9LAXIUH.mjs → device-lease-CL9dZf73.mjs} +52 -3
- package/dist/{device-pool-BoezJd-c.mjs → device-pool-MuoiyRcu.mjs} +3 -3
- package/dist/{device-remote-C0_o-tJH.mjs → device-remote-DT9ngZiD.mjs} +10 -10
- package/dist/{doctor-LfDlbR-N.mjs → doctor-D2iysNNH.mjs} +47 -18
- package/dist/{error-diagnostics-CCxjn31V.mjs → error-diagnostics-BoiOhck8.mjs} +6 -6
- package/dist/{gc-C4nQFcTX.mjs → gc-eA-PRBX8.mjs} +853 -126
- package/dist/{guide-BPBlh9ML.mjs → guide-BfZ0OmiP.mjs} +736 -110
- package/dist/{guide-status-i9ogHt4v.mjs → guide-status-BaMYu55M.mjs} +1 -1
- package/dist/{idle-S3tFZ53_.mjs → idle-DxLi5OkN.mjs} +42 -9
- package/dist/{in-use-D9XYEgFD.mjs → in-use-wYEaKQM9.mjs} +9 -5
- package/dist/{ios-CDzWL1mj.mjs → ios-CcfCeZ19.mjs} +98 -54
- package/dist/{ios-CZWOHTTc.mjs → ios-CmEzZiOj.mjs} +10 -5
- package/dist/{ios-device-D9cLxVr5.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
- package/dist/{launch-verify-B2oR_kFN.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
- package/dist/{logs-SzuYudOj.mjs → logs-DcSRbC2t.mjs} +11 -10
- package/dist/{metro-Dzo30hvJ.mjs → metro-BPeY1YMh.mjs} +4 -4
- package/dist/{named-ports-Dn1o61D9.mjs → named-ports-n7tnJsI8.mjs} +78 -6
- package/dist/native-runtime-CiiFHt0e.mjs +74 -0
- package/dist/{ownership-Dqnrjrio.mjs → ownership-Bz2kETAS.mjs} +1 -1
- package/dist/{ownership-B8d4kXBx.mjs → ownership-PUP4Dwai.mjs} +134 -209
- package/dist/page-BTu7I60K.mjs +29 -0
- package/dist/{ports-Dp_QTwTC.mjs → ports-C5UutqpJ.mjs} +4 -4
- package/dist/{project-B8rbutVC.mjs → project-DGeclhvO.mjs} +53 -17
- package/dist/{reload-nocmx8KB.mjs → reload-C-cV5yy_.mjs} +79 -20
- package/dist/{remote-cache-BRsRZru5.mjs → remote-cache--aehr4Pr.mjs} +3 -3
- package/dist/{server-bare-Gn3ssSdK.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
- package/dist/server-expo-BeYwP_Xy.mjs +2 -0
- package/dist/{server-expo-CeT1nIIk.mjs → server-expo-DUCg52-K.mjs} +2 -2
- package/dist/{settings-bE-Gu3Ti.mjs → settings-ssVBToDR.mjs} +10 -10
- package/dist/{settings-BlyDn2Oz.mjs → settings-xB0Frkg6.mjs} +11 -2
- package/dist/settings.schema.json +53 -1
- package/dist/{simslim-COtEEP7x.mjs → simslim-Bbg9UWrt.mjs} +12 -6
- package/dist/{slot-launch-DIACGEfH.mjs → slot-launch-BIWEW-wI.mjs} +3 -3
- package/dist/{start-BboE1HIS.mjs → start-BZhItkiL.mjs} +11 -10
- package/dist/{start-BFstVBlx.mjs → start-Rw2GD3nV.mjs} +1 -1
- package/dist/{state-DcDgzfu3.mjs → state-DwAU2_-D.mjs} +1 -1
- package/dist/state-l1pYwOre.mjs +148 -0
- package/dist/{stats-DiCtmluY.mjs → stats-CaSwmsSZ.mjs} +2 -2
- package/dist/{status-DI9Tjo7T.mjs → status-TJlL0HWo.mjs} +624 -103
- package/dist/{status-BUASYyy_.mjs → status-sszP4XKU.mjs} +59 -2
- package/dist/{stim-desktop-D0xhpZt8.mjs → stim-desktop-CXt8-Z3J.mjs} +13 -2
- package/dist/{stop-Bn6v-GvS.mjs → stop-BZ2qMiBP.mjs} +37 -12
- package/dist/{stop-BI7QY76M.mjs → stop-DskDKNTU.mjs} +2 -2
- package/dist/supervisor-run.mjs +10 -10
- package/dist/web-run.d.mts +22 -0
- package/dist/web-run.mjs +481 -0
- package/dist/web-sWXd5iek.mjs +394 -0
- package/dist/{worktree-B5sTRskL.mjs → worktree-3cBa-bQt.mjs} +1 -1
- package/dist/{worktree-ChDL5fO1.mjs → worktree-D4Do_XiQ.mjs} +226 -19
- package/dist/{xcode-BzplpQzc.mjs → xcode-LM6A8cNZ.mjs} +5 -5
- package/package.json +5 -5
- package/dist/server-expo-eWi9Coof.mjs +0 -2
- package/dist/workspace-process-lock-BvvJTfo_.mjs +0 -57
package/dist/web-run.mjs
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { A as ownership_claim_exports, O as process_identity_exports } from "./config-0jCcMM0E.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
|
|
3
|
+
import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
|
|
4
|
+
import { l as signalProcessTree } from "./metro-BPeY1YMh.mjs";
|
|
5
|
+
import { t as relaunchWithLogFile } from "./detached-entry-DJHoca49.mjs";
|
|
6
|
+
import { r as recordCreatedDevice } from "./created-devices-Cd558h31.mjs";
|
|
7
|
+
import { c as webDir, d as webProfileDir, g as removeSingletonFiles, h as liveProfileHolder, i as clearWebRecord, m as chromeProcessState, n as browserLogFile, o as updateWebRecord, p as writeWebRecord, s as webClaimRoot, u as webLogFile } from "./state-l1pYwOre.mjs";
|
|
8
|
+
import { t as connectOwnedBrowser } from "./cdp-Dq_ypAlJ.mjs";
|
|
9
|
+
import { n as PHONE_SCREEN, r as chromeArgs } from "./chrome-B5hWgQJS.mjs";
|
|
10
|
+
import { existsSync, mkdirSync, openSync, realpathSync } from "node:fs";
|
|
11
|
+
import { LOG_ROTATE_BYTES } from "@stim-cli/core";
|
|
12
|
+
import { WEB_VIEWPORTS } from "@stim-cli/core/state";
|
|
13
|
+
import { isAbsolute, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
//#region src/web/events.ts
|
|
16
|
+
const MAX_FRAMES = 20;
|
|
17
|
+
const CONSOLE_LEVELS = {
|
|
18
|
+
debug: "debug",
|
|
19
|
+
log: "info",
|
|
20
|
+
info: "info",
|
|
21
|
+
dir: "info",
|
|
22
|
+
table: "info",
|
|
23
|
+
trace: "info",
|
|
24
|
+
warning: "warn",
|
|
25
|
+
error: "error",
|
|
26
|
+
assert: "error"
|
|
27
|
+
};
|
|
28
|
+
const LOG_ENTRY_LEVELS = {
|
|
29
|
+
verbose: "debug",
|
|
30
|
+
info: "info",
|
|
31
|
+
warning: "warn",
|
|
32
|
+
error: "error"
|
|
33
|
+
};
|
|
34
|
+
function describeArg(arg) {
|
|
35
|
+
if (arg.type === "string") return String(arg.value);
|
|
36
|
+
if (arg.unserializableValue !== void 0) return arg.unserializableValue;
|
|
37
|
+
if (arg.type === "undefined") return "undefined";
|
|
38
|
+
if (arg.value !== void 0) return typeof arg.value === "object" ? JSON.stringify(arg.value) : String(arg.value);
|
|
39
|
+
if (arg.type === "object" && arg.subtype !== "error" && arg.preview?.properties) return describePreview(arg.preview, arg.subtype ?? arg.preview.subtype);
|
|
40
|
+
return arg.description ?? arg.type ?? "";
|
|
41
|
+
}
|
|
42
|
+
function describePreview(preview, subtype) {
|
|
43
|
+
const array = subtype === "array";
|
|
44
|
+
const entries = (preview.properties ?? []).map((property) => {
|
|
45
|
+
const value = property.type === "string" ? JSON.stringify(property.value ?? "") : property.value ?? "";
|
|
46
|
+
return array ? value : `${property.name}: ${value}`;
|
|
47
|
+
});
|
|
48
|
+
if (preview.overflow) entries.push("...");
|
|
49
|
+
return array ? `[${entries.join(", ")}]` : `{${entries.join(", ")}}`;
|
|
50
|
+
}
|
|
51
|
+
function frames(stackTrace) {
|
|
52
|
+
const callFrames = stackTrace?.callFrames;
|
|
53
|
+
if (!Array.isArray(callFrames)) return [];
|
|
54
|
+
return callFrames.slice(0, MAX_FRAMES).map((frame) => ({
|
|
55
|
+
file: frame.url || null,
|
|
56
|
+
line: typeof frame.lineNumber === "number" ? frame.lineNumber + 1 : null,
|
|
57
|
+
column: typeof frame.columnNumber === "number" ? frame.columnNumber + 1 : null,
|
|
58
|
+
fn: frame.functionName || null
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
function withStack(record, stack) {
|
|
62
|
+
return stack.length ? {
|
|
63
|
+
...record,
|
|
64
|
+
stack
|
|
65
|
+
} : record;
|
|
66
|
+
}
|
|
67
|
+
/** `Runtime.consoleAPICalled`: a console call in the page, as a `client` record. */
|
|
68
|
+
function consoleRecord(params) {
|
|
69
|
+
const type = String(params.type ?? "log");
|
|
70
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
71
|
+
return withStack({
|
|
72
|
+
src: "client",
|
|
73
|
+
platform: "web",
|
|
74
|
+
level: CONSOLE_LEVELS[type] ?? "info",
|
|
75
|
+
msg: args.map(describeArg).join(" ")
|
|
76
|
+
}, type === "error" || type === "assert" || type === "trace" ? frames(params.stackTrace) : []);
|
|
77
|
+
}
|
|
78
|
+
/** `Runtime.exceptionThrown`: an uncaught error or rejected promise in the page, as a `client` error. */
|
|
79
|
+
function exceptionRecord(params) {
|
|
80
|
+
const details = params.exceptionDetails ?? {};
|
|
81
|
+
const description = (details.exception?.description ?? describeArg(details.exception ?? {})).split("\n")[0];
|
|
82
|
+
const prefix = details.text ?? "Uncaught";
|
|
83
|
+
const text = description ? `${prefix} ${description}` : prefix;
|
|
84
|
+
const stack = frames(details.stackTrace);
|
|
85
|
+
return withStack({
|
|
86
|
+
src: "client",
|
|
87
|
+
platform: "web",
|
|
88
|
+
level: "error",
|
|
89
|
+
event: "web_exception",
|
|
90
|
+
msg: text
|
|
91
|
+
}, stack.length ? stack : details.url ? [{
|
|
92
|
+
file: details.url,
|
|
93
|
+
line: typeof details.lineNumber === "number" ? details.lineNumber + 1 : null,
|
|
94
|
+
column: typeof details.columnNumber === "number" ? details.columnNumber + 1 : null,
|
|
95
|
+
fn: null
|
|
96
|
+
}] : []);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `Log.entryAdded`: a message the browser itself logged, such as a CSP or mixed-content violation. Null for its
|
|
100
|
+
* `network` entries, which repeat the failed requests the Network domain already reports.
|
|
101
|
+
*/
|
|
102
|
+
function logEntryRecord(params) {
|
|
103
|
+
const entry = params.entry ?? {};
|
|
104
|
+
if (entry.source === "network") return null;
|
|
105
|
+
const where = entry.url ? ` (${entry.url})` : "";
|
|
106
|
+
return {
|
|
107
|
+
src: "device",
|
|
108
|
+
platform: "web",
|
|
109
|
+
level: LOG_ENTRY_LEVELS[entry.level ?? ""] ?? "info",
|
|
110
|
+
event: "web_browser_log",
|
|
111
|
+
msg: `${entry.source ? `${entry.source}: ` : ""}${entry.text ?? ""}${where}`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** A request the page made that failed to load or answered with an HTTP error. */
|
|
115
|
+
function networkFailureRecord({ url, method, errorText, status, canceled, document }) {
|
|
116
|
+
const failure = status === void 0 ? errorText ?? "failed" : `HTTP ${status}`;
|
|
117
|
+
return {
|
|
118
|
+
src: "device",
|
|
119
|
+
platform: "web",
|
|
120
|
+
level: canceled ? "debug" : document || status === void 0 || status >= 500 ? "error" : "warn",
|
|
121
|
+
event: canceled ? "web_request_canceled" : document ? "web_document_failed" : "web_request_failed",
|
|
122
|
+
...status === void 0 ? {} : { status },
|
|
123
|
+
msg: `${method} ${url} ${canceled ? "canceled" : `failed: ${failure}`}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/web/run.ts
|
|
128
|
+
const USAGE = "Usage: web-run --root <path> --chrome <path> --url <url> --port <n> [--launch-id <id>] [--headed] [--viewport desktop|phone] [--ignore-certificate-errors]";
|
|
129
|
+
function parseArgs(argv) {
|
|
130
|
+
const values = {};
|
|
131
|
+
let headless = true;
|
|
132
|
+
let ignoreCertificateErrors = false;
|
|
133
|
+
for (let i = 0; i < argv.length; i++) {
|
|
134
|
+
const arg = argv[i];
|
|
135
|
+
if (arg === "--headed") headless = false;
|
|
136
|
+
else if (arg === "--ignore-certificate-errors") ignoreCertificateErrors = true;
|
|
137
|
+
else if ([
|
|
138
|
+
"--root",
|
|
139
|
+
"--chrome",
|
|
140
|
+
"--url",
|
|
141
|
+
"--port",
|
|
142
|
+
"--viewport",
|
|
143
|
+
"--launch-id"
|
|
144
|
+
].includes(arg)) {
|
|
145
|
+
const value = argv[++i];
|
|
146
|
+
if (value === void 0) return { error: `${arg} needs a value. ${USAGE}` };
|
|
147
|
+
values[arg.slice(2)] = value;
|
|
148
|
+
} else return { error: `Unknown browser supervisor argument "${arg}". ${USAGE}` };
|
|
149
|
+
}
|
|
150
|
+
const { root, chrome, url } = values;
|
|
151
|
+
if (!root || !isAbsolute(root)) return { error: `--root must be an absolute path. ${USAGE}` };
|
|
152
|
+
if (!chrome || !isAbsolute(chrome)) return { error: `--chrome must be an absolute path. ${USAGE}` };
|
|
153
|
+
if (!url || !/^https?:\/\//.test(url)) return { error: `--url must be an http or https URL. ${USAGE}` };
|
|
154
|
+
const port = Number(values.port);
|
|
155
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return { error: `--port must be a TCP port. ${USAGE}` };
|
|
156
|
+
const viewport = values.viewport ?? "desktop";
|
|
157
|
+
if (!WEB_VIEWPORTS.includes(viewport)) return { error: `--viewport must be desktop or phone. ${USAGE}` };
|
|
158
|
+
return {
|
|
159
|
+
root: resolve(root),
|
|
160
|
+
launchId: values["launch-id"] ?? null,
|
|
161
|
+
chrome,
|
|
162
|
+
url,
|
|
163
|
+
port,
|
|
164
|
+
headless,
|
|
165
|
+
viewport,
|
|
166
|
+
ignoreCertificateErrors
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const DEVTOOLS_WAIT_MS = 2e4;
|
|
170
|
+
const CHROME_EXIT_WAIT_MS = 5e3;
|
|
171
|
+
const POLL_MS = 50;
|
|
172
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
173
|
+
function describe(error) {
|
|
174
|
+
return error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
176
|
+
function isWebBundle(url) {
|
|
177
|
+
return /\.bundle(?:\?|$)/.test(url) && /[?&]platform=web(?:&|$)/.test(url);
|
|
178
|
+
}
|
|
179
|
+
async function runWebSupervisor(options, { onExit = (code) => process.exit(code), stderr = (line) => console.error(line) } = {}) {
|
|
180
|
+
const root = realpathSync(options.root);
|
|
181
|
+
mkdirSync(webDir(root), { recursive: true });
|
|
182
|
+
const writer = createNdjsonWriter(webLogFile(root), { maxBytes: LOG_ROTATE_BYTES });
|
|
183
|
+
const log = (level, event, msg, extra = {}) => writer.write({
|
|
184
|
+
src: "device",
|
|
185
|
+
platform: "web",
|
|
186
|
+
level,
|
|
187
|
+
event,
|
|
188
|
+
msg,
|
|
189
|
+
...extra
|
|
190
|
+
});
|
|
191
|
+
const processToken = (0, process_identity_exports.captureProcessToken)(process.pid);
|
|
192
|
+
if (!processToken) {
|
|
193
|
+
stderr("Stim browser supervisor: could not capture process identity; refusing to start an unmanaged browser.");
|
|
194
|
+
writer.close();
|
|
195
|
+
onExit(1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const owner = {
|
|
199
|
+
pid: process.pid,
|
|
200
|
+
processToken
|
|
201
|
+
};
|
|
202
|
+
let claim;
|
|
203
|
+
try {
|
|
204
|
+
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
205
|
+
root: webClaimRoot(root),
|
|
206
|
+
mode: "exclusive",
|
|
207
|
+
label: "browser supervisor",
|
|
208
|
+
details: { purpose: "browser supervisor" }
|
|
209
|
+
});
|
|
210
|
+
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
211
|
+
claim = attempt.acquired;
|
|
212
|
+
if (!claim) throw new Error(`another browser supervisor holds ${attempt.held?.path ?? webClaimRoot(root)}`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
stderr(`Stim browser supervisor: ${describe(error)}`);
|
|
215
|
+
writer.close();
|
|
216
|
+
onExit(1);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const profile = webProfileDir(root);
|
|
220
|
+
let chrome = null;
|
|
221
|
+
let chromeRecord = null;
|
|
222
|
+
let cdp = null;
|
|
223
|
+
let finished = false;
|
|
224
|
+
let stopping = false;
|
|
225
|
+
const chromeGone = () => !chromeRecord || chromeProcessState(chromeRecord) === "gone";
|
|
226
|
+
const finish = (code, level, event, msg) => {
|
|
227
|
+
if (finished) return;
|
|
228
|
+
finished = true;
|
|
229
|
+
cdp?.close();
|
|
230
|
+
const gone = chromeGone();
|
|
231
|
+
if (gone) {
|
|
232
|
+
try {
|
|
233
|
+
removeSingletonFiles(profile);
|
|
234
|
+
} catch {}
|
|
235
|
+
if (claim) (0, ownership_claim_exports.clearClaimChild)(claim);
|
|
236
|
+
}
|
|
237
|
+
if (!gone) {
|
|
238
|
+
log("error", event, `${msg}; Chrome pid ${chromeRecord?.pid} is still running, so its record and claim are kept for stim stop`);
|
|
239
|
+
writer.close();
|
|
240
|
+
onExit(1);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
log(level, event, msg);
|
|
244
|
+
try {
|
|
245
|
+
clearWebRecord(root, owner);
|
|
246
|
+
} catch {}
|
|
247
|
+
(0, ownership_claim_exports.releaseClaim)(claim);
|
|
248
|
+
writer.close();
|
|
249
|
+
onExit(code);
|
|
250
|
+
};
|
|
251
|
+
const stopChrome = async () => {
|
|
252
|
+
stopping = true;
|
|
253
|
+
try {
|
|
254
|
+
await Promise.race([cdp?.send("Browser.close"), sleep(3e3)]);
|
|
255
|
+
} catch {}
|
|
256
|
+
const deadline = Date.now() + CHROME_EXIT_WAIT_MS;
|
|
257
|
+
while (!chromeGone() && Date.now() < deadline) await sleep(POLL_MS);
|
|
258
|
+
const state = chromeRecord ? chromeProcessState(chromeRecord) : "gone";
|
|
259
|
+
if (chromeRecord && (state === "running" || state === "lingering")) {
|
|
260
|
+
try {
|
|
261
|
+
signalProcessTree(chromeRecord.pid, "SIGKILL", { group: true });
|
|
262
|
+
} catch {}
|
|
263
|
+
const killDeadline = Date.now() + 2e3;
|
|
264
|
+
while (!chromeGone() && Date.now() < killDeadline) await sleep(POLL_MS);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => {
|
|
268
|
+
stopChrome().then(() => finish(0, "info", "web_browser_stopped", `browser supervisor received ${signal}; closed Chrome`));
|
|
269
|
+
});
|
|
270
|
+
try {
|
|
271
|
+
mkdirSync(profile, { recursive: true });
|
|
272
|
+
recordCreatedDevice("web", profile);
|
|
273
|
+
const holder = liveProfileHolder(profile);
|
|
274
|
+
if (holder !== null) throw new Error(`Chrome pid ${holder} still holds the profile ${profile}`);
|
|
275
|
+
removeSingletonFiles(profile);
|
|
276
|
+
const launch = {
|
|
277
|
+
chrome: options.chrome,
|
|
278
|
+
headless: options.headless,
|
|
279
|
+
viewport: options.viewport,
|
|
280
|
+
ignoreCertificateErrors: options.ignoreCertificateErrors
|
|
281
|
+
};
|
|
282
|
+
writeWebRecord(root, {
|
|
283
|
+
pid: process.pid,
|
|
284
|
+
processToken,
|
|
285
|
+
...launch,
|
|
286
|
+
cdpPort: options.port,
|
|
287
|
+
profile,
|
|
288
|
+
url: options.url,
|
|
289
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
...options.launchId ? { launchId: options.launchId } : {}
|
|
291
|
+
});
|
|
292
|
+
const fd = openSync(browserLogFile(root), "a");
|
|
293
|
+
(0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
294
|
+
chrome = getExecutor().spawn(options.chrome, chromeArgs({
|
|
295
|
+
profile,
|
|
296
|
+
port: options.port,
|
|
297
|
+
...launch
|
|
298
|
+
}), {
|
|
299
|
+
detached: true,
|
|
300
|
+
stdio: [
|
|
301
|
+
"ignore",
|
|
302
|
+
fd,
|
|
303
|
+
fd
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
const pid = chrome.pid;
|
|
307
|
+
const captured = pid === void 0 ? null : (0, process_identity_exports.captureProcessIdentity)(pid);
|
|
308
|
+
if (!pid || !captured?.ok) {
|
|
309
|
+
chrome.kill("SIGKILL");
|
|
310
|
+
throw new Error(`could not identify the Chrome process${captured && !captured.ok ? `: ${captured.reason}` : ""}`);
|
|
311
|
+
}
|
|
312
|
+
chromeRecord = {
|
|
313
|
+
pid,
|
|
314
|
+
processToken: captured.token
|
|
315
|
+
};
|
|
316
|
+
(0, ownership_claim_exports.setClaimChild)(claim, chromeRecord);
|
|
317
|
+
updateWebRecord(root, owner, { chromeProcess: chromeRecord });
|
|
318
|
+
chrome.on("exit", (code, signal) => {
|
|
319
|
+
if (stopping) return;
|
|
320
|
+
(async () => {
|
|
321
|
+
const deadline = Date.now() + 2e3;
|
|
322
|
+
while (!chromeGone() && Date.now() < deadline) await sleep(POLL_MS);
|
|
323
|
+
finish(1, "error", "web_browser_exited", `Chrome exited (${signal ? `signal ${signal}` : `exit code ${code}`}); see ${browserLogFile(root)}`);
|
|
324
|
+
})();
|
|
325
|
+
});
|
|
326
|
+
let connection = null;
|
|
327
|
+
let lastError = null;
|
|
328
|
+
const deadline = Date.now() + DEVTOOLS_WAIT_MS;
|
|
329
|
+
const isFinished = () => finished;
|
|
330
|
+
while (!connection && Date.now() < deadline && !isFinished()) try {
|
|
331
|
+
connection = await connectOwnedBrowser(options.port, pid);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
lastError = error;
|
|
334
|
+
await sleep(100);
|
|
335
|
+
}
|
|
336
|
+
if (!connection) throw new Error(`Chrome did not open DevTools on port ${options.port} within ${DEVTOOLS_WAIT_MS / 1e3}s: ${describe(lastError)}`);
|
|
337
|
+
cdp = connection;
|
|
338
|
+
const version = String((await connection.send("Browser.getVersion")).product ?? "");
|
|
339
|
+
await connection.send("Target.setDiscoverTargets", { discover: true });
|
|
340
|
+
const { targetInfos } = await connection.send("Target.getTargets");
|
|
341
|
+
const page = targetInfos?.find((target) => target.type === "page");
|
|
342
|
+
if (!page) throw new Error("Chrome opened no page");
|
|
343
|
+
const targetId = page.targetId;
|
|
344
|
+
const { sessionId } = await connection.send("Target.attachToTarget", {
|
|
345
|
+
targetId,
|
|
346
|
+
flatten: true
|
|
347
|
+
});
|
|
348
|
+
const requests = /* @__PURE__ */ new Map();
|
|
349
|
+
connection.onEvent((event) => {
|
|
350
|
+
if (event.method === "Target.targetDestroyed" && event.params.targetId === targetId && !stopping) {
|
|
351
|
+
stopChrome().then(() => finish(0, "info", "web_page_closed", "the owned page was closed; closed Chrome"));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (event.method === "Target.targetCrashed" && event.params.targetId === targetId) {
|
|
355
|
+
log("error", "web_page_crashed", `the page renderer crashed (${String(event.params.status ?? "unknown")})`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (event.sessionId !== sessionId) return;
|
|
359
|
+
const params = event.params;
|
|
360
|
+
switch (event.method) {
|
|
361
|
+
case "Runtime.consoleAPICalled":
|
|
362
|
+
writer.write(consoleRecord(params));
|
|
363
|
+
return;
|
|
364
|
+
case "Runtime.exceptionThrown":
|
|
365
|
+
writer.write(exceptionRecord(params));
|
|
366
|
+
return;
|
|
367
|
+
case "Log.entryAdded": {
|
|
368
|
+
const record = logEntryRecord(params);
|
|
369
|
+
if (record) writer.write(record);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
case "Page.loadEventFired":
|
|
373
|
+
log("info", "web_page_loaded", "the page fired its load event");
|
|
374
|
+
return;
|
|
375
|
+
case "Network.requestWillBeSent": {
|
|
376
|
+
const request = params.request;
|
|
377
|
+
const document = params.type === "Document" && params.frameId === targetId;
|
|
378
|
+
requests.set(String(params.requestId), {
|
|
379
|
+
url: request.url,
|
|
380
|
+
method: request.method,
|
|
381
|
+
document
|
|
382
|
+
});
|
|
383
|
+
if (document) log("info", "web_navigation", `navigating to ${request.url}`, {
|
|
384
|
+
url: request.url,
|
|
385
|
+
marker: true
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
case "Network.responseReceived": {
|
|
390
|
+
const facts = requests.get(String(params.requestId));
|
|
391
|
+
const response = params.response;
|
|
392
|
+
if (!facts) return;
|
|
393
|
+
if (facts.document) log("info", "web_document_response", `${response.status} ${response.url}`, { status: response.status });
|
|
394
|
+
if (isWebBundle(response.url)) log(response.status < 400 ? "info" : "error", "web_bundle_response", `${response.status} ${response.url}`, { status: response.status });
|
|
395
|
+
if (response.status >= 400) writer.write(networkFailureRecord({
|
|
396
|
+
...facts,
|
|
397
|
+
url: response.url,
|
|
398
|
+
status: response.status
|
|
399
|
+
}));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
case "Network.loadingFinished":
|
|
403
|
+
requests.delete(String(params.requestId));
|
|
404
|
+
return;
|
|
405
|
+
case "Network.loadingFailed": {
|
|
406
|
+
const facts = requests.get(String(params.requestId));
|
|
407
|
+
requests.delete(String(params.requestId));
|
|
408
|
+
if (!facts) return;
|
|
409
|
+
writer.write(networkFailureRecord({
|
|
410
|
+
...facts,
|
|
411
|
+
errorText: String(params.errorText ?? "failed"),
|
|
412
|
+
canceled: params.canceled === true
|
|
413
|
+
}));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
connection.onClose(() => {
|
|
419
|
+
if (!stopping && chromeGone()) finish(1, "error", "web_browser_exited", "Chrome closed its DevTools connection");
|
|
420
|
+
});
|
|
421
|
+
for (const domain of [
|
|
422
|
+
"Runtime",
|
|
423
|
+
"Log",
|
|
424
|
+
"Network",
|
|
425
|
+
"Page"
|
|
426
|
+
]) await connection.send(`${domain}.enable`, {}, sessionId);
|
|
427
|
+
if (options.viewport === "phone") {
|
|
428
|
+
await connection.send("Emulation.setDeviceMetricsOverride", {
|
|
429
|
+
...PHONE_SCREEN,
|
|
430
|
+
mobile: true
|
|
431
|
+
}, sessionId);
|
|
432
|
+
await connection.send("Emulation.setTouchEmulationEnabled", {
|
|
433
|
+
enabled: true,
|
|
434
|
+
maxTouchPoints: 5
|
|
435
|
+
}, sessionId);
|
|
436
|
+
}
|
|
437
|
+
updateWebRecord(root, owner, {
|
|
438
|
+
targetId,
|
|
439
|
+
version
|
|
440
|
+
});
|
|
441
|
+
log("info", "web_browser_started", `browser supervisor pid ${process.pid} runs ${version} (pid ${pid}) on ${profile}`);
|
|
442
|
+
connection.send("Page.navigate", { url: options.url }, sessionId).catch((error) => {
|
|
443
|
+
log("debug", "web_navigation_slow", `Page.navigate did not answer: ${describe(error)}; the page may still load`);
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
stderr(`Stim browser supervisor: ${describe(error)}`);
|
|
447
|
+
await stopChrome();
|
|
448
|
+
finish(1, "error", "web_browser_failed", `could not start the owned Chrome: ${describe(error)}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function main(argv = process.argv.slice(2)) {
|
|
452
|
+
if (relaunchWithLogFile(argv)) return;
|
|
453
|
+
const parsed = parseArgs(argv);
|
|
454
|
+
if ("error" in parsed) {
|
|
455
|
+
console.error(`Stim browser supervisor: ${parsed.error}`);
|
|
456
|
+
process.exit(2);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (!existsSync(parsed.root)) {
|
|
460
|
+
console.error(`Stim browser supervisor: --root ${parsed.root} does not exist.`);
|
|
461
|
+
process.exit(2);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
process.title = "stim-web";
|
|
465
|
+
await runWebSupervisor(parsed);
|
|
466
|
+
}
|
|
467
|
+
function invokedDirectly() {
|
|
468
|
+
const entry = process.argv[1];
|
|
469
|
+
if (!entry) return false;
|
|
470
|
+
try {
|
|
471
|
+
return resolve(entry) === fileURLToPath(import.meta.url);
|
|
472
|
+
} catch {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (invokedDirectly()) main().catch((error) => {
|
|
477
|
+
console.error(`Stim browser supervisor: ${describe(error)}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
});
|
|
480
|
+
//#endregion
|
|
481
|
+
export { main, parseArgs, runWebSupervisor };
|