@parall/daemon 1.44.0 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/manifest.json +15 -15
- package/bundle/parall-browser-pod.js +29726 -375
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26446 -384
- package/bundle/parall-codex-agent.js +27490 -1549
- package/bundle/parall-daemon.js +31558 -1617
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +229 -2
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/config.d.ts.map +1 -1
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/runtime-bin-resolver.d.ts +7 -1
- package/dist/runtime-bin-resolver.d.ts.map +1 -1
- package/dist/runtime-bin-resolver.js +57 -22
- package/dist/runtimes.d.ts +15 -4
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +60 -5
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +52 -188
- package/dist/win-lifecycle.d.ts +96 -0
- package/dist/win-lifecycle.d.ts.map +1 -0
- package/dist/win-lifecycle.js +229 -0
- package/dist/win-service.d.ts +119 -0
- package/dist/win-service.d.ts.map +1 -0
- package/dist/win-service.js +226 -0
- package/package.json +8 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { execSync, spawn } from 'node:child_process';
|
|
1
|
+
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import * as readline from 'node:readline';
|
|
6
6
|
import { daemonConfigDir, daemonConfigPath } from './config.js';
|
|
7
|
+
import { WIN_TASK_NAME, buildLauncherCjs, buildLauncherVbs, buildTaskXml, encodeUtf16LeBom, winServicePaths, } from './win-service.js';
|
|
8
|
+
import { probePidIdentity, queryTaskDisabled, queryTaskExists, readTrackedPid, stopDaemonWindows, uninstallDaemonWindows, } from './win-lifecycle.js';
|
|
7
9
|
const CONFIG_DIR = daemonConfigDir();
|
|
8
10
|
const CONFIG_PATH = daemonConfigPath();
|
|
9
11
|
function readConfig() {
|
|
@@ -34,6 +36,188 @@ function isMacOS() {
|
|
|
34
36
|
function isLinux() {
|
|
35
37
|
return process.platform === 'linux';
|
|
36
38
|
}
|
|
39
|
+
function isWindows() {
|
|
40
|
+
return process.platform === 'win32';
|
|
41
|
+
}
|
|
42
|
+
// ---- Windows (Task Scheduler) service management ----
|
|
43
|
+
// Artifact generation lives in win-service.ts (pure strings); the
|
|
44
|
+
// stop/uninstall/probe state machines live in win-lifecycle.ts with
|
|
45
|
+
// injected exec/fs (fail-closed contract + unit tests). This section binds
|
|
46
|
+
// the real execFileSync/fs and owns install-time writes. All external
|
|
47
|
+
// commands run through argv arrays — no shell, so spaces / non-ASCII in
|
|
48
|
+
// %USERPROFILE% paths never need quoting here.
|
|
49
|
+
const SLEEP_SIGNAL = new Int32Array(new SharedArrayBuffer(4));
|
|
50
|
+
function sleepSync(ms) {
|
|
51
|
+
Atomics.wait(SLEEP_SIGNAL, 0, 0, ms);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Real-command binding for win-lifecycle: capture the exit code, never throw.
|
|
55
|
+
*
|
|
56
|
+
* Every call is bounded. schtasks talks to the Task Scheduler service over RPC
|
|
57
|
+
* and powershell.exe loads CIM — either can hang (a wedged service, a stalled
|
|
58
|
+
* WMI provider), and an unbounded execFileSync would block `stop` / `status` /
|
|
59
|
+
* `uninstall` forever with no way out. A timeout kills the child and surfaces
|
|
60
|
+
* as status=null → spawnError → 'indeterminate', which the fail-closed
|
|
61
|
+
* lifecycle already handles correctly (state is kept, exit is non-zero).
|
|
62
|
+
*/
|
|
63
|
+
const WIN_CMD_TIMEOUT_MS = 20_000;
|
|
64
|
+
function runWinCmd(file, args) {
|
|
65
|
+
try {
|
|
66
|
+
const stdout = execFileSync(file, args, {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
+
timeout: WIN_CMD_TIMEOUT_MS,
|
|
71
|
+
});
|
|
72
|
+
return { code: 0, stdout };
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const e = err;
|
|
76
|
+
const code = typeof e.status === 'number' ? e.status : null;
|
|
77
|
+
return {
|
|
78
|
+
code,
|
|
79
|
+
stdout: String(e.stdout ?? ''),
|
|
80
|
+
spawnError: code === null ? String(e.message ?? err) : undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function winDeps() {
|
|
85
|
+
return {
|
|
86
|
+
run: runWinCmd,
|
|
87
|
+
fs: {
|
|
88
|
+
readFile(p) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readFileSync(p, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
unlink(p) {
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(p);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return err.code === 'ENOENT';
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
paths: winServicePaths(os.homedir()),
|
|
107
|
+
sleep: sleepSync,
|
|
108
|
+
now: () => Date.now(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function reportWinProblems(problems) {
|
|
112
|
+
for (const problem of problems) {
|
|
113
|
+
console.error(`ERROR: ${problem}`);
|
|
114
|
+
}
|
|
115
|
+
console.error('State was left in place so nothing is orphaned; fix the cause and retry.');
|
|
116
|
+
}
|
|
117
|
+
function installServiceWindows() {
|
|
118
|
+
let npmEntry;
|
|
119
|
+
try {
|
|
120
|
+
npmEntry = fs.realpathSync(process.argv[1] ?? '');
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
console.error('Cannot resolve the daemon entry script; reinstall with `npm install -g @parall/daemon`.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
if (/[\\/]_npx[\\/]/.test(npmEntry)) {
|
|
127
|
+
console.warn('Warning: installing from an npx cache path. Run `npm install -g @parall/daemon` and re-run\n' +
|
|
128
|
+
'`parall-daemon service install`, or the service breaks when the npx cache is pruned\n' +
|
|
129
|
+
'(a completed self-update heals this by switching to the overlay bundle).');
|
|
130
|
+
}
|
|
131
|
+
const p = winServicePaths(os.homedir());
|
|
132
|
+
fs.mkdirSync(p.serviceDir, { recursive: true });
|
|
133
|
+
fs.mkdirSync(p.logDir, { recursive: true });
|
|
134
|
+
fs.writeFileSync(p.launcherCjs, buildLauncherCjs({
|
|
135
|
+
npmEntry,
|
|
136
|
+
overlayEntry: p.overlayEntry,
|
|
137
|
+
pidFile: p.pidFile,
|
|
138
|
+
logFile: p.logFile,
|
|
139
|
+
}));
|
|
140
|
+
// Pre-.cjs installs wrote a .js launcher; remove it so nothing stale can
|
|
141
|
+
// be referenced or mistaken for the active artifact.
|
|
142
|
+
try {
|
|
143
|
+
fs.unlinkSync(p.legacyLauncherJs);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
const wscriptExe = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'wscript.exe');
|
|
147
|
+
fs.writeFileSync(p.launcherVbs, encodeUtf16LeBom(buildLauncherVbs({ nodeExe: process.execPath, launcherCjs: p.launcherCjs })));
|
|
148
|
+
fs.writeFileSync(p.taskXml, encodeUtf16LeBom(buildTaskXml({ wscriptExe, launcherVbs: p.launcherVbs })));
|
|
149
|
+
// /F replaces an existing task; without it schtasks prompts interactively
|
|
150
|
+
// and a scripted install hangs.
|
|
151
|
+
const create = runWinCmd('schtasks', ['/Create', '/TN', WIN_TASK_NAME, '/XML', p.taskXml, '/F']);
|
|
152
|
+
if (create.code !== 0) {
|
|
153
|
+
console.error(`ERROR: could not register the Task Scheduler task (schtasks /Create exit ${create.code ?? 'spawn-failed'}).`);
|
|
154
|
+
console.error(` Task XML: ${p.taskXml}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
// `/Run` triggers the task; it does not wait for the daemon, and install
|
|
158
|
+
// deliberately does not wait either. A healthy first launch may self-update
|
|
159
|
+
// and exit(42), which Task Scheduler only re-runs ~1 minute later
|
|
160
|
+
// (RestartOnFailure PT1M) — so any short liveness window here would report a
|
|
161
|
+
// perfectly good install as a failure. Registering + triggering is what this
|
|
162
|
+
// command owns; runtime health is `parall-daemon status` / `logs`.
|
|
163
|
+
const run = runWinCmd('schtasks', ['/Run', '/TN', WIN_TASK_NAME]);
|
|
164
|
+
if (run.code !== 0) {
|
|
165
|
+
console.error(`ERROR: the task registered but would not start (schtasks /Run exit ${run.code ?? 'spawn-failed'}).`);
|
|
166
|
+
console.error(` Start it from Task Scheduler, or check ${p.logFile}.`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
console.log(`Task Scheduler task installed: ${WIN_TASK_NAME} (artifacts in ${p.serviceDir})`);
|
|
170
|
+
console.log('Task registered and triggered. Check `parall-daemon status` for the daemon itself.');
|
|
171
|
+
console.log(`Logs: ${p.logFile}`);
|
|
172
|
+
}
|
|
173
|
+
function stopWindows() {
|
|
174
|
+
const result = stopDaemonWindows(winDeps());
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
reportWinProblems(result.problems);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.log('Daemon stopped (Task Scheduler task disabled). Re-arm with `parall-daemon service install`.');
|
|
180
|
+
}
|
|
181
|
+
function statusWindows() {
|
|
182
|
+
const deps = winDeps();
|
|
183
|
+
const exists = queryTaskExists(deps);
|
|
184
|
+
if (exists === 'indeterminate') {
|
|
185
|
+
console.log('Service: unknown (schtasks query failed)');
|
|
186
|
+
}
|
|
187
|
+
else if (!exists) {
|
|
188
|
+
console.log('Service: not installed');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const disabled = queryTaskDisabled(deps);
|
|
192
|
+
const suffix = disabled === true ? ', disabled' : disabled === 'indeterminate' ? ', state unknown' : '';
|
|
193
|
+
console.log(`Service: installed${suffix} (Task Scheduler: ${WIN_TASK_NAME})`);
|
|
194
|
+
}
|
|
195
|
+
const pid = readTrackedPid(deps);
|
|
196
|
+
if (pid === null) {
|
|
197
|
+
console.log('Daemon: stopped');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
switch (probePidIdentity(deps, pid)) {
|
|
201
|
+
case 'daemon':
|
|
202
|
+
console.log('Daemon: running');
|
|
203
|
+
console.log(`PID: ${pid}`);
|
|
204
|
+
break;
|
|
205
|
+
case 'not-daemon':
|
|
206
|
+
console.log('Daemon: stopped (stale pidfile)');
|
|
207
|
+
break;
|
|
208
|
+
case 'indeterminate':
|
|
209
|
+
console.log(`Daemon: unknown (could not verify pid ${pid} — process query failed)`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function serviceUninstallWindows() {
|
|
214
|
+
const result = uninstallDaemonWindows(winDeps());
|
|
215
|
+
if (!result.ok) {
|
|
216
|
+
reportWinProblems(result.problems);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
console.log('Task Scheduler task uninstalled (logs kept).');
|
|
220
|
+
}
|
|
37
221
|
const PLIST_LABEL = 'com.parall.daemon';
|
|
38
222
|
function plistPath() {
|
|
39
223
|
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
@@ -102,11 +286,20 @@ RestartSec=5
|
|
|
102
286
|
WantedBy=default.target`;
|
|
103
287
|
}
|
|
104
288
|
function installService() {
|
|
289
|
+
// A service-managed daemon (launchd / systemd / Task Scheduler) never
|
|
290
|
+
// inherits the installing shell's environment, so config.json is its only
|
|
291
|
+
// credential channel. `init` writes it; `service install` requires it.
|
|
105
292
|
const config = readConfig();
|
|
106
293
|
if (!config) {
|
|
107
294
|
console.error('No config found. Run `parall-daemon init` first.');
|
|
108
295
|
process.exit(1);
|
|
109
296
|
}
|
|
297
|
+
if (isWindows()) {
|
|
298
|
+
// Windows resolves its own entry (realpath of argv[1]); `which` in
|
|
299
|
+
// getDaemonBin is POSIX-only.
|
|
300
|
+
installServiceWindows();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
110
303
|
const bin = getDaemonBin();
|
|
111
304
|
if (isMacOS()) {
|
|
112
305
|
const dir = path.dirname(plistPath());
|
|
@@ -147,6 +340,10 @@ async function cmdInit() {
|
|
|
147
340
|
function cmdStatus() {
|
|
148
341
|
const config = readConfig();
|
|
149
342
|
console.log(`Config: ${config ? CONFIG_PATH : 'not configured'}`);
|
|
343
|
+
if (isWindows()) {
|
|
344
|
+
statusWindows();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
150
347
|
if (isMacOS()) {
|
|
151
348
|
try {
|
|
152
349
|
const output = execSync(`launchctl print gui/$(id -u)/${PLIST_LABEL} 2>&1`, {
|
|
@@ -173,6 +370,10 @@ function cmdStatus() {
|
|
|
173
370
|
}
|
|
174
371
|
}
|
|
175
372
|
function cmdStop() {
|
|
373
|
+
if (isWindows()) {
|
|
374
|
+
stopWindows();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
176
377
|
if (isMacOS()) {
|
|
177
378
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`, {
|
|
178
379
|
stdio: 'inherit',
|
|
@@ -191,6 +392,28 @@ function cmdLogs(lines) {
|
|
|
191
392
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
192
393
|
return;
|
|
193
394
|
}
|
|
395
|
+
if (isWindows()) {
|
|
396
|
+
const p = winServicePaths(os.homedir());
|
|
397
|
+
if (!fs.existsSync(p.logFile)) {
|
|
398
|
+
console.log('No log file found at', p.logFile);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// The path is handed to PowerShell through the ENVIRONMENT, never through
|
|
402
|
+
// the script text: `$env:` expansion happens after parsing, so no quoting
|
|
403
|
+
// scheme (and no backtick / $() sequence in a profile path) can alter the
|
|
404
|
+
// command. -LiteralPath additionally stops `[ ]` being read as a wildcard.
|
|
405
|
+
// `lines` is dispatch-validated, but re-derive an integer here so this call
|
|
406
|
+
// site is provably interpolation-free on its own.
|
|
407
|
+
const tailCount = Number.parseInt(lines, 10);
|
|
408
|
+
const tail = Number.isInteger(tailCount) && tailCount > 0 ? tailCount : 50;
|
|
409
|
+
const child = spawn('powershell.exe', [
|
|
410
|
+
'-NoProfile',
|
|
411
|
+
'-Command',
|
|
412
|
+
`Get-Content -LiteralPath $env:PRLL_DAEMON_LOG_PATH -Tail ${tail} -Wait`,
|
|
413
|
+
], { stdio: 'inherit', env: { ...process.env, PRLL_DAEMON_LOG_PATH: p.logFile } });
|
|
414
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
194
417
|
const logPath = path.join(os.homedir(), 'Library', 'Logs', 'parall-daemon.log');
|
|
195
418
|
if (!fs.existsSync(logPath)) {
|
|
196
419
|
console.log('No log file found at', logPath);
|
|
@@ -200,6 +423,10 @@ function cmdLogs(lines) {
|
|
|
200
423
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
201
424
|
}
|
|
202
425
|
function cmdServiceUninstall() {
|
|
426
|
+
if (isWindows()) {
|
|
427
|
+
serviceUninstallWindows();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
203
430
|
if (isMacOS()) {
|
|
204
431
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
205
432
|
if (fs.existsSync(plistPath()))
|
|
@@ -269,7 +496,7 @@ Usage:
|
|
|
269
496
|
parall-daemon stop Stop the background service
|
|
270
497
|
parall-daemon update [--check] Check for / apply daemon updates
|
|
271
498
|
parall-daemon logs [-n LINES] Tail daemon logs
|
|
272
|
-
parall-daemon service install Install as background service (launchd/systemd)
|
|
499
|
+
parall-daemon service install Install as background service (launchd/systemd/Task Scheduler)
|
|
273
500
|
parall-daemon service uninstall Uninstall background service
|
|
274
501
|
parall-daemon help Show this help
|
|
275
502
|
`.trim());
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare class BrowserCdpClient {
|
|
2
|
+
private readonly socket;
|
|
3
|
+
private nextId;
|
|
4
|
+
private readonly pending;
|
|
5
|
+
private closed;
|
|
6
|
+
private constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Connect to the browser-level CDP endpoint behind host:port (`/json/version`).
|
|
9
|
+
* `timeoutMs` bounds BOTH the /json/version fetch and the websocket handshake —
|
|
10
|
+
* injectable so tests can force a fast timeout against a hung server, and so a
|
|
11
|
+
* caller with a wall-clock budget can hand down its remaining budget.
|
|
12
|
+
*/
|
|
13
|
+
static connect(host: string, port: number, timeoutMs?: number): Promise<BrowserCdpClient>;
|
|
14
|
+
/** Send a browser-level CDP command and await its result. */
|
|
15
|
+
command<T>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
16
|
+
close(): void;
|
|
17
|
+
private onMessage;
|
|
18
|
+
private failAll;
|
|
19
|
+
}
|
|
20
|
+
/** The browserContextId owning a page target (empty for the default context). */
|
|
21
|
+
export declare function targetBrowserContextId(cdp: BrowserCdpClient, targetId: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* The full cookie jar of a BrowserContext (HttpOnly included) as CDP
|
|
24
|
+
* `Network.Cookie` objects — the capture side of cookie-preserving proxy
|
|
25
|
+
* reconciliation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getContextCookies(cdp: BrowserCdpClient, browserContextId: string): Promise<Array<Record<string, unknown>>>;
|
|
28
|
+
/** Restore a captured jar into a (fresh) BrowserContext. */
|
|
29
|
+
export declare function setContextCookies(cdp: BrowserCdpClient, browserContextId: string, cookies: Array<Record<string, unknown>>): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Map a CDP `Network.Cookie` (as returned by Storage.getCookies) to a
|
|
32
|
+
* `Network.CookieParam` accepted by Storage.setCookies. Allowlisted fields only:
|
|
33
|
+
* read-side extras (`size`, `session`, `sameParty`, …) are not valid params. A
|
|
34
|
+
* session cookie (session=true / expires=-1) is restored WITHOUT `expires`,
|
|
35
|
+
* which is exactly what makes it a session cookie again. This is
|
|
36
|
+
* higher-fidelity than bb-browser's own account-JSON persistence (which drops
|
|
37
|
+
* `sameSite=None` and partition keys).
|
|
38
|
+
*/
|
|
39
|
+
export declare function toCookieParam(cookie: Record<string, unknown>): Record<string, unknown>;
|
|
40
|
+
//# sourceMappingURL=browser-cdp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-cdp.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-cdp.ts"],"names":[],"mappings":"AAyCA,qBAAa,gBAAgB;IAKP,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJ3C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO;IAOP;;;;;OAKG;WACU,OAAO,CAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,SAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;IAgD5B,6DAA6D;IAC7D,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA0B5E,KAAK,IAAI,IAAI;IAUb,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,OAAO;CAOhB;AAED,iFAAiF;AACjF,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,gBAAgB,EACrB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAMzC;AAED,4DAA4D;AAC5D,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,gBAAgB,EACrB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBtF"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal browser-level CDP client — Parall's direct line to the Chromium that
|
|
4
|
+
* bb-browser manages, for the few operations bb-browser's HTTP surface cannot
|
|
5
|
+
* express (bb-browser is off-limits for changes; Chrome accepts multiple CDP
|
|
6
|
+
* clients on the browser endpoint, and Parall already consumes CDP directly for
|
|
7
|
+
* the live viewer's page websockets + /json/activate).
|
|
8
|
+
*
|
|
9
|
+
* Used for cookie-preserving proxy reconciliation: `Storage.getCookies` /
|
|
10
|
+
* `Storage.setCookies` against a specific browserContextId — the full jar
|
|
11
|
+
* including HttpOnly cookies, which no page-level API can read — and
|
|
12
|
+
* `Target.getTargetInfo` to map an owned tab to its BrowserContext.
|
|
13
|
+
*/
|
|
14
|
+
const CONNECT_TIMEOUT_MS = 5_000;
|
|
15
|
+
const COMMAND_TIMEOUT_MS = 10_000;
|
|
16
|
+
/**
|
|
17
|
+
* Force-close a socket that never finished connecting WITHOUT risking an
|
|
18
|
+
* unhandled 'error'. Once the connect Promise has removed its own error listener,
|
|
19
|
+
* a bare `terminate()` on a half-open handshake can still emit 'error'
|
|
20
|
+
* (ECONNRESET / abort) with no listener attached — which Node re-raises as an
|
|
21
|
+
* uncaughtException that crashes the daemon. Attach a permanent no-op error sink
|
|
22
|
+
* first, then terminate; swallow a synchronous throw from an already-dead socket.
|
|
23
|
+
*/
|
|
24
|
+
function safeTerminate(socket) {
|
|
25
|
+
socket.on('error', () => { });
|
|
26
|
+
try {
|
|
27
|
+
socket.terminate();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// terminate() on an already-closed socket can throw synchronously — ignore.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class BrowserCdpClient {
|
|
34
|
+
socket;
|
|
35
|
+
nextId = 1;
|
|
36
|
+
pending = new Map();
|
|
37
|
+
closed = false;
|
|
38
|
+
constructor(socket) {
|
|
39
|
+
this.socket = socket;
|
|
40
|
+
socket.on('message', (data) => this.onMessage(data));
|
|
41
|
+
const fail = (reason) => this.failAll(new Error(reason));
|
|
42
|
+
socket.on('close', () => fail('CDP browser connection closed'));
|
|
43
|
+
socket.on('error', (err) => fail(`CDP browser connection error: ${String(err)}`));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Connect to the browser-level CDP endpoint behind host:port (`/json/version`).
|
|
47
|
+
* `timeoutMs` bounds BOTH the /json/version fetch and the websocket handshake —
|
|
48
|
+
* injectable so tests can force a fast timeout against a hung server, and so a
|
|
49
|
+
* caller with a wall-clock budget can hand down its remaining budget.
|
|
50
|
+
*/
|
|
51
|
+
static async connect(host, port, timeoutMs = CONNECT_TIMEOUT_MS) {
|
|
52
|
+
const resp = await fetch(`http://${host}:${port}/json/version`, {
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
+
});
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
throw new Error(`CDP /json/version returned ${resp.status}`);
|
|
57
|
+
}
|
|
58
|
+
const info = (await resp.json());
|
|
59
|
+
if (!info.webSocketDebuggerUrl) {
|
|
60
|
+
throw new Error('CDP /json/version did not report a webSocketDebuggerUrl');
|
|
61
|
+
}
|
|
62
|
+
const socket = new WebSocket(info.webSocketDebuggerUrl);
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
// Explicit single-settle guard: whichever of open / error / timeout fires
|
|
65
|
+
// first wins and the others are no-ops. cleanup() also removes the listeners,
|
|
66
|
+
// but the flag makes the invariant obvious and covers a synchronous re-entry.
|
|
67
|
+
let settled = false;
|
|
68
|
+
const cleanup = () => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
socket.removeListener('open', onOpen);
|
|
71
|
+
socket.removeListener('error', onError);
|
|
72
|
+
};
|
|
73
|
+
const settle = (fn) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
cleanup();
|
|
78
|
+
fn();
|
|
79
|
+
};
|
|
80
|
+
const onOpen = () => settle(resolve);
|
|
81
|
+
const onError = (err) => settle(() => {
|
|
82
|
+
safeTerminate(socket); // free the fd without a later unhandled 'error'
|
|
83
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
84
|
+
});
|
|
85
|
+
const timer = setTimeout(() => settle(() => {
|
|
86
|
+
safeTerminate(socket); // stop the pending connect + free the fd
|
|
87
|
+
reject(new Error('CDP browser websocket connect timed out'));
|
|
88
|
+
}), timeoutMs);
|
|
89
|
+
socket.once('open', onOpen);
|
|
90
|
+
socket.once('error', onError);
|
|
91
|
+
});
|
|
92
|
+
return new BrowserCdpClient(socket);
|
|
93
|
+
}
|
|
94
|
+
/** Send a browser-level CDP command and await its result. */
|
|
95
|
+
command(method, params = {}) {
|
|
96
|
+
if (this.closed)
|
|
97
|
+
return Promise.reject(new Error('CDP client is closed'));
|
|
98
|
+
const id = this.nextId++;
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
reject(new Error(`CDP ${method} timed out`));
|
|
103
|
+
}, COMMAND_TIMEOUT_MS);
|
|
104
|
+
this.pending.set(id, {
|
|
105
|
+
resolve: resolve,
|
|
106
|
+
reject,
|
|
107
|
+
timer,
|
|
108
|
+
});
|
|
109
|
+
this.socket.send(JSON.stringify({ id, method, params }), (err) => {
|
|
110
|
+
if (err) {
|
|
111
|
+
const entry = this.pending.get(id);
|
|
112
|
+
if (entry) {
|
|
113
|
+
clearTimeout(entry.timer);
|
|
114
|
+
this.pending.delete(id);
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
close() {
|
|
122
|
+
this.closed = true;
|
|
123
|
+
this.failAll(new Error('CDP client closed'));
|
|
124
|
+
try {
|
|
125
|
+
this.socket.close();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* already closed */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
onMessage(data) {
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(String(data));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return; // not a JSON frame we understand — ignore (events etc.)
|
|
138
|
+
}
|
|
139
|
+
if (typeof parsed.id !== 'number')
|
|
140
|
+
return; // CDP event, not a command reply
|
|
141
|
+
const entry = this.pending.get(parsed.id);
|
|
142
|
+
if (!entry)
|
|
143
|
+
return;
|
|
144
|
+
this.pending.delete(parsed.id);
|
|
145
|
+
clearTimeout(entry.timer);
|
|
146
|
+
if (parsed.error) {
|
|
147
|
+
entry.reject(new Error(parsed.error.message || 'CDP command failed'));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
entry.resolve(parsed.result ?? {});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
failAll(err) {
|
|
154
|
+
for (const [id, entry] of this.pending) {
|
|
155
|
+
this.pending.delete(id);
|
|
156
|
+
clearTimeout(entry.timer);
|
|
157
|
+
entry.reject(err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** The browserContextId owning a page target (empty for the default context). */
|
|
162
|
+
export async function targetBrowserContextId(cdp, targetId) {
|
|
163
|
+
const { targetInfo } = await cdp.command('Target.getTargetInfo', { targetId });
|
|
164
|
+
return targetInfo?.browserContextId ?? '';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The full cookie jar of a BrowserContext (HttpOnly included) as CDP
|
|
168
|
+
* `Network.Cookie` objects — the capture side of cookie-preserving proxy
|
|
169
|
+
* reconciliation.
|
|
170
|
+
*/
|
|
171
|
+
export async function getContextCookies(cdp, browserContextId) {
|
|
172
|
+
const { cookies } = await cdp.command('Storage.getCookies', { browserContextId });
|
|
173
|
+
return cookies ?? [];
|
|
174
|
+
}
|
|
175
|
+
/** Restore a captured jar into a (fresh) BrowserContext. */
|
|
176
|
+
export async function setContextCookies(cdp, browserContextId, cookies) {
|
|
177
|
+
if (cookies.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
await cdp.command('Storage.setCookies', {
|
|
180
|
+
cookies: cookies.map(toCookieParam),
|
|
181
|
+
browserContextId,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Map a CDP `Network.Cookie` (as returned by Storage.getCookies) to a
|
|
186
|
+
* `Network.CookieParam` accepted by Storage.setCookies. Allowlisted fields only:
|
|
187
|
+
* read-side extras (`size`, `session`, `sameParty`, …) are not valid params. A
|
|
188
|
+
* session cookie (session=true / expires=-1) is restored WITHOUT `expires`,
|
|
189
|
+
* which is exactly what makes it a session cookie again. This is
|
|
190
|
+
* higher-fidelity than bb-browser's own account-JSON persistence (which drops
|
|
191
|
+
* `sameSite=None` and partition keys).
|
|
192
|
+
*/
|
|
193
|
+
export function toCookieParam(cookie) {
|
|
194
|
+
const out = {
|
|
195
|
+
name: cookie.name,
|
|
196
|
+
value: cookie.value,
|
|
197
|
+
domain: cookie.domain,
|
|
198
|
+
path: cookie.path,
|
|
199
|
+
};
|
|
200
|
+
if (typeof cookie.secure === 'boolean')
|
|
201
|
+
out.secure = cookie.secure;
|
|
202
|
+
if (typeof cookie.httpOnly === 'boolean')
|
|
203
|
+
out.httpOnly = cookie.httpOnly;
|
|
204
|
+
if (typeof cookie.sameSite === 'string')
|
|
205
|
+
out.sameSite = cookie.sameSite;
|
|
206
|
+
if (typeof cookie.expires === 'number' && cookie.expires > 0 && cookie.session !== true) {
|
|
207
|
+
out.expires = cookie.expires;
|
|
208
|
+
}
|
|
209
|
+
if (typeof cookie.priority === 'string')
|
|
210
|
+
out.priority = cookie.priority;
|
|
211
|
+
if (typeof cookie.sourceScheme === 'string')
|
|
212
|
+
out.sourceScheme = cookie.sourceScheme;
|
|
213
|
+
if (typeof cookie.sourcePort === 'number')
|
|
214
|
+
out.sourcePort = cookie.sourcePort;
|
|
215
|
+
if (cookie.partitionKey !== undefined)
|
|
216
|
+
out.partitionKey = cookie.partitionKey;
|
|
217
|
+
return out;
|
|
218
|
+
}
|