@xbrowser/cli 0.14.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 +858 -0
- package/dist/admin-6UTU2RZ2.js +281 -0
- package/dist/admin-MDGF4CET.js +285 -0
- package/dist/admin-RPJJ5CAF.js +282 -0
- package/dist/browser-GWBH6OJK.js +46 -0
- package/dist/browser-I2HJZ7IP.js +48 -0
- package/dist/browser-R7B255ML.js +46 -0
- package/dist/chunk-2ONMTDLK.js +2050 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-43VX3TYN.js +83 -0
- package/dist/chunk-ATFTAKMN.js +267 -0
- package/dist/chunk-DESA2KMG.js +77 -0
- package/dist/chunk-DTJRVA76.js +206 -0
- package/dist/chunk-F3ZWFCJJ.js +2051 -0
- package/dist/chunk-FF5WHQHN.js +135 -0
- package/dist/chunk-HINTG75P.js +77 -0
- package/dist/chunk-KDYXFLAC.js +1503 -0
- package/dist/chunk-KTSQU4QT.js +29 -0
- package/dist/chunk-L53IDAWK.js +68 -0
- package/dist/chunk-M7CMBPCA.js +100 -0
- package/dist/chunk-NFGO7J2I.js +29 -0
- package/dist/chunk-OLB6UJ25.js +438 -0
- package/dist/chunk-OPRXFZVE.js +52 -0
- package/dist/chunk-RS6YYWTK.js +685 -0
- package/dist/chunk-VEDJ5XSQ.js +196 -0
- package/dist/chunk-VEKPHQBR.js +47 -0
- package/dist/chunk-VUJDJCIN.js +437 -0
- package/dist/chunk-YEN2ODUI.js +14 -0
- package/dist/chunk-ZZ2TFWIV.js +1382 -0
- package/dist/cli.js +11012 -0
- package/dist/convert-4DUWZIKH.js +205 -0
- package/dist/convert-EKQVHKB4.js +11 -0
- package/dist/daemon-client-3IJD6X4B.js +59 -0
- package/dist/daemon-client-GX2UYIW4.js +241 -0
- package/dist/daemon-client-XWSSQBEA.js +58 -0
- package/dist/daemon-main.js +9910 -0
- package/dist/extract-EGRXZSSK.js +67 -0
- package/dist/extract-JUOQQX4V.js +11 -0
- package/dist/filter-OLAE26HN.js +51 -0
- package/dist/filter-VID2GGZ7.js +9 -0
- package/dist/human-interaction-QPHNDD76.js +8 -0
- package/dist/index.d.ts +2313 -0
- package/dist/index.js +13839 -0
- package/dist/marketplace-FCVN5OTZ.js +706 -0
- package/dist/marketplace-FPT5YLKB.js +351 -0
- package/dist/marketplace-W545W4FR.js +706 -0
- package/dist/network-store-2S5HATEV.js +194 -0
- package/dist/network-store-BN6QEZ7R.js +196 -0
- package/dist/network-store-YAF5OIBH.js +12 -0
- package/dist/parse-action-dsl-DRSPBALP.js +72 -0
- package/dist/parse-action-dsl-T3DYC33D.js +74 -0
- package/dist/proxy-WKGUCH2C.js +7 -0
- package/dist/session-recorder-ILSSV2UC.js +6 -0
- package/dist/session-recorder-XET3DNML.js +7 -0
- package/package.json +111 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CDPInterceptorProxy
|
|
3
|
+
} from "./chunk-ZZ2TFWIV.js";
|
|
4
|
+
|
|
5
|
+
// src/browser.ts
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { chromium } from "playwright";
|
|
11
|
+
|
|
12
|
+
// src/utils/cdp.ts
|
|
13
|
+
async function fetchNoProxy(url) {
|
|
14
|
+
const savedProxy = {
|
|
15
|
+
http_proxy: process.env.http_proxy,
|
|
16
|
+
https_proxy: process.env.https_proxy,
|
|
17
|
+
HTTP_PROXY: process.env.HTTP_PROXY,
|
|
18
|
+
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
|
19
|
+
all_proxy: process.env.all_proxy,
|
|
20
|
+
ALL_PROXY: process.env.ALL_PROXY
|
|
21
|
+
};
|
|
22
|
+
for (const key of Object.keys(savedProxy)) delete process.env[key];
|
|
23
|
+
try {
|
|
24
|
+
return await fetch(url);
|
|
25
|
+
} finally {
|
|
26
|
+
for (const [key, val] of Object.entries(savedProxy)) {
|
|
27
|
+
if (val !== void 0) process.env[key] = val;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function resolveCDPEndpoint(raw) {
|
|
32
|
+
if (raw === "auto") {
|
|
33
|
+
const httpResp = await fetchNoProxy("http://localhost:9222/json/version");
|
|
34
|
+
const data = await httpResp.json();
|
|
35
|
+
if (!data.webSocketDebuggerUrl) {
|
|
36
|
+
throw new Error("Could not auto-discover CDP endpoint from localhost:9222");
|
|
37
|
+
}
|
|
38
|
+
return data.webSocketDebuggerUrl;
|
|
39
|
+
}
|
|
40
|
+
if (/^\d+$/.test(raw)) {
|
|
41
|
+
const port = raw;
|
|
42
|
+
const httpResp = await fetchNoProxy(`http://localhost:${port}/json/version`);
|
|
43
|
+
const data = await httpResp.json();
|
|
44
|
+
if (!data.webSocketDebuggerUrl) {
|
|
45
|
+
throw new Error(`Could not discover CDP endpoint from localhost:${port}`);
|
|
46
|
+
}
|
|
47
|
+
return data.webSocketDebuggerUrl;
|
|
48
|
+
}
|
|
49
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
50
|
+
try {
|
|
51
|
+
const httpResp = await fetchNoProxy(`${raw}/json/version`);
|
|
52
|
+
const data = await httpResp.json();
|
|
53
|
+
if (!data.webSocketDebuggerUrl) {
|
|
54
|
+
throw new Error(`Could not discover CDP endpoint from ${raw}`);
|
|
55
|
+
}
|
|
56
|
+
return data.webSocketDebuggerUrl;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Failed to fetch WebSocket URL from ${raw}, using endpoint directly: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/browser.ts
|
|
66
|
+
function logSessionEvent(event, details) {
|
|
67
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
|
|
68
|
+
const pid = process.pid;
|
|
69
|
+
console.error(`[SESSION] ${ts} [PID:${pid}] ${event} | ${details}`);
|
|
70
|
+
}
|
|
71
|
+
var SESSION_DIR = join(homedir(), ".xbrowser", "sessions");
|
|
72
|
+
function sessionFile(name) {
|
|
73
|
+
return join(SESSION_DIR, `${name}.json`);
|
|
74
|
+
}
|
|
75
|
+
function ensureSessionDir() {
|
|
76
|
+
mkdirSync(SESSION_DIR, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
79
|
+
var _sharedBrowser = null;
|
|
80
|
+
var _sharedCdpProxy = null;
|
|
81
|
+
var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
|
|
82
|
+
var idleTimer = null;
|
|
83
|
+
function resetIdleTimer() {
|
|
84
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
85
|
+
idleTimer = setTimeout(async () => {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
let allIdle = true;
|
|
88
|
+
const idleSessions = [];
|
|
89
|
+
for (const [, s] of sessions) {
|
|
90
|
+
if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
|
|
91
|
+
allIdle = false;
|
|
92
|
+
} else {
|
|
93
|
+
idleSessions.push(`${s.name}(${(now - s.lastActivityAt) / 1e3}s idle)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (allIdle && (sessions.size > 0 || _sharedBrowser)) {
|
|
97
|
+
logSessionEvent("idle_timeout", `Sessions idle for >${IDLE_TIMEOUT_MS / 6e4}min. Sessions: ${idleSessions.join(", ") || "all"}. Calling destroyBrowser()`);
|
|
98
|
+
await destroyBrowser().catch(() => {
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}, IDLE_TIMEOUT_MS);
|
|
102
|
+
if (idleTimer && typeof idleTimer.unref === "function") {
|
|
103
|
+
idleTimer.unref();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function touchSession(id) {
|
|
107
|
+
const s = sessions.get(id);
|
|
108
|
+
if (s) s.lastActivityAt = Date.now();
|
|
109
|
+
resetIdleTimer();
|
|
110
|
+
}
|
|
111
|
+
process.on("exit", () => {
|
|
112
|
+
for (const session of sessions.values()) {
|
|
113
|
+
if (session.isCDP) {
|
|
114
|
+
logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
|
|
115
|
+
} else {
|
|
116
|
+
logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
|
|
117
|
+
try {
|
|
118
|
+
session.browser?.close();
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (_sharedBrowser) {
|
|
124
|
+
logSessionEvent("process_exit", "Closing shared browser.");
|
|
125
|
+
try {
|
|
126
|
+
_sharedBrowser.close();
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
_sharedBrowser = null;
|
|
130
|
+
}
|
|
131
|
+
if (_sharedCdpProxy) {
|
|
132
|
+
try {
|
|
133
|
+
_sharedCdpProxy.stop();
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
_sharedCdpProxy = null;
|
|
137
|
+
}
|
|
138
|
+
sessions.clear();
|
|
139
|
+
});
|
|
140
|
+
async function getCDPTargets(cdpEndpoint) {
|
|
141
|
+
try {
|
|
142
|
+
const ep = String(cdpEndpoint);
|
|
143
|
+
let host = "localhost";
|
|
144
|
+
let port = "9222";
|
|
145
|
+
if (ep.startsWith("http://") || ep.startsWith("https://")) {
|
|
146
|
+
const u = new URL(ep);
|
|
147
|
+
host = u.hostname;
|
|
148
|
+
port = u.port || "9222";
|
|
149
|
+
} else if (/^\d+$/.test(ep)) {
|
|
150
|
+
port = ep;
|
|
151
|
+
}
|
|
152
|
+
const url = `http://${host}:${port}/json/list`;
|
|
153
|
+
const resp = await fetch(url);
|
|
154
|
+
return await resp.json();
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function findTargetPage(cdpEndpoint, target) {
|
|
160
|
+
const targets = await getCDPTargets(cdpEndpoint);
|
|
161
|
+
const pages = targets.filter((t) => t.url && !t.url.startsWith("about:blank") && !t.url.startsWith("chrome://"));
|
|
162
|
+
const byId = pages.find((t) => t.id === target);
|
|
163
|
+
if (byId) return { pageId: byId.id, wsUrl: byId.webSocketDebuggerUrl, title: byId.title, url: byId.url };
|
|
164
|
+
const lowerTarget = target.toLowerCase();
|
|
165
|
+
const byTitle = pages.find((t) => t.title && t.title.toLowerCase().includes(lowerTarget));
|
|
166
|
+
if (byTitle) return { pageId: byTitle.id, wsUrl: byTitle.webSocketDebuggerUrl, title: byTitle.title, url: byTitle.url };
|
|
167
|
+
const byUrl = pages.find((t) => t.url.toLowerCase().includes(lowerTarget));
|
|
168
|
+
if (byUrl) return { pageId: byUrl.id, wsUrl: byUrl.webSocketDebuggerUrl, title: byUrl.title, url: byUrl.url };
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function resolveLaunchOpts(ctx) {
|
|
172
|
+
if (ctx.cdpEndpoint) {
|
|
173
|
+
return { cdpEndpoint: ctx.cdpEndpoint };
|
|
174
|
+
}
|
|
175
|
+
return { headless: true };
|
|
176
|
+
}
|
|
177
|
+
var CHROMIUM_CANDIDATES = [
|
|
178
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
179
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
180
|
+
"/usr/bin/chromium-browser",
|
|
181
|
+
"/usr/bin/chromium",
|
|
182
|
+
"/usr/bin/google-chrome"
|
|
183
|
+
];
|
|
184
|
+
function discoverChromiumPath() {
|
|
185
|
+
for (const p of CHROMIUM_CANDIDATES) {
|
|
186
|
+
if (existsSync(p)) return p;
|
|
187
|
+
}
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
async function createBrowser(options) {
|
|
191
|
+
if (options?.cdpEndpoint) {
|
|
192
|
+
const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
193
|
+
if (options.intercept) {
|
|
194
|
+
const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
|
|
195
|
+
const proxy = new CDPInterceptorProxy(config);
|
|
196
|
+
const proxyPort = await proxy.start();
|
|
197
|
+
console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
|
|
198
|
+
return await chromium.connectOverCDP(`ws://localhost:${proxyPort}`);
|
|
199
|
+
}
|
|
200
|
+
return await chromium.connectOverCDP(realEndpoint);
|
|
201
|
+
}
|
|
202
|
+
const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
|
|
203
|
+
return await chromium.launch({ executablePath, headless: options?.headless ?? true });
|
|
204
|
+
}
|
|
205
|
+
async function getBrowser(options) {
|
|
206
|
+
if (_sharedBrowser) return _sharedBrowser;
|
|
207
|
+
_sharedBrowser = await createBrowser(options);
|
|
208
|
+
if (options?.cdpEndpoint && options.intercept) {
|
|
209
|
+
}
|
|
210
|
+
return _sharedBrowser;
|
|
211
|
+
}
|
|
212
|
+
function findSession(name) {
|
|
213
|
+
for (const [, session] of sessions) {
|
|
214
|
+
if (session.name === name) return session;
|
|
215
|
+
}
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
function getSessionById(id) {
|
|
219
|
+
return sessions.get(id);
|
|
220
|
+
}
|
|
221
|
+
function setActivePage(session, page) {
|
|
222
|
+
session.page = page;
|
|
223
|
+
session.lastActivityAt = Date.now();
|
|
224
|
+
}
|
|
225
|
+
function saveSessionDiskMeta(name, data) {
|
|
226
|
+
ensureSessionDir();
|
|
227
|
+
const file = sessionFile(name);
|
|
228
|
+
let existing = {};
|
|
229
|
+
try {
|
|
230
|
+
existing = JSON.parse(readFileSync(file, "utf8"));
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
Object.assign(existing, data, { name });
|
|
234
|
+
writeFileSync(file, JSON.stringify(existing, null, 2));
|
|
235
|
+
}
|
|
236
|
+
function readSessionDiskMeta(name) {
|
|
237
|
+
const file = sessionFile(name);
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function deleteSessionDiskMeta(name) {
|
|
245
|
+
const file = sessionFile(name);
|
|
246
|
+
try {
|
|
247
|
+
unlinkSync(file);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function findOrRestoreSession(name, cdpEndpoint) {
|
|
252
|
+
const inMem = findSession(name);
|
|
253
|
+
if (inMem) return inMem;
|
|
254
|
+
const meta = readSessionDiskMeta(name);
|
|
255
|
+
if (!meta) return void 0;
|
|
256
|
+
const ep = cdpEndpoint || meta.cdpEndpoint;
|
|
257
|
+
if (!ep) return void 0;
|
|
258
|
+
try {
|
|
259
|
+
const b = await createBrowser({ cdpEndpoint: ep });
|
|
260
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
261
|
+
let contexts = b.contexts();
|
|
262
|
+
if (contexts.length === 0) {
|
|
263
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
264
|
+
contexts = b.contexts();
|
|
265
|
+
}
|
|
266
|
+
const context = contexts[0] || await b.newContext();
|
|
267
|
+
const savedUrl = meta.conversationUrl || meta.url;
|
|
268
|
+
const targetHostname = savedUrl ? (() => {
|
|
269
|
+
try {
|
|
270
|
+
return new URL(savedUrl).hostname;
|
|
271
|
+
} catch {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
})() : "";
|
|
275
|
+
let page = null;
|
|
276
|
+
let fallbackPage = null;
|
|
277
|
+
for (const ctx of contexts) {
|
|
278
|
+
const pages = ctx.pages();
|
|
279
|
+
for (const p of pages) {
|
|
280
|
+
const pUrl = p.url();
|
|
281
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
|
|
282
|
+
if (targetHostname && pUrl.includes(targetHostname)) {
|
|
283
|
+
page = p;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (!fallbackPage) {
|
|
287
|
+
fallbackPage = p;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (page) break;
|
|
292
|
+
}
|
|
293
|
+
page = page || fallbackPage;
|
|
294
|
+
if (!page) {
|
|
295
|
+
const targets = await getCDPTargets(ep);
|
|
296
|
+
const matchTarget = targets.find(
|
|
297
|
+
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (targetHostname ? t.url.includes(targetHostname) : true)
|
|
298
|
+
);
|
|
299
|
+
if (matchTarget && matchTarget.url) {
|
|
300
|
+
page = await context.newPage();
|
|
301
|
+
await page.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!page) {
|
|
306
|
+
const pages = context.pages();
|
|
307
|
+
page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await Promise.race([
|
|
311
|
+
page.evaluate(() => true),
|
|
312
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
313
|
+
]);
|
|
314
|
+
} catch {
|
|
315
|
+
console.log(`[Session] "${name}" restored page unresponsive, creating fresh session`);
|
|
316
|
+
deleteSessionDiskMeta(name);
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
const targetUrl = meta.conversationUrl || meta.url;
|
|
320
|
+
if (targetUrl && page.url() !== targetUrl && !page.url().includes(new URL(targetUrl).hostname)) {
|
|
321
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const session = {
|
|
325
|
+
id: meta.id || randomUUID(),
|
|
326
|
+
name,
|
|
327
|
+
context,
|
|
328
|
+
page,
|
|
329
|
+
browser: b,
|
|
330
|
+
createdAt: meta.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
331
|
+
lastActivityAt: Date.now(),
|
|
332
|
+
isCDP: true,
|
|
333
|
+
cdpEndpoint: ep
|
|
334
|
+
};
|
|
335
|
+
for (const [existingId, existingSession] of sessions) {
|
|
336
|
+
if (existingSession.name === name) {
|
|
337
|
+
logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingId}" during restore`);
|
|
338
|
+
sessions.delete(existingId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
sessions.set(session.id, session);
|
|
342
|
+
resetIdleTimer();
|
|
343
|
+
await installNetworkCapture(page, name);
|
|
344
|
+
return session;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
console.error(`[Session Restore] Failed for "${name}":`, e.message);
|
|
347
|
+
deleteSessionDiskMeta(name);
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function createEphemeralContext(options) {
|
|
352
|
+
if (options?.cdpEndpoint) {
|
|
353
|
+
const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
|
|
354
|
+
const b2 = await chromium.connectOverCDP(endpoint);
|
|
355
|
+
const contexts = b2.contexts();
|
|
356
|
+
const ctx = contexts[0] || await b2.newContext();
|
|
357
|
+
const page2 = await ctx.newPage();
|
|
358
|
+
resetIdleTimer();
|
|
359
|
+
ephemeralConnections.set(page2, b2);
|
|
360
|
+
return { context: ctx, page: page2 };
|
|
361
|
+
}
|
|
362
|
+
const b = await getBrowser(options);
|
|
363
|
+
const context = await b.newContext();
|
|
364
|
+
const page = await context.newPage();
|
|
365
|
+
resetIdleTimer();
|
|
366
|
+
return { context, page };
|
|
367
|
+
}
|
|
368
|
+
var ephemeralConnections = /* @__PURE__ */ new WeakMap();
|
|
369
|
+
async function closeEphemeralContext(context) {
|
|
370
|
+
try {
|
|
371
|
+
const pages = context.pages();
|
|
372
|
+
for (const p of pages) {
|
|
373
|
+
const conn = ephemeralConnections.get(p);
|
|
374
|
+
if (conn) {
|
|
375
|
+
ephemeralConnections.delete(p);
|
|
376
|
+
await conn.close();
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await context.close();
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
if (sessions.size === 0 && idleTimer) {
|
|
384
|
+
clearTimeout(idleTimer);
|
|
385
|
+
idleTimer = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function getAllSessions() {
|
|
389
|
+
return Array.from(sessions.values());
|
|
390
|
+
}
|
|
391
|
+
async function installNetworkCapture(page, sessionName) {
|
|
392
|
+
if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
|
|
393
|
+
const { networkStore } = await import("./network-store-BN6QEZ7R.js");
|
|
394
|
+
page.on("response", async (response) => {
|
|
395
|
+
try {
|
|
396
|
+
const request = response.request();
|
|
397
|
+
const url = response.url();
|
|
398
|
+
const contentType = response.headers()["content-type"] || "";
|
|
399
|
+
const headers = {};
|
|
400
|
+
for (const [k, v] of Object.entries(response.headers())) {
|
|
401
|
+
headers[k] = v;
|
|
402
|
+
}
|
|
403
|
+
const requestHeaders = {};
|
|
404
|
+
for (const [k, v] of Object.entries(request.headers())) {
|
|
405
|
+
requestHeaders[k] = v;
|
|
406
|
+
}
|
|
407
|
+
let requestBody = void 0;
|
|
408
|
+
const method = request.method();
|
|
409
|
+
const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
|
|
410
|
+
if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
|
|
411
|
+
try {
|
|
412
|
+
const postData = request.postData();
|
|
413
|
+
if (postData) {
|
|
414
|
+
try {
|
|
415
|
+
requestBody = JSON.parse(postData);
|
|
416
|
+
} catch {
|
|
417
|
+
requestBody = postData;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
let responseBody = void 0;
|
|
424
|
+
let size = 0;
|
|
425
|
+
const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
|
|
426
|
+
if (isJsonish) {
|
|
427
|
+
try {
|
|
428
|
+
const text = await response.text();
|
|
429
|
+
size = text.length;
|
|
430
|
+
if (size <= 10240) {
|
|
431
|
+
try {
|
|
432
|
+
responseBody = JSON.parse(text);
|
|
433
|
+
} catch {
|
|
434
|
+
responseBody = text.slice(0, 200);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
try {
|
|
441
|
+
const text = await response.text();
|
|
442
|
+
size = text.length;
|
|
443
|
+
} catch {
|
|
444
|
+
size = 0;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
networkStore.add(sessionName, {
|
|
448
|
+
timestamp: Date.now(),
|
|
449
|
+
method,
|
|
450
|
+
url,
|
|
451
|
+
path: new URL(url).pathname,
|
|
452
|
+
status: response.status(),
|
|
453
|
+
contentType,
|
|
454
|
+
size,
|
|
455
|
+
headers,
|
|
456
|
+
body: responseBody,
|
|
457
|
+
requestHeaders,
|
|
458
|
+
requestBody,
|
|
459
|
+
resourceType: request.resourceType()
|
|
460
|
+
});
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async function createSession(name, url, options) {
|
|
466
|
+
const existing = findSession(name);
|
|
467
|
+
if (existing) {
|
|
468
|
+
logSessionEvent("replace_session", `name="${name}" id="${existing.id}" \u2014 closing existing session before creating new one`);
|
|
469
|
+
await closeSessionByName(name);
|
|
470
|
+
}
|
|
471
|
+
const b = await createBrowser(options);
|
|
472
|
+
const isCDP = !!options?.cdpEndpoint;
|
|
473
|
+
let context;
|
|
474
|
+
let page;
|
|
475
|
+
if (isCDP) {
|
|
476
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
477
|
+
let contexts = b.contexts();
|
|
478
|
+
if (contexts.length === 0) {
|
|
479
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
480
|
+
contexts = b.contexts();
|
|
481
|
+
}
|
|
482
|
+
context = contexts[0] || await b.newContext();
|
|
483
|
+
let targetPage = null;
|
|
484
|
+
for (const ctx of contexts) {
|
|
485
|
+
const pages = ctx.pages();
|
|
486
|
+
for (const p of pages) {
|
|
487
|
+
const pUrl = p.url();
|
|
488
|
+
if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
|
|
489
|
+
targetPage = p;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (targetPage) break;
|
|
494
|
+
}
|
|
495
|
+
if (!targetPage && options?.cdpEndpoint) {
|
|
496
|
+
const targets = await getCDPTargets(options.cdpEndpoint);
|
|
497
|
+
const matchTarget = targets.find(
|
|
498
|
+
(t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (url ? t.url.includes(new URL(url).hostname) : true)
|
|
499
|
+
);
|
|
500
|
+
if (matchTarget && matchTarget.url) {
|
|
501
|
+
targetPage = await context.newPage();
|
|
502
|
+
await targetPage.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!targetPage) {
|
|
507
|
+
const pages = context.pages();
|
|
508
|
+
if (pages.length > 0) {
|
|
509
|
+
targetPage = pages[0];
|
|
510
|
+
} else {
|
|
511
|
+
targetPage = await context.newPage();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
page = targetPage;
|
|
515
|
+
} else {
|
|
516
|
+
context = await b.newContext({ viewport: { width: 1920, height: 1080 } });
|
|
517
|
+
page = await context.newPage();
|
|
518
|
+
}
|
|
519
|
+
if (url && page.url() !== url) {
|
|
520
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const session = {
|
|
524
|
+
id: randomUUID(),
|
|
525
|
+
name,
|
|
526
|
+
context,
|
|
527
|
+
page,
|
|
528
|
+
browser: b,
|
|
529
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
530
|
+
lastActivityAt: Date.now(),
|
|
531
|
+
isCDP,
|
|
532
|
+
cdpEndpoint: options?.cdpEndpoint
|
|
533
|
+
};
|
|
534
|
+
sessions.set(session.id, session);
|
|
535
|
+
logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
|
|
536
|
+
resetIdleTimer();
|
|
537
|
+
await installNetworkCapture(page, name);
|
|
538
|
+
return session;
|
|
539
|
+
}
|
|
540
|
+
async function closeSessionByName(name) {
|
|
541
|
+
for (const [id, session] of sessions) {
|
|
542
|
+
if (session.name === name || session.id === name) {
|
|
543
|
+
logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
|
|
544
|
+
if (session.isCDP) {
|
|
545
|
+
try {
|
|
546
|
+
await session.page.close();
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
if (session.browser) {
|
|
550
|
+
await session.browser.close().catch(() => {
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
await session.context.close();
|
|
555
|
+
if (session.browser) {
|
|
556
|
+
await session.browser.close().catch(() => {
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
sessions.delete(id);
|
|
561
|
+
const file2 = sessionFile(session.name);
|
|
562
|
+
try {
|
|
563
|
+
unlinkSync(file2);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const { networkStore, commandLogStore } = await import("./network-store-BN6QEZ7R.js");
|
|
568
|
+
networkStore.clear(session.name);
|
|
569
|
+
commandLogStore.clear(session.name);
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const { SessionRecorder } = await import("./session-recorder-XET3DNML.js");
|
|
574
|
+
SessionRecorder.cleanup(session.name);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const file = sessionFile(name);
|
|
581
|
+
try {
|
|
582
|
+
unlinkSync(file);
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
async function closeAllSessions() {
|
|
588
|
+
const names = [...sessions.values()].map((s) => `${s.name}(${s.page.url()})`).join(", ");
|
|
589
|
+
if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
|
|
590
|
+
for (const [id, session] of sessions) {
|
|
591
|
+
try {
|
|
592
|
+
if (!session.isCDP) {
|
|
593
|
+
await session.context.close();
|
|
594
|
+
}
|
|
595
|
+
if (session.browser) {
|
|
596
|
+
await session.browser.close().catch(() => {
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
sessions.delete(id);
|
|
600
|
+
} catch {
|
|
601
|
+
sessions.delete(id);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function destroyBrowser() {
|
|
606
|
+
logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all browsers.`);
|
|
607
|
+
if (idleTimer) {
|
|
608
|
+
clearTimeout(idleTimer);
|
|
609
|
+
idleTimer = null;
|
|
610
|
+
}
|
|
611
|
+
await closeAllSessions();
|
|
612
|
+
if (_sharedBrowser) {
|
|
613
|
+
await _sharedBrowser.close().catch(() => {
|
|
614
|
+
});
|
|
615
|
+
_sharedBrowser = null;
|
|
616
|
+
}
|
|
617
|
+
if (_sharedCdpProxy) {
|
|
618
|
+
await _sharedCdpProxy.stop().catch(() => {
|
|
619
|
+
});
|
|
620
|
+
_sharedCdpProxy = null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function resetForTesting() {
|
|
624
|
+
sessions.clear();
|
|
625
|
+
_sharedBrowser = null;
|
|
626
|
+
_sharedCdpProxy = null;
|
|
627
|
+
try {
|
|
628
|
+
for (const f of readdirSync(SESSION_DIR)) {
|
|
629
|
+
unlinkSync(join(SESSION_DIR, f));
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function ensureProcessCanExit() {
|
|
635
|
+
if (idleTimer) {
|
|
636
|
+
clearTimeout(idleTimer);
|
|
637
|
+
idleTimer = null;
|
|
638
|
+
}
|
|
639
|
+
for (const session of sessions.values()) {
|
|
640
|
+
if (session.browser) {
|
|
641
|
+
if (session.isCDP) {
|
|
642
|
+
await session.browser.close().catch(() => {
|
|
643
|
+
});
|
|
644
|
+
} else {
|
|
645
|
+
await session.browser.close().catch(() => {
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
sessions.clear();
|
|
651
|
+
if (_sharedBrowser) {
|
|
652
|
+
await _sharedBrowser.close().catch(() => {
|
|
653
|
+
});
|
|
654
|
+
_sharedBrowser = null;
|
|
655
|
+
}
|
|
656
|
+
if (_sharedCdpProxy) {
|
|
657
|
+
await _sharedCdpProxy.stop().catch(() => {
|
|
658
|
+
});
|
|
659
|
+
_sharedCdpProxy = null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export {
|
|
664
|
+
touchSession,
|
|
665
|
+
findTargetPage,
|
|
666
|
+
resolveLaunchOpts,
|
|
667
|
+
createBrowser,
|
|
668
|
+
getBrowser,
|
|
669
|
+
findSession,
|
|
670
|
+
getSessionById,
|
|
671
|
+
setActivePage,
|
|
672
|
+
saveSessionDiskMeta,
|
|
673
|
+
readSessionDiskMeta,
|
|
674
|
+
deleteSessionDiskMeta,
|
|
675
|
+
findOrRestoreSession,
|
|
676
|
+
createEphemeralContext,
|
|
677
|
+
closeEphemeralContext,
|
|
678
|
+
getAllSessions,
|
|
679
|
+
createSession,
|
|
680
|
+
closeSessionByName,
|
|
681
|
+
closeAllSessions,
|
|
682
|
+
destroyBrowser,
|
|
683
|
+
resetForTesting,
|
|
684
|
+
ensureProcessCanExit
|
|
685
|
+
};
|