← Commit history

v2.0.671 (insiders): Every profile's lane survives a restart in its own jar: pup restores each profile's lane browser at boot (validated by the folder its processes run), adopts an unknown lane browser only into the profile it reports and only after that restore, re-adopts each window on its own profile's lane, and relaunches any window it finds inside another profile's browser. Before, the first unknown browser after an upgrade became the adom-you lane, so adom-you windows ran with another profile's cookies, and windows on a second profile were never re-adopted (AdomLapper and CRR, 2026-09-21).

John Lauer ·1858802618 ·1mo ago ·parent 4dabf4e
7 files changed +117−37
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.669"+version = "2.0.671" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.669"+version = "2.0.671" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.669"+version = "2.0.671" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.669"+version = "2.0.671" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.669"+version = "2.0.671" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.669"+version = "2.0.671" dependencies = [  "base64",  "png 0.17.16",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session",  # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.669"+version = "2.0.671" edition = "2021" license = "MIT" 
rust/browser/src/lib.rs+4−1
@@ -413,7 +413,10 @@ pub fn stage_extensions_lane(host: &dyn Host, port: u16, profile: &str, toolbar_             // them in; empty = leave the file inert).             if let Some(js) = identity_shim.as_deref() { let _ = fs::write(dir.join("identity-shim.js"), js); }             let lane_field = lane.map(|l| format!(",\"lane\":\"{l}\"")).unwrap_or_default();-            let _ = fs::write(dir.join("port.json"), format!("{{\"port\":{port},\"version\":\"{}\",\"shell\":\"rust\"{lane_field}}}", pup_core::version()));+            // 2.0.670: the profile rides along so the worker can say WHICH jar it runs in; pup adopts an unknown+            // lane browser only into that profile's lane (AdomLapper, 2026-09-21: the "services" browser was+            // adopted as adom-you and two adom-you windows ran in the wrong cookie jar).+            let _ = fs::write(dir.join("port.json"), format!("{{\"port\":{port},\"version\":\"{}\",\"shell\":\"rust\",\"profile\":{}{lane_field}}}", pup_core::version(), serde_json::to_string(profile).unwrap_or_else(|_| "\"\"".into())));         }         // 2.0.498: stamp the manifest version with pup's own version. Chrome CACHES an unpacked extension's         // service worker in the profile and only reloads it when the manifest version changes — so every
rust/verbs/src/extdrive.rs+26−4
@@ -47,6 +47,8 @@ impl LaneState { } static DEFAULT_LANE: std::sync::OnceLock<std::sync::Arc<LaneState>> = std::sync::OnceLock::new(); static LANES: Mutex<Vec<std::sync::Arc<LaneState>>> = Mutex::new(Vec::new());+/// Set once restore_extdrive has read every lane file at boot; adoption of unknown lanes waits for it.+pub static RESTORED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); thread_local! { static CUR_PROFILE: std::cell::RefCell<String> = std::cell::RefCell::new(String::new()); } pub fn default_lane() -> std::sync::Arc<LaneState> { DEFAULT_LANE.get_or_init(|| std::sync::Arc::new(LaneState::new("adom-you"))).clone() } pub fn st() -> std::sync::Arc<LaneState> {@@ -61,6 +63,11 @@ pub fn enter_profile(profile: &str) {     CUR_PROFILE.with(|c| *c.borrow_mut() = profile.to_string()); } pub fn leave_profile() { CUR_PROFILE.with(|c| c.borrow_mut().clear()); }+/// The lane for `profile`, created if pup has none yet (the default lane for adom-you).+pub fn ensure_lane(profile: &str) -> std::sync::Arc<LaneState> {+    if let Some(l) = lane_for_profile(profile) { return l; }+    let l = std::sync::Arc::new(LaneState::new(profile)); LANES.lock().unwrap().push(l.clone()); l+} pub fn lane_for_profile(profile: &str) -> Option<std::sync::Arc<LaneState>> {     if profile.is_empty() || profile == "adom-you" { return Some(default_lane()); }     LANES.lock().unwrap().iter().find(|l| l.profile == profile).cloned()@@ -187,10 +194,25 @@ pub fn hello(v: Value) {         m.insert(key, now);     }     let dl = default_lane();-    if !matched && !lane.is_empty() && *dl.last_poll.lock().unwrap() == 0 {-        *dl.token.lock().unwrap() = lane.clone();-        pup_core::log(format!("[extdrive] adopted the running extension browser (lane {lane}); pup had lost track of it"));-        return;+    // 2.0.670: adopt an unknown lane browser ONLY into the lane of the profile it says it runs. It used to become the+    // adom-you lane whatever its jar: on AdomLapper (2026-09-21) the "services" browser was adopted after an upgrade+    // and adom-you windows opened inside it with the wrong cookies. A worker too old to report its profile is adopted+    // as adom-you only while pup knows no other profile's lane.+    let hp = v["profile"].as_str().unwrap_or("").to_string();+    // 2.0.670: the HTTP server answers before the boot restore has read the lane files; an adoption in that gap+    // took the svc-test browser as adom-you (CRR, 2026-09-21 13:34:43). Wait for the restore; the hello repeats.+    if !matched && !lane.is_empty() && !RESTORED.load(std::sync::atomic::Ordering::SeqCst) { return; }+    if !matched && !lane.is_empty() {+        let dest = if !hp.is_empty() { Some(ensure_lane(&hp)) } else if LANES.lock().unwrap().is_empty() { Some(dl.clone()) } else { None };+        match dest {+            Some(d) if *d.last_poll.lock().unwrap() == 0 => {+                *d.token.lock().unwrap() = lane.clone();+                pup_core::log(format!("[extdrive] adopted the running extension browser (lane {lane}) as the {} lane{}; pup had lost track of it", d.profile, if hp.is_empty() { " (its worker does not report a profile)" } else { "" }));+                return;+            }+            None => { pup_core::log(format!("[extdrive] a pup extension with an unknown lane {lane:?} and no reported profile said hello; NOT adopted (pup runs more than one profile's lane and cannot tell which jar it is)")); return; }+            _ => {}+        }     }     if !matched && lane.is_empty() && *dl.last_poll.lock().unwrap() == 0 {         *TOKENLESS_HELLO_AT.lock().unwrap() = pup_core::now_ms();
rust/verbs/src/lib.rs+43−12
@@ -127,20 +127,51 @@ fn save_lane(ctx: &Ctx) {  /// Re-attach to an extension browser that outlived this pup process. Called at boot. pub fn restore_extdrive(ctx: &Ctx) {-    let p = lane_file(ctx);-    let Ok(txt) = std::fs::read_to_string(&p) else { return };-    let Ok(v) = serde_json::from_str::<Value>(&txt) else { return };-    let lane = v["lane"].as_str().unwrap_or("").to_string();-    let pids: Vec<u32> = v["pids"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|n| n as u32)).collect()).unwrap_or_default();-    let alive: Vec<u32> = pids.into_iter().filter(|p| ctx.host.alive(*p)).collect();-    if lane.is_empty() || alive.is_empty() {-        let _ = std::fs::remove_file(&p);   // nothing survived; do not hold a token no browser carries-        return;+    // 2.0.670: EVERY profile's lane comes back, not only adom-you's, and a lane file is trusted only when its recorded+    // processes really run that profile's folder. Before, only extlane/lane.json was read; the other lane browsers+    // said hello with tokens pup no longer knew, and the first one was adopted as adom-you (AdomLapper, 2026-09-21).+    struct Done; impl Drop for Done { fn drop(&mut self) { extdrive::RESTORED.store(true, std::sync::atomic::Ordering::SeqCst); } }+    let _done = Done;+    let by_profile = pup_browser_pids_by_profile(ctx);+    let Ok(rd) = std::fs::read_dir(ctx.host.state_dir()) else { return };+    let mut seen: Vec<String> = Vec::new();+    for e in rd.flatten() {+        let name = e.file_name().to_string_lossy().to_string();+        let profile = if name == "extlane" { "adom-you".to_string() } else if let Some(p) = name.strip_prefix("extlane-") { p.to_string() } else { continue };+        let p = e.path().join("lane.json");+        let Ok(txt) = std::fs::read_to_string(&p) else { continue };+        let Ok(v) = serde_json::from_str::<Value>(&txt) else { continue };+        let lane = v["lane"].as_str().unwrap_or("").to_string();+        let pids: Vec<u32> = v["pids"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|n| n as u32)).collect()).unwrap_or_default();+        let truth = by_profile.get(&profile).cloned().unwrap_or_default();+        let alive: Vec<u32> = pids.iter().copied().filter(|p| truth.contains(p)).collect();+        if lane.is_empty() || alive.is_empty() || seen.contains(&lane) {+            let wrong = pids.iter().any(|p| ctx.host.alive(*p));+            if wrong && alive.is_empty() { pup_core::log(format!("[extdrive] the {profile} lane file pointed at processes that are not running the {profile} profile; dropped it (that browser keeps its own lane)")); }+            let _ = std::fs::remove_file(&p);+            continue;+        }+        seen.push(lane.clone());+        let l = extdrive::ensure_lane(&profile);+        *l.token.lock().unwrap() = lane.clone();+        *l.pids.lock().unwrap() = alive.clone();+        pup_core::log(format!("[extdrive] re-attached to the {profile} extension browser that outlived this pup ({} process(es), lane {lane})", alive.len()));     }-    *extdrive::st().token.lock().unwrap() = lane.clone();-    *extdrive::st().pids.lock().unwrap() = alive.clone();-    pup_core::log(format!("[extdrive] re-attached to the extension browser that outlived this pup ({} process(es), lane {lane})", alive.len())); }++/// 2.0.670: every pup browser process on the box, keyed by the profile folder its command line names+/// (`pup-profiles\<name>`). One process listing, so callers can tell which jar a pid really runs.+pub fn pup_browser_pids_by_profile(ctx: &Ctx) -> std::collections::HashMap<String, Vec<u32>> {+    let mut m: std::collections::HashMap<String, Vec<u32>> = std::collections::HashMap::new();+    if !cfg!(windows) { return m; }+    let script = "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" | ForEach-Object { if ($_.CommandLine -match 'pup-profiles[\\\\/]([^\\\\/\\\" ]+)') { \"$($_.ProcessId)|$($matches[1])\" } }";+    for l in ctx.host.run_shell_script(script, Duration::from_secs(20)).lines() {+        let mut it = l.trim().splitn(2, '|');+        if let (Some(pid), Some(prof)) = (it.next().and_then(|x| x.parse::<u32>().ok()), it.next()) { m.entry(prof.to_string()).or_default().push(pid); }+    }+    m+}+ /// The consumer-identity JS shim staged into the lane extension (None when consumerBrands is off). pub fn lane_shim(ctx: &Ctx) -> Option<String> {     let shim: Option<String> = if ctx.settings.get("consumerBrands", "on") != "off" { consumer_identity_strings(ctx).map(|v| format!(
rust/verbs/src/recovery.rs+24−2
@@ -74,8 +74,15 @@ impl Drop for RelaunchGuard { fn drop(&mut self) { note_relaunched(&self.0); IN_ /// originals (AdomLapper, 2026-09-13: the 17-tab proof window orphaned, a duplicate opened beside it). The /// session file carries windowId; the live `tabs` listing says whether that window still exists and which /// tabs it holds, so the session is rebuilt from what Chrome actually has.+/// Runs the body on the lane of the file's OWN profile. 2.0.670: recovery threads sit on the adom-you lane, so a+/// svc-test window was looked for among adom-you's tabs, never found, and stayed forgotten (CRR, 2026-09-21).+struct ProfileScope;+impl ProfileScope { fn enter(profile: &str) -> ProfileScope { crate::extdrive::enter_profile(profile); ProfileScope } }+impl Drop for ProfileScope { fn drop(&mut self) { crate::extdrive::leave_profile(); } }+ fn readopt_ext(ctx: &Ctx, f: &Value) -> bool {     if f["transport"].as_str() != Some("ext") { return false; }+    let _scope = ProfileScope::enter(f["profile"].as_str().unwrap_or("adom-you"));     let id = f["sessionId"].as_str().unwrap_or("").to_string();     let Some(wid) = f["windowId"].as_i64() else { return false };     if !crate::extdrive::alive(ctx) {@@ -121,7 +128,21 @@ pub fn relaunch_from_file(ctx: &Ctx, f: &Value) {     if restore_cancelled(&id) { pup_core::log(format!("[recovery] \"{id}\": its restore was cancelled by a close; not relaunching")); ctx.sessions.delete_file(&id); return; }     if readopt_ext(ctx, f) { RELAUNCHES.lock().unwrap().retain(|(s, _)| *s != id); return; }     if let Some(h) = f["_hwnd"].as_i64().filter(|&h| h > 0) {-        if ctx.host.is_alive(h) { pup_core::log(format!("[recovery] \"{id}\": its window {h} is alive on glass; NOT relaunching (worker quiet, will re-adopt on the next tick; its file is kept)")); return; }+        if ctx.host.is_alive(h) {+            // 2.0.670: alive is not enough; it must be alive in ITS profile's browser. A window left inside another+            // profile's browser (adopted as the wrong lane before 2.0.670) would otherwise be "kept" forever, never+            // re-adopted, running in the wrong cookie jar. That one is closed and relaunched in its own profile.+            let prof = f["profile"].as_str().unwrap_or("adom-you").to_string();+            let by = crate::pup_browser_pids_by_profile(ctx);+            let owner_prof = by.iter().find(|(_, pids)| !ctx.host.windows_for_pids(pids, None).iter().all(|w| w.hwnd != h)).map(|(p, _)| p.clone());+            match owner_prof {+                Some(op) if op != prof => {+                    pup_core::log(format!("[recovery] \"{id}\": its window {h} is alive but inside the {op} profile's browser, not {prof}; closing it there and relaunching it in {prof}"));+                    let _ = ctx.ab.call("desktop_close_window", json!({ "hwnd": h, "reason": format!("pup: the \"{id}\" window was running in the wrong profile ({op}); reopening it in {prof}") }), &crate::identity::internal_caller(), Duration::from_secs(10));+                }+                _ => { pup_core::log(format!("[recovery] \"{id}\": its window {h} is alive on glass; NOT relaunching (worker quiet, will re-adopt on the next tick; its file is kept)")); return; }+            }+        }     }     let attempts = { let mut m = RELAUNCHES.lock().unwrap(); match m.iter_mut().find(|(s, _)| *s == id) { Some(e) => { e.1 += 1; e.1 } None => { m.push((id.clone(), 1)); 1 } } };     if attempts > 2 { pup_core::log(format!("[recovery] \"{id}\" failed to come back twice — giving up on it (the page itself may be crashing Chrome); removing its file")); ctx.sessions.delete_file(&id); crate::push_cleanup_report(format!("pup could NOT auto-recover the \"{id}\" window: it crashed again on relaunch ({url}). Open it again yourself with pup_open_window if you still need it.")); return; }@@ -574,7 +595,8 @@ pub fn health_tick(ctx: &Ctx) {         // read as "gone" every tick and tore the live window down).         if f["transport"].as_str() == Some("ext") {             if readopt_ext(ctx, &f) { continue; }-            if !crate::extdrive::alive(ctx) && crate::extdrive_pids().is_empty() { relaunch_from_file(ctx, &f); }+            let dead = { let _scope = ProfileScope::enter(f["profile"].as_str().unwrap_or("adom-you")); !crate::extdrive::alive(ctx) && crate::extdrive_pids().is_empty() };+            if dead { relaunch_from_file(ctx, &f); }             continue;         }         let profile = f["profile"].as_str().unwrap_or("adom-you").to_string();
src/extension/worker.js+7−5
@@ -15,6 +15,7 @@ const CANDIDATE_PORTS = [64230, 8851]; let _pkgPort = null; let _pkgLane = '';   // only the extension-DRIVE browser gets a lane token in port.json let _pkgRead = false;   // ONLY a successful read may be memoized+let _pkgProfile = '';   // 2.0.670: the jar this lane browser runs (port.json), reported in every hello async function packagedPort() {   // 2.0.590: this read hung forever on a fresh worker instance after a reload in place (measured on winvm:   // hellos every 30 s with the right lane, /extcmd never hit again, the drain stuck in this fetch). The@@ -28,12 +29,13 @@ async function packagedPort() {     const j = await r.json();     _pkgPort = (j && j.port) || 0;     _pkgLane = (j && j.lane) || '';+    _pkgProfile = (j && j.profile) || '';     _pkgRead = true;-    try { chrome.storage.local.set({ pupPkgPort: _pkgPort, pupPkgLane: _pkgLane }); } catch (e) {}+    try { chrome.storage.local.set({ pupPkgPort: _pkgPort, pupPkgLane: _pkgLane, pupPkgProfile: _pkgProfile }); } catch (e) {}   } catch (e) {     try {-      const st = await new Promise((res) => chrome.storage.local.get(['pupPkgPort', 'pupPkgLane'], res));-      if (st && st.pupPkgLane) { _pkgPort = st.pupPkgPort || 0; _pkgLane = st.pupPkgLane; }+      const st = await new Promise((res) => chrome.storage.local.get(['pupPkgPort', 'pupPkgLane', 'pupPkgProfile'], res));+      if (st && st.pupPkgLane) { _pkgPort = st.pupPkgPort || 0; _pkgLane = st.pupPkgLane; _pkgProfile = st.pupPkgProfile || ''; }     } catch (e2) {}   }   return _pkgPort === null ? 0 : _pkgPort;@@ -235,7 +237,7 @@ async function pupExtPollOnce() {     try {       fetch(`http://127.0.0.1:${found.port}/exthello`, {         method: 'POST', headers: { 'Content-Type': 'application/json' },-        body: JSON.stringify({ lane: _pkgLane, pkg: _pkgPort, read: _pkgRead, worker: (self.location && self.location.pathname) || 'worker.js', tries: pupExtDiag.tries }),+        body: JSON.stringify({ lane: _pkgLane, profile: _pkgProfile, pkg: _pkgPort, read: _pkgRead, worker: (self.location && self.location.pathname) || 'worker.js', tries: pupExtDiag.tries }),         signal: AbortSignal.timeout(1500),       }).catch(() => {});     } catch (e) {}@@ -565,7 +567,7 @@ async function pupExtHello() {     const found = await findPort();     if (!found) return;     await fetch(`http://127.0.0.1:${found.port}/exthello`, { method: 'POST', headers: { 'Content-Type': 'application/json' },-      body: JSON.stringify({ lane: _pkgLane, pkg: _pkgPort, read: _pkgRead, worker: (self.location && self.location.pathname) || 'worker.js', tries: pupExtDiag.tries, standalone: true }),+      body: JSON.stringify({ lane: _pkgLane, profile: _pkgProfile, pkg: _pkgPort, read: _pkgRead, worker: (self.location && self.location.pathname) || 'worker.js', tries: pupExtDiag.tries, standalone: true }),       signal: AbortSignal.timeout(1500) });   } catch (e) {} }