This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

workspace-manager: don't restore Firefox windows when sway tree query fails

Under load, the swaymsg get_tree subprocess could time out, the host
returned an empty windows list, and the extension treated that as "no
Firefox windows anywhere" — restoring a window for a workspace that
already had one. Distinguish query failure from a genuinely empty
result and skip restoration when state is unknown.

+71 -44
+18 -7
tools/workspace-manager/workspace-tabs-host.py
··· 38 38 sys.stdout.buffer.flush() 39 39 40 40 41 - def run(cmd: list[str]) -> str: 41 + def run(cmd: list[str], timeout: float = 2) -> str: 42 42 """Run a command and return stripped stdout, or empty string on failure.""" 43 43 try: 44 - r = subprocess.run(cmd, capture_output=True, text=True, timeout=2) # noqa: S603 44 + r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) # noqa: S603 45 45 except (OSError, subprocess.TimeoutExpired): 46 46 return "" 47 47 return r.stdout.strip() if r.returncode == 0 else "" ··· 71 71 return {} 72 72 73 73 74 - def get_firefox_windows() -> list[dict[str, str]]: 75 - """Get all Firefox windows and their workspaces from sway tree.""" 74 + def get_firefox_windows() -> list[dict[str, str]] | None: 75 + """Get all Firefox windows and their workspaces from sway tree. 76 + 77 + Returns None if the sway tree could not be queried (so callers can 78 + distinguish "no Firefox windows" from "we don't know"). Otherwise 79 + returns the list of Firefox windows, possibly empty. 80 + """ 81 + raw = run(["swaymsg", "-t", "get_tree", "-r"], timeout=5) 82 + if not raw: 83 + return None 76 84 try: 77 - tree: dict[str, Any] = json.loads(run(["swaymsg", "-t", "get_tree", "-r"])) 85 + tree: dict[str, Any] = json.loads(raw) 78 86 except (json.JSONDecodeError, ValueError): 79 - return [] 87 + return None 80 88 81 89 results: list[dict[str, str]] = [] 82 90 ··· 120 128 run(["swaymsg", "move", "container", "to", "workspace", msg.get("name", "")]) 121 129 return {"id": mid, "ok": True} 122 130 if cmd == "map_windows": 123 - return {"id": mid, "windows": get_firefox_windows()} 131 + windows = get_firefox_windows() 132 + if windows is None: 133 + return {"id": mid, "error": "Failed to query sway tree"} 134 + return {"id": mid, "windows": windows} 124 135 if cmd == "save_tabs": 125 136 workspace = msg.get("workspace", "") 126 137 tabs = msg.get("tabs", [])
+53 -37
tools/workspace-manager/firefox-extension/background.js
··· 88 88 89 89 // ── Firefox window <-> workspace mapping ──────────────────── 90 90 91 + // Throws on query failure so callers can distinguish "no Firefox windows" 92 + // from "we don't know" — a silent empty result here under load caused 93 + // duplicate window restoration. 91 94 async function getFirefoxWindowMappings() { 92 - try { 93 - const resp = await sendNative("map_windows"); 94 - if (!resp.windows) return new Map(); 95 + const resp = await sendNative("map_windows"); 96 + if (resp.error) throw new Error(resp.error); 97 + if (!resp.windows) throw new Error("map_windows: no windows field"); 95 98 96 - const allWindows = await browser.windows.getAll({ populate: true }); 97 - const result = new Map(); 98 - const matched = new Set(); 99 + const allWindows = await browser.windows.getAll({ populate: true }); 100 + const result = new Map(); 101 + const matched = new Set(); 99 102 100 - for (const mapping of resp.windows) { 101 - const swayTitle = mapping.windowTitle || ""; 102 - for (const win of allWindows) { 103 - if (matched.has(win.id) || win.id === anchorWindowId) continue; 104 - const activeTab = win.tabs && win.tabs.find((t) => t.active); 105 - if ( 106 - activeTab && 107 - activeTab.title && 108 - swayTitle.startsWith(activeTab.title) 109 - ) { 110 - result.set(mapping.workspace, win.id); 111 - matched.add(win.id); 112 - break; 113 - } 103 + for (const mapping of resp.windows) { 104 + const swayTitle = mapping.windowTitle || ""; 105 + for (const win of allWindows) { 106 + if (matched.has(win.id) || win.id === anchorWindowId) continue; 107 + const activeTab = win.tabs && win.tabs.find((t) => t.active); 108 + if ( 109 + activeTab && 110 + activeTab.title && 111 + swayTitle.startsWith(activeTab.title) 112 + ) { 113 + result.set(mapping.workspace, win.id); 114 + matched.add(win.id); 115 + break; 114 116 } 115 117 } 116 - return result; 117 - } catch (e) { 118 - return new Map(); 119 118 } 119 + return result; 120 120 } 121 121 122 122 async function getWorkspacesWithFirefox() { 123 - try { 124 - const resp = await sendNative("map_windows"); 125 - if (!resp.windows) return new Set(); 126 - return new Set(resp.windows.map((w) => w.workspace)); 127 - } catch (e) { 128 - return new Set(); 129 - } 123 + const resp = await sendNative("map_windows"); 124 + if (resp.error) throw new Error(resp.error); 125 + if (!resp.windows) throw new Error("map_windows: no windows field"); 126 + return new Set(resp.windows.map((w) => w.workspace)); 130 127 } 131 128 132 129 // ── Anchor window (keeps Firefox alive) ───────────────────── ··· 173 170 174 171 // ── Tab saving (to workspace file) ───────────────────────── 175 172 176 - async function saveTabsForWorkspace(workspace) { 173 + async function saveTabsForWorkspace(workspace, mappings) { 177 174 if (!workspace) return; 178 175 179 - const mappings = await getFirefoxWindowMappings(); 180 176 const windowId = mappings.get(workspace); 181 177 if (!windowId) return; 182 178 ··· 197 193 } 198 194 199 195 async function saveAllTabs() { 200 - const mappings = await getFirefoxWindowMappings(); 196 + let mappings; 197 + try { 198 + mappings = await getFirefoxWindowMappings(); 199 + } catch (e) { 200 + console.warn("Skipping save — could not query window mappings:", e); 201 + return; 202 + } 201 203 for (const [workspace] of mappings) { 202 - await saveTabsForWorkspace(workspace); 204 + await saveTabsForWorkspace(workspace, mappings); 203 205 } 204 206 } 205 207 ··· 277 279 // Don't restore tabs during startup — only after user switches workspace 278 280 if (!initialized) return; 279 281 280 - // Check if this workspace already has a Firefox window 281 - const withFirefox = await getWorkspacesWithFirefox(); 282 + // Check if this workspace already has a Firefox window. If we can't tell 283 + // (e.g. swaymsg timed out under load), skip — restoring blindly would 284 + // duplicate an existing window. 285 + let withFirefox; 286 + try { 287 + withFirefox = await getWorkspacesWithFirefox(); 288 + } catch (e) { 289 + console.warn(`Skipping restore for ${workspace} — query failed:`, e); 290 + return; 291 + } 282 292 if (withFirefox.has(workspace)) return; 283 293 284 294 // No Firefox window — restore saved tabs ··· 462 472 463 473 // Restore tabs for the initial workspace if it has saved tabs from a previous session 464 474 if (lastWorkspace) { 465 - const withFirefox = await getWorkspacesWithFirefox(); 475 + let withFirefox; 476 + try { 477 + withFirefox = await getWorkspacesWithFirefox(); 478 + } catch (e) { 479 + console.warn("Skipping initial restore — query failed:", e); 480 + return; 481 + } 466 482 if (!withFirefox.has(lastWorkspace)) { 467 483 const resp = await sendNative("get_tabs", { workspace: lastWorkspace }); 468 484 const tabs = resp.tabs || [];