git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit0d36b74397c454141c165442f6d1000e357f4f23
parent93284477cc
authorLucas Galante <[email protected]>
date2026-08-14 02:00
fix: identify the tracked cloud popup by exe, not by comm

`CloudPopupTracker::running_pid` guarded against pid reuse by comparing
`/proc/<pid>/comm` to "cce-cloud". comm is capped at 15 characters, so it
silently truncates for over half the binaries in this workspace — all three
`cce-keyring-unlock*` collapse onto the same string, and cce-status-interface
reads as "cce-status-inte". "cce-cloud" is nine characters, which is the only
reason the comparison worked; renaming the popup binary to anything longer
would have made this always return None, quietly convincing the tracker that
no popup is ever open (so it would neither close nor supersede its own).

Compare the exe link's basename instead, which is exact at any length. That
needs the kernel's `" (deleted)"` marker stripped: `ccebuild install` unlinks
before writing, so any popup that outlives a reinstall reads as
`cce-cloud (deleted)` — the one respect in which comm was the more forgiving
of the two, and a regression this would have introduced if taken naively.

Two tests: the deleted-marker stripping, and a pid that is definitely alive
but is not a cce-cloud (the test runner's own), which shows the gate is
identity rather than mere liveness — something the existing state-machine
tests cannot show, since they use pids with no process behind them at all.

Same root cause as ccebuild's argv prefilter (cce-compositor@d565a17):
identify a process by what it is, not by a name that depends on how it was
started or how long it is.

Co-Authored-By: Claude <[email protected]>

 src/process.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 48 insertions(+), 2 deletions(-)

diff --git a/src/process.rs b/src/process.rs
index 4e5ad27..e211cce 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -91,6 +91,13 @@ pub struct CloudPopupTracker {
     active_source: Option<String>,
 }
 
+/// Basename of a `/proc/<pid>/exe` link, with the `" (deleted)"` marker the
+/// kernel appends once the binary has been unlinked stripped off.
+fn exe_basename(exe: &std::path::Path) -> Option<&str> {
+    let name = exe.file_name()?.to_str()?;
+    Some(name.strip_suffix(" (deleted)").unwrap_or(name))
+}
+
 impl CloudPopupTracker {
     pub fn new() -> Self {
         Self::default()
@@ -98,10 +105,22 @@ impl CloudPopupTracker {
 
     /// The tracked popup's pid, if that process is still alive and still a
     /// `cce-cloud` (guards against pid reuse).
+    ///
+    /// Identity comes from the exe link, not `/proc/<pid>/comm`: comm is capped
+    /// at 15 characters, so it silently truncates for over half the cce
+    /// binaries (all three `cce-keyring-unlock*` collapse onto one string).
+    /// `cce-cloud` fits today, which is the only reason a comm comparison
+    /// worked — a rename to anything longer would have quietly made this always
+    /// return `None`, leaving the tracker convinced no popup is ever open.
+    ///
+    /// The exe link needs the `" (deleted)"` suffix stripped: `ccebuild install`
+    /// unlinks before writing, so every popup running across a reinstall reads
+    /// as `cce-cloud (deleted)` — the one case where comm was the more forgiving
+    /// of the two.
     fn running_pid(&self) -> Option<u32> {
         let pid = self.active_pid?;
-        let comm = std::fs::read_to_string(format!("/proc/{}/comm", pid)).ok()?;
-        (comm.trim() == "cce-cloud").then_some(pid)
+        let exe = std::fs::read_link(format!("/proc/{}/exe", pid)).ok()?;
+        (exe_basename(&exe) == Some("cce-cloud")).then_some(pid)
     }
 
     /// Whether the tracked popup is currently running.
@@ -273,6 +292,33 @@ mod tests {
         cleanup_spawned_processes();
     }
 
+    #[test]
+    fn exe_basename_strips_the_deleted_marker() {
+        use std::path::Path;
+        assert_eq!(exe_basename(Path::new("/home/u/.local/bin/cce-cloud")), Some("cce-cloud"));
+        // `ccebuild install` unlinks before writing, so a popup that outlives a
+        // reinstall reads like this — still a cce-cloud, and the tracker must
+        // keep recognizing it or it loses the ability to close its own popup.
+        assert_eq!(
+            exe_basename(Path::new("/home/u/.local/bin/cce-cloud (deleted)")),
+            Some("cce-cloud")
+        );
+        assert_eq!(exe_basename(Path::new("/usr/bin/foot")), Some("foot"));
+    }
+
+    #[test]
+    fn running_pid_rejects_a_live_process_that_is_not_cce_cloud() {
+        // The state-machine tests below use pids that are almost certainly
+        // dead, so `running_pid` returns None for want of a process at all.
+        // This one adopts a pid that definitely IS alive — the test runner —
+        // to show the gate is identity, not mere liveness. (Only `is_open` is
+        // called here: it never signals, so the runner is in no danger.)
+        let mut t = CloudPopupTracker::new();
+        assert_eq!(t.click("layout"), CloudPopupClick::Open);
+        t.on_spawned(std::process::id(), "layout");
+        assert!(!t.is_open(), "a live non-cce-cloud pid must not count as an open popup");
+    }
+
     // --- CloudPopupTracker state machine ---
     // (pids here are never live cce-cloud processes, so running_pid() is
     // always None — these tests cover the pending/source transitions; the