@parall/daemon 1.33.0 → 1.35.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 +9 -9
- package/bundle/parall-claude-agent.js +182 -94
- package/bundle/parall-codex-agent.js +177 -93
- package/bundle/parall-daemon.js +767 -83
- package/dist/clip-runtime/browser-profile-manager.d.ts +13 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +94 -69
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +188 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -0
- package/dist/clip-runtime/browser-viewer-streamer.js +595 -0
- package/dist/clip-runtime/subprocess.d.ts +11 -0
- package/dist/clip-runtime/subprocess.d.ts.map +1 -0
- package/dist/clip-runtime/subprocess.js +33 -0
- package/dist/supervisor.d.ts +10 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +48 -6
- package/package.json +6 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ViewerTurnConfig } from './browser-viewer-streamer.js';
|
|
1
2
|
type BrowserProfileStatus = 'running' | 'stopped' | 'error';
|
|
2
3
|
interface BrowserProfileManagerOptions {
|
|
3
4
|
homeDir: string;
|
|
@@ -22,6 +23,7 @@ export declare class BrowserProfileManager {
|
|
|
22
23
|
private readonly ensuredAccounts;
|
|
23
24
|
private readonly ensuringAccounts;
|
|
24
25
|
private readonly reportedStatuses;
|
|
26
|
+
private readonly viewer;
|
|
25
27
|
constructor(opts: BrowserProfileManagerOptions);
|
|
26
28
|
invoke({ profileId, account, command, input }: BrowserInvokeRequest): Promise<unknown>;
|
|
27
29
|
ensureRuntime(profileId: string): Promise<void>;
|
|
@@ -29,6 +31,14 @@ export declare class BrowserProfileManager {
|
|
|
29
31
|
stopProfile(profileId: string): Promise<void>;
|
|
30
32
|
resetProfile(profileId: string): Promise<void>;
|
|
31
33
|
stop(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Viewer control command entrypoint — delegates to BrowserViewerStreamer
|
|
36
|
+
* (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
|
|
37
|
+
* subprocesses and viewer tab navigation.
|
|
38
|
+
*/
|
|
39
|
+
handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
|
|
40
|
+
/** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
41
|
+
private cdpEndpoint;
|
|
32
42
|
private prepareOpenProfile;
|
|
33
43
|
private openProfileTabOnce;
|
|
34
44
|
private resetProfileOnce;
|
|
@@ -54,6 +64,9 @@ export declare class BrowserProfileManager {
|
|
|
54
64
|
private findAccountTabOnHost;
|
|
55
65
|
private ensureAccount;
|
|
56
66
|
private ensureAccountOwnedTab;
|
|
67
|
+
private profileAlreadyHasOpenTab;
|
|
68
|
+
private hasAccountOwnedTab;
|
|
69
|
+
private findAccountTabMatchingUrl;
|
|
57
70
|
private reportStatus;
|
|
58
71
|
private sendCommand;
|
|
59
72
|
private ensureDaemon;
|
|
@@ -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":"AAMA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAG5F,KAAK,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5D,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,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,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7F;AAeD,UAAU,oBAAoB;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AA0BD,qBAAa,qBAAqB;IAYpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,QAAQ,CAA4C;IAC5D,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAuC;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAI9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAElB,IAAI,EAAE,4BAA4B;IAWzD,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CtF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBhE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7C,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B;;;;OAIG;IACG,mBAAmB,CACvB,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;IAInC,qEAAqE;YACvD,WAAW;YAWX,kBAAkB;YAYlB,kBAAkB;YAUlB,gBAAgB;YAOhB,oBAAoB;YAKpB,yBAAyB;YAezB,aAAa;YAWb,mBAAmB;IAqBjC;;;;;;;;;;;;OAYG;YACW,uBAAuB;IAiCrC,oFAAoF;YACtE,oBAAoB;YAuBpB,aAAa;YAiCb,qBAAqB;YAKrB,wBAAwB;YAQxB,kBAAkB;YAQlB,yBAAyB;IAqBvC,OAAO,CAAC,YAAY;YAON,WAAW;YAaX,YAAY;YAmBZ,kBAAkB;YAUlB,aAAa;YAUb,iBAAiB;YAsBjB,WAAW;YAmDX,IAAI;CA2BnB"}
|
|
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import * as net from 'node:net';
|
|
6
5
|
import * as path from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { BrowserViewerStreamer } from './browser-viewer-streamer.js';
|
|
8
|
+
import { findFreePort, formatErrorForLog, sleep } from './subprocess.js';
|
|
8
9
|
const BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15_000;
|
|
9
10
|
class BrowserCommandError extends Error {
|
|
10
11
|
method;
|
|
@@ -24,9 +25,20 @@ export class BrowserProfileManager {
|
|
|
24
25
|
ensuredAccounts = new Set();
|
|
25
26
|
ensuringAccounts = new Map();
|
|
26
27
|
reportedStatuses = new Map();
|
|
28
|
+
// Live viewer orchestration (bb-viewer streamer subprocesses + viewer nav)
|
|
29
|
+
// lives in BrowserViewerStreamer; the manager hands it the account-scoped
|
|
30
|
+
// bb-browser command path via the host interface.
|
|
31
|
+
viewer;
|
|
27
32
|
constructor(opts) {
|
|
28
33
|
this.opts = opts;
|
|
29
34
|
mkdirSync(this.opts.homeDir, { recursive: true });
|
|
35
|
+
this.viewer = new BrowserViewerStreamer({
|
|
36
|
+
log: this.opts.log,
|
|
37
|
+
sendBrowserCommand: (request) => this.sendCommand(request),
|
|
38
|
+
ensureAccount: (account) => this.ensureAccount(account),
|
|
39
|
+
withDaemonRecovery: (operation, label) => this.withDaemonRecovery(operation, label),
|
|
40
|
+
cdpEndpoint: () => this.cdpEndpoint(),
|
|
41
|
+
});
|
|
30
42
|
}
|
|
31
43
|
async invoke({ profileId, account, command, input }) {
|
|
32
44
|
if (!profileId)
|
|
@@ -88,32 +100,14 @@ export class BrowserProfileManager {
|
|
|
88
100
|
// chatty invoke path while making lifecycle outcomes authoritative.
|
|
89
101
|
this.reportedStatuses.delete(profileId);
|
|
90
102
|
const url = normalizeStartUrl(startUrl);
|
|
91
|
-
let prepareResult = {
|
|
92
|
-
accountCreated: false,
|
|
93
|
-
accountExistedBeforePrepare: true,
|
|
94
|
-
};
|
|
95
|
-
let firstAccountExistedBeforePrepare;
|
|
96
|
-
let recoveredDuringPrepare = false;
|
|
97
|
-
try {
|
|
98
|
-
prepareResult = await this.withDaemonRecovery(() => this.prepareOpenProfile(profileId, url, (exists) => {
|
|
99
|
-
firstAccountExistedBeforePrepare ??= exists;
|
|
100
|
-
}), `prepare open profile ${profileId}`, () => {
|
|
101
|
-
recoveredDuringPrepare = true;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
-
this.reportStatus(profileId, 'error', message);
|
|
107
|
-
throw err;
|
|
108
|
-
}
|
|
109
103
|
try {
|
|
110
|
-
await this.
|
|
104
|
+
await this.withDaemonRecovery(async () => {
|
|
105
|
+
const prepareResult = await this.prepareOpenProfile(profileId, url);
|
|
106
|
+
await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated);
|
|
107
|
+
}, `open profile ${profileId}`);
|
|
111
108
|
this.reportStatus(profileId, 'running');
|
|
112
109
|
}
|
|
113
110
|
catch (err) {
|
|
114
|
-
if (isRecoverableBrowserDaemonError(err)) {
|
|
115
|
-
await this.restartDaemon(`open profile ${profileId}`, err);
|
|
116
|
-
}
|
|
117
111
|
const message = err instanceof Error ? err.message : String(err);
|
|
118
112
|
this.reportStatus(profileId, 'error', message);
|
|
119
113
|
throw err;
|
|
@@ -123,6 +117,8 @@ export class BrowserProfileManager {
|
|
|
123
117
|
if (!profileId)
|
|
124
118
|
throw new Error('browser profile id is required');
|
|
125
119
|
this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
|
|
120
|
+
// A stopped profile can have no live viewer — tear down its streamer.
|
|
121
|
+
this.viewer.killProfileStreamer(profileId);
|
|
126
122
|
// bb-browser-pro does not expose a single-account "dispose context" command.
|
|
127
123
|
// Server-side stopped status is the authority that gates future invokes.
|
|
128
124
|
this.reportStatus(profileId, 'stopped');
|
|
@@ -131,6 +127,8 @@ export class BrowserProfileManager {
|
|
|
131
127
|
if (!profileId)
|
|
132
128
|
throw new Error('browser profile id is required');
|
|
133
129
|
this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
|
|
130
|
+
// Reset destroys the account context the viewer is connected to — kill it.
|
|
131
|
+
this.viewer.killProfileStreamer(profileId);
|
|
134
132
|
try {
|
|
135
133
|
await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
|
|
136
134
|
this.reportStatus(profileId, 'stopped');
|
|
@@ -142,6 +140,8 @@ export class BrowserProfileManager {
|
|
|
142
140
|
}
|
|
143
141
|
}
|
|
144
142
|
async stop() {
|
|
143
|
+
// Tear down all per-profile bb-viewer streamers before the bb-browser daemon.
|
|
144
|
+
this.viewer.shutdown();
|
|
145
145
|
const daemon = this.daemon;
|
|
146
146
|
this.daemon = null;
|
|
147
147
|
this.starting = null;
|
|
@@ -160,23 +160,35 @@ export class BrowserProfileManager {
|
|
|
160
160
|
daemon.child.kill('SIGTERM');
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Viewer control command entrypoint — delegates to BrowserViewerStreamer
|
|
165
|
+
* (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
|
|
166
|
+
* subprocesses and viewer tab navigation.
|
|
167
|
+
*/
|
|
168
|
+
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
169
|
+
return this.viewer.handleViewerCommand(profileId, sessionId, command, input, turn);
|
|
170
|
+
}
|
|
171
|
+
/** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
172
|
+
async cdpEndpoint() {
|
|
173
|
+
const daemon = await this.ensureDaemon();
|
|
174
|
+
const status = await this.post('/status', daemon, undefined, 5_000);
|
|
175
|
+
const host = status.cdpHost?.trim();
|
|
176
|
+
const port = status.cdpPort;
|
|
177
|
+
if (!host || typeof port !== 'number' || !Number.isFinite(port)) {
|
|
178
|
+
throw new Error('bb-browser /status did not report a CDP endpoint');
|
|
179
|
+
}
|
|
180
|
+
return { host, port };
|
|
181
|
+
}
|
|
182
|
+
async prepareOpenProfile(profileId, url) {
|
|
166
183
|
const accountCreated = await this.ensureAccount(profileId, url !== 'about:blank' ? url : undefined);
|
|
167
184
|
await this.sendCommand({ method: 'tab_list', account: profileId });
|
|
168
|
-
return { accountCreated
|
|
185
|
+
return { accountCreated };
|
|
169
186
|
}
|
|
170
|
-
async openProfileTabOnce(profileId, url, accountCreated
|
|
187
|
+
async openProfileTabOnce(profileId, url, accountCreated) {
|
|
171
188
|
if (accountCreated)
|
|
172
189
|
return;
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
return;
|
|
176
|
-
const host = safeUrlHost(url);
|
|
177
|
-
if (host && (await this.findAccountTabOnHost(profileId, host)) !== undefined)
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
190
|
+
if (await this.profileAlreadyHasOpenTab(profileId, url))
|
|
191
|
+
return;
|
|
180
192
|
await this.sendCommand({ method: 'tab_new', account: profileId, url });
|
|
181
193
|
}
|
|
182
194
|
async resetProfileOnce(profileId) {
|
|
@@ -332,13 +344,46 @@ export class BrowserProfileManager {
|
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
346
|
async ensureAccountOwnedTab(account) {
|
|
347
|
+
if (await this.hasAccountOwnedTab(account))
|
|
348
|
+
return;
|
|
349
|
+
await this.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
|
|
350
|
+
}
|
|
351
|
+
async profileAlreadyHasOpenTab(profileId, url) {
|
|
352
|
+
// Lifecycle `open` can be delivered more than once (WS + pending reconcile,
|
|
353
|
+
// retry, or duplicate user action). Treat only the requested target URL as
|
|
354
|
+
// already-open; a same-host different path is still a distinct user intent.
|
|
355
|
+
if (url === 'about:blank')
|
|
356
|
+
return this.hasAccountOwnedTab(profileId);
|
|
357
|
+
return this.findAccountTabMatchingUrl(profileId, url);
|
|
358
|
+
}
|
|
359
|
+
async hasAccountOwnedTab(account) {
|
|
335
360
|
const list = await this.sendCommand({ method: 'tab_list', account });
|
|
336
361
|
const tabs = Array.isArray(list.tabs)
|
|
337
362
|
? list.tabs
|
|
338
363
|
: [];
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
364
|
+
return tabs.some((tab) => tab?.account === account);
|
|
365
|
+
}
|
|
366
|
+
async findAccountTabMatchingUrl(account, url) {
|
|
367
|
+
const expected = comparableUrl(url);
|
|
368
|
+
if (!expected)
|
|
369
|
+
return false;
|
|
370
|
+
const list = await this.sendCommand({ method: 'tab_list', account });
|
|
371
|
+
const tabs = Array.isArray(list.tabs)
|
|
372
|
+
? list.tabs
|
|
373
|
+
: [];
|
|
374
|
+
for (const tab of tabs) {
|
|
375
|
+
if (tab?.account !== account || typeof tab.url !== 'string')
|
|
376
|
+
continue;
|
|
377
|
+
const actual = comparableUrl(tab.url);
|
|
378
|
+
if (!actual)
|
|
379
|
+
continue;
|
|
380
|
+
const sameResource = hostsMatch(actual.host, expected.host) && actual.target === expected.target;
|
|
381
|
+
const sameProtocol = actual.protocol === expected.protocol;
|
|
382
|
+
const redirectedUpgrade = expected.protocol === 'http:' && actual.protocol === 'https:';
|
|
383
|
+
if (sameResource && (sameProtocol || redirectedUpgrade))
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
342
387
|
}
|
|
343
388
|
reportStatus(profileId, status, errorMsg) {
|
|
344
389
|
const marker = `${status}\u0000${errorMsg ?? ''}`;
|
|
@@ -374,7 +419,7 @@ export class BrowserProfileManager {
|
|
|
374
419
|
this.starting = null;
|
|
375
420
|
}
|
|
376
421
|
}
|
|
377
|
-
async withDaemonRecovery(operation, label
|
|
422
|
+
async withDaemonRecovery(operation, label) {
|
|
378
423
|
try {
|
|
379
424
|
return await operation();
|
|
380
425
|
}
|
|
@@ -382,7 +427,6 @@ export class BrowserProfileManager {
|
|
|
382
427
|
if (!isRecoverableBrowserDaemonError(err))
|
|
383
428
|
throw err;
|
|
384
429
|
await this.restartDaemon(label, err);
|
|
385
|
-
onRecovered?.();
|
|
386
430
|
return operation();
|
|
387
431
|
}
|
|
388
432
|
}
|
|
@@ -403,7 +447,7 @@ export class BrowserProfileManager {
|
|
|
403
447
|
this.starting = null;
|
|
404
448
|
this.ensuredAccounts.clear();
|
|
405
449
|
this.ensuringAccounts.clear();
|
|
406
|
-
this.opts.log.warn(`[bb-browser] ${label} hit
|
|
450
|
+
this.opts.log.warn(`[bb-browser] ${label} hit an unavailable Chrome/CDP page target; restarting bb-browser-daemon (${formatErrorForLog(err)})`);
|
|
407
451
|
if (!daemon)
|
|
408
452
|
return;
|
|
409
453
|
try {
|
|
@@ -534,21 +578,23 @@ function isRecoverableBrowserDaemonError(err) {
|
|
|
534
578
|
const lower = message.toLowerCase();
|
|
535
579
|
return (lower.includes('chrome not connected') ||
|
|
536
580
|
lower.includes('cdp at 127.0.0.1') ||
|
|
581
|
+
lower.includes('no page target found') ||
|
|
537
582
|
(lower.includes('bb-browser /command returned 503') &&
|
|
538
583
|
(lower.includes('chrome') || lower.includes('cdp'))));
|
|
539
584
|
}
|
|
540
|
-
function
|
|
585
|
+
function comparableUrl(url) {
|
|
541
586
|
try {
|
|
542
|
-
|
|
587
|
+
const parsed = new URL(url);
|
|
588
|
+
return {
|
|
589
|
+
protocol: parsed.protocol,
|
|
590
|
+
host: parsed.host,
|
|
591
|
+
target: `${parsed.pathname}${parsed.search}${parsed.hash}`,
|
|
592
|
+
};
|
|
543
593
|
}
|
|
544
594
|
catch {
|
|
545
|
-
return
|
|
595
|
+
return null;
|
|
546
596
|
}
|
|
547
597
|
}
|
|
548
|
-
function formatErrorForLog(err) {
|
|
549
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
550
|
-
return message.replace(/\s+/g, ' ').slice(0, 300);
|
|
551
|
-
}
|
|
552
598
|
function resolveBbBrowserDaemonPath() {
|
|
553
599
|
// Bundle channels (CDN self-update, desktop, npm bin — all run the flat
|
|
554
600
|
// esbuild artifact with NO node_modules) ship bb-browser as a sibling flat
|
|
@@ -569,27 +615,6 @@ function resolveBbBrowserDaemonPath() {
|
|
|
569
615
|
}
|
|
570
616
|
return full;
|
|
571
617
|
}
|
|
572
|
-
async function findFreePort() {
|
|
573
|
-
return new Promise((resolve, reject) => {
|
|
574
|
-
const server = net.createServer();
|
|
575
|
-
server.unref();
|
|
576
|
-
server.on('error', reject);
|
|
577
|
-
server.listen(0, '127.0.0.1', () => {
|
|
578
|
-
const address = server.address();
|
|
579
|
-
server.close(() => {
|
|
580
|
-
if (address && typeof address === 'object') {
|
|
581
|
-
resolve(address.port);
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
reject(new Error('failed to allocate free port'));
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
618
|
function randomToken() {
|
|
591
619
|
return randomBytes(16).toString('hex');
|
|
592
620
|
}
|
|
593
|
-
function sleep(ms) {
|
|
594
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
595
|
-
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** Optional managed TURN relay config carried on a `stream.start` command. */
|
|
2
|
+
export interface ViewerTurnConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
credential?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The slice of BrowserProfileManager the viewer streamer depends on: the
|
|
9
|
+
* account-scoped bb-browser /command path and the daemon lifecycle helpers.
|
|
10
|
+
* One-directional — the manager constructs the streamer; the streamer never
|
|
11
|
+
* imports the manager.
|
|
12
|
+
*/
|
|
13
|
+
export interface ViewerStreamerHost {
|
|
14
|
+
log: {
|
|
15
|
+
info(msg: string): void;
|
|
16
|
+
warn(msg: string): void;
|
|
17
|
+
error(msg: string): void;
|
|
18
|
+
};
|
|
19
|
+
/** Account-scoped bb-browser `/command` (BrowserProfileManager.sendCommand). */
|
|
20
|
+
sendBrowserCommand(request: Record<string, unknown> & {
|
|
21
|
+
method: string;
|
|
22
|
+
account?: string;
|
|
23
|
+
}): Promise<Record<string, unknown>>;
|
|
24
|
+
ensureAccount(account: string): Promise<boolean>;
|
|
25
|
+
withDaemonRecovery<T>(operation: () => Promise<T>, label: string): Promise<T>;
|
|
26
|
+
/** cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
27
|
+
cdpEndpoint(): Promise<{
|
|
28
|
+
host: string;
|
|
29
|
+
port: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export declare class BrowserViewerStreamer {
|
|
33
|
+
private readonly host;
|
|
34
|
+
private readonly streamers;
|
|
35
|
+
private stopping;
|
|
36
|
+
constructor(host: ViewerStreamerHost);
|
|
37
|
+
/**
|
|
38
|
+
* Handle a viewer control command for a profile. Throws on any failure (the
|
|
39
|
+
* supervisor maps a throw to a `{error:{message}}` reply); never returns a
|
|
40
|
+
* partial result. `data.input` / `data.turn` / `data.session_id` come from
|
|
41
|
+
* MachineBrowserProfileViewerData.
|
|
42
|
+
*/
|
|
43
|
+
handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
|
|
44
|
+
/**
|
|
45
|
+
* stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
|
|
46
|
+
* one), resolve the profile's account-scoped page-target CDP ws URL, and run
|
|
47
|
+
* the streamer's `connect` command. Records sessionId on the streamer entry.
|
|
48
|
+
*/
|
|
49
|
+
private viewerStreamStart;
|
|
50
|
+
/**
|
|
51
|
+
* stream.answer — apply the client's WebRTC answer to the profile's streamer.
|
|
52
|
+
* Rejects if the streamer's sessionId no longer matches (a stale viewer
|
|
53
|
+
* session from a superseded stream.start).
|
|
54
|
+
*/
|
|
55
|
+
private viewerStreamAnswer;
|
|
56
|
+
/**
|
|
57
|
+
* stream.close — best-effort stop the streamer, then kill + remove it. A
|
|
58
|
+
* stale session's close (independent HTTP legs can arrive out of order after
|
|
59
|
+
* a retry) must NOT tear down a successor session's live streamer, so a
|
|
60
|
+
* mismatched sessionId is ignored. An EMPTY sessionId gets the same stale
|
|
61
|
+
* treatment — a client closed mid-handshake never received a session id, and
|
|
62
|
+
* its unscoped close racing the next session's stream.start would kill that
|
|
63
|
+
* session's live streamer (React StrictMode's dev double-mount exercises
|
|
64
|
+
* exactly this ordering). The only unscoped close is an explicit
|
|
65
|
+
* `{force:true}` input, reserved for a server-initiated admin disconnect.
|
|
66
|
+
*/
|
|
67
|
+
private viewerStreamClose;
|
|
68
|
+
/**
|
|
69
|
+
* stream.switch — point the existing streamer at a different account-scoped
|
|
70
|
+
* tab. input {tab} is the bb-browser short tab id / target id. Guarded by
|
|
71
|
+
* sessionId like stream.answer: a superseded session must not steer the
|
|
72
|
+
* current streamer, and an EMPTY sessionId is rejected the same way — every
|
|
73
|
+
* real caller has its session id by the time it can switch (it arrives with
|
|
74
|
+
* the stream.start response), so a session-less switch is by definition not
|
|
75
|
+
* the current viewer.
|
|
76
|
+
*/
|
|
77
|
+
private viewerStreamSwitch;
|
|
78
|
+
/**
|
|
79
|
+
* close — close an account tab via bb-browser, then, if the closed tab was
|
|
80
|
+
* the one being streamed, re-point the streamer at the profile's next active
|
|
81
|
+
* tab. Without the re-switch the streamer stays bound to a destroyed CDP
|
|
82
|
+
* target and the viewer goes black. Returns `streamed_tab_id` when a
|
|
83
|
+
* re-switch happened so the client can adopt the new active tab.
|
|
84
|
+
*/
|
|
85
|
+
private viewerCloseTab;
|
|
86
|
+
/**
|
|
87
|
+
* Bring a CDP page target to the foreground via Chrome's DevTools HTTP
|
|
88
|
+
* endpoint (`/json/activate/{targetId}`, same host:port as the CDP ws). Chrome
|
|
89
|
+
* does not composite backgrounded tabs, so `Page.startScreencast` on a
|
|
90
|
+
* background tab yields zero frames (black viewer). Best-effort: a failure
|
|
91
|
+
* must not block the stream. host:port + targetId are parsed from the cdpUrl
|
|
92
|
+
* (`ws://host:port/devtools/page/<targetId>`) to avoid an extra /status call.
|
|
93
|
+
*/
|
|
94
|
+
private bringTargetToFront;
|
|
95
|
+
/**
|
|
96
|
+
* Forward a navigation command (tab_list/tab_new/open/reload/back/forward/
|
|
97
|
+
* close) to bb-browser scoped to the profile's account. This lets the human
|
|
98
|
+
* in the live viewer navigate and handle OAuth popups directly. Reuses the
|
|
99
|
+
* same account-scoped sendCommand path the agent invokes use.
|
|
100
|
+
*
|
|
101
|
+
* SECURITY — the `account` field alone does NOT scope tab addressing:
|
|
102
|
+
* bb-browser's ensurePageTarget resolves a client-supplied tab/tabId against
|
|
103
|
+
* EVERY page target in the shared Chrome (all accounts), and tab_list
|
|
104
|
+
* returns every tab with `account` as a mere annotation (verified in
|
|
105
|
+
* @pinixai/bb-browser-pro@0.15.0 dist/daemon.js ensurePageTarget/tab_list).
|
|
106
|
+
* On a multi-profile host that would let one profile's viewer list,
|
|
107
|
+
* navigate, and close other profiles' logged-in tabs. So before forwarding:
|
|
108
|
+
* (a) any tab ref must resolve through accountTabTargetId, which throws on
|
|
109
|
+
* tabs the profile's account does not own; (b) ref-less tab-addressed
|
|
110
|
+
* commands are pinned to the profile's own active tab instead of
|
|
111
|
+
* bb-browser's account-blind global current tab; (c) tab_list output is
|
|
112
|
+
* filtered to the profile's own rows; and (d) open/tab_new URLs must be
|
|
113
|
+
* http(s)/about:blank — file:// or chrome:// would read the host
|
|
114
|
+
* filesystem / browser internals straight into the video stream.
|
|
115
|
+
*/
|
|
116
|
+
private viewerForwardNav;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve `ws://<cdpHost>:<cdpPort>/devtools/page/<targetId>` for a PAGE
|
|
119
|
+
* target owned by THIS profile's account — NOT bb-browser's global current
|
|
120
|
+
* tab (which `getCurrentTabCdpUrl`/`getTabCdpUrl` in the bundled daemon use
|
|
121
|
+
* via `cdp.currentTargetId`, an account-blind global; see daemon.js
|
|
122
|
+
* line ~10389/10410). When `tabRef` is given (stream.switch) we resolve that
|
|
123
|
+
* specific account tab; otherwise the profile's active page tab.
|
|
124
|
+
*
|
|
125
|
+
* Evidence from the installed @pinixai/bb-browser-pro@0.15.0 dist/daemon.js:
|
|
126
|
+
* - GET /status (handleStatus, line ~1659-1668) returns `cdpHost` + `cdpPort`
|
|
127
|
+
* — the CDP host/port the daemon's Chrome listens on.
|
|
128
|
+
* - The CDP page ws URL format is `ws://<cdp.host>:<cdp.port>/devtools/page/<page.id>`
|
|
129
|
+
* (getCurrentTabCdpUrl, line ~10392), where `page.id` is the full CDP
|
|
130
|
+
* targetId (getTargets maps `t.targetId` → `.id`, line ~2461-2466).
|
|
131
|
+
* - tab_list (command handler, line ~1041-1059) returns per-tab
|
|
132
|
+
* `{ tabId: t.id, tab: <shortId>, account: <accountName>, url, ... }`, so
|
|
133
|
+
* `tabId` is exactly the full targetId for the devtools URL and `account`
|
|
134
|
+
* is the per-tab attribution we filter on (reused by findAccountTabOnHost).
|
|
135
|
+
*/
|
|
136
|
+
private resolveAccountPageCdpUrl;
|
|
137
|
+
/**
|
|
138
|
+
* Full CDP targetId of the profile account's active page tab (the owned tab
|
|
139
|
+
* Chrome marks active, else the first owned tab). If the account owns no
|
|
140
|
+
* page tab yet, open `about:blank` for it first (mirrors the agent-invoke
|
|
141
|
+
* path's "always give a fresh account an owned tab" rule).
|
|
142
|
+
*/
|
|
143
|
+
private accountActivePageTargetId;
|
|
144
|
+
/**
|
|
145
|
+
* Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
|
|
146
|
+
* Chrome marks `active` when it belongs to this account, else the first owned
|
|
147
|
+
* tab (the global active flag is account-blind — another profile's tab may
|
|
148
|
+
* hold it, which must not leak here).
|
|
149
|
+
*/
|
|
150
|
+
private firstAccountPageTargetId;
|
|
151
|
+
/**
|
|
152
|
+
* Full CDP targetId for a specific account tab, addressed by the bb-browser
|
|
153
|
+
* short tab id (`tab`) or full target id (`tabId`). Used by stream.switch.
|
|
154
|
+
*/
|
|
155
|
+
private accountTabTargetId;
|
|
156
|
+
/** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
|
|
157
|
+
private accountTabs;
|
|
158
|
+
/**
|
|
159
|
+
* Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
|
|
160
|
+
* is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
|
|
161
|
+
* defaulting to `bb-viewer` on PATH — we NEVER download it. A free port is
|
|
162
|
+
* allocated per streamer. Stores the entry in `this.streamers` once healthy.
|
|
163
|
+
*/
|
|
164
|
+
private spawnStreamer;
|
|
165
|
+
/**
|
|
166
|
+
* SIGTERM with a SIGKILL escalation: a bb-viewer that hangs or ignores TERM
|
|
167
|
+
* would otherwise outlive its teardown holding the port + CDP session (the
|
|
168
|
+
* shell-level pattern-kill only exists on the hosted pod, not BYOC). The
|
|
169
|
+
* escalation timer is unref'd so it never holds the daemon open; it is
|
|
170
|
+
* cleared on exit. Caveat: a daemon process that exits immediately after
|
|
171
|
+
* shutdown() abandons the timer — acceptable, the TERM was still sent.
|
|
172
|
+
*/
|
|
173
|
+
private killChild;
|
|
174
|
+
/**
|
|
175
|
+
* POST a command to the profile's bb-viewer `/command` endpoint. Parses the
|
|
176
|
+
* `{result}` / `{error:{message}}` envelope (bb-viewer api.go) and returns
|
|
177
|
+
* the inner result, throwing the error message on failure.
|
|
178
|
+
*/
|
|
179
|
+
private streamerCommand;
|
|
180
|
+
/** Kill + remove the profile's bb-viewer streamer, if any. Idempotent. */
|
|
181
|
+
killProfileStreamer(profileId: string): void;
|
|
182
|
+
/**
|
|
183
|
+
* Tear down every profile streamer and refuse new spawns (daemon stop).
|
|
184
|
+
* Idempotent.
|
|
185
|
+
*/
|
|
186
|
+
shutdown(): void;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=browser-viewer-streamer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-viewer-streamer.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-viewer-streamer.ts"],"names":[],"mappings":"AAsCA,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAcD;;;;;GAKG;AACH,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,gFAAgF;IAChF,kBAAkB,CAChB,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,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,kBAAkB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9E,gEAAgE;IAChE,WAAW,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,qBAAa,qBAAqB;IAUpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAI9D,OAAO,CAAC,QAAQ,CAAS;gBAEI,IAAI,EAAE,kBAAkB;IAErD;;;;;OAKG;IACG,mBAAmB,CACvB,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;IAyBnC;;;;OAIG;YACW,iBAAiB;IA+C/B;;;;OAIG;YACW,kBAAkB;IAgBhC;;;;;;;;;;OAUG;YACW,iBAAiB;IAuB/B;;;;;;;;OAQG;YACW,kBAAkB;IAuBhC;;;;;;OAMG;YACW,cAAc;IAiC5B;;;;;;;OAOG;YACW,kBAAkB;IAqBhC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,gBAAgB;IA6C9B;;;;;;;;;;;;;;;;;;OAkBG;YACW,wBAAwB;IAetC;;;;;OAKG;YACW,yBAAyB;IAevC;;;;;OAKG;YACW,wBAAwB;IAatC;;;OAGG;YACW,kBAAkB;IAehC,yFAAyF;YAC3E,WAAW;IAgBzB;;;;;OAKG;YACW,aAAa;IA2F3B;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IAqBjB;;;;OAIG;YACW,eAAe;IA+B7B,0EAA0E;IAC1E,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAO5C;;;OAGG;IACH,QAAQ,IAAI,IAAI;CAMjB"}
|