← Commit history

Reclaim unmanaged pup windows (openerless popups and orphans whose session died) so they can never sit unbranded in the taskbar

John Lauer ·d788565e19 · ·parent 35e75a4
4 files changed +104−7
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.46",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.46/adom-bridge-puppeteer-v1.9.46.zip",-  "sha256": "22ce18f95b9f1c881659574bf29d4ba7968e4b6e442b04a48afdd35acbcccf15",-  "size": 294696,+  "version": "1.9.47",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.47/adom-bridge-puppeteer-v1.9.47.zip",+  "sha256": "9c05af6098f98ee92b24c3cfabac4c3c14441769ae2730b8fd69f058205d4c31",+  "size": 296576,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.46+1.9.47
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.46",+  "version": "1.9.47",   "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.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+98−1
@@ -2493,7 +2493,8 @@ function adPayload(r) { } let _uiWatchId = null; let _uiWatchSeq = 0;-let _lastDragCheck = 0;   // v1.9.42 throttle cursor for the drag-out check+let _lastDragCheck = 0;+let _lastReclaim = 0;   // v1.9.42 throttle cursor for the drag-out check let _uiWatchSupported = null;   // null = unprobed, false = AD too old async function ensureWindowOpenedWatch() {   if (process.platform !== 'win32' || _uiWatchSupported === false || _uiWatchId) return;@@ -2589,6 +2590,90 @@ async function adoptDraggedOutTabs(sessionId) {   } catch (e) { console.log(`[drag] adopt threw: ${e.message}`); }   return adopted; }+// v1.9.47 — RECLAIM UNMANAGED WINDOWS. The gap that kept John's taskbar wrong.+//+// adoptDraggedOutTabs only ever looked at tabs pup ALREADY TRACKS, so it could never see a window+// pup has no tab for. Two everyday cases produce exactly that, and both leave a permanently+// UNBRANDED taskbar button (raw "Google Chrome for Testing"), which is what a taskbar audit found:+//   1. An OPENERLESS new window. wirePopupTracking deliberately drops targets with no opener (it+//      must not await target.page() on one — that re-enters the CDP pipe and hangs newPage; see+//      v1.8.18). Correct there, but nothing ever picked those windows up afterwards. Measured: one+//      ti.com session had spawned TWO untracked windows (an stm32 search + a product page).+//   2. A window whose SESSION DIED but whose Chrome kept running — e.g. the session file was lost,+//      so pup is not even connected to that profile any more.+//+// So reclamation must work from the WINDOW side, not the tab side: enumerate every page in every+// pup profile, group by OS window, and adopt any window no session owns. Runs on a timer (out of+// band — never inside a targetcreated handler, to keep the pipe safe).+async function reclaimUnmanagedWindows() {+  let reclaimed = 0;+  try {+    // Part B first: pup profiles whose Chrome is alive but that pup is not connected to at all.+    // Without this an orphan is invisible — it has no browser entry, so Part A cannot see its pages.+    let profileDirs = [];+    try { profileDirs = fs.readdirSync(PROFILES_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name); } catch (e) {}+    for (const profileName of profileDirs) {+      if (browsers.has(profileName)) continue;+      let port = null;+      try { port = parseInt(fs.readFileSync(path.join(PROFILES_DIR, profileName, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0], 10) || null; } catch (e) {}+      if (!port) continue;+      const resp = await fetch(`http://127.0.0.1:${port}/json/version`).catch(() => null);+      if (!resp || !resp.ok) continue;   // stale port file, Chrome is gone — leave it to the reaper+      try {+        const browser = await puppeteer.connect({ browserURL: `http://127.0.0.1:${port}`, defaultViewport: null });+        browser._cdpPort = port;+        browsers.set(profileName, { browser, refCount: 0 });+        wirePopupTracking(browser, profileName);+        browser.on('disconnected', () => handleBrowserDisconnect(browser, profileName));+        console.log(`[reclaim] re-connected to ORPHANED profile "${profileName}" on port ${port} (Chrome outlived its session)`);+      } catch (e) { continue; }+    }++    // Part A: any page in a connected profile that no session owns.+    for (const [profileName, be] of [...browsers]) {+      let pages = [];+      try { pages = await be.browser.pages(); } catch (e) { continue; }+      const owned = new Set();+      for (const s of sessions.values()) for (const t of (s.tabs || [])) owned.add(t.page);+      const loose = [];+      for (const p of pages) {+        if (owned.has(p)) continue;+        let u = ''; try { u = p.url(); } catch (e) { continue; }+        // about:blank is how a window is born; devtools/chrome pages are not user content.+        if (!u || u === 'about:blank' || /^(devtools|chrome|edge):/i.test(u)) continue;+        loose.push(p);+      }+      if (!loose.length) continue;+      // Group by OS window so tabs of the SAME reclaimed window land in ONE session (not one each).+      const byWindow = new Map();+      for (const p of loose) {+        let wid = null;+        try { wid = await pageWindowId(p); } catch (e) {}+        const key = wid == null ? `nowin-${loose.indexOf(p)}` : `w${wid}`;+        if (!byWindow.has(key)) byWindow.set(key, []);+        byWindow.get(key).push(p);+      }+      for (const [key, group] of byWindow) {+        const newId = `${profileName}-win${Math.random().toString(36).slice(2, 6)}`;+        const ns = createSession(profileName);+        ns._sessionId = newId;+        ns.owner = 'reclaimed';+        for (const p of group) attachTab(ns, newId, p);+        sessions.set(newId, ns);+        be.refCount++;+        let u = null; try { u = group[0].url(); } catch (e) {}+        // Title FIRST — every downstream brand step resolves the window by its "(session: <id>)".+        try { await retagSessionTitle(group[0], newId, profileName); } catch (e) {}+        console.log(`[reclaim] UNMANAGED window on "${profileName}" (${group.length} tab(s)) adopted as "${newId}" — ${(u || '').slice(0, 60)}`);+        refreshWindowChrome(ns, newId).catch(() => {});+        try { saveSessionFile(newId, profileName, u); } catch (e) {}+        reclaimed++;+      }+    }+  } catch (e) { console.log(`[reclaim] threw: ${e.message}`); }+  if (reclaimed) console.log(`[reclaim] adopted ${reclaimed} previously-unbranded window(s)`);+  return reclaimed;+} // Drain the window-opened ring; a new window means SOME session may have lost a tab. Cheap: one // drain call, then the CDP check only for multi-tab sessions. async function drainWindowOpenedEvents() {@@ -2612,6 +2697,13 @@ async function drainWindowOpenedEvents() {     // polling it was meant to avoid. Two cheap guards, in order:     //   1. Nothing multi-tab? A single-tab window cannot be split, so there is nothing to detect.     //   2. At most one check per THROTTLE_MS, no matter how many events arrive.+    // v1.9.47: a window-opened event may also mean an UNMANAGED window appeared (openerless popup),+    // which the splittable check below cannot see — it only looks at tabs we already track.+    const THROTTLE_RECLAIM = 4000;+    if (Date.now() - _lastReclaim > THROTTLE_RECLAIM) {+      _lastReclaim = Date.now();+      reclaimUnmanagedWindows().catch(() => {});+    }     const splittable = [...sessions].filter(([, sess]) => (sess.tabs || []).length >= 2);     if (!splittable.length) return;     const THROTTLE_MS = 1500;@@ -7334,6 +7426,11 @@ server.listen(PORT, BIND_HOST, async () => {     } catch (e) {}   }, 60000); +  // v1.9.47: reclaim on boot (catches windows orphaned by a crash/restart) and then as a slow safety+  // net, so an unbranded window can never persist even if its window-opened event was missed.+  setTimeout(() => { reclaimUnmanagedWindows().catch(() => {}); }, 12000);+  setInterval(() => { reclaimUnmanagedWindows().catch(() => {}); }, 90000);+   // Periodic cleanup: every 5 minutes, remove stale session files with dead PIDs   setInterval(async () => {     const files = listSessionFiles();