@monoes/monobrowse 2.24.2 → 2.24.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monobrowse",
3
- "version": "2.24.2",
3
+ "version": "2.24.6",
4
4
  "description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "src"
36
36
  ],
37
37
  "engines": {
38
- "node": ">=22.12.0"
38
+ "node": ">=22.22.0"
39
39
  },
40
40
  "keywords": [
41
41
  "browser-automation",
@@ -58,11 +58,11 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "commander": "^12.1.0",
61
- "ws": "^8.21.0"
61
+ "ws": "^8.22.2"
62
62
  },
63
63
  "devDependencies": {
64
64
  "typescript": "^7.0.2",
65
- "vitest": "^4.1.4"
65
+ "vitest": "^5.1.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
@@ -68,13 +68,15 @@ async function profile(name: string, opts: { young?: boolean } = {}): Promise<st
68
68
  }
69
69
 
70
70
  describe('#395 profileDirOwnerPid', () => {
71
- it('reads the pid from exactly our two name patterns', () => {
71
+ it('reads the pid from exactly our name patterns', () => {
72
72
  expect(profileDirOwnerPid('monomind-browser-9222-1234-0a1b2c3d')).toBe(1234);
73
73
  expect(profileDirOwnerPid('monomind-browse-4321-0a1b2c3d')).toBe(4321);
74
+ expect(profileDirOwnerPid('monomind-browse-headed-4321-0a1b2c3d')).toBe(4321);
74
75
  for (const name of [
75
76
  'monomind-browse-4321-0A1B2C3D',
76
77
  'monomind-browse-4321-0a1b2c3d0',
77
78
  'monomind-browse-x-0a1b2c3d',
79
+ 'monomind-browse-headed-x-0a1b2c3d',
78
80
  'monomind-browser-4321-0a1b2c3d',
79
81
  'xmonomind-browse-4321-0a1b2c3d',
80
82
  'org.chromium.Chromium.abc123',
@@ -89,12 +91,14 @@ describe('#395 sweepStaleProfileDirs', () => {
89
91
  const pid = await deadPid();
90
92
  const a = await profile(`monomind-browser-9222-${pid}-aaaaaaaa`);
91
93
  const b = await profile(`monomind-browse-${pid}-bbbbbbbb`);
94
+ const c = await profile(`monomind-browse-headed-${pid}-cccccccc`);
92
95
 
93
96
  const removed = await sweepStaleProfileDirs({ root });
94
97
 
95
- expect(removed.sort()).toEqual([a, b].sort());
98
+ expect(removed.sort()).toEqual([a, b, c].sort());
96
99
  expect(existsSync(a)).toBe(false);
97
100
  expect(existsSync(b)).toBe(false);
101
+ expect(existsSync(c)).toBe(false);
98
102
  });
99
103
 
100
104
  it('keeps young dirs, dirs whose pid is alive, non-matching names, symlinks and files', async () => {
@@ -48,32 +48,34 @@ const REAP_CONNECT_TIMEOUT_MS = 3000;
48
48
  * the process is gone, on both paths (#395). In a fresh process the dir and
49
49
  * its ownership come from the persisted session record.
50
50
  */
51
- export async function closeBrowser(client: CdpClient, port: number): Promise<void> {
51
+ export async function closeBrowser(client: CdpClient | null, port: number): Promise<void> {
52
52
  let gracefullyClosed = false;
53
53
  let closeTimer: ReturnType<typeof setTimeout> | undefined;
54
- try {
55
- await Promise.race([
56
- client.send('Browser.close', {}),
57
- // Deliberately NOT unref'd, for the same reason waitForProcessExit()'s
58
- // poll below is not. This timer is the only thing that settles the race
59
- // when Chrome's socket dies without ever acknowledging the close, and
60
- // closeBrowser() is actively awaited by callers. An unref'd timer does
61
- // not hold the event loop open, so Node would consider the loop drained
62
- // and exit — status 0, before the caller's verdict was ever printed.
63
- // Cleared in the finally so a close that IS acknowledged does not pin
64
- // the loop for the rest of BROWSER_CLOSE_TIMEOUT_MS.
65
- new Promise<never>((_, reject) => {
66
- closeTimer = setTimeout(
67
- () => reject(new Error('Browser.close timed out')),
68
- BROWSER_CLOSE_TIMEOUT_MS,
69
- );
70
- }),
71
- ]);
72
- gracefullyClosed = true;
73
- } catch {
74
- // fall through to PID-kill fallback below
75
- } finally {
76
- clearTimeout(closeTimer);
54
+ if (client) {
55
+ try {
56
+ await Promise.race([
57
+ client.send('Browser.close', {}),
58
+ // Deliberately NOT unref'd, for the same reason waitForProcessExit()'s
59
+ // poll below is not. This timer is the only thing that settles the race
60
+ // when Chrome's socket dies without ever acknowledging the close, and
61
+ // closeBrowser() is actively awaited by callers. An unref'd timer does
62
+ // not hold the event loop open, so Node would consider the loop drained
63
+ // and exit — status 0, before the caller's verdict was ever printed.
64
+ // Cleared in the finally so a close that IS acknowledged does not pin
65
+ // the loop for the rest of BROWSER_CLOSE_TIMEOUT_MS.
66
+ new Promise<never>((_, reject) => {
67
+ closeTimer = setTimeout(
68
+ () => reject(new Error('Browser.close timed out')),
69
+ BROWSER_CLOSE_TIMEOUT_MS,
70
+ );
71
+ }),
72
+ ]);
73
+ gracefullyClosed = true;
74
+ } catch {
75
+ // fall through to PID-kill fallback below
76
+ } finally {
77
+ clearTimeout(closeTimer);
78
+ }
77
79
  }
78
80
 
79
81
  let pid = launchedPids.get(port);
@@ -25,8 +25,8 @@ import { basename, dirname, join } from 'node:path';
25
25
 
26
26
  /** `monomind-browser-<port>-<pid>-<uuid8>` (launchBrowser's default profile). */
27
27
  const LAUNCH_PROFILE_RE = /^monomind-browser-\d+-(\d+)-[0-9a-f]{8}$/;
28
- /** `monomind-browse-<pid>-<uuid8>` (a CLI browse session's profile). */
29
- const SESSION_PROFILE_RE = /^monomind-browse-(\d+)-[0-9a-f]{8}$/;
28
+ /** `monomind-browse-<pid>-<uuid8>` or `monomind-browse-headed-<pid>-<uuid8>` (a CLI browse session's profile). */
29
+ const SESSION_PROFILE_RE = /^monomind-browse(?:-headed)?-(\d+)-[0-9a-f]{8}$/;
30
30
 
31
31
  /** A directory younger than this is never swept: its launch may still be
32
32
  * starting (Chrome has not yet put its pid anywhere we can see). */
@@ -63,8 +63,9 @@ export function launchProfileDirPath(port: number, id: string, root = tmpdir()):
63
63
  }
64
64
 
65
65
  /** Name for a new CLI session profile, directly under `root`. */
66
- export function sessionProfileDirPath(id: string, root = tmpdir()): string {
67
- return join(root, `monomind-browse-${process.pid}-${id}`);
66
+ export function sessionProfileDirPath(id: string, root = tmpdir(), headed = false): string {
67
+ const prefix = headed ? 'monomind-browse-headed' : 'monomind-browse';
68
+ return join(root, `${prefix}-${process.pid}-${id}`);
68
69
  }
69
70
 
70
71
  /** The pid encoded in a profile directory name we create, or null when
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { tmpdir } from 'node:os';
11
- import { join } from 'node:path';
11
+ import { sessionProfileDirPath } from '../browser/profile-dir.js';
12
12
  import type { CdpClient } from '../index.js';
13
13
  import { output } from './output.js';
14
14
  import { getBrowser, session } from './session.js';
@@ -110,10 +110,8 @@ export async function switchToHeaded(url: string, port: number): Promise<void> {
110
110
  headedPort = await browser.launchBrowser({
111
111
  port: 0,
112
112
  headless: false,
113
- userDataDir: join(
114
- tmpdir(),
115
- `monomind-browse-headed-${process.pid}-${randomUUID().slice(0, 8)}`,
116
- ),
113
+ userDataDir: sessionProfileDirPath(randomUUID().slice(0, 8), tmpdir(), true),
114
+ ownsUserDataDir: true,
117
115
  });
118
116
  } catch (err) {
119
117
  // Headed launch failed (no display, locked down env). Restore headless and rethrow.
@@ -127,51 +125,60 @@ export async function switchToHeaded(url: string, port: number): Promise<void> {
127
125
  `Cannot open headed browser: ${err instanceof Error ? err.message : String(err)}`,
128
126
  );
129
127
  }
130
- const conn = await browser.connectToTarget(headedPort);
131
- session.client = conn.client;
132
- session.sessionId = conn.sessionId;
133
- session.targetId = conn.target.id;
134
- session.refs = new Map();
135
128
 
136
- if (savedCookies.length) {
137
- try {
138
- await browser.setCookies(
139
- session.client,
140
- session.sessionId,
141
- savedCookies as Parameters<typeof browser.setCookies>[2],
142
- );
143
- } catch {
144
- /* ignore */
145
- }
146
- }
147
- await browser.openUrl(session.client, session.sessionId, url);
129
+ let authCookies: unknown[] = [];
130
+ let authLocalStorage: Record<string, string> = {};
131
+ try {
132
+ const conn = await browser.connectToTarget(headedPort);
133
+ session.client = conn.client;
134
+ session.sessionId = conn.sessionId;
135
+ session.targetId = conn.target.id;
136
+ session.refs = new Map();
148
137
 
149
- output.printInfo(
150
- 'Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...',
151
- );
152
- await waitForEnter();
138
+ if (savedCookies.length) {
139
+ try {
140
+ await browser.setCookies(
141
+ session.client,
142
+ session.sessionId,
143
+ savedCookies as Parameters<typeof browser.setCookies>[2],
144
+ );
145
+ } catch {
146
+ /* ignore */
147
+ }
148
+ }
149
+ await browser.openUrl(session.client, session.sessionId, url);
153
150
 
154
- // Capture post-auth cookies
155
- const authCookies = await browser
156
- .getCookies(session.client, session.sessionId)
157
- .catch(() => [] as unknown[]);
158
- const authLocalStorage = await browser
159
- .getLocalStorage(session.client, session.sessionId)
160
- .catch(() => ({}) as Record<string, string>);
151
+ output.printInfo(
152
+ 'Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...',
153
+ );
154
+ await waitForEnter();
161
155
 
162
- // Close headed session — actually terminate the underlying Chrome process
163
- // (Browser.close CDP command, PID-kill fallback), not just our CDP client
164
- // connection, so the visible authenticated window doesn't linger forever.
165
- browser.teardownRouteInterception(session.sessionId);
166
- browser.stopRequestCapture(session.sessionId);
167
- browser.teardownDialogHandling(session.sessionId);
168
- browser.teardownConsoleCapture(session.sessionId);
169
- await browser.closeBrowser(session.client, headedPort);
170
- session.client.close();
171
- session.client = null;
172
- session.sessionId = '';
173
- session.parentSessionId = '';
174
- session.targetId = '';
156
+ // Capture post-auth cookies
157
+ authCookies = await browser
158
+ .getCookies(session.client, session.sessionId)
159
+ .catch(() => [] as unknown[]);
160
+ authLocalStorage = await browser
161
+ .getLocalStorage(session.client, session.sessionId)
162
+ .catch(() => ({}) as Record<string, string>);
163
+ } finally {
164
+ // Close headed session — actually terminate the underlying Chrome process
165
+ // (Browser.close CDP command, PID-kill fallback), not just our CDP client
166
+ // connection, so the visible authenticated window doesn't linger forever.
167
+ if (session.sessionId) {
168
+ browser.teardownRouteInterception(session.sessionId);
169
+ browser.stopRequestCapture(session.sessionId);
170
+ browser.teardownDialogHandling(session.sessionId);
171
+ browser.teardownConsoleCapture(session.sessionId);
172
+ }
173
+ await browser.closeBrowser(session.client, headedPort);
174
+ if (session.client) {
175
+ session.client.close();
176
+ session.client = null;
177
+ }
178
+ session.sessionId = '';
179
+ session.parentSessionId = '';
180
+ session.targetId = '';
181
+ }
175
182
 
176
183
  // Relaunch headless and restore session
177
184
  session.port = await browser.launchBrowser({ port, headless: true });