git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commited9a3f21e420cb4f680809a5dbd8c4fcfaa53998
parent15c4e94e3e
authorLucas Galante <[email protected]>
date2026-06-23 14:31
feat: restart once=false startup programs on config reload

 src/server/config.rs         |   2 +-
 src/server/run_server.rs     |  63 ++++++---------------
 src/server/server.rs         |   1 +
 src/server/window_manager.rs | 132 +++++++++++++++++++++++++++++++++----------
 4 files changed, 120 insertions(+), 78 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 50d2ecb..058d22f 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -163,7 +163,7 @@ pub struct GestureBindConfig {
     pub command: Option<String>,
 }
 
-#[derive(Debug, Deserialize, Clone)]
+#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
 pub struct StartupConfig {
     pub exec: String,
     #[serde(default)]
diff --git a/src/server/run_server.rs b/src/server/run_server.rs
index 9328a6f..e2f9cff 100644
--- a/src/server/run_server.rs
+++ b/src/server/run_server.rs
@@ -182,53 +182,17 @@ pub fn run_server() {
         std::process::exit(1);
     }
 
-    let mut startup_pids = Vec::new();
     // Spawn TOML startup programs
-    for prog in &server.wm.startup {
-        log::info!("spawning TOML startup program: {}", prog.exec);
-        let cmd = prog.exec.clone();
+    let current_startup = server.wm.startup.clone();
+    for prog in current_startup {
         unsafe {
-            match nix::unistd::fork() {
-                Ok(nix::unistd::ForkResult::Child) => {
-                    process::cleanup_child();
-                    std::env::set_var("WAYLAND_DISPLAY", &socket_str);
-
-                    if !args.no_xwayland && !server.xwayland.is_null() {
-                        let xwayland_cast = server.xwayland as *mut server::WlrXwayland;
-                        if !(*xwayland_cast).display_name.is_null() {
-                            let display_name = CStr::from_ptr((*xwayland_cast).display_name)
-                                .to_string_lossy()
-                                .into_owned();
-                            std::env::set_var("DISPLAY", display_name);
-                        }
-                    }
-
-                    let cmd_c = CString::new(cmd).unwrap();
-                    let sh_c = CString::new("/bin/sh").unwrap();
-                    let c_c = CString::new("-c").unwrap();
-                    let args = [sh_c.as_c_str(), c_c.as_c_str(), cmd_c.as_c_str()];
-                    
-                    let env: Vec<CString> = std::env::vars()
-                        .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
-                        .collect();
-                    let env_ptrs: Vec<&CStr> = env.iter().map(|s| s.as_c_str()).collect();
-
-                    let _ = nix::unistd::execve(&sh_c, &args, &env_ptrs);
-                    std::process::exit(1);
-                }
-                Ok(nix::unistd::ForkResult::Parent { child }) => {
-                    startup_pids.push(child);
-                }
-                Err(e) => {
-                    log::error!("failed to fork child for startup program: {}", e);
-                }
-            }
+            server.wm.spawn_startup_program(prog);
         }
     }
 
     struct ServerGuard {
         init_pid: Option<nix::unistd::Pid>,
-        startup_pids: Vec<nix::unistd::Pid>,
+        wm: *mut crate::window_manager::WindowManager,
     }
     impl Drop for ServerGuard {
         fn drop(&mut self) {
@@ -239,12 +203,16 @@ pub fn run_server() {
                     nix::sys::signal::Signal::SIGTERM,
                 );
             }
-            for pid in &self.startup_pids {
-                log::info!("sending SIGTERM to startup program pid {}", pid);
-                let _ = nix::sys::signal::kill(
-                    *pid,
-                    nix::sys::signal::Signal::SIGTERM,
-                );
+            unsafe {
+                if !self.wm.is_null() {
+                    for (_, pid) in &(*self.wm).startup_pids {
+                        log::info!("sending SIGTERM to startup program pid {}", pid);
+                        let _ = nix::sys::signal::kill(
+                            *pid,
+                            nix::sys::signal::Signal::SIGTERM,
+                        );
+                    }
+                }
             }
         }
     }
@@ -294,7 +262,7 @@ pub fn run_server() {
 
     let _guard = ServerGuard {
         init_pid: child_pgid,
-        startup_pids,
+        wm: &mut server.wm as *mut crate::window_manager::WindowManager,
     };
 
     log::info!("running server");
@@ -303,5 +271,6 @@ pub fn run_server() {
     }
 
     log::info!("shutting down server");
+    std::mem::drop(_guard);
     server.deinit();
 }
diff --git a/src/server/server.rs b/src/server/server.rs
index 036381a..07a0bd2 100644
--- a/src/server/server.rs
+++ b/src/server/server.rs
@@ -832,6 +832,7 @@ impl Default for Server {
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.gesture_binds, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.ipc_rx, None);
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.startup, Vec::new());
+            std::ptr::write(&mut (*server.as_mut_ptr()).wm.startup_pids, Vec::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).wm.status_sender, None);
             std::ptr::write(&mut (*server.as_mut_ptr()).layer_shell.surfaces, crate::slotmap::SlotMap::new());
             std::ptr::write(&mut (*server.as_mut_ptr()).inspector, crate::inspector::Inspector::new());
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 6cf1129..03c81c3 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -66,6 +66,7 @@ pub struct WindowManager {
     pub ipc_rx: Option<std::sync::mpsc::Receiver<crate::ipc_server::IpcRequest>>,
     pub ipc_timer: *mut ffi::wl_event_source,
     pub startup: Vec<crate::config::StartupConfig>,
+    pub startup_pids: Vec<(crate::config::StartupConfig, nix::unistd::Pid)>,
     pub status_sender: Option<crate::status_server::StatusSender>,
     pub output_scale: f32,
     pub input_rules: Vec<crate::config::InputDeviceConfigRule>,
@@ -127,6 +128,7 @@ impl WindowManager {
         self.ipc_rx = None;
         self.ipc_timer = std::ptr::null_mut();
         self.startup = Vec::new();
+        self.startup_pids = Vec::new();
         self.status_sender = None;
         self.input_rules = Vec::new();
         self.input_config = crate::config::InputConfig::default();
@@ -1248,29 +1250,19 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
             }
             Action::Reload => {
                 log::info!("monolithic execute_action: Reload requested");
-                if let Some(path) = crate::config::default_config_path() {
-                    match crate::config::parse_config(&path, self) {
-                        Ok(()) => {
-                            self.dirty_windowing();
-                            let _ = std::process::Command::new("notify-send")
-                                .arg("cce")
-                                .arg("Configuration reloaded successfully")
-                                .spawn();
-                        }
-                        Err(e) => {
-                            log::error!("failed to reload config: {}", e);
-                            let _ = std::process::Command::new("notify-send")
-                                .arg("cce")
-                                .arg(format!("Failed to reload config:\n{}", e))
-                                .spawn();
-                        }
+                match self.reload_config() {
+                    Ok(()) => {
+                        let _ = std::process::Command::new("notify-send")
+                            .arg("cce")
+                            .arg("Configuration reloaded successfully")
+                            .spawn();
+                    }
+                    Err(e) => {
+                        let _ = std::process::Command::new("notify-send")
+                            .arg("cce")
+                            .arg(format!("Failed to reload config:\n{}", e))
+                            .spawn();
                     }
-                } else {
-                    log::error!("no config file found to reload");
-                    let _ = std::process::Command::new("notify-send")
-                        .arg("cce")
-                        .arg("No config file found to reload")
-                        .spawn();
                 }
             }
             Action::Exit => {
@@ -1676,16 +1668,11 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                 "ok\n".to_string()
             }
             "reload" => {
-                if let Some(path) = crate::config::default_config_path() {
-                    match crate::config::parse_config(&path, self) {
-                        Ok(()) => {
-                            self.dirty_windowing();
-                            "ok\n".to_string()
-                        }
+                unsafe {
+                    match self.reload_config() {
+                        Ok(()) => "ok\n".to_string(),
                         Err(e) => format!("error: failed to reload config: {}\n", e),
                     }
-                } else {
-                    "error: no config file found\n".to_string()
                 }
             }
             "retile" => {
@@ -1895,6 +1882,91 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
             curr = next;
         }
     }
+
+    pub unsafe fn spawn_startup_program(&mut self, prog: crate::config::StartupConfig) {
+        log::info!("spawning TOML startup program: {}", prog.exec);
+        let cmd = prog.exec.clone();
+        match nix::unistd::fork() {
+            Ok(nix::unistd::ForkResult::Child) => {
+                crate::process::cleanup_child();
+
+                if !self.server.is_null() && !(*self.server).xwayland.is_null() {
+                    let xwayland_cast = (*self.server).xwayland as *mut crate::server::WlrXwayland;
+                    if !(*xwayland_cast).display_name.is_null() {
+                        let display_name = std::ffi::CStr::from_ptr((*xwayland_cast).display_name)
+                            .to_string_lossy()
+                            .into_owned();
+                        std::env::set_var("DISPLAY", display_name);
+                    }
+                }
+
+                let env: Vec<std::ffi::CString> = std::env::vars()
+                    .map(|(k, v)| std::ffi::CString::new(format!("{}={}", k, v)).unwrap())
+                    .collect();
+                let env_ptrs: Vec<&std::ffi::CStr> = env.iter().map(|s| s.as_c_str()).collect();
+                let sh_c = std::ffi::CString::new("/bin/sh").unwrap();
+                let c_c = std::ffi::CString::new("-c").unwrap();
+                let cmd_c = std::ffi::CString::new(cmd).unwrap();
+                let args = [sh_c.as_c_str(), c_c.as_c_str(), cmd_c.as_c_str()];
+                let _ = nix::unistd::execve(&sh_c, &args, &env_ptrs);
+                std::process::exit(1);
+            }
+            Ok(nix::unistd::ForkResult::Parent { child }) => {
+                self.startup_pids.push((prog, child));
+            }
+            Err(e) => {
+                log::error!("failed to fork child for startup program: {}", e);
+            }
+        }
+    }
+
+    pub unsafe fn reload_config(&mut self) -> Result<(), String> {
+        if let Some(path) = crate::config::default_config_path() {
+            let old_pids = std::mem::take(&mut self.startup_pids);
+            match crate::config::parse_config(&path, self) {
+                Ok(()) => {
+                    self.dirty_windowing();
+
+                    // Process old PIDs
+                    for (old_prog, old_pid) in old_pids {
+                        // If it is still in new startup and once == true, keep it running
+                        let still_exists_and_once = self.startup.iter().any(|p| p.exec == old_prog.exec && p.once);
+                        if still_exists_and_once {
+                            self.startup_pids.push((old_prog, old_pid));
+                        } else {
+                            log::info!("Terminating old startup program pid {} ({})", old_pid, old_prog.exec);
+                            let _ = nix::sys::signal::kill(old_pid, nix::sys::signal::Signal::SIGTERM);
+                        }
+                    }
+
+                    // Spawn new/restarted programs
+                    let current_startup = self.startup.clone();
+                    for prog in current_startup {
+                        if prog.once {
+                            // Only spawn if not already running
+                            let running = self.startup_pids.iter().any(|(p, _)| p.exec == prog.exec);
+                            if !running {
+                                self.spawn_startup_program(prog);
+                            }
+                        } else {
+                            // once == false: spawn a new instance
+                            self.spawn_startup_program(prog);
+                        }
+                    }
+
+                    Ok(())
+                }
+                Err(e) => {
+                    self.startup_pids = old_pids;
+                    log::error!("failed to reload config: {}", e);
+                    Err(e)
+                }
+            }
+        } else {
+            log::error!("no config file found to reload");
+            Err("No config file found".to_string())
+        }
+    }
 }
 
 unsafe extern "C" fn handle_ipc_timer(data: *mut std::ffi::c_void) -> std::os::raw::c_int {