@crouton-kit/capture 1.4.27 → 1.4.28
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/bin/capture +24 -13
- package/package.json +1 -1
- package/src/cdp/browser-process.ts +33 -7
- package/src/cdp/commands/tab/launch.ts +2 -2
- package/src/cdp/commands/tab/list.ts +1 -1
package/bin/capture
CHANGED
|
@@ -5654,8 +5654,12 @@ function forgetRecordFile(file) {
|
|
|
5654
5654
|
}
|
|
5655
5655
|
}
|
|
5656
5656
|
function stillOurs(record5) {
|
|
5657
|
+
return processOwnership(record5) === "ours";
|
|
5658
|
+
}
|
|
5659
|
+
function processOwnership(record5) {
|
|
5657
5660
|
const observed = processPidBirthProvider.read(record5.pid);
|
|
5658
|
-
|
|
5661
|
+
if (observed.status === "unknown") return "unknown";
|
|
5662
|
+
return observed.status === "found" && sameBirth(observed.identity, record5.birth) ? "ours" : "gone";
|
|
5659
5663
|
}
|
|
5660
5664
|
function removeProfile(profileDir) {
|
|
5661
5665
|
fs2.rmSync(assertUnderCaptureRoot(profileDir), { recursive: true, force: true });
|
|
@@ -5677,26 +5681,33 @@ async function reap(record5) {
|
|
|
5677
5681
|
forgetRecordFile(recordPath(record5.token));
|
|
5678
5682
|
removeProfile(record5.profileDir);
|
|
5679
5683
|
}
|
|
5680
|
-
async function
|
|
5684
|
+
async function probeEndpoint(port) {
|
|
5681
5685
|
try {
|
|
5682
5686
|
const response = await fetch(`http://${CDP_LOOPBACK_HOST}:${port}/json/version`, {
|
|
5683
5687
|
signal: AbortSignal.timeout(ENDPOINT_PROBE_MS)
|
|
5684
5688
|
});
|
|
5685
|
-
return response.ok;
|
|
5686
|
-
} catch {
|
|
5687
|
-
|
|
5689
|
+
return response.ok ? "up" : "down";
|
|
5690
|
+
} catch (error4) {
|
|
5691
|
+
const code = error4.cause?.code;
|
|
5692
|
+
return code === "ECONNREFUSED" ? "down" : "unknown";
|
|
5688
5693
|
}
|
|
5689
5694
|
}
|
|
5695
|
+
async function endpointAnswers(port) {
|
|
5696
|
+
return await probeEndpoint(port) === "up";
|
|
5697
|
+
}
|
|
5690
5698
|
async function sweepOwnedBrowsers() {
|
|
5691
5699
|
const reaped = [];
|
|
5692
5700
|
for (const record5 of readOwnedBrowsers()) {
|
|
5693
|
-
|
|
5701
|
+
const ownership2 = processOwnership(record5);
|
|
5702
|
+
if (ownership2 === "unknown") continue;
|
|
5703
|
+
if (ownership2 === "gone") {
|
|
5694
5704
|
forgetRecordFile(recordPath(record5.token));
|
|
5695
5705
|
removeProfile(record5.profileDir);
|
|
5696
5706
|
reaped.push({ port: record5.port, pid: record5.pid, reason: "exited" });
|
|
5697
5707
|
continue;
|
|
5698
5708
|
}
|
|
5699
|
-
if (record5.port === null
|
|
5709
|
+
if (record5.port === null && Date.now() - Date.parse(record5.startedAt) <= READY_TIMEOUT_MS + ENDPOINT_PROBE_MS) continue;
|
|
5710
|
+
if (record5.port === null || await probeEndpoint(record5.port) === "down") {
|
|
5700
5711
|
await reap(record5);
|
|
5701
5712
|
reaped.push({ port: record5.port, pid: record5.pid, reason: "unreachable" });
|
|
5702
5713
|
continue;
|
|
@@ -10969,7 +10980,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
10969
10980
|
return { entries, incompleteLifecycles };
|
|
10970
10981
|
}
|
|
10971
10982
|
function captureVersion() {
|
|
10972
|
-
const version = "1.4.
|
|
10983
|
+
const version = "1.4.28";
|
|
10973
10984
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
10974
10985
|
}
|
|
10975
10986
|
function token() {
|
|
@@ -13071,7 +13082,7 @@ function wallTime(value) {
|
|
|
13071
13082
|
}
|
|
13072
13083
|
}
|
|
13073
13084
|
function captureVersion2() {
|
|
13074
|
-
const version = "1.4.
|
|
13085
|
+
const version = "1.4.28";
|
|
13075
13086
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
13076
13087
|
}
|
|
13077
13088
|
function deepFreeze(value) {
|
|
@@ -230406,7 +230417,7 @@ function buildBrowserLaunchedResult(browser, swept) {
|
|
|
230406
230417
|
line(text`mode `, data(browser.headless ? "headless" : "headed"))
|
|
230407
230418
|
])
|
|
230408
230419
|
];
|
|
230409
|
-
if (swept > 0) sections.push(fact`swept ${swept} previously-launched browser(s) that had exited,
|
|
230420
|
+
if (swept > 0) sections.push(fact`swept ${swept} previously-launched browser(s) that had exited, refused CDP connections, or gone idle.`);
|
|
230410
230421
|
return {
|
|
230411
230422
|
tag: "browser-launched",
|
|
230412
230423
|
attrs: { port: browser.port, pid: browser.pid, headless: browser.headless },
|
|
@@ -230454,7 +230465,7 @@ input:
|
|
|
230454
230465
|
--headed run with a visible window instead of headless
|
|
230455
230466
|
|
|
230456
230467
|
output: <browser-launched port=\u2026 pid=\u2026> \u2014 the port to pass as --port (or CDP_PORT), the pid capture owns, and the executable that answered with its provenance (CAPTURE_BROWSER, puppeteer-cache, or system).
|
|
230457
|
-
effects: spawns a detached browser with a private profile under CAPTURE_ROOT, and registers it as capture-owned. Capture stops it on \`capture tab quit\`, or on the first later capture invocation once it has been unused for ${IDLE_REAP_MS / 6e4} minutes; a browser capture did not start is never signalled. Also sweeps previously-launched browsers that have exited,
|
|
230468
|
+
effects: spawns a detached browser with a private profile under CAPTURE_ROOT, and registers it as capture-owned. Capture stops it on \`capture tab quit\`, or on the first later capture invocation once it has been unused for ${IDLE_REAP_MS / 6e4} minutes; a browser capture did not start is never signalled. Also sweeps previously-launched browsers that have exited, refused CDP connections, or gone idle.`;
|
|
230458
230469
|
}
|
|
230459
230470
|
});
|
|
230460
230471
|
|
|
@@ -230632,7 +230643,7 @@ input:
|
|
|
230632
230643
|
--port <port> probe one endpoint only, instead of discovering every localhost CDP endpoint; an active session's endpoint does not narrow it (CDP_PORT still pins one endpoint)
|
|
230633
230644
|
|
|
230634
230645
|
output: <tabs endpoints=\u2026 tabs=\u2026> \u2014 one section per reachable endpoint (port, app, page-tab count, preferred marker, and a capture-owned pid marker on any browser \`tab launch\` started) listing each page tab's target id (first 8 chars, enough for --target), title, and url; an endpoint that failed the tab fetch is an endpoints-unreachable fact, never a silent omission.
|
|
230635
|
-
effects: probes local CDP endpoints over HTTP/WebSocket; no page-observable writes. Also sweeps capture-launched browsers, stopping any that have exited,
|
|
230646
|
+
effects: probes local CDP endpoints over HTTP/WebSocket; no page-observable writes. Also sweeps capture-launched browsers, stopping any that have exited, refused CDP connections, or gone idle \u2014 browsers capture did not start are never signalled.`;
|
|
230636
230647
|
ARC_ENDPOINT_NOTE = text` note: Arc exposes only tabs opened over CDP as page targets; the tabs in its windows are not listed. Open one with "capture tab open <url> --port <port>", then "capture session start --port <port> --target <id>".`;
|
|
230637
230648
|
}
|
|
230638
230649
|
});
|
|
@@ -267641,7 +267652,7 @@ I/O contract: flags and positionals on input; one rendered prose block on stdout
|
|
|
267641
267652
|
A CDP-enabled browser must be running; \`capture tab list\` is the probe, and \`capture tab launch\` starts one capture owns and reaps when nothing is (never hand-roll a detached browser \u2014 nothing reaps that). Capture never signals a browser it did not start, so attaching to your own with --port is unchanged. CDP_PORT / CDP_TARGET pin the browser + tab for target-selecting commands \u2014 precedence: explicit flag > active session > env. \`session start\` adopts only an explicit --target. When CRTR_NODE_ID is set (a crtr agent node), a browser command with no --port, no --target and no active session of that node is refused instead of falling back to CDP_PORT, CDP_TARGET or localhost discovery, because the browser found could belong to another node: run \`capture session start --url <url>\`, or pass --port or --target. \`capture tab list\` and commands that only read a recorded artifact are not refused.`;
|
|
267642
267653
|
}
|
|
267643
267654
|
function printVersion() {
|
|
267644
|
-
const declared = "1.4.
|
|
267655
|
+
const declared = "1.4.28";
|
|
267645
267656
|
if (declared) {
|
|
267646
267657
|
console.log(declared);
|
|
267647
267658
|
return;
|
package/package.json
CHANGED
|
@@ -191,8 +191,16 @@ function forgetRecordFile(file: string): void {
|
|
|
191
191
|
* ONLY gate that authorizes a signal; an `unknown` read (a provider that could
|
|
192
192
|
* not answer) is deliberately NOT a kill authorization. */
|
|
193
193
|
function stillOurs(record: OwnedBrowser): boolean {
|
|
194
|
+
return processOwnership(record) === 'ours';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `gone` — the process exited, or its pid was recycled by a stranger; `unknown` —
|
|
198
|
+
* the provider could not answer (e.g. its `sysctl` read timed out on a loaded
|
|
199
|
+
* host), which says nothing about whether the browser is alive. */
|
|
200
|
+
function processOwnership(record: OwnedBrowser): 'ours' | 'gone' | 'unknown' {
|
|
194
201
|
const observed = processPidBirthProvider.read(record.pid);
|
|
195
|
-
|
|
202
|
+
if (observed.status === 'unknown') return 'unknown';
|
|
203
|
+
return observed.status === 'found' && sameBirth(observed.identity, record.birth) ? 'ours' : 'gone';
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
/**
|
|
@@ -229,17 +237,26 @@ async function reap(record: OwnedBrowser): Promise<void> {
|
|
|
229
237
|
removeProfile(record.profileDir);
|
|
230
238
|
}
|
|
231
239
|
|
|
232
|
-
|
|
240
|
+
/** `up` — answered; `down` — the connection was refused or the answer was an
|
|
241
|
+
* error, so nothing is serving CDP there; `unknown` — no answer within the probe
|
|
242
|
+
* window or the request failed some other way. A browser starved of CPU on a
|
|
243
|
+
* loaded host answers slowly, so `unknown` is never evidence it is unusable. */
|
|
244
|
+
async function probeEndpoint(port: number): Promise<'up' | 'down' | 'unknown'> {
|
|
233
245
|
try {
|
|
234
246
|
const response = await fetch(`http://${CDP_LOOPBACK_HOST}:${port}/json/version`, {
|
|
235
247
|
signal: AbortSignal.timeout(ENDPOINT_PROBE_MS),
|
|
236
248
|
});
|
|
237
|
-
return response.ok;
|
|
238
|
-
} catch {
|
|
239
|
-
|
|
249
|
+
return response.ok ? 'up' : 'down';
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const code = (error as { cause?: { code?: unknown } }).cause?.code;
|
|
252
|
+
return code === 'ECONNREFUSED' ? 'down' : 'unknown';
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
|
|
256
|
+
async function endpointAnswers(port: number): Promise<boolean> {
|
|
257
|
+
return (await probeEndpoint(port)) === 'up';
|
|
258
|
+
}
|
|
259
|
+
|
|
243
260
|
/**
|
|
244
261
|
* Reconciles the registry with reality and reaps what no longer earns its
|
|
245
262
|
* memory. Run at the top of every `tab` leaf that touches owned browsers — this
|
|
@@ -249,7 +266,12 @@ async function endpointAnswers(port: number): Promise<boolean> {
|
|
|
249
266
|
export async function sweepOwnedBrowsers(): Promise<ReapedBrowser[]> {
|
|
250
267
|
const reaped: ReapedBrowser[] = [];
|
|
251
268
|
for (const record of readOwnedBrowsers()) {
|
|
252
|
-
|
|
269
|
+
const ownership = processOwnership(record);
|
|
270
|
+
// An unreadable process identity proves nothing: removing the profile of a
|
|
271
|
+
// browser that is in fact running would pull its user-data-dir out from
|
|
272
|
+
// under it. Leave the record for a sweep that can read it.
|
|
273
|
+
if (ownership === 'unknown') continue;
|
|
274
|
+
if (ownership === 'gone') {
|
|
253
275
|
// Crashed, or SIGKILLed by someone else. Nothing to signal — just drop
|
|
254
276
|
// the claim and the profile it left behind.
|
|
255
277
|
forgetRecordFile(recordPath(record.token));
|
|
@@ -257,7 +279,11 @@ export async function sweepOwnedBrowsers(): Promise<ReapedBrowser[]> {
|
|
|
257
279
|
reaped.push({ port: record.port, pid: record.pid, reason: 'exited' });
|
|
258
280
|
continue;
|
|
259
281
|
}
|
|
260
|
-
|
|
282
|
+
// A record with no port is a launch in flight in another capture process
|
|
283
|
+
// (the registry is shared by every agent on the host), not a dead browser —
|
|
284
|
+
// until the launch's own readiness deadline has passed.
|
|
285
|
+
if (record.port === null && Date.now() - Date.parse(record.startedAt) <= READY_TIMEOUT_MS + ENDPOINT_PROBE_MS) continue;
|
|
286
|
+
if (record.port === null || (await probeEndpoint(record.port)) === 'down') {
|
|
261
287
|
await reap(record);
|
|
262
288
|
reaped.push({ port: record.port, pid: record.pid, reason: 'unreachable' });
|
|
263
289
|
continue;
|
|
@@ -21,7 +21,7 @@ input:
|
|
|
21
21
|
--headed run with a visible window instead of headless
|
|
22
22
|
|
|
23
23
|
output: <browser-launched port=… pid=…> — the port to pass as --port (or CDP_PORT), the pid capture owns, and the executable that answered with its provenance (CAPTURE_BROWSER, puppeteer-cache, or system).
|
|
24
|
-
effects: spawns a detached browser with a private profile under CAPTURE_ROOT, and registers it as capture-owned. Capture stops it on \`capture tab quit\`, or on the first later capture invocation once it has been unused for ${IDLE_REAP_MS / 60000} minutes; a browser capture did not start is never signalled. Also sweeps previously-launched browsers that have exited,
|
|
24
|
+
effects: spawns a detached browser with a private profile under CAPTURE_ROOT, and registers it as capture-owned. Capture stops it on \`capture tab quit\`, or on the first later capture invocation once it has been unused for ${IDLE_REAP_MS / 60000} minutes; a browser capture did not start is never signalled. Also sweeps previously-launched browsers that have exited, refused CDP connections, or gone idle.`;
|
|
25
25
|
|
|
26
26
|
/** Pure `<browser-launched>` result builder — exported for tests. */
|
|
27
27
|
export function buildBrowserLaunchedResult(
|
|
@@ -34,7 +34,7 @@ export function buildBrowserLaunchedResult(
|
|
|
34
34
|
line(text`mode `, data(browser.headless ? 'headless' : 'headed')),
|
|
35
35
|
]),
|
|
36
36
|
];
|
|
37
|
-
if (swept > 0) sections.push(fact`swept ${swept} previously-launched browser(s) that had exited,
|
|
37
|
+
if (swept > 0) sections.push(fact`swept ${swept} previously-launched browser(s) that had exited, refused CDP connections, or gone idle.`);
|
|
38
38
|
return {
|
|
39
39
|
tag: 'browser-launched',
|
|
40
40
|
attrs: { port: browser.port, pid: browser.pid, headless: browser.headless },
|
|
@@ -33,7 +33,7 @@ input:
|
|
|
33
33
|
--port <port> probe one endpoint only, instead of discovering every localhost CDP endpoint; an active session's endpoint does not narrow it (CDP_PORT still pins one endpoint)
|
|
34
34
|
|
|
35
35
|
output: <tabs endpoints=… tabs=…> — one section per reachable endpoint (port, app, page-tab count, preferred marker, and a capture-owned pid marker on any browser \`tab launch\` started) listing each page tab's target id (first 8 chars, enough for --target), title, and url; an endpoint that failed the tab fetch is an endpoints-unreachable fact, never a silent omission.
|
|
36
|
-
effects: probes local CDP endpoints over HTTP/WebSocket; no page-observable writes. Also sweeps capture-launched browsers, stopping any that have exited,
|
|
36
|
+
effects: probes local CDP endpoints over HTTP/WebSocket; no page-observable writes. Also sweeps capture-launched browsers, stopping any that have exited, refused CDP connections, or gone idle — browsers capture did not start are never signalled.`;
|
|
37
37
|
|
|
38
38
|
export interface TabRow {
|
|
39
39
|
id: string;
|