@parall/daemon 1.36.0 → 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 +384 -32
- package/bundle/parall-claude-agent.js +426 -51
- package/bundle/parall-codex-agent.js +425 -49
- package/bundle/parall-daemon.js +33336 -31520
- package/dist/browser-pod.d.ts +7 -0
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +73 -3
- 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 +34 -5
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +166 -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 -1
- 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 +64 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +629 -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
|
@@ -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
|
|
@@ -321,10 +376,15 @@ export class BrowserProfileManager {
|
|
|
321
376
|
if (!isBrowserAccountInfoUnauthenticatedError(err)) {
|
|
322
377
|
if (!isBrowserAccountNotFoundError(err))
|
|
323
378
|
throw err;
|
|
379
|
+
// Resolve the proxy ONLY here, at the single account-creation choke point.
|
|
380
|
+
// A thrown resolve fails closed (no account rather than direct egress for a
|
|
381
|
+
// proxy-configured profile). bb-browser binds the proxy at account_create.
|
|
382
|
+
const proxy = this.opts.resolveProxy ? await this.opts.resolveProxy(account) : null;
|
|
324
383
|
await this.sendCommand({
|
|
325
384
|
method: 'account_create',
|
|
326
385
|
account,
|
|
327
386
|
...(accountUrl ? { accountUrl } : {}),
|
|
387
|
+
...proxyCreateFields(proxy),
|
|
328
388
|
});
|
|
329
389
|
accountCreated = true;
|
|
330
390
|
}
|
|
@@ -403,20 +463,39 @@ export class BrowserProfileManager {
|
|
|
403
463
|
async ensureDaemon() {
|
|
404
464
|
if (this.restarting)
|
|
405
465
|
await this.restarting;
|
|
466
|
+
if (this.stopping)
|
|
467
|
+
throw new Error('browser profile manager is stopping');
|
|
406
468
|
if (this.daemon &&
|
|
407
469
|
this.daemon.child.exitCode === null &&
|
|
408
470
|
this.daemon.child.signalCode === null) {
|
|
409
471
|
return this.daemon;
|
|
410
472
|
}
|
|
411
|
-
if (this.starting)
|
|
412
|
-
|
|
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
|
+
}
|
|
413
484
|
this.starting = this.startDaemon();
|
|
485
|
+
const startPromise = this.starting;
|
|
414
486
|
try {
|
|
415
|
-
|
|
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;
|
|
416
494
|
return this.daemon;
|
|
417
495
|
}
|
|
418
496
|
finally {
|
|
419
|
-
this.starting
|
|
497
|
+
if (this.starting === startPromise)
|
|
498
|
+
this.starting = null;
|
|
420
499
|
}
|
|
421
500
|
}
|
|
422
501
|
async withDaemonRecovery(operation, label) {
|
|
@@ -456,20 +535,67 @@ export class BrowserProfileManager {
|
|
|
456
535
|
catch {
|
|
457
536
|
/* best-effort */
|
|
458
537
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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);
|
|
462
544
|
}
|
|
463
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() {
|
|
464
567
|
const host = '127.0.0.1';
|
|
465
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
|
+
}
|
|
466
585
|
const token = randomToken();
|
|
467
586
|
const daemonPath = resolveBbBrowserDaemonPath();
|
|
468
|
-
const child = spawn(process.execPath, [
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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),
|
|
473
599
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
474
600
|
});
|
|
475
601
|
// A spawn/exec failure (ENOENT, EACCES, …) emits 'error' on the child, which
|
|
@@ -502,7 +628,11 @@ export class BrowserProfileManager {
|
|
|
502
628
|
await sleep(200);
|
|
503
629
|
}
|
|
504
630
|
}
|
|
505
|
-
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);
|
|
506
636
|
throw new Error('bb-browser-daemon did not start in time');
|
|
507
637
|
}
|
|
508
638
|
async post(pathName, daemon, body, timeoutMs = 10_000) {
|
|
@@ -618,3 +748,15 @@ function resolveBbBrowserDaemonPath() {
|
|
|
618
748
|
function randomToken() {
|
|
619
749
|
return randomBytes(16).toString('hex');
|
|
620
750
|
}
|
|
751
|
+
/** Render a proxy config into bb-browser `account_create` fields (camelCase, the
|
|
752
|
+
* bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
|
|
753
|
+
function proxyCreateFields(proxy) {
|
|
754
|
+
if (!proxy?.server)
|
|
755
|
+
return {};
|
|
756
|
+
const fields = { proxyServer: proxy.server };
|
|
757
|
+
if (proxy.username)
|
|
758
|
+
fields.proxyUsername = proxy.username;
|
|
759
|
+
if (proxy.password)
|
|
760
|
+
fields.proxyPassword = proxy.password;
|
|
761
|
+
return fields;
|
|
762
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { type BrowserInvokeRequest, type BrowserInvoker, type BrowserProfileManagerOptions, type BrowserProfileStatus } from './browser-profile-manager.js';
|
|
2
|
+
import type { ViewerTurnConfig } from './browser-viewer-streamer.js';
|
|
3
|
+
/**
|
|
4
|
+
* The subset of BrowserProfileManager the pool drives. Extracted so tests can
|
|
5
|
+
* inject a fake runtime — the real manager spawns bb-browser + Chromium on the
|
|
6
|
+
* first command, which a unit test must not do — via
|
|
7
|
+
* {@link BrowserProfilePoolOptions.createManager}.
|
|
8
|
+
*/
|
|
9
|
+
export interface BrowserProfileRuntime {
|
|
10
|
+
invoke(request: BrowserInvokeRequest): Promise<unknown>;
|
|
11
|
+
ensureRuntime(profileId: string, opts?: {
|
|
12
|
+
forceStatusReport?: boolean;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
openProfile(profileId: string, startUrl?: string): Promise<void>;
|
|
15
|
+
handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
|
|
16
|
+
stopProfile(profileId: string): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface BrowserProfilePoolOptions {
|
|
20
|
+
/** Parent dir under which each profile gets its own `<baseHomeDir>/<profileId>` home. */
|
|
21
|
+
baseHomeDir: string;
|
|
22
|
+
log: BrowserProfileManagerOptions['log'];
|
|
23
|
+
/** Report a profile's status to the server. Unlike the manager's 3-arg callback,
|
|
24
|
+
* the pool's carries the lifecycle generation the op was acting under (PR #1650),
|
|
25
|
+
* so the server can reject a stale report. The pool injects the generation for
|
|
26
|
+
* manager-driven reports (it owns the per-op generation); supervisor passes it
|
|
27
|
+
* straight to client.reportBrowserProfileStatus. */
|
|
28
|
+
reportStatus?: (profileId: string, status: BrowserProfileStatus, errorMsg?: string, generation?: number) => void;
|
|
29
|
+
/** Per-profile outbound proxy resolver (#1642), threaded to each profile's
|
|
30
|
+
* manager so its isolated Chromium binds its own egress proxy at account_create. */
|
|
31
|
+
resolveProxy?: BrowserProfileManagerOptions['resolveProxy'];
|
|
32
|
+
/**
|
|
33
|
+
* Per-profile runtime factory. Defaults to a real {@link BrowserProfileManager};
|
|
34
|
+
* overridable in tests to avoid spawning a real bb-browser / Chromium.
|
|
35
|
+
*/
|
|
36
|
+
createManager?: (opts: BrowserProfileManagerOptions) => BrowserProfileRuntime;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* BrowserProfilePool — true per-profile isolation for the BYOC daemon supervisor.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS EXISTS. A single bb-browser/Chromium shared across profiles isolates
|
|
42
|
+
* only at the bb-browser "account" (CDP BrowserContext) level. That is real for
|
|
43
|
+
* cookies/storage, but bb-browser's COMMAND routing is account-blind: any command
|
|
44
|
+
* issued without an explicit tabId falls through to a single GLOBAL current tab
|
|
45
|
+
* (cdp.currentTargetId, else targets[0]) regardless of the `account` field
|
|
46
|
+
* (bb-browser-pro daemon.js ensurePageTarget). So when the global current tab
|
|
47
|
+
* belongs to profile A, a command meant for profile B executes in A's context —
|
|
48
|
+
* e.g. logging into a second account overwrites the first, and two profiles can
|
|
49
|
+
* never hold two different sessions at once (reproduced on a real Chrome).
|
|
50
|
+
*
|
|
51
|
+
* THE FIX. Give every profile its OWN bb-browser daemon + Chromium + on-disk home
|
|
52
|
+
* (`<baseHomeDir>/<profileId>`). Each Chromium then hosts exactly one profile, so
|
|
53
|
+
* there is no cross-profile global tab to leak into — isolation is enforced by the
|
|
54
|
+
* OS process + user-data-dir boundary, not by remembering to scope every command.
|
|
55
|
+
* The pool owns one {@link BrowserProfileManager} per profile and routes by
|
|
56
|
+
* profileId; an idle (stopped) profile holds no Chromium, so cost scales with the
|
|
57
|
+
* number of CONCURRENTLY-OPEN profiles, not the number that ever existed.
|
|
58
|
+
*
|
|
59
|
+
* NOT used by the hosted browser pod (`browser-pod.ts`): a pod serves exactly one
|
|
60
|
+
* profile, so it keeps using a single BrowserProfileManager directly (and its
|
|
61
|
+
* BrowserStateStore snapshots that manager's flat homeDir to S3 — the pool's
|
|
62
|
+
* per-profile nesting would break that). The pool is supervisor-only.
|
|
63
|
+
*
|
|
64
|
+
* OWNERSHIP. The pool is a per-profile runtime REGISTRY + the per-profile mutex
|
|
65
|
+
* (create / stop / list-active / invoke / viewer, serialized via `enqueue()`). The
|
|
66
|
+
* DESIRED-STATE coordinator is the supervisor: the server (clip-service) is the
|
|
67
|
+
* SSOT for which profiles should be live, and `DaemonSupervisor.reconcileBrowserProfiles`
|
|
68
|
+
* converges the local registry to it (open/ensure desired, stop the rest via
|
|
69
|
+
* `activeProfileIds()`). The pool keeps one bit of lifecycle policy — the `stopped`
|
|
70
|
+
* fence — because invoke (hub channel) and viewer funnel through it and must reject
|
|
71
|
+
* a stale command without a server round-trip.
|
|
72
|
+
*/
|
|
73
|
+
export declare class BrowserProfilePool implements BrowserInvoker {
|
|
74
|
+
private readonly opts;
|
|
75
|
+
private readonly managers;
|
|
76
|
+
private readonly opChains;
|
|
77
|
+
private readonly stopped;
|
|
78
|
+
private readonly stopSeq;
|
|
79
|
+
private readonly generations;
|
|
80
|
+
private readonly opGeneration;
|
|
81
|
+
private stopping;
|
|
82
|
+
constructor(opts: BrowserProfilePoolOptions);
|
|
83
|
+
/** Inbound hub browser invoke for a profile — routed to that profile's own daemon. */
|
|
84
|
+
invoke(request: BrowserInvokeRequest): Promise<unknown>;
|
|
85
|
+
private runWithGeneration;
|
|
86
|
+
/**
|
|
87
|
+
* Reconcile-time: ensure the profile's runtime (account + tab) is live. If
|
|
88
|
+
* `sinceSeq` (the fence generation observed when this op was enqueued) is given
|
|
89
|
+
* and a newer stop has been observed since, skip — a stale reconcile/open queued
|
|
90
|
+
* before a stop must not un-fence and revive a profile about to be torn down
|
|
91
|
+
* (the pending stop wins; last-write-wins).
|
|
92
|
+
*/
|
|
93
|
+
ensureRuntime(profileId: string, sinceSeq?: number, generation?: number, opts?: {
|
|
94
|
+
forceStatusReport?: boolean;
|
|
95
|
+
}): Promise<void>;
|
|
96
|
+
/** Lifecycle `open` — open (or reuse) the profile's start tab. `sinceSeq`: see ensureRuntime. */
|
|
97
|
+
openProfile(profileId: string, startUrl?: string, sinceSeq?: number, generation?: number): Promise<void>;
|
|
98
|
+
/** True if a newer stop was observed after this revive captured `sinceSeq`. */
|
|
99
|
+
private isStaleRevive;
|
|
100
|
+
/** Live-viewer control command for a profile — routed to its own daemon/streamer. */
|
|
101
|
+
handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
|
|
102
|
+
/**
|
|
103
|
+
* Lifecycle `stop` (explicit, owner-driven) — stop the profile's viewer + tear
|
|
104
|
+
* down its bb-browser daemon and Chromium to free the resources (an idle profile
|
|
105
|
+
* must not keep a browser running). Confirms `stopped` back to the server: the
|
|
106
|
+
* stop API already set the server row to `stopped`, and this report keeps the
|
|
107
|
+
* daemon authoritative over the actual runtime state (the no-runtime case still
|
|
108
|
+
* confirms). For reconcile-driven cleanup of profiles the server no longer owns
|
|
109
|
+
* here, use {@link releaseRuntime} — it must NOT report.
|
|
110
|
+
*/
|
|
111
|
+
stopProfile(profileId: string, generation?: number): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Reconcile teardown (negative convergence) — release a local runtime the server
|
|
114
|
+
* no longer lists as live on this machine (stopped / deleted / reassigned).
|
|
115
|
+
* Fences + disposes like a stop, but does NOT report status: here the server is
|
|
116
|
+
* the desired-state SSOT, so a daemon-side `stopped` push is semantically wrong
|
|
117
|
+
* (for a profile reassigned to another machine it isn't ours to report — the
|
|
118
|
+
* server's machine-binding check rejects it as 404 anyway) and only emits
|
|
119
|
+
* spurious calls. Local-only cleanup.
|
|
120
|
+
*/
|
|
121
|
+
releaseRuntime(profileId: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Synchronously fence a profile (reject late invoke/viewer until an explicit
|
|
124
|
+
* reopen) WITHOUT tearing anything down. The supervisor calls this the moment a
|
|
125
|
+
* stop/reset lifecycle event is received, before the queued stop/reset op runs:
|
|
126
|
+
* a hub invoke — which bypasses the supervisor queue and hits {@link invoke}
|
|
127
|
+
* directly — authorized just before the server row flipped to `stopped` could
|
|
128
|
+
* otherwise arrive in that window and spawn a transient Chromium the stop then
|
|
129
|
+
* immediately tears down. Fencing now rejects it instead. An in-flight invoke
|
|
130
|
+
* already past the fence check is unaffected (the queued stop serializes behind
|
|
131
|
+
* it); open/ensure clears the fence. Idempotent; no-op once stopping.
|
|
132
|
+
*/
|
|
133
|
+
fence(profileId: string): void;
|
|
134
|
+
/** The current fence generation for a profile — captured by a revive at enqueue. */
|
|
135
|
+
stopSeqOf(profileId: string): number;
|
|
136
|
+
/**
|
|
137
|
+
* Lifecycle `reset` — wipe the profile's persisted state (cookies, localStorage,
|
|
138
|
+
* IndexedDB, cache) for a clean slate. Removes BOTH the per-profile home AND any
|
|
139
|
+
* unmigrated legacy flat cookie file — a reset done before the profile's first
|
|
140
|
+
* post-upgrade open would otherwise leave the legacy file in place, and the lazy
|
|
141
|
+
* migration would re-import the old login on the next open, silently undoing the
|
|
142
|
+
* reset. Done AFTER the daemon is stopped so Chromium has released its files.
|
|
143
|
+
* Isolated by construction — it can never touch another profile's state.
|
|
144
|
+
* Fail-closed: if the wipe fails (e.g. Chromium still holds a file, or a
|
|
145
|
+
* permission error), report `error` and throw rather than report `stopped` —
|
|
146
|
+
* a reset that left state on disk must not be marked clean, since the next open
|
|
147
|
+
* could restore the old login. Reports `stopped` only on a confirmed wipe.
|
|
148
|
+
*/
|
|
149
|
+
resetProfile(profileId: string, generation?: number, resetGeneration?: number): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Apply a reset the daemon hasn't yet (caught on reconnect when the server's
|
|
152
|
+
* reset_generation is newer than this daemon's recorded one). Same wipe as
|
|
153
|
+
* resetProfile but WITHOUT a status report — the caller drives desired state (an
|
|
154
|
+
* open/ensure reports running for a still-desired profile; a stopped profile is
|
|
155
|
+
* left stopped). Must run BEFORE any open/ensure for the profile so a reopen starts
|
|
156
|
+
* from clean state, never the old cookies (the supervisor enqueues it first).
|
|
157
|
+
*/
|
|
158
|
+
applyResetWipe(profileId: string, resetGeneration: number): Promise<void>;
|
|
159
|
+
private doResetWipe;
|
|
160
|
+
/**
|
|
161
|
+
* Reconnect cleanup for a profile the server no longer lists AT ALL (deleted or
|
|
162
|
+
* reassigned away): dispose any runtime and wipe the per-profile home — the old host
|
|
163
|
+
* must not keep its cookies/storage. NO status report (the server is the SSOT and the
|
|
164
|
+
* row is gone there); fences too. Idempotent (a wiped home simply isn't found again).
|
|
165
|
+
*/
|
|
166
|
+
wipeAbsent(profileId: string): Promise<void>;
|
|
167
|
+
/**
|
|
168
|
+
* Tear down every profile's daemon (supervisor shutdown). TERMINAL: the
|
|
169
|
+
* supervisor discards the pool after this, so `stopping` stays set forever —
|
|
170
|
+
* manager() refuses any further op. Drain outstanding per-profile chains BEFORE
|
|
171
|
+
* clearing the maps, so a queued op whose `prev` settles only after stop()
|
|
172
|
+
* returns can't slip through manager() and recreate a runtime (orphaning a
|
|
173
|
+
* Chromium past shutdown); `stopping` is the backstop if one still races in.
|
|
174
|
+
*/
|
|
175
|
+
stop(): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Registry view: profile ids that currently hold a live local runtime. The
|
|
178
|
+
* supervisor (the desired-state coordinator) diffs this against the server's
|
|
179
|
+
* desired-live set on full reconcile and stops the extras via stopProfile() —
|
|
180
|
+
* the convergence policy lives there, not here; the pool only owns runtime
|
|
181
|
+
* lifecycle mechanics.
|
|
182
|
+
*/
|
|
183
|
+
activeProfileIds(): string[];
|
|
184
|
+
/**
|
|
185
|
+
* Run `op` after every prior operation for this profile settles — a per-profile
|
|
186
|
+
* serialization so invoke / open / stop / reset / viewer never interleave for
|
|
187
|
+
* one profile (different profiles still run in parallel). Bounded: bb-browser
|
|
188
|
+
* commands time out (~30s), so a stop never waits forever behind a hung invoke.
|
|
189
|
+
* Also normalizes a synchronous throw from `op` into a rejected promise.
|
|
190
|
+
*
|
|
191
|
+
* QUEUE CONTRACT. This is the FINAL per-profile mutex — EVERY pool entrypoint
|
|
192
|
+
* (invoke, open, ensure, stop, reset, viewer) goes through it, so it alone
|
|
193
|
+
* guarantees per-profile mutual exclusion. The supervisor's separate
|
|
194
|
+
* `enqueueBrowserProfileOp` is a higher-level ORDERING layer for the ops it
|
|
195
|
+
* originates (WS lifecycle/viewer events + reconcile), not a second mutex. Hub
|
|
196
|
+
* invokes arrive from the clip runtime and do NOT pass through the supervisor
|
|
197
|
+
* queue — only this one. So: do not assume "all profile ops share one queue";
|
|
198
|
+
* they share THIS one.
|
|
199
|
+
*/
|
|
200
|
+
private enqueue;
|
|
201
|
+
/**
|
|
202
|
+
* Get (or lazily create) the dedicated manager for a profile. Synchronous so the
|
|
203
|
+
* get-or-create is atomic — the bb-browser daemon itself starts lazily on first
|
|
204
|
+
* command, not here.
|
|
205
|
+
*/
|
|
206
|
+
private manager;
|
|
207
|
+
/** Stop a manager and drop it from the map (identity-checked so a concurrent
|
|
208
|
+
* recreate during the async stop is never evicted). */
|
|
209
|
+
private dispose;
|
|
210
|
+
private profileHome;
|
|
211
|
+
/** Legacy shared-layout cookie file for a profile (pre per-profile homes). */
|
|
212
|
+
private legacyAccountFile;
|
|
213
|
+
/**
|
|
214
|
+
* Remove every on-disk trace of a profile: its per-profile home AND any
|
|
215
|
+
* unmigrated legacy flat cookie file — otherwise a later open would re-import
|
|
216
|
+
* the legacy cookies (see resetProfile). Throws if any target could not be
|
|
217
|
+
* removed so the caller can fail the reset closed rather than report a clean
|
|
218
|
+
* slate while state survives on disk.
|
|
219
|
+
*/
|
|
220
|
+
private removeProfileState;
|
|
221
|
+
private metaFile;
|
|
222
|
+
/** The reset_generation this daemon has applied for the profile (0 if unknown). */
|
|
223
|
+
appliedResetGeneration(profileId: string): number;
|
|
224
|
+
private writeResetGeneration;
|
|
225
|
+
/**
|
|
226
|
+
* Profile ids that have ANY local state on this host — the reconcile's inventory for
|
|
227
|
+
* finding deleted/reassigned profiles whose residue must be wiped. Union of the two
|
|
228
|
+
* storage layouts this pool owns:
|
|
229
|
+
* - per-profile home dirs `<baseHomeDir>/brp_*`
|
|
230
|
+
* - legacy flat account files `<baseHomeDir>/accounts/brp_*.json` (pre per-profile
|
|
231
|
+
* homes; a profile deleted before its first post-upgrade reopen has ONLY this file
|
|
232
|
+
* and no home dir, so home-only enumeration would miss its login residue)
|
|
233
|
+
* Only names passing the strict brp_ check are returned, and the two layouts are
|
|
234
|
+
* deduped, so reconnect cleanup never touches the `accounts/` dir itself, a hidden
|
|
235
|
+
* dir, `legacy.json`, or any non-profile entry. removeProfileState wipes both layouts.
|
|
236
|
+
*/
|
|
237
|
+
localStateProfileIds(): string[];
|
|
238
|
+
/**
|
|
239
|
+
* One-time, lazy migration from the legacy shared layout. Before per-profile
|
|
240
|
+
* homes, all profiles shared one bb-browser home and bb-browser persisted each
|
|
241
|
+
* profile's cookies to `<baseHomeDir>/accounts/<profileId>.json` (account name =
|
|
242
|
+
* profileId). Move that file into the profile's own `accounts/` dir on first use
|
|
243
|
+
* so existing cookie logins survive the upgrade instead of forcing a re-login.
|
|
244
|
+
* Only cookies are migrated — the shared `chrome-data/` was a single comingled
|
|
245
|
+
* Chromium profile that cannot be split per profile, and bb-browser never
|
|
246
|
+
* persisted localStorage/IndexedDB across restarts anyway.
|
|
247
|
+
*/
|
|
248
|
+
private migrateFlatProfileState;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=browser-profile-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"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"}
|