pi-browser-harness 0.10.0 → 0.10.2
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/CHANGELOG.md +14 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/client.ts +12 -7
- package/src/profile/bind.ts +2 -1
- package/src/profile/launch.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-browser-harness will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.10.2 — 2026-07-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Page info no longer fails on a missing `documentElement`.** The page-info probe read `document.documentElement.scrollWidth`/`scrollHeight` directly, which throws when `documentElement` is absent — leaving every caller with an evaluation error instead of a page snapshot. The probe now falls back to zeroed scroll dimensions and returns a usable `PageInfo`.
|
|
10
|
+
|
|
11
|
+
## 0.10.1 — 2026-07-26
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Chrome process leak on profile seed failure.** When a profile is pinned and `seedProfileWindow` spawns Chrome via `openProfileWindow`, the spawned child is kept as `detached: true` + `unref()`. If Chromium's ProcessSingleton fails to delegate (user-data-dir mismatch, multiple browsers, Chrome busy/crashed), the spawned process starts a full second browser instance. The sentinel never appears, the seed times out, but the spawned Chrome process was never killed — accumulating zombie browser instances that consume RAM. The fix stores the child process reference, exposes a `kill()` on the returned handle, and calls it on seed timeout.
|
|
16
|
+
- **Unnecessary Chrome re-spawns on every `start()`.** `profileContextId` was unconditionally cleared on every `start()`, forcing a new profile-window launch via ProcessSingleton even when Chrome hadn't restarted and the context was still valid. Now the context is cleared only when the browser UUID in the WebSocket URL changes — which Chrome re-mints on every launch.
|
|
17
|
+
- **`setProfilePin` no longer clears context when the pin hasn't changed.** Loading the same pin from disk on `session_start` was clearing `profileContextId`, forcing yet another Chrome spawn on the next `start()`.
|
|
18
|
+
|
|
5
19
|
## 0.10.0 — 2026-07-25
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -50,9 +50,9 @@ The first time you connect, you'll be asked which browser profile the agent shou
|
|
|
50
50
|
|
|
51
51
|
```
|
|
52
52
|
Select the browser profile the agent should use
|
|
53
|
-
|
|
54
|
-
✓
|
|
55
|
-
|
|
53
|
+
Work (work@example.com)
|
|
54
|
+
✓ Personal (personal@example.com)
|
|
55
|
+
Testing (test@example.com)
|
|
56
56
|
— Clear selection (use whichever window is focused) —
|
|
57
57
|
```
|
|
58
58
|
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -170,11 +170,12 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
170
170
|
}
|
|
171
171
|
const connected = await transport.connect(wsUrl, { timeoutMs: 10_000 });
|
|
172
172
|
if (!connected.success) return connected;
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
|
|
174
|
+
const newBrowserId: string = wsUrl.split("/").pop() ?? "unknown";
|
|
175
|
+
if (!remote || remote.browserId !== newBrowserId) {
|
|
176
|
+
profileContextId = undefined;
|
|
175
177
|
}
|
|
176
|
-
|
|
177
|
-
profileContextId = undefined;
|
|
178
|
+
remote = { cdpUrl: wsUrl, browserId: newBrowserId };
|
|
178
179
|
const seeded = await seedIfPinned();
|
|
179
180
|
if (!seeded.success) {
|
|
180
181
|
await transport.close();
|
|
@@ -255,7 +256,7 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
255
256
|
const currentTid = session.current()?.targetId;
|
|
256
257
|
const cached = currentTid ? pageCaches.get(currentTid) : undefined;
|
|
257
258
|
if (cached && !dirty && Date.now() - cached.at < PAGE_INFO_TTL_MS) return ok(cached.info);
|
|
258
|
-
const expr = safeJs`JSON.stringify({url:location.href,title:document.title,w:innerWidth,h:innerHeight,sx:scrollX,sy:scrollY,pw:
|
|
259
|
+
const expr = safeJs`JSON.stringify((function(){var e=document.documentElement||{scrollWidth:0,scrollHeight:0};return {url:location.href,title:document.title,w:innerWidth,h:innerHeight,sx:scrollX,sy:scrollY,pw:e.scrollWidth,ph:e.scrollHeight};})())`;
|
|
259
260
|
const raw = await evaluateJs(expr);
|
|
260
261
|
if (!raw.success) return raw;
|
|
261
262
|
if (typeof raw.data !== "string") return err(cdpError("invalid_response", "page info evaluation did not return a string"));
|
|
@@ -397,9 +398,13 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
|
|
|
397
398
|
userDataDir: () => userDataDir,
|
|
398
399
|
profilePin: () => profilePin,
|
|
399
400
|
setProfilePin: (pin) => {
|
|
401
|
+
const changed = pin === null
|
|
402
|
+
? profilePin !== null
|
|
403
|
+
: profilePin === null || profilePin.userDataDir !== pin.userDataDir || profilePin.profileDir !== pin.profileDir;
|
|
400
404
|
profilePin = pin;
|
|
401
|
-
|
|
402
|
-
|
|
405
|
+
if (changed) {
|
|
406
|
+
profileContextId = undefined;
|
|
407
|
+
}
|
|
403
408
|
},
|
|
404
409
|
profileContextId: () => profileContextId,
|
|
405
410
|
setProfileContextId: (contextId) => {
|
package/src/profile/bind.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const seedProfileWindow = async (
|
|
|
93
93
|
return err(cdpError("discovery_failed", `${launched.error} — ${manualFallbackHint(pin)}`));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
const { sentinelUrl, cleanup } = launched.data;
|
|
96
|
+
const { sentinelUrl, cleanup, kill } = launched.data;
|
|
97
97
|
const deadline = Date.now() + SEED_DEADLINE_MS;
|
|
98
98
|
let seed: PageTarget | undefined;
|
|
99
99
|
while (Date.now() < deadline && !seed) {
|
|
@@ -102,6 +102,7 @@ export const seedProfileWindow = async (
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (!seed) {
|
|
105
|
+
kill();
|
|
105
106
|
await cleanup();
|
|
106
107
|
return err(cdpError(
|
|
107
108
|
"timeout",
|
package/src/profile/launch.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* identification correct when the user opens tabs at the same moment.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { spawn } from "node:child_process";
|
|
18
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { rm, writeFile } from "node:fs/promises";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
@@ -42,6 +42,7 @@ export type ProfileWindowHandle = {
|
|
|
42
42
|
readonly sentinelUrl: string;
|
|
43
43
|
/** Remove the sentinel file. Safe to call more than once. */
|
|
44
44
|
readonly cleanup: () => Promise<void>;
|
|
45
|
+
readonly kill: () => void;
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const SENTINEL_HTML = (token: string): string =>
|
|
@@ -68,10 +69,11 @@ export const openProfileWindow = async (
|
|
|
68
69
|
if (req.explicitUserDataDir) args.push(`--user-data-dir=${req.explicitUserDataDir}`);
|
|
69
70
|
args.push(`--profile-directory=${req.profileDir}`, "--new-window", sentinelUrl);
|
|
70
71
|
|
|
72
|
+
let child: ChildProcess | null = null;
|
|
71
73
|
try {
|
|
72
74
|
// No shell: arguments are passed as an array, so profile names and paths
|
|
73
75
|
// containing spaces survive intact on every platform.
|
|
74
|
-
|
|
76
|
+
child = spawn(req.exePath, args, { detached: true, stdio: "ignore" });
|
|
75
77
|
child.unref();
|
|
76
78
|
// The launcher process exits as soon as the running browser takes over the
|
|
77
79
|
// command line; an error here means the binary itself could not be started.
|
|
@@ -86,5 +88,11 @@ export const openProfileWindow = async (
|
|
|
86
88
|
cleanup: async () => {
|
|
87
89
|
await rm(file, { force: true }).catch(() => {});
|
|
88
90
|
},
|
|
91
|
+
kill: () => {
|
|
92
|
+
if (child) {
|
|
93
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
94
|
+
child = null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
89
97
|
});
|
|
90
98
|
};
|