flashwright 1.64.1-flashwright.1 → 1.64.1-flashwright.3
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/cli.js +11 -13
- package/lib/cli/testActions.js +2 -1
- package/lib/codeCache.js +31 -10
- package/lib/codeCache.js.txt +2 -2
- package/lib/common/index.js +12 -0
- package/lib/common/index.js.txt +2 -2
- package/lib/index.js +3 -0
- package/lib/matchers/expect.js +3 -2
- package/lib/matchers/expect.js.txt +2 -2
- package/lib/runner/index.js +47 -32
- package/lib/runner/index.js.txt +5 -4
- package/lib/runner/session.js +748 -0
- package/lib/runner/session.js.txt +8 -0
- package/lib/runner/workerStandby.js +11 -2
- package/lib/runner/workerStandby.js.txt +4 -3
- package/lib/speculativeBrowser.js +110 -3
- package/lib/worker/workerProcessEntry.js +135 -13
- package/lib/worker/workerProcessEntry.js.txt +6 -5
- package/package.json +3 -2
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// packages/playwright/src/runner/session.ts
|
|
31
|
+
var session_exports = {};
|
|
32
|
+
__export(session_exports, {
|
|
33
|
+
flushSessionBrowsers: () => flushSessionBrowsers,
|
|
34
|
+
isWarmSessionRun: () => isWarmSessionRun,
|
|
35
|
+
receiveWorkerMessage: () => receiveWorkerMessage,
|
|
36
|
+
requestSessionBrowser: () => requestSessionBrowser,
|
|
37
|
+
runCli: () => runCli,
|
|
38
|
+
sessionStandbyEnv: () => sessionStandbyEnv,
|
|
39
|
+
trackDetachedTeardown: () => trackDetachedTeardown,
|
|
40
|
+
trackSessionWorker: () => trackSessionWorker
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(session_exports);
|
|
43
|
+
var import_fs = __toESM(require("fs"));
|
|
44
|
+
var import_net = __toESM(require("net"));
|
|
45
|
+
var import_os = __toESM(require("os"));
|
|
46
|
+
var import_path = __toESM(require("path"));
|
|
47
|
+
var import_url = __toESM(require("url"));
|
|
48
|
+
var kIdleTimeout = (Number(process.env.FLASHWRIGHT_SESSION_TIMEOUT) || 5 * 60) * 1e3;
|
|
49
|
+
var kVolatileEnv = /* @__PURE__ */ new Set(["_", "PWD", "OLDPWD", "SHLVL", "COLUMNS", "LINES"]);
|
|
50
|
+
var kInteractiveArgs = ["--ui", "--ui-host", "--ui-port", "--debug", "--list", "-h", "--help"];
|
|
51
|
+
var kBrowserMessage = "__flashwright_browser__";
|
|
52
|
+
var kBrowserRequest = "__flashwright_browser_request__";
|
|
53
|
+
var kReturnBrowser = "__flashwright_return_browser__";
|
|
54
|
+
var kBrowserReturned = "__flashwright_browser_returned__";
|
|
55
|
+
var kMaxBrowsers = 8;
|
|
56
|
+
function runCli(argv, hooks) {
|
|
57
|
+
if (argv[2] === "cli") {
|
|
58
|
+
hooks.installCodeCache("cli");
|
|
59
|
+
hooks.runCliClient();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (process.env.FLASHWRIGHT_SESSION_ROLE === "warm") {
|
|
63
|
+
runWarmProcess(hooks).catch((error) => {
|
|
64
|
+
process.stderr.write(`Flashwright session: ${error.stack || error}
|
|
65
|
+
`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (argv[2] === "test" && sessionRequested() && !argv.some((arg) => kInteractiveArgs.includes(arg.split("=")[0]))) {
|
|
71
|
+
const request = createRequest(argv);
|
|
72
|
+
runInSession(request).then((outcome) => runLocally(argv, hooks, outcome === "start" ? request : void 0), () => runLocally(argv, hooks));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
runLocally(argv, hooks);
|
|
76
|
+
}
|
|
77
|
+
function runLocally(argv, hooks, startSessionWith) {
|
|
78
|
+
hooks.installCodeCache(argv[2] === "test" ? "runner" : "cli");
|
|
79
|
+
if (startSessionWith)
|
|
80
|
+
startSession(startSessionWith);
|
|
81
|
+
if (argv[2] === "test")
|
|
82
|
+
hooks.prespawnWorkers(argv);
|
|
83
|
+
hooks.loadProgram().parse(argv);
|
|
84
|
+
}
|
|
85
|
+
function sessionRequested() {
|
|
86
|
+
if (process.env.PWDEBUG || process.env.PWTEST_WATCH || debuggerAttached() || process.platform === "win32")
|
|
87
|
+
return false;
|
|
88
|
+
const value = process.env.FLASHWRIGHT_SESSION;
|
|
89
|
+
if (value)
|
|
90
|
+
return value !== "0" && value !== "false";
|
|
91
|
+
return !process.env.CI;
|
|
92
|
+
}
|
|
93
|
+
function debuggerAttached() {
|
|
94
|
+
if (!process.execArgv.length && !process.env.NODE_OPTIONS)
|
|
95
|
+
return false;
|
|
96
|
+
return !!require("inspector").url();
|
|
97
|
+
}
|
|
98
|
+
function childProcess() {
|
|
99
|
+
return require("child_process");
|
|
100
|
+
}
|
|
101
|
+
function buildStamp() {
|
|
102
|
+
const files = ["program.js", "common/index.js", "runner/index.js", "runner/session.js", "worker/workerProcessEntry.js"].map((file) => import_path.default.join(__dirname, "..", file));
|
|
103
|
+
const coreBundle = import_path.default.join(__dirname, "..", "..", "..", "playwright-core", "lib", "coreBundle.js");
|
|
104
|
+
try {
|
|
105
|
+
files.push(import_fs.default.existsSync(coreBundle) ? coreBundle : require.resolve("playwright-core/lib/coreBundle"));
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
return files.map((file) => {
|
|
109
|
+
try {
|
|
110
|
+
const stat = import_fs.default.statSync(file);
|
|
111
|
+
return `${file}:${stat.size}:${stat.mtimeMs}`;
|
|
112
|
+
} catch {
|
|
113
|
+
return file;
|
|
114
|
+
}
|
|
115
|
+
}).join("|");
|
|
116
|
+
}
|
|
117
|
+
function terminalProfile() {
|
|
118
|
+
return {
|
|
119
|
+
stdout: process.stdout.isTTY ? `${process.stdout.columns || 0}x${process.stdout.rows || 0}` : "false",
|
|
120
|
+
stdinTTY: !!process.stdin.isTTY
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function stableEnv(env) {
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const [key, value] of Object.entries(env)) {
|
|
126
|
+
if (value !== void 0 && !kVolatileEnv.has(key) && !key.startsWith("FLASHWRIGHT_SESSION_"))
|
|
127
|
+
result[key] = value;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function createRequest(argv) {
|
|
132
|
+
return {
|
|
133
|
+
type: "run",
|
|
134
|
+
argv,
|
|
135
|
+
cwd: process.cwd(),
|
|
136
|
+
env: stableEnv(process.env),
|
|
137
|
+
execPath: process.execPath,
|
|
138
|
+
execArgv: process.execArgv,
|
|
139
|
+
cliPath: argv[1],
|
|
140
|
+
build: buildStamp(),
|
|
141
|
+
profile: terminalProfile()
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function warmKey(request) {
|
|
145
|
+
const env = Object.entries(stableEnv(request.env)).sort(([a], [b]) => a < b ? -1 : 1);
|
|
146
|
+
return require("crypto").createHash("sha1").update(JSON.stringify([env, request.execPath, request.execArgv, request.cliPath, request.build, request.profile])).digest("hex");
|
|
147
|
+
}
|
|
148
|
+
function sessionSocketPath(cwd) {
|
|
149
|
+
const uid = import_os.default.userInfo().uid;
|
|
150
|
+
const directory = import_path.default.join(import_os.default.tmpdir(), `fw-${uid}`);
|
|
151
|
+
try {
|
|
152
|
+
import_fs.default.mkdirSync(directory, { mode: 448 });
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const stat = import_fs.default.statSync(directory);
|
|
157
|
+
if (!stat.isDirectory() || stat.uid !== uid || stat.mode & 63)
|
|
158
|
+
return;
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const id = fnv1a64([import_path.default.join(__dirname, ".."), process.execPath, cwd].join("\0"));
|
|
163
|
+
const socketFile = import_path.default.join(directory, `${id}.sock`);
|
|
164
|
+
return Buffer.byteLength(socketFile) < 100 ? socketFile : void 0;
|
|
165
|
+
}
|
|
166
|
+
function fnv1a64(text) {
|
|
167
|
+
let hash = 0xcbf29ce484222325n;
|
|
168
|
+
for (const byte of Buffer.from(text))
|
|
169
|
+
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
|
|
170
|
+
return hash.toString(16).padStart(16, "0");
|
|
171
|
+
}
|
|
172
|
+
function runInSession(request) {
|
|
173
|
+
const socketFile = sessionSocketPath(request.cwd);
|
|
174
|
+
if (!socketFile)
|
|
175
|
+
return Promise.resolve("local");
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const socket = import_net.default.connect(socketFile);
|
|
178
|
+
let started = false;
|
|
179
|
+
let exited = false;
|
|
180
|
+
let buffer = "";
|
|
181
|
+
const lost = () => {
|
|
182
|
+
process.stderr.write("Flashwright session: the connection to the test run was lost.\n");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
};
|
|
185
|
+
socket.on("error", () => {
|
|
186
|
+
if (started)
|
|
187
|
+
lost();
|
|
188
|
+
else
|
|
189
|
+
resolve("start");
|
|
190
|
+
});
|
|
191
|
+
socket.on("close", () => {
|
|
192
|
+
if (!started)
|
|
193
|
+
resolve("local");
|
|
194
|
+
else if (!exited)
|
|
195
|
+
lost();
|
|
196
|
+
});
|
|
197
|
+
socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
|
|
198
|
+
socket.on("data", (chunk) => {
|
|
199
|
+
buffer += chunk.toString();
|
|
200
|
+
let newline;
|
|
201
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
202
|
+
const message = JSON.parse(buffer.slice(0, newline));
|
|
203
|
+
buffer = buffer.slice(newline + 1);
|
|
204
|
+
if (message.type === "cold") {
|
|
205
|
+
socket.destroy();
|
|
206
|
+
resolve(message.stale ? "start" : "local");
|
|
207
|
+
} else if (message.type === "started") {
|
|
208
|
+
started = true;
|
|
209
|
+
let interrupted = false;
|
|
210
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
211
|
+
process.on(signal, () => {
|
|
212
|
+
if (interrupted)
|
|
213
|
+
process.exit(130);
|
|
214
|
+
interrupted = true;
|
|
215
|
+
socket.write(JSON.stringify({ type: "signal", signal }) + "\n");
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} else if (message.type === "stdout" || message.type === "stderr") {
|
|
219
|
+
(message.type === "stdout" ? process.stdout : process.stderr).write(Buffer.from(message.data, "base64"));
|
|
220
|
+
} else if (message.type === "exit") {
|
|
221
|
+
exited = true;
|
|
222
|
+
socket.destroy();
|
|
223
|
+
const code = message.code ?? (message.signal === "SIGINT" ? 130 : 1);
|
|
224
|
+
process.stdout.write("", () => process.stderr.write("", () => process.exit(code)));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function startSession(request) {
|
|
231
|
+
const socketFile = sessionSocketPath(request.cwd);
|
|
232
|
+
if (!socketFile)
|
|
233
|
+
return;
|
|
234
|
+
const child = childProcess().spawn(process.execPath, [__filename, "--flashwright-session", socketFile, request.build], {
|
|
235
|
+
detached: true,
|
|
236
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
237
|
+
env: { ...process.env, FLASHWRIGHT_SESSION_ROLE: "session" }
|
|
238
|
+
});
|
|
239
|
+
child.on("error", () => {
|
|
240
|
+
});
|
|
241
|
+
child.unref();
|
|
242
|
+
child.channel?.unref?.();
|
|
243
|
+
child.send({ method: "request", params: request });
|
|
244
|
+
joinSession(child, false);
|
|
245
|
+
}
|
|
246
|
+
var sessionChannel;
|
|
247
|
+
var offersBrowsers = false;
|
|
248
|
+
var lastRequestId = 0;
|
|
249
|
+
var standbyRequests = /* @__PURE__ */ new Map();
|
|
250
|
+
var workerParts = /* @__PURE__ */ new Map();
|
|
251
|
+
var pendingSends = /* @__PURE__ */ new Set();
|
|
252
|
+
var sessionWorkers = /* @__PURE__ */ new Set();
|
|
253
|
+
var sessionStandbys = /* @__PURE__ */ new Set();
|
|
254
|
+
function joinSession(channel, offers) {
|
|
255
|
+
sessionChannel = channel;
|
|
256
|
+
offersBrowsers = offers;
|
|
257
|
+
process.env.FLASHWRIGHT_SESSION_HANDOFF = "1";
|
|
258
|
+
}
|
|
259
|
+
function sessionStandbyEnv() {
|
|
260
|
+
return offersBrowsers && sessionChannel?.connected ? { FLASHWRIGHT_SESSION_OFFER: "1" } : {};
|
|
261
|
+
}
|
|
262
|
+
function requestSessionBrowser(child) {
|
|
263
|
+
if (!sessionChannel)
|
|
264
|
+
return;
|
|
265
|
+
sessionStandbys.add(child);
|
|
266
|
+
child.once("exit", () => sessionStandbys.delete(child));
|
|
267
|
+
if (!offersBrowsers || !sessionChannel.connected)
|
|
268
|
+
return;
|
|
269
|
+
const id = String(++lastRequestId);
|
|
270
|
+
standbyRequests.set(id, child);
|
|
271
|
+
child.once("exit", () => standbyRequests.delete(id));
|
|
272
|
+
sessionChannel.send({ method: kBrowserRequest, params: { id } });
|
|
273
|
+
}
|
|
274
|
+
function receiveWorkerMessage(child, message, handle) {
|
|
275
|
+
if (message?.method !== kBrowserMessage)
|
|
276
|
+
return false;
|
|
277
|
+
const browser = collectBrowserPart(workerParts, message, handle);
|
|
278
|
+
if (browser)
|
|
279
|
+
forwardBrowser(browser);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
function trackSessionWorker(child) {
|
|
283
|
+
if (!sessionChannel)
|
|
284
|
+
return;
|
|
285
|
+
sessionStandbys.delete(child);
|
|
286
|
+
sessionWorkers.add(child);
|
|
287
|
+
child.once("disconnect", () => sessionWorkers.delete(child));
|
|
288
|
+
}
|
|
289
|
+
function forwardBrowser(browser) {
|
|
290
|
+
if (!sessionChannel?.connected || browser.read.readableLength) {
|
|
291
|
+
killBrowser(browser);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
pendingSends.add(sendBrowser(sessionChannel, browser));
|
|
295
|
+
}
|
|
296
|
+
function isWarmSessionRun() {
|
|
297
|
+
return !!outputBytes;
|
|
298
|
+
}
|
|
299
|
+
var detachedTeardowns = /* @__PURE__ */ new Set();
|
|
300
|
+
function trackDetachedTeardown(exited) {
|
|
301
|
+
detachedTeardowns.add(exited);
|
|
302
|
+
void exited.finally(() => detachedTeardowns.delete(exited));
|
|
303
|
+
}
|
|
304
|
+
async function flushSessionBrowsers(exitCode) {
|
|
305
|
+
if (!sessionChannel)
|
|
306
|
+
return;
|
|
307
|
+
if (outputBytes && exitCode !== void 0 && sessionChannel.connected)
|
|
308
|
+
await new Promise((resolve) => sessionChannel.send({ type: "exiting", code: exitCode, ...outputBytes }, void 0, resolve));
|
|
309
|
+
await Promise.all([...sessionStandbys].map(returnStandbyBrowser));
|
|
310
|
+
const deadline = Date.now() + 2e3;
|
|
311
|
+
while ((sessionWorkers.size || detachedTeardowns.size) && Date.now() < deadline)
|
|
312
|
+
await Promise.race([...detachedTeardowns, new Promise((resolve) => setTimeout(resolve, 5))]);
|
|
313
|
+
await Promise.all(pendingSends);
|
|
314
|
+
for (const parts of workerParts.values())
|
|
315
|
+
killBrowser(parts);
|
|
316
|
+
workerParts.clear();
|
|
317
|
+
if (sessionChannel.connected)
|
|
318
|
+
sessionChannel.disconnect();
|
|
319
|
+
}
|
|
320
|
+
var outputBytes;
|
|
321
|
+
function countOutputBytes() {
|
|
322
|
+
outputBytes = { stdout: 0, stderr: 0 };
|
|
323
|
+
for (const name of ["stdout", "stderr"]) {
|
|
324
|
+
const stream = process[name];
|
|
325
|
+
const write = stream.write;
|
|
326
|
+
stream.write = function(chunk, encoding, callback) {
|
|
327
|
+
outputBytes[name] += typeof chunk === "string" ? Buffer.byteLength(chunk, typeof encoding === "string" ? encoding : "utf8") : chunk.length;
|
|
328
|
+
return write.apply(this, arguments);
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function returnStandbyBrowser(child) {
|
|
333
|
+
if (!child.connected)
|
|
334
|
+
return Promise.resolve();
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
const done = () => {
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
child.off("message", onMessage);
|
|
339
|
+
resolve();
|
|
340
|
+
};
|
|
341
|
+
const timer = setTimeout(done, 2e3);
|
|
342
|
+
const onMessage = (message, handle) => {
|
|
343
|
+
if (message?.method === kBrowserReturned)
|
|
344
|
+
done();
|
|
345
|
+
else if (message?.method === kBrowserMessage)
|
|
346
|
+
receiveWorkerMessage(child, message, handle);
|
|
347
|
+
};
|
|
348
|
+
child.on("message", onMessage);
|
|
349
|
+
child.once("exit", done);
|
|
350
|
+
child.send({ method: kReturnBrowser });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function onSessionMessage(message, handle) {
|
|
354
|
+
if (message?.method !== kBrowserMessage)
|
|
355
|
+
return;
|
|
356
|
+
const child = standbyRequests.get(message.params.id);
|
|
357
|
+
if (message.params.part !== "write")
|
|
358
|
+
standbyRequests.delete(message.params.id);
|
|
359
|
+
if (child?.connected) {
|
|
360
|
+
child.send(message, handle);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const browser = collectBrowserPart(workerParts, message, handle);
|
|
364
|
+
if (browser)
|
|
365
|
+
forwardBrowser(browser);
|
|
366
|
+
}
|
|
367
|
+
function collectBrowserPart(parts, message, handle) {
|
|
368
|
+
const { id, part, metadata } = message.params;
|
|
369
|
+
if (!part || !handle)
|
|
370
|
+
return;
|
|
371
|
+
const browser = parts.get(id) ?? { id };
|
|
372
|
+
browser[part] = handle;
|
|
373
|
+
if (metadata)
|
|
374
|
+
browser.metadata = metadata;
|
|
375
|
+
parts.set(id, browser);
|
|
376
|
+
if (!browser.write || !browser.read || !browser.metadata)
|
|
377
|
+
return;
|
|
378
|
+
parts.delete(id);
|
|
379
|
+
return browser;
|
|
380
|
+
}
|
|
381
|
+
function sendBrowser(channel, browser) {
|
|
382
|
+
const { id, metadata } = browser;
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
channel.send({ method: kBrowserMessage, params: { id, part: "write", metadata } }, browser.write, (error) => {
|
|
385
|
+
if (error) {
|
|
386
|
+
killBrowser(browser);
|
|
387
|
+
resolve();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
channel.send({ method: kBrowserMessage, params: { id, part: "read" } }, browser.read, () => {
|
|
391
|
+
channel.send({ method: kBrowserMessage, params: { id, sent: true } }, void 0, () => resolve());
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function killBrowser(browser) {
|
|
397
|
+
browser.write?.destroy();
|
|
398
|
+
browser.read?.destroy();
|
|
399
|
+
if (!browser.metadata)
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
process.kill(-browser.metadata.pid, "SIGKILL");
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
const directories = browser.metadata.tempDirectories;
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
for (const directory of directories)
|
|
408
|
+
import_fs.default.rm(directory, { recursive: true, force: true, maxRetries: 5 }, () => {
|
|
409
|
+
});
|
|
410
|
+
}, 500).unref();
|
|
411
|
+
}
|
|
412
|
+
function runSession(socketFile, build) {
|
|
413
|
+
const warmProcesses = [];
|
|
414
|
+
const browsers = [];
|
|
415
|
+
const waiting = [];
|
|
416
|
+
let startingRunKey;
|
|
417
|
+
let lastRequest;
|
|
418
|
+
let idleTimer;
|
|
419
|
+
let activeRuns = 0;
|
|
420
|
+
const shutdown = () => {
|
|
421
|
+
for (const warm of warmProcesses.splice(0))
|
|
422
|
+
warm.child.kill();
|
|
423
|
+
for (const browser of browsers.splice(0))
|
|
424
|
+
killBrowser(browser);
|
|
425
|
+
server.close();
|
|
426
|
+
try {
|
|
427
|
+
import_fs.default.unlinkSync(socketFile);
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
process.exit(0);
|
|
431
|
+
};
|
|
432
|
+
const resetIdleTimer = () => {
|
|
433
|
+
clearTimeout(idleTimer);
|
|
434
|
+
idleTimer = setTimeout(() => {
|
|
435
|
+
if (!activeRuns && !startingRunKey)
|
|
436
|
+
shutdown();
|
|
437
|
+
}, kIdleTimeout);
|
|
438
|
+
};
|
|
439
|
+
const runsThatMayHandOver = (key) => (startingRunKey === key ? 1 : 0) + warmProcesses.filter((warm) => warm.running && warm.connected && warm.key === key).length;
|
|
440
|
+
const answer = (warm, id) => {
|
|
441
|
+
let browser;
|
|
442
|
+
for (const candidate of browsers) {
|
|
443
|
+
if (candidate.key === warm.key && (!browser || (candidate.metadata.lastUsed ?? 0) > (browser.metadata.lastUsed ?? 0)))
|
|
444
|
+
browser = candidate;
|
|
445
|
+
}
|
|
446
|
+
if (browser)
|
|
447
|
+
remove(browser);
|
|
448
|
+
if (!warm.child.connected) {
|
|
449
|
+
if (browser)
|
|
450
|
+
browsers.push(browser);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!browser) {
|
|
454
|
+
warm.child.send({ method: kBrowserMessage, params: { id, none: true } });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
browser.read.removeAllListeners("data");
|
|
458
|
+
browser.read.removeAllListeners("close");
|
|
459
|
+
browser.read.pause();
|
|
460
|
+
void sendBrowser(warm.child, { ...browser, id });
|
|
461
|
+
};
|
|
462
|
+
const onBrowserRequest = (warm, id) => {
|
|
463
|
+
if (browsers.some((browser) => browser.key === warm.key) || warm.running || !runsThatMayHandOver(warm.key))
|
|
464
|
+
answer(warm, id);
|
|
465
|
+
else
|
|
466
|
+
waiting.push({ warm, id });
|
|
467
|
+
};
|
|
468
|
+
const answerWaiting = (predicate) => {
|
|
469
|
+
for (const entry of waiting.filter(predicate)) {
|
|
470
|
+
waiting.splice(waiting.indexOf(entry), 1);
|
|
471
|
+
answer(entry.warm, entry.id);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
const onBrowserReturned = (key, browser) => {
|
|
475
|
+
if (browser.read.readableLength) {
|
|
476
|
+
killBrowser(browser);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const pooled = { ...browser, key };
|
|
480
|
+
browser.read.on("data", () => {
|
|
481
|
+
remove(pooled);
|
|
482
|
+
killBrowser(pooled);
|
|
483
|
+
});
|
|
484
|
+
browser.read.on("close", () => {
|
|
485
|
+
remove(pooled);
|
|
486
|
+
killBrowser(pooled);
|
|
487
|
+
});
|
|
488
|
+
browsers.push(pooled);
|
|
489
|
+
const entry = waiting.find((entry2) => entry2.warm.key === key);
|
|
490
|
+
if (entry) {
|
|
491
|
+
waiting.splice(waiting.indexOf(entry), 1);
|
|
492
|
+
answer(entry.warm, entry.id);
|
|
493
|
+
}
|
|
494
|
+
const sameKind = browsers.filter((browser2) => browser2.key === key).sort((a, b) => (a.metadata.lastUsed ?? 0) - (b.metadata.lastUsed ?? 0));
|
|
495
|
+
for (const extra of sameKind.slice(0, Math.max(0, sameKind.length - kMaxBrowsers))) {
|
|
496
|
+
remove(extra);
|
|
497
|
+
killBrowser(extra);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
const remove = (browser) => {
|
|
501
|
+
const index = browsers.indexOf(browser);
|
|
502
|
+
if (index !== -1)
|
|
503
|
+
browsers.splice(index, 1);
|
|
504
|
+
};
|
|
505
|
+
const spawnWarm = (request) => {
|
|
506
|
+
const key = warmKey(request);
|
|
507
|
+
if (warmProcesses.some((warm2) => warm2.key === key && !warm2.running))
|
|
508
|
+
return;
|
|
509
|
+
const standbys = browsers.filter((browser) => browser.key === key).length;
|
|
510
|
+
const child = childProcess().spawn(request.execPath, [...request.execArgv, request.cliPath, ...request.argv.slice(2)], {
|
|
511
|
+
cwd: request.cwd,
|
|
512
|
+
env: { ...warmEnv(request), FLASHWRIGHT_SESSION_ROLE: "warm", FLASHWRIGHT_SESSION_STANDBYS: String(standbys) },
|
|
513
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"]
|
|
514
|
+
});
|
|
515
|
+
const warm = { child, key, ready: Promise.resolve(false), running: false, connected: true, parts: /* @__PURE__ */ new Map() };
|
|
516
|
+
warm.ready = new Promise((resolve) => {
|
|
517
|
+
child.on("message", (message, handle2) => {
|
|
518
|
+
if (message?.type === "warm") {
|
|
519
|
+
resolve(true);
|
|
520
|
+
} else if (message?.type === "exiting") {
|
|
521
|
+
warm.exiting?.(message);
|
|
522
|
+
} else if (message?.method === kBrowserRequest) {
|
|
523
|
+
onBrowserRequest(warm, message.params.id);
|
|
524
|
+
} else if (message?.method === kBrowserMessage) {
|
|
525
|
+
const browser = collectBrowserPart(warm.parts, message, handle2);
|
|
526
|
+
if (browser)
|
|
527
|
+
onBrowserReturned(key, browser);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
child.once("exit", () => resolve(false));
|
|
531
|
+
});
|
|
532
|
+
warmProcesses.push(warm);
|
|
533
|
+
child.once("disconnect", () => {
|
|
534
|
+
warm.connected = false;
|
|
535
|
+
for (const parts2 of warm.parts.values())
|
|
536
|
+
killBrowser(parts2);
|
|
537
|
+
warm.parts.clear();
|
|
538
|
+
answerWaiting((entry) => entry.warm === warm || !runsThatMayHandOver(entry.warm.key));
|
|
539
|
+
});
|
|
540
|
+
child.once("exit", () => {
|
|
541
|
+
const index = warmProcesses.indexOf(warm);
|
|
542
|
+
if (index !== -1)
|
|
543
|
+
warmProcesses.splice(index, 1);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
const handle = async (socket, request) => {
|
|
547
|
+
resetIdleTimer();
|
|
548
|
+
if (request.build !== build) {
|
|
549
|
+
socket.end(JSON.stringify({ type: "cold", stale: true }) + "\n");
|
|
550
|
+
shutdown();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
lastRequest = request;
|
|
554
|
+
const key = warmKey(request);
|
|
555
|
+
const index = warmProcesses.findIndex((warm2) => warm2.key === key && !warm2.running);
|
|
556
|
+
const warm = index === -1 ? void 0 : warmProcesses[index];
|
|
557
|
+
if (!warm || !await warm.ready) {
|
|
558
|
+
warm?.child.kill();
|
|
559
|
+
socket.end(JSON.stringify({ type: "cold" }) + "\n");
|
|
560
|
+
if (!startingRunKey)
|
|
561
|
+
spawnWarm(request);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
activeRuns++;
|
|
565
|
+
warm.running = true;
|
|
566
|
+
answerWaiting((entry) => entry.warm === warm);
|
|
567
|
+
const send = (message) => {
|
|
568
|
+
if (!socket.destroyed)
|
|
569
|
+
socket.write(JSON.stringify(message) + "\n");
|
|
570
|
+
};
|
|
571
|
+
send({ type: "started" });
|
|
572
|
+
const relayed = { stdout: 0, stderr: 0 };
|
|
573
|
+
let exiting;
|
|
574
|
+
let finished = false;
|
|
575
|
+
const finish = (code, signal) => {
|
|
576
|
+
if (finished)
|
|
577
|
+
return;
|
|
578
|
+
finished = true;
|
|
579
|
+
send({ type: "exit", code, signal });
|
|
580
|
+
socket.end();
|
|
581
|
+
spawnWarm(lastRequest ?? request);
|
|
582
|
+
};
|
|
583
|
+
const finishIfRelayed = () => {
|
|
584
|
+
if (exiting && relayed.stdout >= exiting.stdout && relayed.stderr >= exiting.stderr)
|
|
585
|
+
finish(exiting.code, null);
|
|
586
|
+
};
|
|
587
|
+
warm.exiting = (message) => {
|
|
588
|
+
exiting = message;
|
|
589
|
+
finishIfRelayed();
|
|
590
|
+
};
|
|
591
|
+
for (const name of ["stdout", "stderr"]) {
|
|
592
|
+
warm.child[name].on("data", (data) => {
|
|
593
|
+
send({ type: name, data: data.toString("base64") });
|
|
594
|
+
relayed[name] += data.length;
|
|
595
|
+
finishIfRelayed();
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
let buffer = "";
|
|
599
|
+
socket.on("data", (chunk) => {
|
|
600
|
+
buffer += chunk.toString();
|
|
601
|
+
let newline;
|
|
602
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
603
|
+
const message = JSON.parse(buffer.slice(0, newline));
|
|
604
|
+
buffer = buffer.slice(newline + 1);
|
|
605
|
+
if (message.type === "signal")
|
|
606
|
+
warm.child.kill(message.signal);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
socket.on("close", () => {
|
|
610
|
+
if (!finished && warm.child.exitCode === null && warm.child.signalCode === null)
|
|
611
|
+
warm.child.kill("SIGINT");
|
|
612
|
+
});
|
|
613
|
+
warm.child.once("exit", (code, signal) => {
|
|
614
|
+
activeRuns--;
|
|
615
|
+
resetIdleTimer();
|
|
616
|
+
if (warm.child.stdout.readableEnded && warm.child.stderr.readableEnded)
|
|
617
|
+
finish(code, signal);
|
|
618
|
+
else
|
|
619
|
+
warm.child.once("close", () => finish(code, signal));
|
|
620
|
+
});
|
|
621
|
+
warm.child.send(request);
|
|
622
|
+
};
|
|
623
|
+
const listen = () => server.listen(socketFile, () => import_fs.default.chmodSync(socketFile, 384));
|
|
624
|
+
const server = import_net.default.createServer((socket) => {
|
|
625
|
+
let buffer = "";
|
|
626
|
+
const onData = (chunk) => {
|
|
627
|
+
buffer += chunk.toString();
|
|
628
|
+
const newline = buffer.indexOf("\n");
|
|
629
|
+
if (newline === -1)
|
|
630
|
+
return;
|
|
631
|
+
socket.off("data", onData);
|
|
632
|
+
const request = JSON.parse(buffer.slice(0, newline));
|
|
633
|
+
handle(socket, request).catch(() => socket.destroy());
|
|
634
|
+
};
|
|
635
|
+
socket.on("data", onData);
|
|
636
|
+
socket.on("error", () => {
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
server.on("error", (error) => {
|
|
640
|
+
if (error.code !== "EADDRINUSE")
|
|
641
|
+
process.exit(0);
|
|
642
|
+
const probe = import_net.default.connect(socketFile);
|
|
643
|
+
probe.on("connect", () => process.exit(0));
|
|
644
|
+
probe.on("error", () => {
|
|
645
|
+
try {
|
|
646
|
+
import_fs.default.unlinkSync(socketFile);
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
listen();
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
listen();
|
|
653
|
+
resetIdleTimer();
|
|
654
|
+
process.on("SIGTERM", shutdown);
|
|
655
|
+
const parts = /* @__PURE__ */ new Map();
|
|
656
|
+
process.on("message", (message, handle2) => {
|
|
657
|
+
if (message?.method === "request") {
|
|
658
|
+
lastRequest = message.params;
|
|
659
|
+
startingRunKey = warmKey(message.params);
|
|
660
|
+
} else if (message?.method === kBrowserMessage && startingRunKey) {
|
|
661
|
+
const browser = collectBrowserPart(parts, message, handle2);
|
|
662
|
+
if (browser)
|
|
663
|
+
onBrowserReturned(startingRunKey, browser);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
process.on("disconnect", () => {
|
|
667
|
+
for (const browser of parts.values())
|
|
668
|
+
killBrowser(browser);
|
|
669
|
+
parts.clear();
|
|
670
|
+
startingRunKey = void 0;
|
|
671
|
+
resetIdleTimer();
|
|
672
|
+
if (lastRequest)
|
|
673
|
+
spawnWarm(lastRequest);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
function warmEnv(request) {
|
|
677
|
+
const env = { ...request.env };
|
|
678
|
+
const injected = [];
|
|
679
|
+
if (request.profile.stdout !== "false") {
|
|
680
|
+
for (const [key, value] of [["PLAYWRIGHT_FORCE_TTY", request.profile.stdout], ["FORCE_COLOR", "1"]]) {
|
|
681
|
+
if (env[key] === void 0) {
|
|
682
|
+
env[key] = value;
|
|
683
|
+
injected.push(key);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
env.FLASHWRIGHT_SESSION_INJECTED = injected.join(",");
|
|
688
|
+
return env;
|
|
689
|
+
}
|
|
690
|
+
async function runWarmProcess(hooks) {
|
|
691
|
+
joinSession(process, true);
|
|
692
|
+
process.on("message", onSessionMessage);
|
|
693
|
+
hooks.installCodeCache("runner");
|
|
694
|
+
const program = hooks.loadProgram();
|
|
695
|
+
require(import_path.default.join(__dirname, "index.js"));
|
|
696
|
+
const packageRoot = import_path.default.join(__dirname, "..", "..");
|
|
697
|
+
require(import_path.default.join(packageRoot, "test.js"));
|
|
698
|
+
await import(import_url.default.pathToFileURL(import_path.default.join(packageRoot, "test.mjs")).href).catch(() => {
|
|
699
|
+
});
|
|
700
|
+
await import(import_url.default.pathToFileURL(import_path.default.join(import_path.default.dirname(process.argv[1]), "index.mjs")).href).catch(() => {
|
|
701
|
+
});
|
|
702
|
+
for (const key of (process.env.FLASHWRIGHT_SESSION_INJECTED || "").split(",").filter(Boolean))
|
|
703
|
+
delete process.env[key];
|
|
704
|
+
delete process.env.FLASHWRIGHT_SESSION_INJECTED;
|
|
705
|
+
delete process.env.FLASHWRIGHT_SESSION_ROLE;
|
|
706
|
+
hooks.prespawnWorkers(process.argv);
|
|
707
|
+
const request = await new Promise((resolve) => {
|
|
708
|
+
const onMessage = (message) => {
|
|
709
|
+
if (message?.type !== "run")
|
|
710
|
+
return;
|
|
711
|
+
process.off("message", onMessage);
|
|
712
|
+
resolve(message);
|
|
713
|
+
};
|
|
714
|
+
process.on("message", onMessage);
|
|
715
|
+
process.send({ type: "warm" });
|
|
716
|
+
});
|
|
717
|
+
process.channel?.unref();
|
|
718
|
+
process.chdir(request.cwd);
|
|
719
|
+
for (const key of Object.keys(process.env)) {
|
|
720
|
+
if (!(key in request.env) && key !== "FLASHWRIGHT_SESSION_HANDOFF")
|
|
721
|
+
delete process.env[key];
|
|
722
|
+
}
|
|
723
|
+
Object.assign(process.env, request.env);
|
|
724
|
+
process.argv = request.argv;
|
|
725
|
+
if (request.profile.stdout !== "false") {
|
|
726
|
+
const [columns, rows] = request.profile.stdout.split("x").map(Number);
|
|
727
|
+
Object.assign(process.stdout, { isTTY: true, columns, rows });
|
|
728
|
+
}
|
|
729
|
+
if (request.profile.stdinTTY)
|
|
730
|
+
Object.assign(process.stdin, { isTTY: true });
|
|
731
|
+
countOutputBytes();
|
|
732
|
+
globalThis[/* @__PURE__ */ Symbol.for("flashwright.codeCache")]?.refreshAtExit();
|
|
733
|
+
hooks.retargetWorkers(request.argv);
|
|
734
|
+
program.parse(request.argv);
|
|
735
|
+
}
|
|
736
|
+
if (process.argv[2] === "--flashwright-session")
|
|
737
|
+
runSession(process.argv[3], process.argv[4]);
|
|
738
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
739
|
+
0 && (module.exports = {
|
|
740
|
+
flushSessionBrowsers,
|
|
741
|
+
isWarmSessionRun,
|
|
742
|
+
receiveWorkerMessage,
|
|
743
|
+
requestSessionBrowser,
|
|
744
|
+
runCli,
|
|
745
|
+
sessionStandbyEnv,
|
|
746
|
+
trackDetachedTeardown,
|
|
747
|
+
trackSessionWorker
|
|
748
|
+
});
|