← Commit history

Reparent Chrome out of the bridge's process tree so pup windows survive a bridge restart (step 1 of the durable-profile plan). Measured root cause: Node's { detached:true } gives a new process GROUP but the child stays a TREE child of the bridge node process (parentPid = bridge), so AD's tree-kill on every restart/upgrade takes Chrome with it — verified 7 Chrome procs going to 0 across a restart. Every pup window was dying on every upgrade, and Chrome was never getting a clean shutdown, which also blocks the durable 'you' profile (a repeatedly dirty-killed profile rots). Fix (Windows only; mac/Linux unaffected): launch Chrome through a transient 'cmd /c start' that returns and exits within milliseconds, orphaning Chrome from the bridge tree so a later taskkill /T no longer reaches it. CDP connects by PORT so the lost direct PID handle is harmless; the real Chrome pid is re-resolved after connect by matching --remote-debugging-port in the command line (with a short retry). The launcher's immediate exit is expected, so the alive-check is skipped on the reparent path and CDP is polled for the full budget; failure cleanup kills by profile since child.pid is the exited cmd. Quoting built and passed verbatim (windowsVerbatimArguments) to avoid the cmd re-parse hazard.

John Lauer ·7853a4c261 ·3mo ago ·parent f5765a4
4 files changed +49−14
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.121",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.121/adom-bridge-puppeteer-v1.9.121.zip",-  "sha256": "eddd9ab32dc84696e28014f37b2430b699610f77d00efcae2cd24ef0814060d2",-  "size": 387420,+  "version": "1.9.122",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.122/adom-bridge-puppeteer-v1.9.122.zip",+  "sha256": "f8f2f825d7bddfd0daa0924deff198bed82fb2cddf2c1d80138c7cca1c60b15b",+  "size": 388449,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.121+1.9.122
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.121",+  "version": "1.9.122",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+43−8
@@ -4886,11 +4886,29 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer       // v1.8.34: snapshot the user's foreground window right before launch (background only)       // so we can hand keyboard focus back to it after the browser grabs it.       const priorForeground = foreground ? null : osGetForegroundWindow();-      const child = spawn(chromePath, chromeArgs, {-        detached: true,-        stdio: 'ignore',-        windowsHide: false,-      });+      // v1.9.122 — REPARENT Chrome out of the bridge's process TREE (Windows).+      // Measured root cause of "every pup window dies on a bridge restart": Node's+      // { detached:true } gives a new process GROUP but the child stays a TREE child of+      // node (parentPid = the bridge), so AD's tree-kill on restart takes Chrome with it.+      // Chrome killed dirty = never a clean shutdown, and (with the durable "you" profile+      // coming next) a rotting profile. Fix: launch through a transient `cmd /c start`,+      // which returns instantly and EXITS, orphaning Chrome from node within milliseconds.+      // A later `taskkill /T` on the bridge no longer finds Chrome. CDP connects by PORT+      // (below), so losing the direct PID handle is fine — we re-resolve it after connect.+      // macOS/Linux keep the direct spawn (no tree-kill problem observed there).+      const reparented = process.platform === 'win32' && !!userDataDir;+      let child;+      if (reparented) {+        // Full control of quoting (the taskbar-command lesson): build the command string+        // ourselves and pass it verbatim, rather than letting Node re-quote through cmd.+        const winQuote = (a) => /[\s"]/.test(a) ? '"' + String(a).replace(/"/g, '""') + '"' : a;+        const startCmd = `start "" /b ${winQuote(chromePath)} ${chromeArgs.map(winQuote).join(' ')}`;+        child = spawn('cmd.exe', ['/d', '/s', '/c', startCmd], {+          windowsVerbatimArguments: true, windowsHide: true, detached: true, stdio: 'ignore',+        });+      } else {+        child = spawn(chromePath, chromeArgs, { detached: true, stdio: 'ignore', windowsHide: false });+      }       child.unref();        // Wait for the browser to start and CDP to be ready (30s — cold start can be slow).@@ -4908,6 +4926,9 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer           const resp = await fetch(`http://127.0.0.1:${debugPort}/json/version`);           if (resp.ok) { connected = true; break; }         } catch {}+        // v1.9.122: the reparent launcher (cmd) exits within ms BY DESIGN, so its death+        // carries no information — poll CDP for the whole budget instead of bailing early.+        if (reparented) continue;         // Is the process we spawned still alive?         let alive = true;         try {@@ -4930,9 +4951,11 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer       }        if (!connected) {-        // Kill the zombie we spawned before throwing+        // Kill the zombie we spawned before throwing. v1.9.122: when reparented, child.pid is+        // the already-exited launcher, so a stranded Chrome is only reachable via its profile.         try {-          if (process.platform === 'win32') execSync(`taskkill /F /T /PID ${child.pid}`, { stdio: 'ignore' });+          if (reparented) { await killChromeHoldingProfile(); }+          else if (process.platform === 'win32') execSync(`taskkill /F /T /PID ${child.pid}`, { stdio: 'ignore' });           else process.kill(child.pid, 'SIGKILL');         } catch {}         const reason = childDied ? 'Chrome process exited immediately' : 'CDP never became ready';@@ -4940,7 +4963,19 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer       }        const b = await puppeteer.connect({ browserURL: `http://127.0.0.1:${debugPort}`, defaultViewport: null });-      b._detachedPid = child.pid;+      // v1.9.122: recover the real Chrome pid by its debug port (the reparent path's child.pid+      // is the exited cmd). Retry briefly — the process is definitely up once CDP answered.+      let resolvedPid = child.pid;+      if (reparented) {+        for (let t = 0; t < 4; t++) {+          const procs = await _psProcs(`$_.CommandLine -like '*--remote-debugging-port=${debugPort}*'`);+          const hit = procs.find(x => /--user-data-dir/.test(x.cmd));+          if (hit) { resolvedPid = hit.pid; break; }+          await new Promise(r => setTimeout(r, 300));+        }+        console.log(`[reparent] Chrome for "${profileName}" reparented off the bridge tree; real PID ${resolvedPid} (launcher was ${child.pid})`);+      }+      b._detachedPid = resolvedPid;       b._cdpPort = debugPort;       b._priorForeground = priorForeground; // v1.8.34: user's window to hand focus back to       console.log(`Launched detached Chrome (PID ${child.pid}) on CDP port ${debugPort} for profile "${profileName}"`);