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

commit760da92561135f60ea0b30a3b283bad40a8eceb6
parentcbe8b7f46a
authorLucas Galante <[email protected]>
date2026-09-03 21:27
config: get_app_name strips the kernel's " (deleted)" marker

/proc/self/exe reads "<path> (deleted)" once the binary on disk has been
replaced, and `ccebuild install` unlinks before writing. get_app_name()
re-derives the name from that link on every call, so a still-running
client resolved its per-app override to
"~/.config/cce/<name> (deleted)/config.kdl" on its next config reload and
silently lost the whole file. Seen 2026-09-03: a dependents sweep
reinstalled cce-status-interface under the running bar, and the next
config.kdl save dropped its `module { }` block — droplet bubbles reverted
to the plain boxes, height fell to the shared bar_height — until relogin.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AkxxSogNMxUr8zBM2RL35C

 src/config.rs | 32 ++++++++++++++++++++++++++++----
 1 file changed, 28 insertions(+), 4 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index 7f6b412..c07a78d 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -212,13 +212,26 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
     serde_json::Value::Object(map)
 }
 
+/// The calling app's name — the basename of its executable — used to locate
+/// its per-app config (`~/.config/cce/<name>/config.kdl`) and its `input.kdl`
+/// domain.
+///
+/// Derived from `/proc/self/exe` on every call, which the kernel renders as
+/// `<path> (deleted)` once the binary on disk has been replaced (`ccebuild
+/// install` unlinks before writing). Without the strip, a still-running client
+/// would resolve its override to `~/.config/cce/<name> (deleted)/config.kdl`
+/// on its next config reload and silently lose the whole file — the status
+/// bar's droplet bubbles reverted to square boxes this way on 2026-09-03.
 pub fn get_app_name() -> Option<String> {
     std::env::current_exe()
         .ok()
-        .and_then(|p| {
-            p.file_name()
-                .and_then(|s| s.to_str().map(|ss| ss.to_string()))
-        })
+        .and_then(|p| p.file_name().and_then(|s| s.to_str().map(app_name_from_exe_basename)))
+}
+
+/// [`get_app_name`]'s normalization: the kernel's ` (deleted)` marker on an
+/// unlinked executable is not part of the name.
+fn app_name_from_exe_basename(basename: &str) -> String {
+    basename.strip_suffix(" (deleted)").unwrap_or(basename).to_string()
 }
 
 pub fn get_app_config_path(app_name: &str) -> std::path::PathBuf {
@@ -795,6 +808,17 @@ pub fn get_kdl_type_annotations(kdl_content: &str, key_paths: &[String]) -> Vec<
 
 #[cfg(test)]
 mod tests {
+    #[test]
+    fn app_name_strips_the_kernels_deleted_marker() {
+        use super::app_name_from_exe_basename as name;
+        assert_eq!(name("cce-status-interface"), "cce-status-interface");
+        assert_eq!(name("cce-status-interface (deleted)"), "cce-status-interface");
+        // Only the exact trailing marker: a name that merely contains the
+        // word, or an unspaced variant, is left alone.
+        assert_eq!(name("cce-deleted-files"), "cce-deleted-files");
+        assert_eq!(name("cce-x(deleted)"), "cce-x(deleted)");
+    }
+
     use super::*;
 
     #[test]