← Commit history

v2.0.668 (insiders): No more taskbar-identity residue: a window's AUMID is unregistered when it closes; a sweep a minute after boot (and pup_sweep_identities on demand) unregisters every thread, per-window and category identity no live window holds, deletes dead shortcuts in pup's Start Menu folder and stale tile/icon generations. arav-rog had 116 dead keys and 20 dead shortcuts (John, 2026-09-20).

John Lauer ·e234b91620 ·1mo ago ·parent 4931191
7 files changed +100−14
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.665"+version = "2.0.668" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.665"+version = "2.0.668" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.665"+version = "2.0.668" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.665"+version = "2.0.668" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.665"+version = "2.0.668" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.665"+version = "2.0.668" 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.665"+version = "2.0.668" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+1
@@ -118,6 +118,7 @@ fn main() {             pup_verbs::highfps::start_loop(c.clone());             pup_verbs::extras::start_identity_audit(c.clone());             pup_verbs::extras::start_profile_janitor(c.clone());+            { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot"); }); }             if let Ok(v) = c.ab.call("bridge_info", json!({}), &Caller::default(), Duration::from_secs(4)) { *c.ad_version.lock().unwrap() = v.get("version").and_then(|x| x.as_str()).map(|s| s.to_string()); }         });     }
rust/host-win/src/lib.rs+26
@@ -143,6 +143,32 @@ mod imp {                 Some(shown)             }         }+        fn list_aumids(&self, prefix: &str) -> Vec<String> {+            extern "system" {+                fn RegOpenKeyExW(hkey: isize, sub: *const u16, opts: u32, sam: u32, out: *mut isize) -> i32;+                fn RegEnumKeyExW(hkey: isize, idx: u32, name: *mut u16, name_len: *mut u32, res: *const u32, cls: *mut u16, cls_len: *mut u32, ft: *mut c_void) -> i32;+                fn RegCloseKey(hkey: isize) -> i32;+            }+            const HKEY_CURRENT_USER: isize = 0x80000001u32 as i32 as isize;+            const KEY_READ: u32 = 0x20019;+            let sub: Vec<u16> = "Software\\Classes\\AppUserModelId".encode_utf16().chain(std::iter::once(0)).collect();+            let mut out = Vec::new();+            unsafe {+                let mut h: isize = 0;+                if RegOpenKeyExW(HKEY_CURRENT_USER, sub.as_ptr(), 0, KEY_READ, &mut h) != 0 { return out; }+                let mut i = 0u32;+                loop {+                    let mut buf = [0u16; 512]; let mut len = buf.len() as u32;+                    let r = RegEnumKeyExW(h, i, buf.as_mut_ptr(), &mut len, std::ptr::null(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());+                    if r != 0 { break; }+                    let name = String::from_utf16_lossy(&buf[..len as usize]);+                    if name.starts_with(prefix) { out.push(name); }+                    i += 1;+                }+                RegCloseKey(h);+            }+            out+        }         fn stop_flash(&self, hwnd: i64) -> bool {             #[repr(C)] struct FLASHWINFO { cb_size: u32, hwnd: HWND, flags: u32, count: u32, timeout: u32 }             extern "system" { fn FlashWindowEx(p: *const FLASHWINFO) -> i32; }
rust/host/src/lib.rs+4
@@ -72,6 +72,10 @@ pub trait HostWindows: Send + Sync {     /// the ms until the first show, None if it never showed within `timeout_ms`. A per-poll EnumWindows was     /// 22 ms behind Chrome's first show (z-watch, 2.0.639); the compositor paints a frame in 16.     fn park_on_first_show(&self, hwnd: i64, timeout_ms: u64) -> Option<u64> { let _ = (hwnd, timeout_ms); None }+    /// 2.0.666: the subkey names under HKCU\Software\Classes\AppUserModelId that start with `prefix`+    /// (pup's own identities), so a boot sweep can unregister the ones no live window holds. John found+    /// 116 dead thread identities on arav-rog on 2026-09-20 ("are you still leaving aumid icon residue?").+    fn list_aumids(&self, prefix: &str) -> Vec<String> { let _ = prefix; Vec::new() }     /// Milliseconds since the last keyboard/mouse input (GetLastInputInfo); None where unknown.     fn idle_ms(&self) -> Option<u64> { None }     /// The primary monitor's work area (x, y, w, h), excluding the taskbar.
rust/verbs/src/identity.rs+53
@@ -54,6 +54,59 @@ pub fn boot(ctx: &Ctx) {     } } +/// 2.0.666: unregister every pup identity no live window holds, and drop tile/icon generations nothing+/// references. Until now only pup_uninstall ever called desktop_unregister_app_identity, so every thread+/// that ever opened a window left its taskbar identity registry key (and, with aumidShortcut on, a Start Menu+/// entry Windows lists as an App): 116 keys on arav-rog by 2026-09-20. Runs a minute after boot (after+/// recovery has re-listed the windows it brings back) and on every window close for that window's identity.+pub fn sweep_dead_identities(ctx: &Ctx, why: &str) -> Value {+    let live: std::collections::HashSet<String> = ctx.sessions.all().iter().filter_map(|s| s.app_id.clone()).collect();+    let mine = [format!("{}.thread.", id::APP_ID), format!("{}.cat.", id::APP_ID), format!("{}.w.", id::APP_ID)];+    let mut removed = Vec::new(); let mut failed = 0usize;+    for k in ctx.host.list_aumids(&format!("{}.", id::APP_ID)) {+        if live.contains(&k) || !mine.iter().any(|p| k.starts_with(p)) { continue; }+        match ctx.ab.call("desktop_unregister_app_identity", json!({ "appId": k, "anchorFolder": ANCHOR_FOLDER }), &internal_caller(), AB_T) { Ok(_) => removed.push(k), Err(_) => failed += 1 }+    }+    // tile / icon generations: keep the current one and any an active identity still points at+    let mut dirs = Vec::new();+    for (root, cur) in [("pup-thread-tiles", id::TILE_GEN), ("pup-icons", id::ICON_GEN)] {+        let Ok(rd) = std::fs::read_dir(ctx.host.temp_dir().join(root)) else { continue };+        for e in rd.flatten() {+            let name = e.file_name().to_string_lossy().to_string();+            if name == cur { continue; }+            let in_use = live.iter().any(|a| a.contains(&format!(".{name}")) || a.ends_with(&name));+            if in_use { continue; }+            if std::fs::remove_dir_all(e.path()).is_ok() { dirs.push(format!("{root}/{name}")); }+        }+    }+    // the anchor folder is pup's own: every .lnk in it that no live window's thread or id matches is dead+    // (ab's unregister only removes the anchor of the key it is given; keys removed by older builds left theirs)+    let anchor_on = ctx.settings.get("aumidShortcut", "off") == "on";+    let mut lnks = Vec::new();+    if let Ok(appdata) = std::env::var("APPDATA") {+        let folder = PathBuf::from(appdata).join("Microsoft").join("Windows").join("Start Menu").join("Programs").join(ANCHOR_FOLDER);+        let keep: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| vec![s.owner.clone().unwrap_or_default(), s.id.clone()]).collect();+        if let Ok(rd) = std::fs::read_dir(&folder) {+            for e in rd.flatten() {+                let name = e.file_name().to_string_lossy().to_string();+                let Some(stem) = name.strip_suffix(".lnk") else { continue };+                if keep.contains(stem) || (anchor_on && stem == id::DISPLAY_NAME) { continue; }+                if std::fs::remove_file(e.path()).is_ok() { lnks.push(stem.to_string()); }+            }+            if std::fs::read_dir(&folder).map(|mut r| r.next().is_none()).unwrap_or(false) { let _ = std::fs::remove_dir(&folder); }+        }+    }+    if !removed.is_empty() || !dirs.is_empty() || !lnks.is_empty() || failed > 0 { pup_core::log(format!("[identity] sweep ({why}): unregistered {} dead identit{} ({failed} failed), removed {} dead Start Menu shortcut(s), {} stale icon generation(s){}", removed.len(), if removed.len() == 1 { "y" } else { "ies" }, lnks.len(), dirs.len(), if dirs.is_empty() { String::new() } else { format!(": {}", dirs.join(", ")) })); }+    json!({ "unregistered": removed.len(), "failed": failed, "shortcutsRemoved": lnks, "iconDirsRemoved": dirs, "live": live.len(), "sample": removed.iter().take(10).collect::<Vec<_>>() })+}++/// Called after a window closed: its identity goes unless another live window shares it (category+/// AUMIDs are shared; thread AUMIDs are per thread and other windows of the thread hold the same one).+pub fn release_identity(ctx: &Ctx, app_id: &str) {+    if app_id == id::APP_ID || ctx.sessions.all().iter().any(|s| s.app_id.as_deref() == Some(app_id)) { return; }+    let _ = ctx.ab.call("desktop_unregister_app_identity", json!({ "appId": app_id, "anchorFolder": ANCHOR_FOLDER }), &internal_caller(), AB_T);+}+ pub fn mode(ctx: &Ctx) -> String {     ctx.settings.all().get("taskbarMode").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| if ctx.settings.get("aumidIcons", "on") != "on" { "plain".into() } else if ctx.settings.get("threadTiles", "on") == "on" { "tiles".into() } else { "aumid".into() }) }
rust/verbs/src/lib.rs+3−1
@@ -535,7 +535,7 @@ pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewar     "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard",     "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click",     "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",-    "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];+    "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];  pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {     let t0 = Instant::now();@@ -635,6 +635,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {         "pup_deps" => extras::deps(ctx),         "pup_install_toolbar" => extras::install_toolbar(ctx),         "pup_reap_profiles" => extras::reap_profiles(ctx, args),+        "pup_sweep_identities" => { let r = identity::sweep_dead_identities(ctx, "verb"); ok(pup_core::merge(r, json!({ "_hint": "Unregistered every pup taskbar identity (its registry key and any Start Menu anchor) that no live window holds, and removed icon generations nothing references. pup runs this a minute after every boot and releases each window's identity on close (2.0.666)." }))) },         "pup_uninstall" => extras::uninstall(ctx, args),         "pup_move_to_workspace" => extras::move_to_workspace(ctx, caller, args),         "pup_assisted_click" => assist::assisted_click(ctx, caller, args),@@ -2013,6 +2014,7 @@ pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {             continue;         }         ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone());   // again: a late save loses+        if let Some(a) = s.app_id.as_deref() { identity::release_identity(ctx, a); }   // 2.0.666: no dead AUMID left behind         // last ext window: let the lane browser go, the same rule the CDP browsers follow below         if transport::is_ext(&s) && !ctx.sessions.all().iter().any(|x| transport::is_ext(x) && x.profile == s.profile) {             // 2.0.659: the LAST window of this profile's lane ends that lane's browser (the default lane's rule unchanged)