← Commit history

Recover crashed renderers ('Aw, Snap! Out of Memory'). A crashed renderer detaches the page's main frame, so browser_reload/browser_navigate threw 'detached Frame' — the recovery the crash hint promised could not actually run, leaving the window stuck on the Aw-Snap page. New respawnCrashedRenderer() drives the raw target with Page.navigate to spawn a fresh renderer in the same tab/window/session; browser_reload and browser_navigate now use it (both when the tab is flagged crashed and as a fallback when they hit a detached frame). Plus the renderer crash detector AUTO-heals the tab once (an Aw-Snap tab is already dead, so respawn loses nothing), reporting via the cleanup queue, capped at one attempt to avoid a reload-crash loop. Surfaced by John hitting an Aw-Snap on a shared-profile wiki window after back-to-back heavy tests piled memory into the one shared Chrome.

John Lauer ·e56f49d55e ·3mo ago ·parent 5849fc3
4 files changed +112−16
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.175",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.175/adom-bridge-puppeteer-v1.9.175.zip",-  "sha256": "bb3b59ab83e7347ae3d83d0109817955f35659037af113f68f45f6ea0ad722f8",-  "size": 411671,+  "version": "1.9.176",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.176/adom-bridge-puppeteer-v1.9.176.zip",+  "sha256": "ad90aecf54ecc58f40a1ccc8f32089b72d749aa4c7a6f9289f689fb306036491",+  "size": 413111,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.175+1.9.176
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.175",+  "version": "1.9.176",   "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+106−10
@@ -426,7 +426,29 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user       const t = (session.tabs || []).find(x => x.page === page);       if (t && !t._crashed) {         t._crashed = true; t._crashedAt = Date.now(); t._crashReason = why;-        console.log(`[crash] session "${sessionId}" tab ${t.tabId} renderer CRASHED (${why}) — verbs on it will fast-fail; reload the tab to recover`);+        try { const u = page.url(); if (u && u !== 'about:blank') t._crashUrl = u; } catch (e) {}+        console.log(`[crash] session "${sessionId}" tab ${t.tabId} renderer CRASHED (${why}) — auto-recovering; verbs fast-fail until it's back`);+        // v1.9.176 — AUTO-HEAL the crashed renderer ONCE. An 'Aw, Snap' tab is already dead (it shows+        // Chrome's error page), so respawning it loses nothing and is pure recovery — exactly the+        // resilience John asked for. Cap at one auto-attempt: if it re-crashes (memory still pressured),+        // stop and leave it flagged so the caller decides, rather than spinning a reload-crash loop.+        if ((t._crashAutoAttempts || 0) < 1) {+          t._crashAutoAttempts = (t._crashAutoAttempts || 0) + 1;+          const rurl = t._crashUrl;+          setTimeout(async () => {+            try {+              if (!t._crashed) return;   // a verb already recovered it+              const ok = await respawnCrashedRenderer(page, rurl);+              if (ok) {+                t._crashed = false; delete t._crashedAt; delete t._crashReason;+                console.log(`[crash] session "${sessionId}" tab ${t.tabId} AUTO-recovered (renderer respawned)`);+                recordCleanup(`pup auto-recovered a crashed ("Aw, Snap! Out of Memory") tab in session "${sessionId}" by respawning its renderer${rurl ? ` at ${rurl}` : ''} — the tab had run out of memory (usually many heavy tabs sharing one Chrome); the window is back on its own.`);+              } else {+                console.log(`[crash] session "${sessionId}" tab ${t.tabId} auto-recover did not take — left flagged for manual browser_reload`);+              }+            } catch (e) {}+          }, 2500);+        }       }     } catch (e) {}   };@@ -780,6 +802,38 @@ function resolveTabOrError(session, sessionId, args, { allowCrashed = false } = // v1.9.78: a crashed-renderer tab cannot be screenshotted or evaluated — those operations hang // until the verb times out (20s) and, on a busy window, that read-back wedge is what the user // perceives as "pup crashed". Fail FAST and tell the caller exactly how to recover.+// v1.9.176: recover a CRASHED renderer IN PLACE. A crashed renderer ('Aw, Snap!') detaches the+// page's main frame, so page.reload()/page.goto() throw 'Attempted to use detached Frame' — which is+// exactly why the crash hint's promised recovery ("browser_reload to recover") did NOT work. Driving+// the RAW target with Page.navigate spawns a fresh renderer in the SAME tab/window/session, which+// puppeteer then re-attaches. Returns true once the fresh page answers a probe, false on failure.+async function respawnCrashedRenderer(page, url) {+  let cdp = null;+  try {+    cdp = await page.target().createCDPSession();+    try { await cdp.send('Page.enable'); } catch (e) {}+    await cdp.send('Page.navigate', { url: url || 'about:blank' });+    const deadline = Date.now() + 8000;+    while (Date.now() < deadline) {+      await new Promise(r => setTimeout(r, 400));+      try { await page.evaluate('1'); return true; } catch (e) { /* frame still respawning */ }+    }+    return false;+  } catch (e) {+    console.log(`[crash] respawnCrashedRenderer failed: ${e.message}`);+    return false;+  } finally {+    try { if (cdp) await cdp.detach(); } catch (e) {}+  }+}++// Detached-frame / crashed-renderer error signatures — the raw puppeteer errors that mean the tab's+// renderer is dead and normal page.* calls can't run; the caller should fall back to a respawn.+function _isCrashedFrameError(e) {+  const m = (e && e.message) || String(e || '');+  return /detached Frame|Target crashed|Session closed|Execution context was destroyed|no live page/i.test(m);+}+ function _throwIfCrashed(t, sessionId) {   if (!t || !t._crashed) return;   const secs = Math.round((Date.now() - (t._crashedAt || Date.now())) / 1000);@@ -9217,14 +9271,37 @@ const server = http.createServer(async (req, res) => {           try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args, { allowCrashed: true })); }           catch (e) { sendTabResolveError(res, e); return; }           tab.errors.length = 0;+          // v1.9.176: a CRASHED renderer can't be page.reload()'d (detached frame) — respawn it via+          // raw CDP instead. This is the recovery the crash hint promised but page.reload() couldn't do.+          if (tab._crashed) {+            const rurl = tab._crashUrl || (() => { try { return tab.page.url(); } catch (e) { return null; } })() || args.url;+            const ok = await respawnCrashedRenderer(tab.page, rurl);+            tab._crashed = false; delete tab._crashedAt; delete tab._crashReason;+            console.log(`[crash] session "${resolvedId}" tab ${tabId} recovered via respawn (${ok ? 'ok' : 'sent'})`);+            sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, _recovered: `crashed renderer respawned${rurl ? ' at ' + rurl : ''}` }) });+            return;+          }           // v1.9.109: keep the reload's own HTTP status — page.reload() returns the response, and this           // is the verb an AI uses right after "I fixed it", so a still-broken page must be caught here.-          const _rlResp = await withTimeout(-            tab.page.reload({ waitUntil: 'domcontentloaded', timeout: 25000 }),-            30_000, 'page.reload'-          );+          let _rlResp;+          try {+            _rlResp = await withTimeout(+              tab.page.reload({ waitUntil: 'domcontentloaded', timeout: 25000 }),+              30_000, 'page.reload'+            );+          } catch (e) {+            // The renderer may have crashed between resolve and reload (detached frame) — fall back to respawn.+            if (_isCrashedFrameError(e)) {+              const rurl = tab._crashUrl || (() => { try { return tab.page.url(); } catch (x) { return null; } })() || args.url;+              const ok = await respawnCrashedRenderer(tab.page, rurl);+              tab._crashed = false; delete tab._crashedAt; delete tab._crashReason;+              console.log(`[crash] session "${resolvedId}" tab ${tabId} reload hit a crashed frame — recovered via respawn (${ok ? 'ok' : 'sent'})`);+              sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, _recovered: `crashed renderer respawned${rurl ? ' at ' + rurl : ''}` }) });+              return;+            }+            throw e;+          }           try { tab.page.__pupHttpStatus = _rlResp && typeof _rlResp.status === 'function' ? _rlResp.status() : null; } catch (e) {}-          if (tab._crashed) { tab._crashed = false; delete tab._crashedAt; delete tab._crashReason; console.log(`[crash] session "${resolvedId}" tab ${tabId} recovered via reload`); }           // v1.8.26: the user is signalled by the auto-flash, NOT by raising the window.           // Only raise if this session is one the user is actively watching.           if (session._foreground) { try { await tab.page.bringToFront(); } catch {} }@@ -9264,10 +9341,29 @@ const server = http.createServer(async (req, res) => {           } catch (e) {             console.log(`[creds] applyCredentialsToPage on navigate failed: ${e.message}`);           }-          await withTimeout(-            tab.page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: 25000 }),-            30_000, 'page.goto'-          );+          // v1.9.176: navigating a CRASHED renderer via page.goto throws 'detached Frame'. When the tab+          // is flagged crashed (or goto hits a crashed frame), respawn via raw CDP Page.navigate to the+          // requested URL — that spawns a fresh renderer in place and lands on the target page.+          if (tab._crashed) {+            const ok = await respawnCrashedRenderer(tab.page, args.url);+            tab._crashed = false; delete tab._crashedAt; delete tab._crashReason;+            console.log(`[crash] session "${resolvedId}" tab ${tabId} navigate recovered a crashed renderer via respawn (${ok ? 'ok' : 'sent'})`);+          } else {+            try {+              await withTimeout(+                tab.page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: 25000 }),+                30_000, 'page.goto'+              );+            } catch (e) {+              if (_isCrashedFrameError(e)) {+                const ok = await respawnCrashedRenderer(tab.page, args.url);+                tab._crashed = false; delete tab._crashedAt; delete tab._crashReason;+                console.log(`[crash] session "${resolvedId}" tab ${tabId} navigate hit a crashed frame — recovered via respawn (${ok ? 'ok' : 'sent'})`);+              } else {+                throw e;+              }+            }+          }           tab.errors.length = 0;           // Re-apply title suffix after navigation           try {