@parall/daemon 1.36.1 → 1.37.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 +11 -11
- package/bundle/parall-browser-pod.js +307 -27
- package/bundle/parall-claude-agent.js +363 -50
- package/bundle/parall-codex-agent.js +362 -48
- package/bundle/parall-daemon.js +33473 -31706
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
- package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
- package/dist/clip-runtime/browser-daemon-env.js +70 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +18 -5
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +149 -24
- package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-pool.js +581 -0
- package/dist/clip-runtime/index.d.ts +2 -2
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/clip-runtime/index.js +2 -1
- package/dist/clip-runtime/process-manager.d.ts +5 -3
- package/dist/clip-runtime/process-manager.d.ts.map +1 -1
- package/dist/clip-runtime/subprocess.d.ts +14 -0
- package/dist/clip-runtime/subprocess.d.ts.map +1 -1
- package/dist/clip-runtime/subprocess.js +49 -0
- package/dist/config.d.ts +14 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -26
- package/dist/daemon-main.d.ts +8 -0
- package/dist/daemon-main.d.ts.map +1 -0
- package/dist/daemon-main.js +165 -0
- package/dist/daemon-paths.d.ts +14 -0
- package/dist/daemon-paths.d.ts.map +1 -0
- package/dist/daemon-paths.js +26 -0
- package/dist/daemon-update-mode.d.ts +8 -0
- package/dist/daemon-update-mode.d.ts.map +1 -0
- package/dist/daemon-update-mode.js +18 -0
- package/dist/index.js +41 -167
- package/dist/runtime-bin-resolver.d.ts +4 -0
- package/dist/runtime-bin-resolver.d.ts.map +1 -1
- package/dist/runtime-bin-resolver.js +40 -7
- package/dist/runtime-detector.d.ts +30 -0
- package/dist/runtime-detector.d.ts.map +1 -0
- package/dist/runtime-detector.js +100 -0
- package/dist/supervisor.d.ts +53 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +604 -67
- package/dist/update-health-gate.d.ts +66 -0
- package/dist/update-health-gate.d.ts.map +1 -0
- package/dist/update-health-gate.js +93 -0
- package/dist/updater-manifest.d.ts +2 -1
- package/dist/updater-manifest.d.ts.map +1 -1
- package/dist/updater-manifest.js +38 -7
- package/dist/updater.d.ts +13 -2
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +126 -17
- package/package.json +6 -6
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAkTA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuD9E"}
|
package/dist/cli.js
CHANGED
|
@@ -219,11 +219,11 @@ function cmdServiceUninstall() {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
async function cmdUpdate(checkOnly) {
|
|
222
|
-
const { resolveClaudeDaemonConfig, resolveBundleDir } = await import('./config.js');
|
|
222
|
+
const { resolveClaudeDaemonConfig, resolveBundleDir, resolveUpdateSigningEnabled } = await import('./config.js');
|
|
223
223
|
const { DaemonUpdater } = await import('./updater.js');
|
|
224
224
|
const config = resolveClaudeDaemonConfig(process.env);
|
|
225
225
|
const bundleDir = resolveBundleDir(process.env);
|
|
226
|
-
const signingEnabled = process.env
|
|
226
|
+
const signingEnabled = resolveUpdateSigningEnabled(process.env);
|
|
227
227
|
const updater = new DaemonUpdater(bundleDir, config.updateCdnUrl, {
|
|
228
228
|
info: (msg) => console.log(msg),
|
|
229
229
|
warn: (msg) => console.warn(msg),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the env for a spawned bb-browser daemon: inherit the parent env (so the
|
|
3
|
+
* Chromium it launches keeps PATH / HOME / DISPLAY / XDG / locale / proxy — the
|
|
4
|
+
* hosted browser-docker pod runs Chrome under Xvfb and depends on these), set
|
|
5
|
+
* BB_BROWSER_HOME, and strip the **platform credentials the daemon/pod carries**
|
|
6
|
+
* before they reach the child — bb-browser uses none of them and the Chromium it
|
|
7
|
+
* launches runs untrusted web content, so credentials must never land in its env:
|
|
8
|
+
* - LLM/provider creds (`ANTHROPIC_*` / `OPENAI_*` / `PRLL_CLAUDE_ALLOW_API_KEY`,
|
|
9
|
+
* via the maintained `clearAllProviderCreds` SSOT);
|
|
10
|
+
* - the machine key (`PRLL_API_KEY=mck_`, BYOC control plane);
|
|
11
|
+
* - hosted-pod creds injected by the browser-profile-controller (browser-pod.ts):
|
|
12
|
+
* the platform assignment / pool tokens (`PRLL_ASSIGNMENT_TOKEN`,
|
|
13
|
+
* `PRLL_POOL_TOKEN`, `pba_`) and the S3 browser-state STS creds (`AWS_*`).
|
|
14
|
+
* It also drops the hosted browser-state config the controller injects alongside
|
|
15
|
+
* those creds (`AWS_REGION`, `PRLL_BROWSER_STATE_BUCKET`, `PRLL_STATE_GENERATION`,
|
|
16
|
+
* `PRLL_BROWSER_STATE_ENDPOINT`): bb-browser never reads it, so dropping it shrinks
|
|
17
|
+
* the env surface handed to a process running untrusted content. (Unrelated BYOC
|
|
18
|
+
* host env is out of scope.)
|
|
19
|
+
*
|
|
20
|
+
* ts/daemon/AGENTS.md requires the credential env to be overridden before spawning
|
|
21
|
+
* a child, and with one bb-browser per profile the exposure would otherwise be
|
|
22
|
+
* multiplied N×. Denylist rather than allowlist: the SAME spawn feeds the Xvfb
|
|
23
|
+
* Chromium in the hosted pod, whose full env surface can't be exercised locally, so
|
|
24
|
+
* a too-narrow allowlist would silently break it — a denylist cannot. The hosted
|
|
25
|
+
* keys below are a safe no-op on a BYOC host (its env never carries them). Exported
|
|
26
|
+
* for unit testing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildBrowserDaemonEnv(baseEnv: NodeJS.ProcessEnv, homeDir: string): NodeJS.ProcessEnv;
|
|
29
|
+
//# sourceMappingURL=browser-daemon-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-daemon-env.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-daemon-env.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,UAAU,CAOnB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { clearAllProviderCreds } from '@parall/agent-core';
|
|
2
|
+
// Env sanitization for the spawned bb-browser daemon child (and the Chromium it
|
|
3
|
+
// launches, which runs untrusted web content). Split out of browser-profile-manager
|
|
4
|
+
// to keep that file under the size threshold; this is a distinct concern (env
|
|
5
|
+
// scrubbing) from the daemon/account/tab lifecycle.
|
|
6
|
+
/** Env vars stripped from the bb-browser child — the platform credentials it must
|
|
7
|
+
* never carry, plus hosted browser-state config it doesn't use (in addition to the
|
|
8
|
+
* provider creds scrubbed by clearAllProviderCreds). See buildBrowserDaemonEnv. */
|
|
9
|
+
const BROWSER_DAEMON_STRIPPED_ENV = [
|
|
10
|
+
'PRLL_API_KEY', // machine key (mck_) — BYOC + agent control plane
|
|
11
|
+
'PRLL_PROVIDER_CONFIG', // JSON ProviderConfig blob — may carry BYO LLM api keys
|
|
12
|
+
'PRLL_ASSIGNMENT_TOKEN', // hosted pod platform assignment token (pba_)
|
|
13
|
+
'PRLL_POOL_TOKEN', // hosted warm-pool bootstrap token (pba_)
|
|
14
|
+
'AWS_ACCESS_KEY_ID', // hosted S3 browser-state STS creds (browser-pod.ts)
|
|
15
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
16
|
+
'AWS_SESSION_TOKEN',
|
|
17
|
+
// AWS credential-RESOLUTION vars: the SDK derives creds from these too (IRSA
|
|
18
|
+
// web-identity, ECS/EKS container creds, named profile, on-disk cred/config
|
|
19
|
+
// files), so a child inheriting them could assume the pod's role even without
|
|
20
|
+
// the static keys above. bb-browser uses none of them.
|
|
21
|
+
'AWS_WEB_IDENTITY_TOKEN_FILE',
|
|
22
|
+
'AWS_ROLE_ARN',
|
|
23
|
+
'AWS_ROLE_SESSION_NAME',
|
|
24
|
+
'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
|
|
25
|
+
'AWS_CONTAINER_CREDENTIALS_FULL_URI',
|
|
26
|
+
'AWS_CONTAINER_AUTHORIZATION_TOKEN',
|
|
27
|
+
'AWS_PROFILE',
|
|
28
|
+
'AWS_SHARED_CREDENTIALS_FILE',
|
|
29
|
+
'AWS_CONFIG_FILE',
|
|
30
|
+
'AWS_REGION', // hosted browser-state config — bb-browser does not use it
|
|
31
|
+
'PRLL_BROWSER_STATE_REGION', // browser-pod AWS_REGION fallback — same config family
|
|
32
|
+
'PRLL_BROWSER_STATE_BUCKET',
|
|
33
|
+
'PRLL_STATE_GENERATION',
|
|
34
|
+
'PRLL_BROWSER_STATE_ENDPOINT',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Build the env for a spawned bb-browser daemon: inherit the parent env (so the
|
|
38
|
+
* Chromium it launches keeps PATH / HOME / DISPLAY / XDG / locale / proxy — the
|
|
39
|
+
* hosted browser-docker pod runs Chrome under Xvfb and depends on these), set
|
|
40
|
+
* BB_BROWSER_HOME, and strip the **platform credentials the daemon/pod carries**
|
|
41
|
+
* before they reach the child — bb-browser uses none of them and the Chromium it
|
|
42
|
+
* launches runs untrusted web content, so credentials must never land in its env:
|
|
43
|
+
* - LLM/provider creds (`ANTHROPIC_*` / `OPENAI_*` / `PRLL_CLAUDE_ALLOW_API_KEY`,
|
|
44
|
+
* via the maintained `clearAllProviderCreds` SSOT);
|
|
45
|
+
* - the machine key (`PRLL_API_KEY=mck_`, BYOC control plane);
|
|
46
|
+
* - hosted-pod creds injected by the browser-profile-controller (browser-pod.ts):
|
|
47
|
+
* the platform assignment / pool tokens (`PRLL_ASSIGNMENT_TOKEN`,
|
|
48
|
+
* `PRLL_POOL_TOKEN`, `pba_`) and the S3 browser-state STS creds (`AWS_*`).
|
|
49
|
+
* It also drops the hosted browser-state config the controller injects alongside
|
|
50
|
+
* those creds (`AWS_REGION`, `PRLL_BROWSER_STATE_BUCKET`, `PRLL_STATE_GENERATION`,
|
|
51
|
+
* `PRLL_BROWSER_STATE_ENDPOINT`): bb-browser never reads it, so dropping it shrinks
|
|
52
|
+
* the env surface handed to a process running untrusted content. (Unrelated BYOC
|
|
53
|
+
* host env is out of scope.)
|
|
54
|
+
*
|
|
55
|
+
* ts/daemon/AGENTS.md requires the credential env to be overridden before spawning
|
|
56
|
+
* a child, and with one bb-browser per profile the exposure would otherwise be
|
|
57
|
+
* multiplied N×. Denylist rather than allowlist: the SAME spawn feeds the Xvfb
|
|
58
|
+
* Chromium in the hosted pod, whose full env surface can't be exercised locally, so
|
|
59
|
+
* a too-narrow allowlist would silently break it — a denylist cannot. The hosted
|
|
60
|
+
* keys below are a safe no-op on a BYOC host (its env never carries them). Exported
|
|
61
|
+
* for unit testing.
|
|
62
|
+
*/
|
|
63
|
+
export function buildBrowserDaemonEnv(baseEnv, homeDir) {
|
|
64
|
+
const env = { ...baseEnv, BB_BROWSER_HOME: homeDir };
|
|
65
|
+
clearAllProviderCreds(env); // strips ANTHROPIC_* / OPENAI_* / PRLL_CLAUDE_ALLOW_API_KEY
|
|
66
|
+
for (const key of BROWSER_DAEMON_STRIPPED_ENV) {
|
|
67
|
+
delete env[key];
|
|
68
|
+
}
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ViewerTurnConfig } from './browser-viewer-streamer.js';
|
|
2
|
-
type BrowserProfileStatus = 'running' | 'stopped' | 'error';
|
|
2
|
+
export type BrowserProfileStatus = 'running' | 'stopped' | 'error';
|
|
3
3
|
/** Per-profile outbound proxy (BYO provider credentials) bound at bb-browser
|
|
4
4
|
* account_create. server is host:port (optional http/https/socks5 scheme);
|
|
5
5
|
* username/password authenticate (CDP Fetch.authRequired). Geo/session targeting
|
|
@@ -9,7 +9,7 @@ export interface BrowserProxyConfig {
|
|
|
9
9
|
username?: string;
|
|
10
10
|
password?: string;
|
|
11
11
|
}
|
|
12
|
-
interface BrowserProfileManagerOptions {
|
|
12
|
+
export interface BrowserProfileManagerOptions {
|
|
13
13
|
homeDir: string;
|
|
14
14
|
log: {
|
|
15
15
|
info(msg: string): void;
|
|
@@ -25,24 +25,37 @@ interface BrowserProfileManagerOptions {
|
|
|
25
25
|
* IP for a proxy-configured profile). */
|
|
26
26
|
resolveProxy?: (profileId: string) => Promise<BrowserProxyConfig | null> | BrowserProxyConfig | null;
|
|
27
27
|
}
|
|
28
|
-
interface BrowserInvokeRequest {
|
|
28
|
+
export interface BrowserInvokeRequest {
|
|
29
29
|
profileId: string;
|
|
30
30
|
account: string;
|
|
31
31
|
command: string;
|
|
32
32
|
input: unknown;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The host-side browser invoke surface ClipProcessManager depends on. Both
|
|
36
|
+
* BrowserProfileManager (one bb-browser, multi-account — used by the hosted
|
|
37
|
+
* browser pod) and BrowserProfilePool (one bb-browser per profile — used by the
|
|
38
|
+
* BYOC daemon supervisor for true per-profile isolation) satisfy it, so the
|
|
39
|
+
* synthetic "browser" capability routes through either without caring which.
|
|
40
|
+
*/
|
|
41
|
+
export interface BrowserInvoker {
|
|
42
|
+
invoke(request: BrowserInvokeRequest): Promise<unknown>;
|
|
43
|
+
}
|
|
34
44
|
export declare class BrowserProfileManager {
|
|
35
45
|
private readonly opts;
|
|
36
46
|
private daemon;
|
|
37
47
|
private starting;
|
|
38
48
|
private restarting;
|
|
49
|
+
private stopping;
|
|
39
50
|
private readonly ensuredAccounts;
|
|
40
51
|
private readonly ensuringAccounts;
|
|
41
52
|
private readonly reportedStatuses;
|
|
42
53
|
private readonly viewer;
|
|
43
54
|
constructor(opts: BrowserProfileManagerOptions);
|
|
44
55
|
invoke({ profileId, account, command, input }: BrowserInvokeRequest): Promise<unknown>;
|
|
45
|
-
ensureRuntime(profileId: string
|
|
56
|
+
ensureRuntime(profileId: string, opts?: {
|
|
57
|
+
forceStatusReport?: boolean;
|
|
58
|
+
}): Promise<void>;
|
|
46
59
|
openProfile(profileId: string, startUrl?: string): Promise<void>;
|
|
47
60
|
stopProfile(profileId: string): Promise<void>;
|
|
48
61
|
resetProfile(profileId: string): Promise<void>;
|
|
@@ -90,7 +103,7 @@ export declare class BrowserProfileManager {
|
|
|
90
103
|
private restartDaemon;
|
|
91
104
|
private restartDaemonOnce;
|
|
92
105
|
private startDaemon;
|
|
106
|
+
private startDaemonAttempt;
|
|
93
107
|
private post;
|
|
94
108
|
}
|
|
95
|
-
export {};
|
|
96
109
|
//# sourceMappingURL=browser-profile-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,9 +4,13 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { buildBrowserDaemonEnv } from './browser-daemon-env.js';
|
|
7
8
|
import { BrowserViewerStreamer } from './browser-viewer-streamer.js';
|
|
8
|
-
import { findFreePort, formatErrorForLog, sleep } from './subprocess.js';
|
|
9
|
+
import { findFreePort, formatErrorForLog, sleep, waitForChildExit } from './subprocess.js';
|
|
9
10
|
const BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15_000;
|
|
11
|
+
// Retries for a transient port collision when several daemons boot in parallel
|
|
12
|
+
// (probe-and-close port allocation is TOCTOU); each retry re-rolls fresh ports.
|
|
13
|
+
const BB_BROWSER_DAEMON_START_ATTEMPTS = 3;
|
|
10
14
|
class BrowserCommandError extends Error {
|
|
11
15
|
method;
|
|
12
16
|
account;
|
|
@@ -22,6 +26,11 @@ export class BrowserProfileManager {
|
|
|
22
26
|
daemon = null;
|
|
23
27
|
starting = null;
|
|
24
28
|
restarting = null;
|
|
29
|
+
// Set by stop() and never reset (stop() is terminal — the pool disposes the
|
|
30
|
+
// manager and the hosted pod exits). Makes a startDaemon() that resolves after
|
|
31
|
+
// stop() began un-adoptable, so a late child can't be reassigned to this.daemon
|
|
32
|
+
// and escape teardown.
|
|
33
|
+
stopping = false;
|
|
25
34
|
ensuredAccounts = new Set();
|
|
26
35
|
ensuringAccounts = new Map();
|
|
27
36
|
reportedStatuses = new Map();
|
|
@@ -77,9 +86,19 @@ export class BrowserProfileManager {
|
|
|
77
86
|
throw err;
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
|
-
async ensureRuntime(profileId) {
|
|
89
|
+
async ensureRuntime(profileId, opts = {}) {
|
|
81
90
|
if (!profileId)
|
|
82
91
|
throw new Error('browser profile id is required');
|
|
92
|
+
// forceStatusReport is set by the reconnect reconcile for a `pending` row: the server
|
|
93
|
+
// flipped the row to `pending` (an `open` API call) while the daemon was disconnected
|
|
94
|
+
// but its Chromium stayed alive, so our last report was already `running`. A deduped
|
|
95
|
+
// repeat would be swallowed and leave the server stuck at `pending` — clear the marker
|
|
96
|
+
// (as openProfile / stopProfile do) so this recovery report is authoritative for both
|
|
97
|
+
// `running` and `error`. A plain (non-forced) ensure keeps the dedup so a `running`
|
|
98
|
+
// row's chatty re-confirmations are still suppressed.
|
|
99
|
+
if (opts.forceStatusReport) {
|
|
100
|
+
this.reportedStatuses.delete(profileId);
|
|
101
|
+
}
|
|
83
102
|
try {
|
|
84
103
|
await this.withDaemonRecovery(() => this.ensureAccount(profileId), `ensure runtime for ${profileId}`);
|
|
85
104
|
this.reportStatus(profileId, 'running');
|
|
@@ -140,25 +159,61 @@ export class BrowserProfileManager {
|
|
|
140
159
|
}
|
|
141
160
|
}
|
|
142
161
|
async stop() {
|
|
162
|
+
this.stopping = true;
|
|
143
163
|
// Tear down all per-profile bb-viewer streamers before the bb-browser daemon.
|
|
144
164
|
this.viewer.shutdown();
|
|
165
|
+
// Capture an IN-FLIGHT startup too: a startDaemon() resolving after we snapshot
|
|
166
|
+
// this.daemon would otherwise produce a fresh child this stop never kills. The
|
|
167
|
+
// ensureDaemon stopping-guard refuses to ADOPT it (won't set this.daemon), but
|
|
168
|
+
// only we can shut it down — so await the startup here and tear down whatever it
|
|
169
|
+
// produced. `daemon` and `starting` are mutually exclusive (ensureDaemon sets
|
|
170
|
+
// this.daemon then nulls this.starting in finally), so at most one is set.
|
|
171
|
+
const inflight = this.starting;
|
|
172
|
+
// An in-flight restartDaemonOnce() has already cleared this.daemon/this.starting
|
|
173
|
+
// and is awaiting the PREVIOUS child's waitForChildExit. Join it so stop() does
|
|
174
|
+
// not return — letting the hosted pod's drain checkpoint run — before that child
|
|
175
|
+
// (and its Chromium) has actually exited and flushed state. The stopping guard
|
|
176
|
+
// set above prevents its post-restart retry from spawning a fresh daemon.
|
|
177
|
+
const restarting = this.restarting;
|
|
145
178
|
const daemon = this.daemon;
|
|
146
179
|
this.daemon = null;
|
|
147
180
|
this.starting = null;
|
|
148
181
|
this.ensuredAccounts.clear();
|
|
149
182
|
this.ensuringAccounts.clear();
|
|
150
183
|
this.reportedStatuses.clear();
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
if (restarting) {
|
|
185
|
+
try {
|
|
186
|
+
await restarting;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* restart failure is already logged by restartDaemonOnce */
|
|
190
|
+
}
|
|
158
191
|
}
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
let started = null;
|
|
193
|
+
if (inflight) {
|
|
194
|
+
try {
|
|
195
|
+
started = await inflight;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* startup failed — nothing to tear down */
|
|
199
|
+
}
|
|
161
200
|
}
|
|
201
|
+
// Wait for each child (and its Chromium) to ACTUALLY exit before returning: the
|
|
202
|
+
// same per-profile home is reused right after — the pool reopens/resets it, or a
|
|
203
|
+
// recovery restart spawns a new daemon on it — so returning while Chromium is
|
|
204
|
+
// still tearing down would race a new Chromium / a state wipe on the same
|
|
205
|
+
// user-data-dir.
|
|
206
|
+
await Promise.all([daemon, started]
|
|
207
|
+
.filter((d) => d !== null)
|
|
208
|
+
.map(async (d) => {
|
|
209
|
+
try {
|
|
210
|
+
await this.post('/shutdown', d, undefined, 3_000);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
/* best-effort */
|
|
214
|
+
}
|
|
215
|
+
await waitForChildExit(d.child, this.opts.log);
|
|
216
|
+
}));
|
|
162
217
|
}
|
|
163
218
|
/**
|
|
164
219
|
* Viewer control command entrypoint — delegates to BrowserViewerStreamer
|
|
@@ -408,20 +463,39 @@ export class BrowserProfileManager {
|
|
|
408
463
|
async ensureDaemon() {
|
|
409
464
|
if (this.restarting)
|
|
410
465
|
await this.restarting;
|
|
466
|
+
if (this.stopping)
|
|
467
|
+
throw new Error('browser profile manager is stopping');
|
|
411
468
|
if (this.daemon &&
|
|
412
469
|
this.daemon.child.exitCode === null &&
|
|
413
470
|
this.daemon.child.signalCode === null) {
|
|
414
471
|
return this.daemon;
|
|
415
472
|
}
|
|
416
|
-
if (this.starting)
|
|
417
|
-
|
|
473
|
+
if (this.starting) {
|
|
474
|
+
// A concurrent caller shares the in-flight start, but must re-check the stopping
|
|
475
|
+
// guard after awaiting it — exactly like the owner below. Otherwise stop() beginning
|
|
476
|
+
// mid-start would hand this waiter the freshly-started daemon that stop() is tearing
|
|
477
|
+
// down, and it would post a command into teardown (the resurrect race, via the
|
|
478
|
+
// waiter path). Fail closed instead.
|
|
479
|
+
const started = await this.starting;
|
|
480
|
+
if (this.stopping)
|
|
481
|
+
throw new Error('browser profile manager is stopping');
|
|
482
|
+
return started;
|
|
483
|
+
}
|
|
418
484
|
this.starting = this.startDaemon();
|
|
485
|
+
const startPromise = this.starting;
|
|
419
486
|
try {
|
|
420
|
-
|
|
487
|
+
const started = await startPromise;
|
|
488
|
+
// stop() may have begun while we were starting; it captured this same promise
|
|
489
|
+
// and tears the child down. Do NOT adopt it as this.daemon, or it would escape
|
|
490
|
+
// teardown — exactly the resurrect race this guard closes.
|
|
491
|
+
if (this.stopping)
|
|
492
|
+
throw new Error('browser profile manager is stopping');
|
|
493
|
+
this.daemon = started;
|
|
421
494
|
return this.daemon;
|
|
422
495
|
}
|
|
423
496
|
finally {
|
|
424
|
-
this.starting
|
|
497
|
+
if (this.starting === startPromise)
|
|
498
|
+
this.starting = null;
|
|
425
499
|
}
|
|
426
500
|
}
|
|
427
501
|
async withDaemonRecovery(operation, label) {
|
|
@@ -461,20 +535,67 @@ export class BrowserProfileManager {
|
|
|
461
535
|
catch {
|
|
462
536
|
/* best-effort */
|
|
463
537
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
538
|
+
// Wait for the bb-browser child (and its Chromium) to ACTUALLY exit before
|
|
539
|
+
// returning: the same per-profile home is reused right after — the pool
|
|
540
|
+
// reopens/resets it, or a recovery restart spawns a new daemon on it — so
|
|
541
|
+
// returning while Chromium is still tearing down would race a new Chromium /
|
|
542
|
+
// a state wipe against the old one on the same user-data-dir.
|
|
543
|
+
await waitForChildExit(daemon.child, this.opts.log);
|
|
467
544
|
}
|
|
468
545
|
async startDaemon() {
|
|
546
|
+
// findFreePort() probes by binding :0 then closing, so when the supervisor
|
|
547
|
+
// boots several profiles' bb-browser daemons in parallel two can grab the same
|
|
548
|
+
// just-freed port and the loser's Chromium fails to bind. Retry with fresh
|
|
549
|
+
// ports so a transient collision self-heals instead of stranding the profile
|
|
550
|
+
// until the next reconcile. A port-collision attempt fails fast (the child
|
|
551
|
+
// exits on EADDRINUSE), so the retries don't stack the full start timeout.
|
|
552
|
+
let lastErr;
|
|
553
|
+
for (let attempt = 1; attempt <= BB_BROWSER_DAEMON_START_ATTEMPTS; attempt++) {
|
|
554
|
+
try {
|
|
555
|
+
return await this.startDaemonAttempt();
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
lastErr = err;
|
|
559
|
+
if (attempt < BB_BROWSER_DAEMON_START_ATTEMPTS) {
|
|
560
|
+
this.opts.log.warn(`[bb-browser] daemon start attempt ${attempt} failed; retrying with fresh ports: ${String(err)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
565
|
+
}
|
|
566
|
+
async startDaemonAttempt() {
|
|
469
567
|
const host = '127.0.0.1';
|
|
470
568
|
const port = await findFreePort();
|
|
569
|
+
// Each bb-browser daemon launches its OWN Chromium. bb-browser defaults the
|
|
570
|
+
// CDP port to 9222, so running more than one daemon per host (the BYOC
|
|
571
|
+
// supervisor's BrowserProfilePool spawns one bb-browser per profile for
|
|
572
|
+
// hard isolation) would collide on 9222 → only the first profile's Chrome
|
|
573
|
+
// starts. Bind a free CDP port per daemon; every consumer reads the live
|
|
574
|
+
// port from GET /status (cdpEndpoint), so nothing depends on 9222.
|
|
575
|
+
// findFreePort() probes by binding :0 then closing, so two back-to-back calls
|
|
576
|
+
// can hand back the SAME ephemeral port — the HTTP and CDP ports must differ,
|
|
577
|
+
// so re-roll the CDP port if it collides with the HTTP port.
|
|
578
|
+
let cdpPort = await findFreePort();
|
|
579
|
+
for (let i = 0; cdpPort === port && i < 5; i++) {
|
|
580
|
+
cdpPort = await findFreePort();
|
|
581
|
+
}
|
|
582
|
+
if (cdpPort === port) {
|
|
583
|
+
throw new Error('bb-browser: could not allocate distinct HTTP and CDP ports');
|
|
584
|
+
}
|
|
471
585
|
const token = randomToken();
|
|
472
586
|
const daemonPath = resolveBbBrowserDaemonPath();
|
|
473
|
-
const child = spawn(process.execPath, [
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
587
|
+
const child = spawn(process.execPath, [
|
|
588
|
+
daemonPath,
|
|
589
|
+
'--host',
|
|
590
|
+
host,
|
|
591
|
+
'--port',
|
|
592
|
+
String(port),
|
|
593
|
+
'--token',
|
|
594
|
+
token,
|
|
595
|
+
'--cdp-port',
|
|
596
|
+
String(cdpPort),
|
|
597
|
+
], {
|
|
598
|
+
env: buildBrowserDaemonEnv(process.env, this.opts.homeDir),
|
|
478
599
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
479
600
|
});
|
|
480
601
|
// A spawn/exec failure (ENOENT, EACCES, …) emits 'error' on the child, which
|
|
@@ -507,7 +628,11 @@ export class BrowserProfileManager {
|
|
|
507
628
|
await sleep(200);
|
|
508
629
|
}
|
|
509
630
|
}
|
|
510
|
-
child
|
|
631
|
+
// Await the timed-out child's exit before failing: a retry reuses the SAME
|
|
632
|
+
// per-profile home, so returning while this Chromium is still shutting down
|
|
633
|
+
// would race the next startup against it on the same user-data-dir (same
|
|
634
|
+
// guarantee as stop/restart). waitForChildExit sends SIGTERM, then SIGKILL.
|
|
635
|
+
await waitForChildExit(child, this.opts.log);
|
|
511
636
|
throw new Error('bb-browser-daemon did not start in time');
|
|
512
637
|
}
|
|
513
638
|
async post(pathName, daemon, body, timeoutMs = 10_000) {
|