@parall/daemon 1.45.0 → 1.46.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/bundle/manifest.json +15 -15
- package/bundle/parall-browser-pod.js +29726 -375
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26091 -273
- package/bundle/parall-codex-agent.js +26650 -331
- package/bundle/parall-daemon.js +31384 -1998
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +48 -187
- package/package.json +8 -6
|
@@ -29,6 +29,9 @@ export interface BrowserProfilePoolOptions {
|
|
|
29
29
|
/** Per-profile outbound proxy resolver (#1642), threaded to each profile's
|
|
30
30
|
* manager so its isolated Chromium binds its own egress proxy at account_create. */
|
|
31
31
|
resolveProxy?: BrowserProfileManagerOptions['resolveProxy'];
|
|
32
|
+
/** Proxy readiness probe endpoint, threaded to each manager (see
|
|
33
|
+
* browser-readiness.ts). */
|
|
34
|
+
proxyProbeUrl?: string;
|
|
32
35
|
/**
|
|
33
36
|
* Per-profile runtime factory. Defaults to a real {@link BrowserProfileManager};
|
|
34
37
|
* overridable in tests to avoid spawning a real bb-browser / Chromium.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,4BAA4B,EACjC,KAAK,oBAAoB,EAC1B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAGxD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,mBAAmB,CACjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,yFAAyF;IACzF,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,4BAA4B,CAAC,KAAK,CAAC,CAAC;IACzC;;;;yDAIqD;IACrD,YAAY,CAAC,EAAE,CACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,CAAC,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,KAChB,IAAI,CAAC;IACV;yFACqF;IACrF,YAAY,CAAC,EAAE,4BAA4B,CAAC,cAAc,CAAC,CAAC;IAC5D;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,4BAA4B,KAAK,qBAAqB,CAAC;CAC/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IAwC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAvCjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IAMrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAShE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAO7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAKrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IAKzD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAK1D,OAAO,CAAC,QAAQ,CAAS;gBAEI,IAAI,EAAE,yBAAyB;IAS5D,sFAAsF;IACtF,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;YAiCzC,iBAAiB;IAa/B;;;;;;OAMG;IACH,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,GAAE;QAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAO,GACzC,OAAO,CAAC,IAAI,CAAC;IAahB,iGAAiG;IACjG,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -151,7 +151,23 @@ export class BrowserProfilePool {
|
|
|
151
151
|
if (generation !== undefined)
|
|
152
152
|
this.generations.set(profileId, generation);
|
|
153
153
|
this.stopped.delete(profileId); // explicit open revives a stopped profile
|
|
154
|
-
|
|
154
|
+
try {
|
|
155
|
+
await this.manager(profileId).openProfile(profileId, startUrl);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Failed open: tear the half-started runtime down (no zombie Chromium
|
|
159
|
+
// burning resources behind an `error` row) and re-fence so late hub
|
|
160
|
+
// invokes are rejected until an explicit reopen. The manager already
|
|
161
|
+
// reported `error` with the structured cause; keep the rejection.
|
|
162
|
+
// stopped.add only — no fence() bump, so an open QUEUED behind this one
|
|
163
|
+
// (captured seq unchanged) still revives normally.
|
|
164
|
+
this.stopped.add(profileId);
|
|
165
|
+
const manager = this.managers.get(profileId);
|
|
166
|
+
if (manager) {
|
|
167
|
+
await this.dispose(profileId, manager).catch(() => { });
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
155
171
|
}));
|
|
156
172
|
}
|
|
157
173
|
/** True if a newer stop was observed after this revive captured `sinceSeq`. */
|
|
@@ -416,6 +432,7 @@ export class BrowserProfilePool {
|
|
|
416
432
|
// manager, so each isolated Chromium binds its own egress proxy at
|
|
417
433
|
// account_create (the resolver is keyed by profileId === account).
|
|
418
434
|
resolveProxy: this.opts.resolveProxy,
|
|
435
|
+
proxyProbeUrl: this.opts.proxyProbeUrl,
|
|
419
436
|
});
|
|
420
437
|
this.managers.set(profileId, manager);
|
|
421
438
|
return manager;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BrowserProxyConfig } from './browser-profile-manager.js';
|
|
2
|
+
import type { BrowserTargetRegistry } from './browser-target-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Cookie-preserving proxy reconciliation (desired-state enforcement).
|
|
5
|
+
*
|
|
6
|
+
* bb-browser binds an account's proxy at account_create and REPLAYS the proxy
|
|
7
|
+
* stored in its own account JSON on every later context activation — there is
|
|
8
|
+
* no update API, and `account_delete` (the only rebind path) destroys the
|
|
9
|
+
* cookie jar. So a proxy edit in Parall would either never apply or cost the
|
|
10
|
+
* user their logins. This module closes that gap without touching bb-browser:
|
|
11
|
+
*
|
|
12
|
+
* 1. capture the FULL cookie jar (HttpOnly included) straight from Chromium
|
|
13
|
+
* via CDP `Storage.getCookies({browserContextId})`;
|
|
14
|
+
* 2. persist it as a backup (crash safety — a fresh account JSON starts empty,
|
|
15
|
+
* so a crash mid-reconcile must not lose the jar);
|
|
16
|
+
* 3. `account_delete` → `account_create` with the DESIRED proxy fields;
|
|
17
|
+
* 4. restore the jar into the recreated context via `Storage.setCookies`;
|
|
18
|
+
* 5. DELETE the backup, THEN record the applied fingerprint in the sidecar —
|
|
19
|
+
* in that order (fix #8). Writing the fingerprint LAST guarantees a backup
|
|
20
|
+
* is never left on disk alongside an up-to-date fingerprint, so a later
|
|
21
|
+
* proxy change can never resurrect a stale jar over the current login, and
|
|
22
|
+
* old cookies never linger. bb-browser's own autosave then persists the
|
|
23
|
+
* restored cookies into its new account JSON — Parall never writes that file.
|
|
24
|
+
*
|
|
25
|
+
* Scope note: account BrowserContexts are CDP-created (incognito-style), so
|
|
26
|
+
* their localStorage/IndexedDB were never durable across a Chromium restart in
|
|
27
|
+
* bb-browser's design (only cookies persist, via its account JSON). Every
|
|
28
|
+
* reconcile runs at open-after-stop — i.e. after a Chromium restart — so
|
|
29
|
+
* reconciliation never loses state that would otherwise have survived.
|
|
30
|
+
*/
|
|
31
|
+
export interface ProxyReconcileHost {
|
|
32
|
+
log: {
|
|
33
|
+
info(msg: string): void;
|
|
34
|
+
warn(msg: string): void;
|
|
35
|
+
error(msg: string): void;
|
|
36
|
+
};
|
|
37
|
+
homeDir: string;
|
|
38
|
+
targets: BrowserTargetRegistry;
|
|
39
|
+
sendCommand(request: Record<string, unknown> & {
|
|
40
|
+
method: string;
|
|
41
|
+
account?: string;
|
|
42
|
+
}): Promise<Record<string, unknown>>;
|
|
43
|
+
cdpEndpoint(): Promise<{
|
|
44
|
+
host: string;
|
|
45
|
+
port: number;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/** Render a proxy config into bb-browser `account_create` fields (camelCase, the
|
|
49
|
+
* bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
|
|
50
|
+
export declare function proxyCreateFields(proxy: BrowserProxyConfig | null): Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the account must be rebuilt to honor `desired`. Unknown recorded
|
|
53
|
+
* state (pre-upgrade account, wiped sidecar) reconciles unless BOTH the desired
|
|
54
|
+
* config and the account's visible proxy are direct — the account JSON may
|
|
55
|
+
* carry a stale proxy (or a stale password) that Parall's config no longer
|
|
56
|
+
* matches, and one context rebuild with cookies preserved is invisible.
|
|
57
|
+
*/
|
|
58
|
+
export declare function proxyReconcileNeeded(recordedFingerprint: string | undefined, desired: BrowserProxyConfig | null, accountShowsProxy: boolean): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Rebuild the account's BrowserContext with the desired proxy, preserving its
|
|
61
|
+
* cookie jar. The account MUST exist; callers hold the per-profile op mutex.
|
|
62
|
+
*/
|
|
63
|
+
export declare function reconcileAccountProxy(host: ProxyReconcileHost, account: string, desired: BrowserProxyConfig | null): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Recover a reconcile that crashed after account_delete: the account no longer
|
|
66
|
+
* exists, but a cookie backup does. Called right after a fresh account_create —
|
|
67
|
+
* runs the same settle tail (restore → clear → fingerprint). Without a backup,
|
|
68
|
+
* just records the applied fingerprint for the fresh account.
|
|
69
|
+
*/
|
|
70
|
+
export declare function settlePendingCookieBackup(host: ProxyReconcileHost, account: string, applied: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* A log/error-safe proxy label: scheme + host:port ONLY. The username is auth
|
|
73
|
+
* material (customer / region / session selectors) and `server` may itself embed
|
|
74
|
+
* `user:pass@` userinfo, so neither is ever emitted — only the endpoint.
|
|
75
|
+
*/
|
|
76
|
+
export declare function redactedProxyLabel(proxy: BrowserProxyConfig): string;
|
|
77
|
+
//# sourceMappingURL=browser-proxy-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-proxy-reconcile.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-proxy-reconcile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAQvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,CACT,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED;mFACmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM1F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,OAAO,EAAE,kBAAkB,GAAG,IAAI,EAClC,iBAAiB,EAAE,OAAO,GACzB,OAAO,CAKT;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAqBD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAiDD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAapE"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { BrowserCdpClient, getContextCookies, setContextCookies, targetBrowserContextId, } from './browser-cdp.js';
|
|
2
|
+
import { clearCookieBackup, proxyFingerprint, readCookieBackup, writeAppliedProxyFingerprint, writeCookieBackup, } from './browser-proxy-state.js';
|
|
3
|
+
/** Render a proxy config into bb-browser `account_create` fields (camelCase, the
|
|
4
|
+
* bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
|
|
5
|
+
export function proxyCreateFields(proxy) {
|
|
6
|
+
if (!proxy?.server)
|
|
7
|
+
return {};
|
|
8
|
+
const fields = { proxyServer: proxy.server };
|
|
9
|
+
if (proxy.username)
|
|
10
|
+
fields.proxyUsername = proxy.username;
|
|
11
|
+
if (proxy.password)
|
|
12
|
+
fields.proxyPassword = proxy.password;
|
|
13
|
+
return fields;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether the account must be rebuilt to honor `desired`. Unknown recorded
|
|
17
|
+
* state (pre-upgrade account, wiped sidecar) reconciles unless BOTH the desired
|
|
18
|
+
* config and the account's visible proxy are direct — the account JSON may
|
|
19
|
+
* carry a stale proxy (or a stale password) that Parall's config no longer
|
|
20
|
+
* matches, and one context rebuild with cookies preserved is invisible.
|
|
21
|
+
*/
|
|
22
|
+
export function proxyReconcileNeeded(recordedFingerprint, desired, accountShowsProxy) {
|
|
23
|
+
const want = proxyFingerprint(desired);
|
|
24
|
+
if (recordedFingerprint !== undefined)
|
|
25
|
+
return recordedFingerprint !== want;
|
|
26
|
+
if (desired === null && !accountShowsProxy)
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rebuild the account's BrowserContext with the desired proxy, preserving its
|
|
32
|
+
* cookie jar. The account MUST exist; callers hold the per-profile op mutex.
|
|
33
|
+
*/
|
|
34
|
+
export async function reconcileAccountProxy(host, account, desired) {
|
|
35
|
+
const want = proxyFingerprint(desired);
|
|
36
|
+
host.log.info(`[bb-browser] reconciling proxy for ${account} → ${desired ? redactedProxyLabel(desired) : 'direct egress'} (${want.slice(0, 19)}…)`);
|
|
37
|
+
// A backup from a crashed prior attempt is the authoritative pre-reconcile jar
|
|
38
|
+
// — the live context may already be a half-rebuilt empty one, so never
|
|
39
|
+
// overwrite the backup with a fresh capture in that case. (Invariant: a backup
|
|
40
|
+
// present ⟹ the fingerprint is NOT yet up to date, so this jar is genuinely
|
|
41
|
+
// pre-reconcile, never a stale leftover — see browser-proxy-state.ts.)
|
|
42
|
+
let jar = readCookieBackup(host.homeDir, account);
|
|
43
|
+
if (!jar) {
|
|
44
|
+
jar = await captureAccountCookies(host, account);
|
|
45
|
+
writeCookieBackup(host.homeDir, account, jar);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
host.log.warn(`[bb-browser] resuming interrupted proxy reconcile for ${account} from cookie backup (${jar.length} cookies)`);
|
|
49
|
+
}
|
|
50
|
+
await host.sendCommand({ method: 'account_delete', account });
|
|
51
|
+
await host.sendCommand({ method: 'account_create', account, ...proxyCreateFields(desired) });
|
|
52
|
+
await settleProxyRebuild(host, account, jar, want);
|
|
53
|
+
host.log.info(`[bb-browser] proxy reconciled for ${account}; ${jar.length} cookies preserved`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The ONE settle tail of every proxy rebuild — restore the jar into the fresh
|
|
57
|
+
* context, clear the backup, write the applied fingerprint, in exactly that
|
|
58
|
+
* order. The ordering is load-bearing: the fingerprint is written LAST, so a
|
|
59
|
+
* crash at any point leaves it stale → the next open reconciles again — and it
|
|
60
|
+
* can never read a leftover backup as if it were pre-reconcile (invariant: a
|
|
61
|
+
* backup never coexists with an up-to-date fingerprint).
|
|
62
|
+
*/
|
|
63
|
+
async function settleProxyRebuild(host, account, jar, want) {
|
|
64
|
+
await restoreAccountCookies(host, account, jar);
|
|
65
|
+
clearCookieBackup(host.homeDir, account);
|
|
66
|
+
writeAppliedProxyFingerprint(host.homeDir, account, want);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Recover a reconcile that crashed after account_delete: the account no longer
|
|
70
|
+
* exists, but a cookie backup does. Called right after a fresh account_create —
|
|
71
|
+
* runs the same settle tail (restore → clear → fingerprint). Without a backup,
|
|
72
|
+
* just records the applied fingerprint for the fresh account.
|
|
73
|
+
*/
|
|
74
|
+
export async function settlePendingCookieBackup(host, account, applied) {
|
|
75
|
+
const jar = readCookieBackup(host.homeDir, account);
|
|
76
|
+
if (!jar) {
|
|
77
|
+
writeAppliedProxyFingerprint(host.homeDir, account, applied);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
host.log.warn(`[bb-browser] restoring ${jar.length} cookies for ${account} from an interrupted proxy reconcile`);
|
|
81
|
+
await settleProxyRebuild(host, account, jar, applied);
|
|
82
|
+
}
|
|
83
|
+
/** Capture the full jar of the account's BrowserContext via CDP. */
|
|
84
|
+
async function captureAccountCookies(host, account) {
|
|
85
|
+
// Ensure the context is live (a stopped account's cookies exist only in
|
|
86
|
+
// bb-browser's JSON until activation re-injects them) — an owned tab forces
|
|
87
|
+
// activation, and its targetId maps to the context id.
|
|
88
|
+
const targetId = await host.targets.ensureOwnedTab(account);
|
|
89
|
+
const { host: cdpHost, port } = await host.cdpEndpoint();
|
|
90
|
+
const cdp = await BrowserCdpClient.connect(cdpHost, port);
|
|
91
|
+
try {
|
|
92
|
+
const contextId = await targetBrowserContextId(cdp, targetId);
|
|
93
|
+
if (!contextId) {
|
|
94
|
+
throw new Error(`owned tab ${targetId} for ${account} reports no browserContextId — refusing to capture the default context's cookies`);
|
|
95
|
+
}
|
|
96
|
+
return await getContextCookies(cdp, contextId);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
cdp.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Restore a jar into the account's (fresh) BrowserContext via CDP. */
|
|
103
|
+
async function restoreAccountCookies(host, account, cookies) {
|
|
104
|
+
const targetId = await host.targets.ensureOwnedTab(account);
|
|
105
|
+
if (cookies.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const { host: cdpHost, port } = await host.cdpEndpoint();
|
|
108
|
+
const cdp = await BrowserCdpClient.connect(cdpHost, port);
|
|
109
|
+
try {
|
|
110
|
+
const contextId = await targetBrowserContextId(cdp, targetId);
|
|
111
|
+
if (!contextId) {
|
|
112
|
+
throw new Error(`owned tab ${targetId} for ${account} reports no browserContextId — refusing to restore cookies into the default context`);
|
|
113
|
+
}
|
|
114
|
+
await setContextCookies(cdp, contextId, cookies);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
cdp.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A log/error-safe proxy label: scheme + host:port ONLY. The username is auth
|
|
122
|
+
* material (customer / region / session selectors) and `server` may itself embed
|
|
123
|
+
* `user:pass@` userinfo, so neither is ever emitted — only the endpoint.
|
|
124
|
+
*/
|
|
125
|
+
export function redactedProxyLabel(proxy) {
|
|
126
|
+
const raw = proxy.server;
|
|
127
|
+
try {
|
|
128
|
+
const u = new URL(raw.includes('://') ? raw : `http://${raw}`);
|
|
129
|
+
// u.host is hostname[:port] with NO userinfo; keep the real scheme when present.
|
|
130
|
+
return raw.includes('://') ? `${u.protocol}//${u.host}` : u.host;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Unparseable — return a FIXED safe placeholder. Do not attempt to salvage a
|
|
134
|
+
// "host" from attacker-controlled malformed text: a hand-rolled strip can still
|
|
135
|
+
// leak a credential or tokenized fragment that URL() rejected but our regex
|
|
136
|
+
// didn't anticipate. If URL() couldn't parse it, emit nothing derived from it.
|
|
137
|
+
return '<proxy>';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BrowserProxyConfig } from './browser-profile-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Proxy desired-state plumbing for browser profiles.
|
|
4
|
+
*
|
|
5
|
+
* Parall's stored profile config is the ONLY source of truth for a profile's
|
|
6
|
+
* proxy. bb-browser persists whatever proxy an account was created with inside
|
|
7
|
+
* its own account JSON and silently replays it on every context activation —
|
|
8
|
+
* that file must never win over Parall's config. So the daemon records the
|
|
9
|
+
* fingerprint of the proxy it ACTUALLY applied in a Parall-owned sidecar (next
|
|
10
|
+
* to, never inside, bb-browser's files) and reconciles on open/ensure whenever
|
|
11
|
+
* the desired config differs (see browser-proxy-reconcile.ts).
|
|
12
|
+
*
|
|
13
|
+
* Validation policy lives in ONE place — the Go API
|
|
14
|
+
* (server/pkg/model/browser_proxy.go validates at create, update, AND open,
|
|
15
|
+
* which re-checks stored legacy rows before any runtime is dispatched). The
|
|
16
|
+
* daemon does NOT re-implement that policy; normalizeProxyConfig below keeps
|
|
17
|
+
* only the minimal fail-closed guards against a config that slipped past the
|
|
18
|
+
* canonical validator (corrupt row, stale env), in exactly the shapes where
|
|
19
|
+
* the runtime would otherwise LEAK silently instead of failing visibly.
|
|
20
|
+
*/
|
|
21
|
+
/** Structured codes for the runtime guards (subset of the Go codes). */
|
|
22
|
+
export declare class BrowserProxyConfigError extends Error {
|
|
23
|
+
readonly code: 'PROXY_SERVER_REQUIRED' | 'PROXY_AUTH_INCOMPLETE' | 'PROXY_SOCKS_AUTH_UNSUPPORTED' | 'PROXY_CREDENTIALS_REQUIRED';
|
|
24
|
+
constructor(code: 'PROXY_SERVER_REQUIRED' | 'PROXY_AUTH_INCOMPLETE' | 'PROXY_SOCKS_AUTH_UNSUPPORTED' | 'PROXY_CREDENTIALS_REQUIRED', message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Minimal fail-closed sanity check on the canonical (Go-validated) proxy DTO.
|
|
28
|
+
* Exactly three guards — each one a shape where bb-browser/Chromium would
|
|
29
|
+
* SILENTLY drop the proxy and egress from the real IP rather than error:
|
|
30
|
+
*
|
|
31
|
+
* 1. credentials without a server (corrupt row — nothing to proxy through);
|
|
32
|
+
* 2. SOCKS5 (Chromium has no SOCKS auth; without auth, bb-browser drops the
|
|
33
|
+
* proxy entirely) — structurally unsupported, matching the API contract;
|
|
34
|
+
* 3. missing/incomplete credentials (bb-browser applies a proxy only when
|
|
35
|
+
* server+username+password are ALL present).
|
|
36
|
+
*
|
|
37
|
+
* Everything else — scheme allowlist, host:port authority shape, length caps —
|
|
38
|
+
* is the Go validator's job and is NOT re-checked here. Returns the config
|
|
39
|
+
* (trimmed server), or null for "no proxy" (direct egress).
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeProxyConfig(proxy: BrowserProxyConfig | null): BrowserProxyConfig | null;
|
|
42
|
+
/**
|
|
43
|
+
* Stable fingerprint of the COMPLETE normalized proxy config (scheme, host,
|
|
44
|
+
* port, username, password). Only ever logged/persisted as a hash — the
|
|
45
|
+
* password itself never appears in logs or the sidecar.
|
|
46
|
+
*/
|
|
47
|
+
export declare function proxyFingerprint(proxy: BrowserProxyConfig | null): string;
|
|
48
|
+
export declare function appliedProxyFingerprint(homeDir: string, account: string): string | undefined;
|
|
49
|
+
export declare function writeAppliedProxyFingerprint(homeDir: string, account: string, fingerprint: string): void;
|
|
50
|
+
/** The pre-reconcile jar for an interrupted reconcile, or null when none. */
|
|
51
|
+
export declare function readCookieBackup(homeDir: string, account: string): Array<Record<string, unknown>> | null;
|
|
52
|
+
export declare function writeCookieBackup(homeDir: string, account: string, cookies: Array<Record<string, unknown>>): void;
|
|
53
|
+
/** Remove the account's cookie backup entirely (post-restore cleanup). */
|
|
54
|
+
export declare function clearCookieBackup(homeDir: string, account: string): void;
|
|
55
|
+
//# sourceMappingURL=browser-proxy-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-proxy-state.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-proxy-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wEAAwE;AACxE,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,IAAI,EACT,uBAAuB,GACvB,uBAAuB,GACvB,8BAA8B,GAC9B,4BAA4B;gBAJvB,IAAI,EACT,uBAAuB,GACvB,uBAAuB,GACvB,8BAA8B,GAC9B,4BAA4B,EAChC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,kBAAkB,GAAG,IAAI,CAmChG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,MAAM,CAQzE;AAmCD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG5F;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,IAAI,CAIN;AAuBD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAKvC;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC,IAAI,CAIN;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxE"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Proxy desired-state plumbing for browser profiles.
|
|
6
|
+
*
|
|
7
|
+
* Parall's stored profile config is the ONLY source of truth for a profile's
|
|
8
|
+
* proxy. bb-browser persists whatever proxy an account was created with inside
|
|
9
|
+
* its own account JSON and silently replays it on every context activation —
|
|
10
|
+
* that file must never win over Parall's config. So the daemon records the
|
|
11
|
+
* fingerprint of the proxy it ACTUALLY applied in a Parall-owned sidecar (next
|
|
12
|
+
* to, never inside, bb-browser's files) and reconciles on open/ensure whenever
|
|
13
|
+
* the desired config differs (see browser-proxy-reconcile.ts).
|
|
14
|
+
*
|
|
15
|
+
* Validation policy lives in ONE place — the Go API
|
|
16
|
+
* (server/pkg/model/browser_proxy.go validates at create, update, AND open,
|
|
17
|
+
* which re-checks stored legacy rows before any runtime is dispatched). The
|
|
18
|
+
* daemon does NOT re-implement that policy; normalizeProxyConfig below keeps
|
|
19
|
+
* only the minimal fail-closed guards against a config that slipped past the
|
|
20
|
+
* canonical validator (corrupt row, stale env), in exactly the shapes where
|
|
21
|
+
* the runtime would otherwise LEAK silently instead of failing visibly.
|
|
22
|
+
*/
|
|
23
|
+
/** Structured codes for the runtime guards (subset of the Go codes). */
|
|
24
|
+
export class BrowserProxyConfigError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
constructor(code, message) {
|
|
27
|
+
super(`${code}: ${message}`);
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.name = 'BrowserProxyConfigError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Minimal fail-closed sanity check on the canonical (Go-validated) proxy DTO.
|
|
34
|
+
* Exactly three guards — each one a shape where bb-browser/Chromium would
|
|
35
|
+
* SILENTLY drop the proxy and egress from the real IP rather than error:
|
|
36
|
+
*
|
|
37
|
+
* 1. credentials without a server (corrupt row — nothing to proxy through);
|
|
38
|
+
* 2. SOCKS5 (Chromium has no SOCKS auth; without auth, bb-browser drops the
|
|
39
|
+
* proxy entirely) — structurally unsupported, matching the API contract;
|
|
40
|
+
* 3. missing/incomplete credentials (bb-browser applies a proxy only when
|
|
41
|
+
* server+username+password are ALL present).
|
|
42
|
+
*
|
|
43
|
+
* Everything else — scheme allowlist, host:port authority shape, length caps —
|
|
44
|
+
* is the Go validator's job and is NOT re-checked here. Returns the config
|
|
45
|
+
* (trimmed server), or null for "no proxy" (direct egress).
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeProxyConfig(proxy) {
|
|
48
|
+
if (!proxy || !proxy.server?.trim()) {
|
|
49
|
+
if (proxy && (proxy.username || proxy.password)) {
|
|
50
|
+
throw new BrowserProxyConfigError('PROXY_SERVER_REQUIRED', 'proxy credentials are set but the proxy server is empty');
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const server = proxy.server.trim();
|
|
55
|
+
const username = proxy.username ?? '';
|
|
56
|
+
const password = proxy.password ?? '';
|
|
57
|
+
if (/^socks5h?:\/\//i.test(server)) {
|
|
58
|
+
throw new BrowserProxyConfigError('PROXY_SOCKS_AUTH_UNSUPPORTED', 'SOCKS5 proxies are not supported by the browser runtime (authenticated HTTP/HTTPS only)');
|
|
59
|
+
}
|
|
60
|
+
if ((username === '') !== (password === '')) {
|
|
61
|
+
throw new BrowserProxyConfigError('PROXY_AUTH_INCOMPLETE', 'proxy_username and proxy_password must be provided together');
|
|
62
|
+
}
|
|
63
|
+
if (username === '') {
|
|
64
|
+
// bb-browser applies a proxy only when server+username+password are ALL
|
|
65
|
+
// present; a credential-less config is silently dropped and the profile
|
|
66
|
+
// egresses from the host's real IP. Fail closed instead of leaking.
|
|
67
|
+
throw new BrowserProxyConfigError('PROXY_CREDENTIALS_REQUIRED', 'the browser runtime cannot apply a credential-less proxy and would silently egress from the real IP');
|
|
68
|
+
}
|
|
69
|
+
return { server, username, password };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Stable fingerprint of the COMPLETE normalized proxy config (scheme, host,
|
|
73
|
+
* port, username, password). Only ever logged/persisted as a hash — the
|
|
74
|
+
* password itself never appears in logs or the sidecar.
|
|
75
|
+
*/
|
|
76
|
+
export function proxyFingerprint(proxy) {
|
|
77
|
+
if (!proxy)
|
|
78
|
+
return 'direct';
|
|
79
|
+
const canonical = JSON.stringify({
|
|
80
|
+
server: proxy.server,
|
|
81
|
+
username: proxy.username ?? '',
|
|
82
|
+
password: proxy.password ?? '',
|
|
83
|
+
});
|
|
84
|
+
return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Applied-proxy sidecar — `<homeDir>/.parall-proxy-state.json`
|
|
88
|
+
//
|
|
89
|
+
// Records, per account, the fingerprint of the proxy configuration the daemon
|
|
90
|
+
// last APPLIED at account_create. Parall-owned (bb-browser never reads it);
|
|
91
|
+
// lives in the profile home so a BYOC reset's home wipe clears it and the
|
|
92
|
+
// hosted pod's S3 snapshot carries it alongside the state it describes.
|
|
93
|
+
// Missing/corrupt reads as "unknown" → reconcile decides conservatively.
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
const PROXY_STATE_FILE = '.parall-proxy-state.json';
|
|
96
|
+
const COOKIE_BACKUP_FILE = '.parall-proxy-cookie-backup.json';
|
|
97
|
+
// Parse a sidecar as a PLAIN OBJECT. `JSON.parse` of `null`, an array, or a
|
|
98
|
+
// primitive does NOT throw, so a corrupt sidecar would otherwise slip past a bare
|
|
99
|
+
// try/catch and crash the next property access/assignment (`state.applied = …`),
|
|
100
|
+
// wedging proxy recovery. Coerce any non-object shape to {} so a corrupt file
|
|
101
|
+
// reads as empty and the reconcile rebuilds it.
|
|
102
|
+
function readSidecarObject(homeDir, file) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(readFileSync(path.join(homeDir, file), 'utf8'));
|
|
105
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
106
|
+
? parsed
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function appliedProxyFingerprint(homeDir, account) {
|
|
114
|
+
const value = readSidecarObject(homeDir, PROXY_STATE_FILE).applied?.[account];
|
|
115
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
116
|
+
}
|
|
117
|
+
export function writeAppliedProxyFingerprint(homeDir, account, fingerprint) {
|
|
118
|
+
const state = readSidecarObject(homeDir, PROXY_STATE_FILE);
|
|
119
|
+
state.applied = { ...state.applied, [account]: fingerprint };
|
|
120
|
+
atomicWriteJson(path.join(homeDir, PROXY_STATE_FILE), state);
|
|
121
|
+
}
|
|
122
|
+
/** The pre-reconcile jar for an interrupted reconcile, or null when none. */
|
|
123
|
+
export function readCookieBackup(homeDir, account) {
|
|
124
|
+
const cookies = readSidecarObject(homeDir, COOKIE_BACKUP_FILE).backups?.[account];
|
|
125
|
+
return Array.isArray(cookies) ? cookies : null;
|
|
126
|
+
}
|
|
127
|
+
export function writeCookieBackup(homeDir, account, cookies) {
|
|
128
|
+
updateCookieBackupFile(homeDir, (file) => {
|
|
129
|
+
file.backups = { ...file.backups, [account]: cookies };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Remove the account's cookie backup entirely (post-restore cleanup). */
|
|
133
|
+
export function clearCookieBackup(homeDir, account) {
|
|
134
|
+
updateCookieBackupFile(homeDir, (file) => {
|
|
135
|
+
if (file.backups)
|
|
136
|
+
delete file.backups[account];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function updateCookieBackupFile(homeDir, mutate) {
|
|
140
|
+
const file = readSidecarObject(homeDir, COOKIE_BACKUP_FILE);
|
|
141
|
+
mutate(file);
|
|
142
|
+
atomicWriteJson(path.join(homeDir, COOKIE_BACKUP_FILE), file);
|
|
143
|
+
}
|
|
144
|
+
function atomicWriteJson(filePath, value) {
|
|
145
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
146
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
147
|
+
writeFileSync(tmp, JSON.stringify(value));
|
|
148
|
+
renameSync(tmp, filePath);
|
|
149
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser quiescence — the Chrome-exit barrier before a hosted pod's FINAL
|
|
3
|
+
* checkpoint.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: bb-browser's SIGTERM handler sends Chrome SIGTERM and
|
|
6
|
+
* `process.exit(0)`s WITHOUT awaiting it, so "the bb-browser child exited"
|
|
7
|
+
* does not mean the user-data-dir is quiet — an orphaned Chromium may still be
|
|
8
|
+
* flushing cookies/IndexedDB, and a snapshot taken then is torn (SQLite
|
|
9
|
+
* mid-write). The final checkpoint must run only after every browser process
|
|
10
|
+
* is provably gone.
|
|
11
|
+
*
|
|
12
|
+
* Ownership model: the daemon spawns bb-browser with `detached: true`, making
|
|
13
|
+
* it the LEADER of a fresh process group; bb-browser spawns Chrome with
|
|
14
|
+
* `detached: false` (verified against its bundled source), so Chrome and every
|
|
15
|
+
* renderer inherit that group. The group id (= the bb-browser child's pid) is
|
|
16
|
+
* therefore a complete, daemon-owned handle on the whole browser tree:
|
|
17
|
+
*
|
|
18
|
+
* - liveness = `kill(-pgid, 0)` (ESRCH ⇒ no member left);
|
|
19
|
+
* - shutdown = one signal to `-pgid` reaches every member;
|
|
20
|
+
* - safety = POSIX reserves a pid while it names a live process group, so
|
|
21
|
+
* the pgid cannot be recycled out from under us while any member lives.
|
|
22
|
+
* EPERM on the probe means "members exist but none are ours" — our whole
|
|
23
|
+
* tree (same uid, always signalable) must already be dead and the id was
|
|
24
|
+
* recycled — so it reads as exited, and signalling stops immediately.
|
|
25
|
+
*
|
|
26
|
+
* No pid scanning, no /proc string matching, no directory-mtime polling: a
|
|
27
|
+
* profile's group contains exactly the processes its manager spawned, so
|
|
28
|
+
* quiescing profile A can never signal profile B.
|
|
29
|
+
*
|
|
30
|
+
* One CDP refusal probe remains as a CONFIRMATION (the DevTools listener lives
|
|
31
|
+
* inside the Chrome process, so a refused connect independently corroborates
|
|
32
|
+
* process exit) — it is not a second ownership authority.
|
|
33
|
+
*
|
|
34
|
+
* Everything is bounded by ONE absolute deadline. If exit cannot be confirmed
|
|
35
|
+
* by then, the answer is `quiesced: false` and the caller must skip the
|
|
36
|
+
* checkpoint — never guess safety.
|
|
37
|
+
*/
|
|
38
|
+
export interface QuiescenceResult {
|
|
39
|
+
quiesced: boolean;
|
|
40
|
+
/** True when exit needed SIGTERM/SIGKILL escalation to the group. */
|
|
41
|
+
escalated: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface QuiescenceLogger {
|
|
45
|
+
info(msg: string): void;
|
|
46
|
+
warn(msg: string): void;
|
|
47
|
+
}
|
|
48
|
+
export interface QuiescenceOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Process-group id of the browser tree (the detached bb-browser child's pid).
|
|
51
|
+
* null → no browser was ever spawned → trivially quiesced.
|
|
52
|
+
*/
|
|
53
|
+
pgid: number | null;
|
|
54
|
+
/** Last known CDP endpoint for the confirmation probe; null → skip it. */
|
|
55
|
+
cdpEndpoint?: {
|
|
56
|
+
host: string;
|
|
57
|
+
port: number;
|
|
58
|
+
} | null;
|
|
59
|
+
/** How long to wait for self-exit (bb-browser's own shutdown already ran). */
|
|
60
|
+
exitDeadlineMs?: number;
|
|
61
|
+
/** Grace after each escalation signal. */
|
|
62
|
+
escalateGraceMs?: number;
|
|
63
|
+
/** Absolute ceiling for the whole barrier. */
|
|
64
|
+
totalDeadlineMs?: number;
|
|
65
|
+
pollMs?: number;
|
|
66
|
+
/** Signal seam (default process.kill) — lets tests keep a group "unkillable". */
|
|
67
|
+
kill?: (pid: number, signal: NodeJS.Signals | 0) => void;
|
|
68
|
+
log?: QuiescenceLogger;
|
|
69
|
+
}
|
|
70
|
+
export declare function awaitBrowserQuiescence(opts: QuiescenceOptions): Promise<QuiescenceResult>;
|
|
71
|
+
//# sourceMappingURL=browser-quiescence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-quiescence.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-quiescence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,0EAA0E;IAC1E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpD,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACxB;AA2DD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqD/F"}
|