libretto 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +112 -137
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +151 -206
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/browserbase.js +1 -0
- package/dist/cli/core/providers/kernel.js +1 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -7
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +139 -187
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +182 -245
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/browserbase.ts +1 -0
- package/src/cli/core/providers/kernel.ts +1 -0
- package/src/cli/core/providers/libretto-cloud.ts +13 -7
- package/src/cli/core/providers/types.ts +12 -1
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
package/dist/cli/core/browser.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
chromium
|
|
3
3
|
} from "playwright";
|
|
4
|
-
import {
|
|
4
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
6
|
import { createServer } from "node:net";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
7
|
import { PROFILES_DIR } from "./context.js";
|
|
11
8
|
import { readLibrettoConfig } from "./config.js";
|
|
12
9
|
import {
|
|
@@ -20,6 +17,7 @@ import {
|
|
|
20
17
|
writeSessionState
|
|
21
18
|
} from "./session.js";
|
|
22
19
|
import { getCloudProviderApi } from "./providers/index.js";
|
|
20
|
+
import { DaemonClient, spawnSessionDaemon } from "./daemon/index.js";
|
|
23
21
|
const CLOSE_WAIT_MS = 1500;
|
|
24
22
|
const FORCE_CLOSE_WAIT_MS = 300;
|
|
25
23
|
async function pickFreePort() {
|
|
@@ -143,20 +141,6 @@ async function resolvePageReferences(pages) {
|
|
|
143
141
|
);
|
|
144
142
|
return refs;
|
|
145
143
|
}
|
|
146
|
-
async function listOpenPages(session, logger) {
|
|
147
|
-
const { browser, page: activePage } = await connect(session, logger);
|
|
148
|
-
try {
|
|
149
|
-
const pages = browser.contexts().flatMap((ctx) => ctx.pages()).filter(isOperationalPage);
|
|
150
|
-
const pageRefs = await resolvePageReferences(pages);
|
|
151
|
-
return pageRefs.map(({ id, page }) => ({
|
|
152
|
-
id,
|
|
153
|
-
url: page.url(),
|
|
154
|
-
active: page === activePage
|
|
155
|
-
}));
|
|
156
|
-
} finally {
|
|
157
|
-
disconnectBrowser(browser, logger, session);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
144
|
async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
161
145
|
logger.info("connect", { session, timeoutMs });
|
|
162
146
|
const state = readSessionStateOrThrow(session);
|
|
@@ -242,7 +226,15 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
|
242
226
|
}
|
|
243
227
|
async function runPages(session, logger) {
|
|
244
228
|
logger.info("pages-start", { session });
|
|
245
|
-
const
|
|
229
|
+
const state = readSessionStateOrThrow(session);
|
|
230
|
+
let pageSummaries;
|
|
231
|
+
if (!state.daemonSocketPath) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Session "${session}" has no daemon socket. The browser daemon may have crashed. Close and reopen the session: libretto close --session ${session}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const client = new DaemonClient(state.daemonSocketPath);
|
|
237
|
+
pageSummaries = await client.pages();
|
|
246
238
|
if (pageSummaries.length === 0) {
|
|
247
239
|
console.log("No pages found.");
|
|
248
240
|
return;
|
|
@@ -302,8 +294,17 @@ async function runOpen(rawUrl, headed, session, logger, options) {
|
|
|
302
294
|
const port = await pickFreePort();
|
|
303
295
|
const runLogPath = logFileForSession(session);
|
|
304
296
|
const browserMode = headed ? "headed" : "headless";
|
|
297
|
+
const authDomain = options?.authProfileDomain ? normalizeDomain(normalizeUrl(options.authProfileDomain)) : void 0;
|
|
298
|
+
if (authDomain) {
|
|
299
|
+
const authProfilePath = getProfilePath(authDomain);
|
|
300
|
+
if (!existsSync(authProfilePath)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`No saved auth profile for "${authDomain}". Save one first: libretto open https://${authDomain} --headed --session <name>, log in, then run: libretto save ${authDomain} --session <name>`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
305
306
|
const supportsSavedProfile = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
306
|
-
const domain = supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0;
|
|
307
|
+
const domain = authDomain ?? (supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0);
|
|
307
308
|
const profilePath = domain ? getProfilePath(domain) : void 0;
|
|
308
309
|
const useProfile = domain ? hasProfile(domain) : false;
|
|
309
310
|
logger.info("open-launching", {
|
|
@@ -319,106 +320,46 @@ async function runOpen(rawUrl, headed, session, logger, options) {
|
|
|
319
320
|
console.log(`Loading saved profile for ${domain}`);
|
|
320
321
|
}
|
|
321
322
|
console.log(`Launching ${browserMode} browser (session: ${session})...`);
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
const { pid, socketPath: daemonSocketPath } = await spawnSessionDaemon({
|
|
324
|
+
config: {
|
|
325
|
+
port,
|
|
326
|
+
url,
|
|
327
|
+
session,
|
|
328
|
+
headed,
|
|
329
|
+
viewport,
|
|
330
|
+
storageStatePath: useProfile ? profilePath : void 0,
|
|
331
|
+
windowPosition
|
|
332
|
+
},
|
|
330
333
|
session,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
process.execPath,
|
|
339
|
-
["--import", tsxImportPath, daemonEntryPath, JSON.stringify(daemonConfig)],
|
|
340
|
-
{
|
|
341
|
-
detached: true,
|
|
342
|
-
stdio: ["ignore", "ignore", childStderrFd]
|
|
343
|
-
}
|
|
344
|
-
);
|
|
345
|
-
child.unref();
|
|
346
|
-
closeSync(childStderrFd);
|
|
347
|
-
logger.info("open-child-spawned", { pid: child.pid, port, session });
|
|
348
|
-
let childSpawnError = null;
|
|
349
|
-
let childEarlyExit = null;
|
|
350
|
-
child.on("error", (err) => {
|
|
351
|
-
childSpawnError = err;
|
|
352
|
-
logger.error("open-child-spawn-error", { error: err, session, port });
|
|
334
|
+
logger,
|
|
335
|
+
logPath: runLogPath,
|
|
336
|
+
// The daemon launches Chromium, installs telemetry, navigates to
|
|
337
|
+
// the URL, and only then starts IPC. Navigation alone can take up
|
|
338
|
+
// to 45s (page.setDefaultNavigationTimeout), so the IPC timeout
|
|
339
|
+
// must cover launch + navigation.
|
|
340
|
+
ipcTimeoutMs: 6e4
|
|
353
341
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
logger.warn("open-child-exited", {
|
|
357
|
-
code,
|
|
358
|
-
signal,
|
|
359
|
-
session,
|
|
342
|
+
writeSessionState(
|
|
343
|
+
{
|
|
360
344
|
port,
|
|
361
|
-
pid
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
const earlyExit = childEarlyExit;
|
|
377
|
-
if (earlyExit !== null) {
|
|
378
|
-
const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
|
|
379
|
-
throw new Error(
|
|
380
|
-
`Browser child process exited before startup (status: ${status}). Check logs: ${runLogPath}`
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
await new Promise((r) => setTimeout(r, cdpPollIntervalMs));
|
|
384
|
-
const ready = await fetch(`http://127.0.0.1:${port}/json/version`).then(() => true).catch(() => false);
|
|
385
|
-
if (i > 0 && i % 5 === 0) {
|
|
386
|
-
logger.info("open-waiting-for-cdp", { attempt: i, port, session });
|
|
387
|
-
}
|
|
388
|
-
if (ready) {
|
|
389
|
-
writeSessionState(
|
|
390
|
-
{
|
|
391
|
-
port,
|
|
392
|
-
pid: child.pid,
|
|
393
|
-
session,
|
|
394
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
395
|
-
status: "active",
|
|
396
|
-
mode: accessMode,
|
|
397
|
-
viewport
|
|
398
|
-
},
|
|
399
|
-
logger
|
|
400
|
-
);
|
|
401
|
-
logger.info("open-success", {
|
|
402
|
-
url,
|
|
403
|
-
mode: browserMode,
|
|
404
|
-
session,
|
|
405
|
-
port,
|
|
406
|
-
pid: child.pid
|
|
407
|
-
});
|
|
408
|
-
console.log(`Browser open (${browserMode}): ${url}`);
|
|
409
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
logger.error("open-timeout", {
|
|
345
|
+
pid,
|
|
346
|
+
session,
|
|
347
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
+
status: "active",
|
|
349
|
+
mode: accessMode,
|
|
350
|
+
viewport,
|
|
351
|
+
daemonSocketPath
|
|
352
|
+
},
|
|
353
|
+
logger
|
|
354
|
+
);
|
|
355
|
+
logger.info("open-success", {
|
|
356
|
+
url,
|
|
357
|
+
mode: browserMode,
|
|
414
358
|
session,
|
|
415
359
|
port,
|
|
416
|
-
pid
|
|
417
|
-
attempts: cdpMaxAttempts
|
|
360
|
+
pid
|
|
418
361
|
});
|
|
419
|
-
|
|
420
|
-
`Failed to connect to browser after ${Math.ceil(cdpStartupTimeoutMs / 1e3)}s. Check startup logs: ${runLogPath}`
|
|
421
|
-
);
|
|
362
|
+
console.log(`Browser open (${browserMode}): ${url}`);
|
|
422
363
|
}
|
|
423
364
|
async function runOpenWithProvider(rawUrl, providerName, provider, session, logger, accessMode = "write-access") {
|
|
424
365
|
const parsedUrl = normalizeUrl(rawUrl);
|
|
@@ -431,67 +372,45 @@ async function runOpenWithProvider(rawUrl, providerName, provider, session, logg
|
|
|
431
372
|
logger.info("open-provider-session-created", {
|
|
432
373
|
provider: providerName,
|
|
433
374
|
sessionId: providerSession.sessionId,
|
|
434
|
-
cdpEndpoint: providerSession.cdpEndpoint
|
|
375
|
+
cdpEndpoint: providerSession.cdpEndpoint,
|
|
376
|
+
liveViewUrl: providerSession.liveViewUrl
|
|
435
377
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
browser = await tryConnectToCDP(
|
|
440
|
-
providerSession.cdpEndpoint,
|
|
441
|
-
logger,
|
|
442
|
-
3e4
|
|
443
|
-
);
|
|
444
|
-
if (!browser) {
|
|
445
|
-
throw new Error(
|
|
446
|
-
`Could not connect to ${providerName} browser at ${providerSession.cdpEndpoint}. The remote session was created but CDP connection failed.`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
const contexts = browser.contexts();
|
|
450
|
-
let page;
|
|
451
|
-
if (contexts.length > 0 && contexts[0].pages().length > 0) {
|
|
452
|
-
page = contexts[0].pages()[0];
|
|
453
|
-
} else {
|
|
454
|
-
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
455
|
-
page = await context.newPage();
|
|
456
|
-
}
|
|
457
|
-
await page.goto(url);
|
|
458
|
-
logger.info("open-provider-navigated", { url, session });
|
|
459
|
-
writeSessionState(
|
|
460
|
-
{
|
|
461
|
-
port: 0,
|
|
462
|
-
cdpEndpoint: providerSession.cdpEndpoint,
|
|
463
|
-
session,
|
|
464
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
465
|
-
status: "active",
|
|
466
|
-
mode: accessMode,
|
|
467
|
-
provider: {
|
|
468
|
-
name: providerName,
|
|
469
|
-
sessionId: providerSession.sessionId
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
logger
|
|
473
|
-
);
|
|
474
|
-
disconnectBrowser(browser, logger, session);
|
|
475
|
-
} catch (err) {
|
|
476
|
-
if (browser) {
|
|
477
|
-
disconnectBrowser(browser, logger, session);
|
|
478
|
-
}
|
|
479
|
-
logger.warn("open-provider-cleanup-after-error", {
|
|
480
|
-
provider: providerName,
|
|
481
|
-
sessionId: providerSession.sessionId,
|
|
482
|
-
error: err
|
|
483
|
-
});
|
|
484
|
-
try {
|
|
485
|
-
await provider.closeSession(providerSession.sessionId);
|
|
486
|
-
} catch (cleanupErr) {
|
|
487
|
-
logger.warn("open-provider-cleanup-failed", {
|
|
488
|
-
provider: providerName,
|
|
489
|
-
sessionId: providerSession.sessionId,
|
|
490
|
-
error: cleanupErr
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
throw err;
|
|
378
|
+
if (providerSession.liveViewUrl) {
|
|
379
|
+
console.log(`View live session: ${providerSession.liveViewUrl}`);
|
|
494
380
|
}
|
|
381
|
+
console.log(`Connecting to ${providerName} browser...`);
|
|
382
|
+
const runLogPath = logFileForSession(session);
|
|
383
|
+
const { pid, socketPath: daemonSocketPath } = await spawnSessionDaemon({
|
|
384
|
+
config: {
|
|
385
|
+
mode: "connect",
|
|
386
|
+
session,
|
|
387
|
+
cdpEndpoint: providerSession.cdpEndpoint,
|
|
388
|
+
url
|
|
389
|
+
},
|
|
390
|
+
session,
|
|
391
|
+
logger,
|
|
392
|
+
logPath: runLogPath,
|
|
393
|
+
// Remote CDP connection + navigation; must cover both.
|
|
394
|
+
ipcTimeoutMs: 6e4,
|
|
395
|
+
onFailure: () => provider.closeSession(providerSession.sessionId)
|
|
396
|
+
});
|
|
397
|
+
writeSessionState(
|
|
398
|
+
{
|
|
399
|
+
port: 0,
|
|
400
|
+
pid,
|
|
401
|
+
cdpEndpoint: providerSession.cdpEndpoint,
|
|
402
|
+
session,
|
|
403
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
404
|
+
status: "active",
|
|
405
|
+
mode: accessMode,
|
|
406
|
+
daemonSocketPath,
|
|
407
|
+
provider: {
|
|
408
|
+
name: providerName,
|
|
409
|
+
sessionId: providerSession.sessionId
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
logger
|
|
413
|
+
);
|
|
495
414
|
logger.info("open-provider-success", {
|
|
496
415
|
url,
|
|
497
416
|
provider: providerName,
|
|
@@ -572,6 +491,12 @@ async function runClose(session, logger) {
|
|
|
572
491
|
console.log(`No browser running for session "${session}".`);
|
|
573
492
|
return;
|
|
574
493
|
}
|
|
494
|
+
if (state.pid != null) {
|
|
495
|
+
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
496
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
497
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
498
|
+
}
|
|
499
|
+
let replayUrl;
|
|
575
500
|
if (state.provider) {
|
|
576
501
|
logger.info("close-provider", {
|
|
577
502
|
session,
|
|
@@ -580,7 +505,8 @@ async function runClose(session, logger) {
|
|
|
580
505
|
});
|
|
581
506
|
try {
|
|
582
507
|
const provider = getCloudProviderApi(state.provider.name);
|
|
583
|
-
await provider.closeSession(state.provider.sessionId);
|
|
508
|
+
const result = await provider.closeSession(state.provider.sessionId);
|
|
509
|
+
replayUrl = result.replayUrl;
|
|
584
510
|
} catch (err) {
|
|
585
511
|
logger.warn("close-provider-error", {
|
|
586
512
|
session,
|
|
@@ -593,16 +519,14 @@ async function runClose(session, logger) {
|
|
|
593
519
|
`Failed to close remote ${state.provider.name} session "${state.provider.sessionId}" for session "${session}". State preserved with status "cleanup-failed". Retry with: libretto close --session ${session}`
|
|
594
520
|
);
|
|
595
521
|
}
|
|
596
|
-
} else {
|
|
597
|
-
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
598
|
-
if (state.pid != null) {
|
|
599
|
-
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
600
|
-
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
601
|
-
}
|
|
602
522
|
}
|
|
523
|
+
unlinkDaemonSocket(state.daemonSocketPath, logger, session);
|
|
603
524
|
clearSessionState(session, logger);
|
|
604
|
-
logger.info("close-success", { session });
|
|
525
|
+
logger.info("close-success", { session, replayUrl });
|
|
605
526
|
console.log(`Browser closed (session: ${session}).`);
|
|
527
|
+
if (replayUrl) {
|
|
528
|
+
console.log(`View recording: ${replayUrl}`);
|
|
529
|
+
}
|
|
606
530
|
}
|
|
607
531
|
function waitForCloseSignalWindow(ms) {
|
|
608
532
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -641,16 +565,32 @@ function resolveClosableSessions(logger) {
|
|
|
641
565
|
session,
|
|
642
566
|
pid: state.pid,
|
|
643
567
|
port: state.port,
|
|
644
|
-
provider: state.provider
|
|
568
|
+
provider: state.provider,
|
|
569
|
+
daemonSocketPath: state.daemonSocketPath
|
|
645
570
|
});
|
|
646
571
|
}
|
|
647
572
|
return { closable, clearedUnreadableStates };
|
|
648
573
|
}
|
|
574
|
+
function unlinkDaemonSocket(socketPath, logger, session) {
|
|
575
|
+
if (!socketPath) return;
|
|
576
|
+
try {
|
|
577
|
+
unlinkSync(socketPath);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
if (err.code !== "ENOENT") {
|
|
580
|
+
logger.warn("close-socket-unlink-failed", {
|
|
581
|
+
session,
|
|
582
|
+
socketPath,
|
|
583
|
+
error: err
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
649
588
|
function clearStoppedSessionStates(sessions, logger, skip) {
|
|
650
589
|
let cleared = 0;
|
|
651
590
|
for (const session of sessions) {
|
|
652
591
|
if (skip?.has(session.session)) continue;
|
|
653
592
|
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
593
|
+
unlinkDaemonSocket(session.daemonSocketPath, logger, session.session);
|
|
654
594
|
clearSessionState(session.session, logger);
|
|
655
595
|
cleared += 1;
|
|
656
596
|
}
|
|
@@ -671,6 +611,7 @@ async function runCloseAll(logger, options) {
|
|
|
671
611
|
return;
|
|
672
612
|
}
|
|
673
613
|
const failedProviderSessions = /* @__PURE__ */ new Set();
|
|
614
|
+
const replayUrls = [];
|
|
674
615
|
for (const target of closable) {
|
|
675
616
|
if (target.provider) {
|
|
676
617
|
logger.info("close-all-provider", {
|
|
@@ -680,7 +621,13 @@ async function runCloseAll(logger, options) {
|
|
|
680
621
|
});
|
|
681
622
|
try {
|
|
682
623
|
const provider = getCloudProviderApi(target.provider.name);
|
|
683
|
-
await provider.closeSession(target.provider.sessionId);
|
|
624
|
+
const result = await provider.closeSession(target.provider.sessionId);
|
|
625
|
+
if (result.replayUrl) {
|
|
626
|
+
replayUrls.push({
|
|
627
|
+
session: target.session,
|
|
628
|
+
replayUrl: result.replayUrl
|
|
629
|
+
});
|
|
630
|
+
}
|
|
684
631
|
} catch (err) {
|
|
685
632
|
logger.warn("close-all-provider-error", {
|
|
686
633
|
session: target.session,
|
|
@@ -697,20 +644,18 @@ async function runCloseAll(logger, options) {
|
|
|
697
644
|
}
|
|
698
645
|
}
|
|
699
646
|
for (const target of closable) {
|
|
700
|
-
if (target.
|
|
647
|
+
if (target.pid == null) continue;
|
|
701
648
|
logger.info("close-all-sigterm", {
|
|
702
649
|
session: target.session,
|
|
703
650
|
pid: target.pid,
|
|
704
651
|
port: target.port
|
|
705
652
|
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
);
|
|
713
|
-
}
|
|
653
|
+
sendSignalToProcessGroupOrPid(
|
|
654
|
+
target.pid,
|
|
655
|
+
"SIGTERM",
|
|
656
|
+
logger,
|
|
657
|
+
target.session
|
|
658
|
+
);
|
|
714
659
|
}
|
|
715
660
|
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
716
661
|
let survivors = closable.filter(
|
|
@@ -781,6 +726,9 @@ async function runCloseAll(logger, options) {
|
|
|
781
726
|
if (forceKilled > 0) {
|
|
782
727
|
console.log(`Force-killed ${forceKilled} session(s).`);
|
|
783
728
|
}
|
|
729
|
+
for (const { session, replayUrl } of replayUrls) {
|
|
730
|
+
console.log(`View recording (${session}): ${replayUrl}`);
|
|
731
|
+
}
|
|
784
732
|
}
|
|
785
733
|
async function runConnect(cdpUrl, session, logger, accessMode = "write-access") {
|
|
786
734
|
logger.info("connect-start", { cdpUrl, session, accessMode });
|
|
@@ -826,36 +774,34 @@ async function runConnect(cdpUrl, session, logger, accessMode = "write-access")
|
|
|
826
774
|
endpoint
|
|
827
775
|
});
|
|
828
776
|
}
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
`CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
const pages = resolveOperationalPages(browser);
|
|
836
|
-
logger.info("connect-pages", {
|
|
777
|
+
const runLogPath = logFileForSession(session);
|
|
778
|
+
const { pid, socketPath: daemonSocketPath, client } = await spawnSessionDaemon({
|
|
779
|
+
config: { mode: "connect", session, cdpEndpoint: endpoint },
|
|
837
780
|
session,
|
|
838
|
-
|
|
839
|
-
|
|
781
|
+
logger,
|
|
782
|
+
logPath: runLogPath,
|
|
783
|
+
ipcTimeoutMs: 1e4
|
|
840
784
|
});
|
|
841
|
-
disconnectBrowser(browser, logger, session);
|
|
842
785
|
writeSessionState(
|
|
843
786
|
{
|
|
844
787
|
port,
|
|
788
|
+
pid,
|
|
845
789
|
cdpEndpoint: endpoint,
|
|
846
790
|
session,
|
|
847
791
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
792
|
status: "active",
|
|
849
|
-
mode: accessMode
|
|
793
|
+
mode: accessMode,
|
|
794
|
+
daemonSocketPath
|
|
850
795
|
},
|
|
851
796
|
logger
|
|
852
797
|
);
|
|
798
|
+
const pages = await client.pages();
|
|
853
799
|
logger.info("connect-success", { cdpUrl: endpoint, session, port });
|
|
854
800
|
console.log(`Connected to ${endpoint} (session: ${session})`);
|
|
855
801
|
console.log(` Pages found: ${pages.length}`);
|
|
856
802
|
if (pages.length > 0) {
|
|
857
803
|
for (const p of pages.slice(0, 5)) {
|
|
858
|
-
console.log(` ${p.url
|
|
804
|
+
console.log(` ${p.url}`);
|
|
859
805
|
}
|
|
860
806
|
if (pages.length > 5) {
|
|
861
807
|
console.log(` ... and ${pages.length - 5} more`);
|
|
@@ -876,7 +822,6 @@ export {
|
|
|
876
822
|
getProfilePath,
|
|
877
823
|
getScreenshotBaseName,
|
|
878
824
|
hasProfile,
|
|
879
|
-
listOpenPages,
|
|
880
825
|
normalizeDomain,
|
|
881
826
|
normalizeUrl,
|
|
882
827
|
resolvePath,
|