@broberg/lens-daemon 0.1.34 → 0.1.36

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": "@broberg/lens-daemon",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "The Lens module of the cardmem daemon (flows, critic, coverage, capture sessions, the /lens/* routes and the cardmem-lens MCP) as a package. The daemon mounts createLensRouter(deps) + createLensMcpRouter(deps). Bun runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/browser.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // auth (F074.4) and concurrent captures never bleed into each other. The
11
11
  // context + page are always closed in a finally, even on capture error.
12
12
 
13
- import { bindProfileChannel, claimProfile, profileDir, type ProfileChannel } from './profiles.ts';
13
+ import { bindProfileChannel, claimProfile, killOwnBrowsersOn, profileDir, type ProfileChannel } from './profiles.ts';
14
14
  import {
15
15
  chromium,
16
16
  webkit,
@@ -404,6 +404,9 @@ export function runBrowserLaunchOptions(b: RunBrowser) {
404
404
  return {
405
405
  channel: b.channel,
406
406
  headless: b.visible !== true,
407
+ // Playwright launches with --no-sandbox by default. This browser carries a real
408
+ // login (Google) and browses the open web, so Chrome's renderer sandbox stays ON.
409
+ chromiumSandbox: true,
407
410
  ignoreDefaultArgs: ['--enable-automation'],
408
411
  args: ['--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage'],
409
412
  };
@@ -444,6 +447,7 @@ async function launchRunBrowser(opts: WithPageOptions, b: RunBrowser): Promise<{
444
447
  });
445
448
  return { context, userDataDir };
446
449
  } catch (err) {
450
+ killOwnBrowsersOn(userDataDir);
447
451
  if (!b.profile) await rm(userDataDir, { recursive: true, force: true }).catch(() => {});
448
452
  const msg = err instanceof Error ? err.message.split('\n')[0]! : String(err);
449
453
  throw new Error(`browser "${b.channel}" did not start: ${msg}${b.channel === 'chrome' ? ' — is Google Chrome installed on the daemon host?' : ''}`);
@@ -18,7 +18,7 @@
18
18
  // are confined to ~/.lens/state so a bad state_path can't clobber an arbitrary
19
19
  // file. The endpoint is loopback-only (127.0.0.1) behind the daemon's auth.
20
20
 
21
- import { bindProfileChannel, claimProfile, profileDir, type ProfileChannel } from './profiles.ts';
21
+ import { bindProfileChannel, claimProfile, killOwnBrowsersOn, profileDir, type ProfileChannel } from './profiles.ts';
22
22
  import { EXTENSION_CHANNEL } from './browser.ts';
23
23
  import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
24
24
  import { z } from 'zod';
@@ -68,6 +68,9 @@ export function profileLaunchOptions(channel: ProfileChannel = EXTENSION_CHANNEL
68
68
  // F009.136 — 'chrome' when the runs that will use this profile run in Google
69
69
  // Chrome; the default stays the extension runs' build.
70
70
  channel,
71
+ // The login window holds the password + 2FA being typed: renderer sandbox ON
72
+ // (Playwright's default is --no-sandbox).
73
+ chromiumSandbox: true,
71
74
  ignoreDefaultArgs: ['--enable-automation'],
72
75
  args: ['--disable-blink-features=AutomationControlled'],
73
76
  };
@@ -418,6 +421,7 @@ export async function startCaptureSession(input: {
418
421
  try {
419
422
  context = await chromium.launchPersistentContext(dir, profileLaunchOptions(channel));
420
423
  } catch (err) {
424
+ killOwnBrowsersOn(dir);
421
425
  releaseProfile();
422
426
  throw err;
423
427
  }
package/src/profiles.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // F009.132 — a fixed browser profile per site. Google binds its session to the
2
2
  // browser that holds it, so a login only survives if it happens in, and is reused
3
3
  // from, ONE profile of ONE browser build. The profile lives on the daemon host.
4
+ import { execFileSync } from 'node:child_process';
4
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
6
  import { homedir } from 'node:os';
6
7
  import { join } from 'node:path';
@@ -52,3 +53,31 @@ export function bindProfileChannel(name: string, channel: ProfileChannel): void
52
53
  );
53
54
  }
54
55
  }
56
+
57
+ // F009.136 — when a launch on a profile fails, Playwright can leave the Chrome it
58
+ // started running (measured twice on 25 Oct: a headless Chrome stayed up holding the
59
+ // profile, so every retry hung on the lock it left). Kill only processes this
60
+ // daemon started on this exact user-data dir.
61
+ export function killOwnBrowsersOn(dir: string): number {
62
+ let out = '';
63
+ try {
64
+ out = execFileSync('ps', ['-eo', 'pid=,ppid=,args='], { encoding: 'utf8' });
65
+ } catch {
66
+ return 0;
67
+ }
68
+ let killed = 0;
69
+ for (const line of out.split('\n')) {
70
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
71
+ // The EXACT dir: a plain substring test matched profile "a" inside "a-b" (measured).
72
+ const flag = `--user-data-dir=${dir}`;
73
+ const at = m ? (` ${m[3]!} `).indexOf(` ${flag} `) : -1;
74
+ if (!m || Number(m[2]) !== process.pid || at < 0) continue;
75
+ try {
76
+ process.kill(Number(m[1]), 'SIGKILL');
77
+ killed++;
78
+ } catch {
79
+ // already gone
80
+ }
81
+ }
82
+ return killed;
83
+ }