← All Pull Requests

macOS build of the Rust pup: host-mac, per-platform manifest, capture guard, mac build script (pup-bridge#128) #4

Closed opened by Kyle Bergstedt

Supersedes PR #3 (same change set plus the capture guard; #3 is closed).

The macOS build of the Rust pup, for pup-bridge#128 (a Mac that syncs the public manifest gets the Windows exe and cannot spawn it).

What this adds, all platform-gated or defaulted so the Windows exe is unchanged:

  • rust/host-mac: the full Host trait on macOS (CGWindowList windows, park = hide the owning app via NSRunningApplication, AX set-bounds + AXRaise, open -g -n non-activating launch, Keychain in the keytar layout, process table / sysctl / vm_stat for the process verbs, .app bundle discovery for system browsers). 9 unit tests, live-probed on Apple Silicon.
  • Host trait additions with Windows-preserving defaults: raise_window (mac raises natively; Windows keeps ab's desktop_bring_to_front), system_browsers (default = the Program Files layout, moved out of pup-browser), closed_window_may_linger (Chrome on mac destroys a closed NSWindow on a later UI turn, so close verifies through the lane), running_browser_executables + pup_browser_processes (defaults keep the Win32_Process queries; on a Mac the PowerShell form answered nothing, so pup_cache_prune deleted every build but the one it had just installed, and the orphan/idle sweeps were empty), cft_cache_platform (puppeteer names mac cache folders mac_arm / mac, not the download tag).
  • verbs: the identity port file lives in the state dir off Windows; the recordings folder opens with the platform opener; the google toast focus names the mac browser.
  • src/extension/worker.js pupCaptureTab: on a Mac captureVisibleTab never settles for a window of a hidden app (pup's park state there), so it is raced against 3 s and falls back to the existing debugger capture. Same code on Windows; the race only matters when the API hangs.
  • Capture guard (Host::capture_frames_begin/end, transport::with_frames): on macOS a window of an app hidden for a while stops producing frames, so every capture of a window parked for about a minute timed out (Kyle's Mac, 2026-10-14). The host now unhides pup's own hidden browsers without activating them (they come back behind the user's app), the capture runs, and they are hidden again; a window parked 75 s screenshots in 1 s. No-op on Windows.
  • tools/rust-build-macos.sh <ver>: universal (arm64 + x86_64) binary, Developer ID signed with hardened runtime, zip with a 0755 adom-pup-bridge + bridge.json (spawn.kind exe, updateManifestUrls {macos, windows}, macos supported / windows unsupported) + the staged adom-bridge-puppeteer-manifest-macos.json. tools/rust-build.sh is untouched.
  • adom-bridge-puppeteer-manifest-macos.json: the macOS manifest (platform: macos), the same shape as the public one; its zip is the per-platform asset adom-bridge-puppeteer-macos-v2.0.719.zip on release 2.0.719 (uploaded separately). Bridge 1.9.217 (macOS) carries a pup seed whose updateManifestUrls.macos points here, so a Mac never follows the Windows line again; the plain updateManifestUrl and adom-bridge-puppeteer-manifest.json stay yours.

Verified on this Mac against the mac build on a spare port with an isolated HOME: tools/parity.py's golden sequence matches 55/56 on this Mac (the one drift is pup_screenshot on the CDP lane returning the base64 envelope instead of a file, and pup_screenshot_full_res on the CDP lane times out for a hidden window; the extension lane, the default, is clean), plus readiness, open (hidden, no focus steal), eval, screenshot, raise, lower, close, quit_idle_browsers. Workspace tests and tools/check-borders.sh green. Cold-start note: across 7 fresh-profile lane starts on this Mac, 2 (back to back, right after a killed previous lane) never had the extension call home within 2x25 s and the open failed; the next 5 came up in 4 to 10 s. Not reproduced since; worth a Mac-side eye on the lane start timeout.

Known gaps on mac, inert: AUMID / jump list / taskbar identity (logs desktop_register_app_identity refused), import.rs / assist.rs / activity.rs (cfg windows no-ops), shots_dir is exe-relative.

🤖 Generated with Claude Code

Diff Skip to comments (1)

--- a/adom-bridge-puppeteer-manifest-macos.json+++ b/adom-bridge-puppeteer-manifest-macos.json@@ -0,0 +1,17 @@+{+  "manifest_version": 1,+  "name": "puppeteer",+  "version": "2.0.719",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.719/adom-bridge-puppeteer-macos-v2.0.719.zip",+  "sha256": "39f3985aea93411477ec4418caab6ba544ff315b798e443c9bb678a1ca5268fc",+  "size": 8403909,+  "platform": "macos",+  "shell": "rust",+  "verbPrefixes": [+    "pup_",+    "browser_"+  ],+  "healthEndpoint": "/status",+  "statusVerb": "pup_readiness",+  "released_at": "2026-10-14T21:41:44Z"+}--- a/rust/bridge/src/main.rs+++ b/rust/bridge/src/main.rs@@ -1,215 +1,217 @@⋯ 65 unchanged lines ⋯     pup_core::set_log_file(pup_core::data_subdir("logs", state_dir.join("pup-logs")).join("pup.log"));     pup_core::log(format!("[pup] {} starting; log file {}", pup_core::version(), pup_core::log_file().map(|p| p.display().to_string()).unwrap_or_default()));     let ab: Arc<dyn pup_core::AbClient> = match Loopback::from_env() { Some(l) => Arc::new(l), None => { pup_core::log("[ab] ADOM_DIRECT_API_URL not set: desktop verbs degrade (hand-launched exe?)"); Arc::new(NoAb) } };+    let _ = pup_verbs::CAPTURE_HOST.set(h.clone());     let ctx = Arc::new(Ctx {         host: h.clone(), ab, settings: Arc::new(Settings::new(&state_dir)), installer: Arc::new(pup_browser::Installer::new()),         sessions: Arc::new(pup_session::Sessions::new(&pup_browser::sessions_dir(&*h))), browsers: Mutex::new(Default::default()), focus_emul: Mutex::new(Default::default()), pending_press: Mutex::new(Default::default()), browser_stall: Mutex::new(Default::default()), browser_idle_since: Mutex::new(Default::default()), tabstate: Ctx::new_tabstate(),⋯ 141 unchanged lines ⋯         });     } }+--- a/rust/browser/src/ext_assets.rs+++ b/rust/browser/src/ext_assets.rs@@ -1,71 +1,73 @@⋯ 19 unchanged lines ⋯         ("popup.js", include_bytes!("../../../src/extension/popup.js")),         ("satoshi-400.woff2", include_bytes!("../../../src/extension/satoshi-400.woff2")),         ("satoshi-500.woff2", include_bytes!("../../../src/extension/satoshi-500.woff2")),+        ("tile.js", include_bytes!("../../../src/extension/tile.js")),         ("worker.js", include_bytes!("../../../src/extension/worker.js")),     ]),     ("extension-identity", "pup-identity-extension", &[⋯ 43 unchanged lines ⋯         ("toolbar.json", include_bytes!("../../../src/extension-nbe/toolbar.json")),     ]), ];+--- a/rust/browser/src/lib.rs+++ b/rust/browser/src/lib.rs@@ -1,825 +1,826 @@⋯ 822 unchanged lines ⋯         let _ = fs::remove_dir_all(&tmp);     } }+--- a/rust/Cargo.lock+++ b/rust/Cargo.lock@@ -1,1649 +1,1650 @@⋯ 9 unchanged lines ⋯  [[package]] name = "adom-pup-bridge"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-browser",  "pup-cdp",⋯ 764 unchanged lines ⋯  [[package]] name = "pup-browser"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-core",  "pup-host",⋯ 5 unchanged lines ⋯  [[package]] name = "pup-cdp"-version = "2.0.721"+version = "2.0.719" dependencies = [  "serde_json",  "thiserror",⋯ 3 unchanged lines ⋯  [[package]] name = "pup-core"-version = "2.0.721"+version = "2.0.719" dependencies = [  "serde",  "serde_json",⋯ 3 unchanged lines ⋯  [[package]] name = "pup-dashboard"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-cdp",  "pup-core",⋯ 6 unchanged lines ⋯  [[package]] name = "pup-host"-version = "2.0.721"+version = "2.0.719" dependencies = [  "serde",  "serde_json",⋯ 2 unchanged lines ⋯  [[package]] name = "pup-host-mac"-version = "2.0.721"+version = "2.0.719" dependencies = [  "core-foundation",  "core-graphics",⋯ 9 unchanged lines ⋯  [[package]] name = "pup-host-win"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-core",  "pup-host",⋯ 3 unchanged lines ⋯  [[package]] name = "pup-identity"-version = "2.0.721"+version = "2.0.719" dependencies = [  "fontdue",  "image",⋯ 3 unchanged lines ⋯  [[package]] name = "pup-session"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-core",  "serde",⋯ 3 unchanged lines ⋯  [[package]] name = "pup-vault"-version = "2.0.721"+version = "2.0.719" dependencies = [  "pup-core",  "pup-host",⋯ 4 unchanged lines ⋯  [[package]] name = "pup-verbs"-version = "2.0.721"+version = "2.0.719" dependencies = [  "base64",  "png 0.17.16",⋯ 748 unchanged lines ⋯ dependencies = [  "zune-core", ]+--- a/rust/host-mac/Cargo.toml+++ b/rust/host-mac/Cargo.toml@@ -1,20 +1,21 @@⋯ 17 unchanged lines ⋯ objc2 = "0.6" objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSGeometry", "NSString"] } objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "libc", "NSRunningApplication", "NSWorkspace", "NSScreen", "NSResponder", "NSApplication"] }+--- a/rust/host-mac/examples/fg_probe.rs+++ b/rust/host-mac/examples/fg_probe.rs@@ -1,9 +1,10 @@⋯ 6 unchanged lines ⋯     let rows = h.windows_for_pids(&[], None);     println!("{} layer-0 windows; first 6: {:?}", rows.len(), rows.iter().take(6).map(|w| (w.hwnd, w.pid, w.class.clone(), w.visible)).collect::<Vec<_>>()); }+--- a/rust/host-mac/examples/hide_ctl.rs+++ b/rust/host-mac/examples/hide_ctl.rs@@ -1,8 +1,9 @@⋯ 5 unchanged lines ⋯     let r = match a[2].as_str() { "show" => h.set_visible_no_activate(hwnd, true), "hide" => h.send_to_bottom_no_activate(hwnd), "raise" => h.raise_window(hwnd), _ => false };     println!("{} {hwnd}: {r}; foreground now {}", a[2], h.foreground()); }+--- a/rust/host-mac/examples/live_probe.rs+++ b/rust/host-mac/examples/live_probe.rs@@ -1,36 +1,37 @@⋯ 33 unchanged lines ⋯     std::thread::sleep(Duration::from_millis(500));     println!("alive after kill: {} window alive: {}", h.alive(pid), h.is_alive(w.hwnd)); }+--- a/rust/host-mac/examples/procs.rs+++ b/rust/host-mac/examples/procs.rs@@ -1,2 +1,3 @@ use pup_host::HostProcess; fn main() { let h = pup_host_mac::MacHost; println!("running browsers: {:?}", h.running_browser_executables()); println!("pup browsers by profile: {:?}", h.pup_browser_processes().iter().take(3).collect::<Vec<_>>()); }+--- a/rust/host-mac/examples/title_probe.rs+++ b/rust/host-mac/examples/title_probe.rs@@ -1,8 +1,9 @@⋯ 5 unchanged lines ⋯     let titled = rows.iter().filter(|w| !w.title.is_empty()).count();     println!("{} windows, {} with titles; chromium: {:?}", rows.len(), titled, rows.iter().filter(|w| w.class.to_lowercase().contains("chrome")).map(|w| (w.pid, w.title.chars().take(30).collect::<String>())).take(5).collect::<Vec<_>>()); }+--- a/rust/host-mac/examples/wins.rs+++ b/rust/host-mac/examples/wins.rs@@ -1,2 +1,3 @@ use pup_host::HostWindows; fn main() { let pid: u32 = std::env::args().nth(1).unwrap().parse().unwrap(); for w in pup_host_mac::MacHost.windows_for_pids(&[pid], None) { println!("{:?}", w); } }+--- a/rust/host-mac/src/lib.rs+++ b/rust/host-mac/src/lib.rs@@ -1,722 +1,742 @@⋯ 21 unchanged lines ⋯ //! verbs tolerate (they match by pid + tag). `set_bounds_no_activate` needs Accessibility and //! returns false without it (the birth path moves windows over CDP first anyway). -// Unix only: the workspace build for the Windows exe compiles every member, and this crate's process code is POSIX.-#![cfg(unix)]- use pup_host::{HostPaths, HostProcess, HostSecrets, HostWindow, HostWindows}; use std::collections::HashMap; use std::path::{Path, PathBuf};⋯ 267 unchanged lines ⋯     }).collect() } +/// Apps capture_frames_begin unhid, for capture_frames_end to park again.+fn capture_unhidden() -> &'static Mutex<Vec<u32>> {+    static U: std::sync::OnceLock<Mutex<Vec<u32>>> = std::sync::OnceLock::new();+    U.get_or_init(|| Mutex::new(Vec::new()))+}+ /// Is the whole app that owns `pid` hidden (Cmd+H state)? None when the pid has no app. fn app_hidden(pid: u32) -> Option<bool> { imp::app_hidden(pid) } /// The user is using this app right now: it is the frontmost application.⋯ 87 unchanged lines ⋯     // Chrome on macOS destroys a closed window's NSWindow on a later UI turn; the window-server entry can     // outlive the close for minutes while the app idles hidden in the background.     fn closed_window_may_linger(&self) -> bool { true }+    // Unhide (no activation) every hidden app among pup's own browser processes, remember them, and hide them+    // again in capture_frames_end. The user's own Chrome is never touched (pup-profiles marker), and an app the+    // user has frontmost is left alone in both directions.+    fn capture_frames_begin(&self) -> bool {+        let mut apps: Vec<u32> = Vec::new();+        for (pid, _) in self.pup_browser_processes() {+            if app_hidden(pid) == Some(true) && !app_is_frontmost(pid) && !apps.contains(&pid) { apps.push(pid); }+        }+        if apps.is_empty() { return false; }+        for pid in &apps { imp::set_app_hidden(*pid, false); }+        // one compositor turn so the first capture after the unhide finds a frame+        std::thread::sleep(std::time::Duration::from_millis(150));+        *capture_unhidden().lock().unwrap() = apps;+        true+    }+    fn capture_frames_end(&self) {+        let apps = std::mem::take(&mut *capture_unhidden().lock().unwrap());+        for pid in apps { if !app_is_frontmost(pid) { imp::set_app_hidden(pid, true); } }+    }     // Windows: FlashWindowEx(FLASHW_STOP). No taskbar flash on macOS; nothing to stop.     fn stop_flash(&self, _hwnd: i64) -> bool { false }     // A GRANTED raise (the verbs gate it on foregroundReason): unhide + activate the owning app, then⋯ 143 unchanged lines ⋯     }     // Windows: powershell -File. macOS: /bin/sh, for the real-Chrome dmg installer script.     fn run_shell_script(&self, script: &str, timeout: std::time::Duration) -> String {-        // Own process group, so a timeout ends the whole pipeline: killing only the shell leaves its children holding-        // stdout open and the read below blocks until they finish (a piped script ran past its timeout).-        use std::os::unix::process::CommandExt;-        let mut child = match std::process::Command::new("/bin/sh").arg("-c").arg(script).process_group(0)+        let mut child = match std::process::Command::new("/bin/sh").arg("-c").arg(script)             .stdin(std::process::Stdio::null()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::null()).spawn() { Ok(c) => c, Err(_) => return String::new() };         let deadline = std::time::Instant::now() + timeout;         loop {             match child.try_wait() {                 Ok(Some(_)) => break,                 Ok(None) if std::time::Instant::now() < deadline => std::thread::sleep(std::time::Duration::from_millis(100)),-                _ => { unsafe { libc::kill(-(child.id() as i32), libc::SIGKILL); } let _ = child.kill(); let _ = child.wait(); break; }+                _ => { let _ = child.kill(); break; }             }         }         let mut out = String::new();⋯ 163 unchanged lines ⋯         assert!(h.secret_get(svc, acct).is_none());     } }+--- a/rust/host/src/lib.rs+++ b/rust/host/src/lib.rs@@ -1,189 +1,199 @@⋯ 91 unchanged lines ⋯     /// the lane browser sat idle in the background, and vanished at the next windows.create). When true, the     /// close path takes the lane's own word that the window is gone instead of waiting for the handle to die.     fn closed_window_may_linger(&self) -> bool { false }+    /// Make pup's PARKED browsers produce frames for a capture. Windows: a bottomed window keeps painting+    /// (occlusion calculation is off), nothing to do. macOS: a window of a hidden app stops rendering after a+    /// while and Page.captureScreenshot / captureVisibleTab then hang (measured 2026-10-14: every capture of a+    /// window hidden for a minute timed out; the same window answered in under a second once its app was+    /// unhidden behind the user's app). So the host unhides pup's own hidden browsers WITHOUT activating them+    /// (they come back at their old z positions, behind the user's work), and `capture_frames_end` hides them+    /// again. Returns true when something was unhidden, so the caller knows to call end.+    fn capture_frames_begin(&self) -> bool { false }+    fn capture_frames_end(&self) {} }  pub trait HostProcess: Send + Sync {⋯ 89 unchanged lines ⋯ /// The whole host, as the binary hands it to every crate. pub trait Host: HostWindows + HostProcess + HostPaths + HostSecrets {} impl<T: HostWindows + HostProcess + HostPaths + HostSecrets> Host for T {}+--- a/rust/verbs/src/extdrive.rs+++ b/rust/verbs/src/extdrive.rs@@ -1,915 +1,916 @@⋯ 450 unchanged lines ⋯     let mut c = json!({ "action": "navigate", "url": args.get("url").and_then(|u| u.as_str()).unwrap_or("about:blank") });     aim(&mut c);     if let Some(t) = args.get("tabId") { c["tabId"] = t.clone(); }-    match run(c, Duration::from_secs(60)) {+    match crate::transport::with_frames(|| run(c, Duration::from_secs(60))) {         Ok(r) => { pin(&r); ok(json!({ "url": r["url"], "title": r["title"], "tabId": r["tabId"], "_hint": "Navigated through the extension (no CDP). Read it with pup_eval." })) }         Err(e) => e,     }⋯ 455 unchanged lines ⋯         assert!(!super::is_blank_or_birth("https://example.com/birth.html")); assert!(!super::is_blank_or_birth("https://wiki.adom.inc/adom/pup-bridge"));     } }+--- a/rust/verbs/src/extras.rs+++ b/rust/verbs/src/extras.rs@@ -1,413 +1,414 @@⋯ 37 unchanged lines ⋯     std::thread::sleep(Duration::from_millis(120));     let pad = args["padding"].as_f64().unwrap_or(0.0).max(0.0);     let clip = json!({ "x": (rect["x"].as_f64().unwrap_or(0.0) - pad).max(0.0), "y": (rect["y"].as_f64().unwrap_or(0.0) - pad).max(0.0), "width": (rect["w"].as_f64().unwrap_or(1.0) + pad * 2.0).max(1.0), "height": (rect["h"].as_f64().unwrap_or(1.0) + pad * 2.0).max(1.0), "scale": 1 });-    let r = match b.cdp.send(Some(&t.cdp_session), "Page.captureScreenshot", json!({ "format": "png", "captureBeyondViewport": true, "clip": clip }), Duration::from_secs(15)) { Ok(r) => r, Err(e) => return err("screenshot_failed", e.to_string(), None) };+    let r = match crate::transport::with_frames(|| b.cdp.send(Some(&t.cdp_session), "Page.captureScreenshot", json!({ "format": "png", "captureBeyondViewport": true, "clip": clip }), Duration::from_secs(15))) { Ok(r) => r, Err(e) => return err("screenshot_failed", e.to_string(), None) };     let png = match r["data"].as_str().and_then(|d| pup_cdp::base64_decode(d).ok()) { Some(p) => p, None => return err("screenshot_failed", "no image data", None) };     let (buf, w, h, ow, oh, resized) = match crate::image::fit_png(&png, crate::MAX_SCREENSHOT_DIM) { Ok(x) => x, Err(e) => return err("screenshot_decode_failed", e, None) };     let path = ctx.shots_dir().join(format!("pup-el-{}.png", pup_core::now_ms()));⋯ 366 unchanged lines ⋯     ok(json!({ "success": true, "dryRun": dry, "cacheDir": norm(&root), "removed": removed, "kept": kept, "failed": failed, "freedMB": mb,         "_hint": format!("{} {} MB of superseded browser builds and leftover downloads{}. Kept the build pup drives and anything in use. pup runs this a minute after every boot (2.0.677).", if dry { "Would free" } else { "Freed" }, mb, if dry { " (dry run: pass dryRun:false to delete)" } else { "" }) })) }+--- a/rust/verbs/src/google.rs+++ b/rust/verbs/src/google.rs@@ -1,473 +1,474 @@⋯ 470 unchanged lines ⋯     }     finish(ctx, caller, &sid, Some(&account), &cont, st) }+--- a/rust/verbs/src/identity.rs+++ b/rust/verbs/src/identity.rs@@ -1,398 +1,399 @@⋯ 395 unchanged lines ⋯     }).collect();     pup_core::ok(json!({ "mode": mode(ctx), "windows": rows, "ok": true, "findings": findings, "_hint": if findings.is_empty() { "What pup believes each window is stamped with, read back from Windows." } else { "One or more windows cannot carry pup identity; see findings." } })) }+--- a/rust/verbs/src/lib.rs+++ b/rust/verbs/src/lib.rs@@ -1,2191 +1,2195 @@⋯ 523 unchanged lines ⋯ pub const SHOT_KEEP_DAYS: u64 = 14; pub const SHOT_KEEP_BYTES: u64 = 1_000_000_000; pub const REC_KEEP_DAYS: u64 = 30; pub const REC_KEEP_BYTES: u64 = 3_000_000_000; +/// The host, reachable from code that has no Ctx (the transport's capture paths). Set once by the binary.+pub static CAPTURE_HOST: std::sync::OnceLock<Arc<dyn Host>> = std::sync::OnceLock::new();+ impl Ctx {     pub fn arc(&self) -> Option<Arc<Ctx>> { self.self_ref.lock().unwrap().upgrade() }     pub fn new_tabstate() -> events::TabStates { Arc::new(Mutex::new(HashMap::new())) }⋯ 1659 unchanged lines ⋯  #[allow(dead_code)] fn _unused(_: Value) -> Value { blocked_on_user(false, "", "", json!({})) }+--- a/rust/verbs/src/record.rs+++ b/rust/verbs/src/record.rs@@ -1,351 +1,352 @@⋯ 348 unchanged lines ⋯     let summary = json!({ "clipCount": r.clips.len(), "totalSizeKB": r.clips.iter().map(|c| c["sizeKB"].as_u64().unwrap_or(0)).sum::<u64>(), "reason": r.reason, "clips": r.clips });     ok(json!({ "success": true, "_hint": SCREEN_HINT, "output": json!({ "ok": true, "sessionSummary": summary }).to_string() })) }+--- a/rust/verbs/src/transport.rs+++ b/rust/verbs/src/transport.rs@@ -1,316 +1,328 @@⋯ 111 unchanged lines ⋯     } } +/// Run a capture with pup's parked browsers producing frames (Host::capture_frames_begin/end; a no-op on+/// Windows, an unhide-behind-the-user on macOS). Every Page.captureScreenshot / captureVisibleTab goes through+/// here so a window parked for a while still answers.+pub fn with_frames<T>(f: impl FnOnce() -> T) -> T {+    let host = crate::CAPTURE_HOST.get().cloned();+    let unhid = host.as_ref().map(|h| h.capture_frames_begin()).unwrap_or(false);+    let r = f();+    if unhid { if let Some(h) = host { h.capture_frames_end(); } }+    r+}+ pub fn screenshot_png(lane: &Lane, t: &Tab, full: bool) -> Result<Vec<u8>, Value> {-    match lane {+    with_frames(|| match lane {         Lane::Cdp(b) => b.cdp.screenshot_png(&t.cdp_session, full, Duration::from_secs(20)).map_err(|e| err("screenshot_timeout", format!("page.screenshot: {e}"), Some("The page did not answer the capture. Reload it, or pup_eval document.readyState."))),         Lane::Ext => {             let r = run(json!({ "action": "screenshot", "tabId": ext_id(t) }), 60)?;⋯ 1 unchanged line ⋯             let b64 = r["dataUrl"].as_str().and_then(|d| d.split(',').nth(1)).unwrap_or("");             pup_cdp::base64_decode(b64).map_err(|e| err("screenshot_decode_failed", e.to_string(), None))         }-    }+    }) }  /// Open a tab in the window. Ext: in the SAME Chrome window (windowId) the session was born with.⋯ 168 unchanged lines ⋯ /// A PNG of the viewport, optionally clipped to a CSS-pixel rect. CDP clips in the capture; the /// extension lane captures the visible tab (device pixels) and the clip is applied in the binary. pub fn capture_viewport_png(lane: &Lane, t: &Tab, clip_css: Option<(f64, f64, f64, f64)>, dpr: f64) -> Result<Vec<u8>, Value> {-    match lane {+    with_frames(|| match lane {         Lane::Cdp(b) => {             let mut p = json!({ "format": "png", "captureBeyondViewport": false });             if let Some((x, y, w, h)) = clip_css { p["clip"] = json!({ "x": x.max(0.0), "y": y.max(0.0), "width": w.max(1.0), "height": h.max(1.0), "scale": 1 }); }⋯ 9 unchanged lines ⋯                 Some((x, y, w, h)) => pup_identity::badge::crop_png(&bytes, (x.max(0.0) * dpr).round() as u32, (y.max(0.0) * dpr).round() as u32, ((w.max(1.0) * dpr).round() as u32).max(1), ((h.max(1.0) * dpr).round() as u32).max(1)).ok_or_else(|| err("capture_failed", "could not decode the captured viewport".to_string(), None)),             }         }-    }+    }) }+--- a/rust/verbs/src/window.rs+++ b/rust/verbs/src/window.rs@@ -1,130 +1,131 @@⋯ 127 unchanged lines ⋯     touch(ctx, &s.id, args.get("note").and_then(|n| n.as_str()));     ok(json!({ "success": true, "output": json!({ "ok": true, "sessionId": s.id, "mode": mode, "painted": applied, "note": args.get("note").and_then(|n| n.as_str()) }).to_string(), "sessionId": s.id, "window": s.id, "aiThread": s.owner, "mode": mode, "applied": applied, "_hint": "Taskbar progress painted on this window's button. Clear it with mode:\"off\" when the long task ends." })) }+--- a/src/extension/worker.js+++ b/src/extension/worker.js@@ -1,639 +1,640 @@⋯ 636 unchanged lines ⋯ chrome.alarms.create('pup-extdrive', { periodInMinutes: 0.5 }); chrome.alarms.onAlarm.addListener((a) => { if (a.name === 'pup-extdrive') pupExtPoll(); }); pupExtPoll();+

Comments

John Lauer ·

Thanks Kyle. The capture guard landed in 2.0.722, on insiders now. It passed the gates on arav-rog and ConfRoomROG: smoke all pass, foreground pass, parity 55/56, jump list 11/14 (baseline).

What I took: only the capture guard, which is the one change that is new since #3:

  • Host::capture_frames_begin/end
  • transport::with_frames and the wraps in transport, extras and extdrive
  • CAPTURE_HOST and its set in main.rs
  • the updated macOS manifest

Why not the whole PR: #4 is built on #3 as you submitted it, before the two fixes I made in 2.0.720. Taking it whole would have undone both:

  1. #![cfg(unix)] on pup-host-mac. Without it, the Windows workspace build fails to compile.
  2. process_group(0) plus a group kill in run_shell_script, so a timeout ends the whole pipeline.

The page repo source has both fixes plus your guard, so please rebase on it before your next change. I'm closing this PR as landed.

Still not in either PR: tools/rust-build-macos.sh. The description lists it, but the diff does not contain it.

One thing to check on the Mac side: capture_unhidden() is a single global list. Two captures at the same time (two windows screenshotting at once) would overwrite each other's list, and one set of apps could stay unhidden. A counter or a per-call list would avoid that.

Your remaining steps:

  • Upload adom-bridge-puppeteer-macos-v2.0.719.zip. That URL still answers 404.
  • Push the macOS manifest.
  • Publish Bridge 1.9.217.

Log in to comment.