From f3e62c43ea2560697eb6a5ea4972639dfebed122 Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Fri, 26 Dec 2025 10:44:17 -0500 Subject: [PATCH] WIP: process spawn API --- Cargo.lock | 37 ++++++ Cargo.toml | 2 + src/daemon/PROCESS_SPAWN_API.md | 123 +++++++++++++++++++ src/daemon/README.md | 86 +++++++++++++ src/daemon/daemon.rs | 29 +++++ src/daemon/ipc.rs | 207 ++++++++++++++++++++++++++++++++ src/daemon/mod.rs | 5 + src/main.rs | 76 +++++++++++- src/ui/ui.rs | 50 ++++++++ 9 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 src/daemon/PROCESS_SPAWN_API.md create mode 100644 src/daemon/README.md create mode 100644 src/daemon/daemon.rs create mode 100644 src/daemon/ipc.rs create mode 100644 src/daemon/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4c1d588..a29bee8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" + [[package]] name = "js-sys" version = "0.3.83" @@ -542,6 +548,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -562,6 +578,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -577,6 +606,8 @@ version = "0.1.0" dependencies = [ "cairo-rs", "chrono", + "serde", + "serde_json", "smithay-client-toolkit", "wayland-client", ] @@ -1052,3 +1083,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" dependencies = [ "bytemuck", ] + +[[package]] +name = "zmij" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" diff --git a/Cargo.toml b/Cargo.toml index 1c477ac..ba2197c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ smithay-client-toolkit = "0.19" wayland-client = "0.31" cairo-rs = { version = "0.20", features = ["v1_16"] } chrono = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/src/daemon/PROCESS_SPAWN_API.md b/src/daemon/PROCESS_SPAWN_API.md new file mode 100644 index 0000000..92368de --- /dev/null +++ b/src/daemon/PROCESS_SPAWN_API.md @@ -0,0 +1,123 @@ +# Process Spawning API + +The daemon now supports spawning graphical processes within the current session. + +## API Messages + +### SpawnProcess +Spawns a new process with the specified command and arguments. + +```rust +use crate::daemon::{IpcClient, IpcMessage, IpcResponse}; + +// Spawn a process with arguments +let message = IpcMessage::SpawnProcess { + command: "firefox".to_string(), + args: vec!["--new-window".to_string(), "https://example.com".to_string()], +}; + +match IpcClient::send_message(&message) { + Ok(IpcResponse::ProcessSpawned { success, pid, message }) => { + if success { + println!("Process spawned with PID: {:?}", pid); + } else { + eprintln!("Failed to spawn: {}", message); + } + } + Ok(other) => eprintln!("Unexpected response: {:?}", other), + Err(e) => eprintln!("IPC error: {}", e), +} +``` + +### LaunchApp (Legacy) +Spawns a process from a command string (command and args in one string). + +```rust +let message = IpcMessage::LaunchApp { + name: "Terminal".to_string(), + command: "alacritty".to_string(), +}; + +match IpcClient::send_message(&message) { + Ok(IpcResponse::ProcessSpawned { success, pid, message }) => { + println!("Launch result: {} (PID: {:?})", message, pid); + } + _ => {} +} +``` + +## Process Management + +### Automatic Cleanup +The daemon automatically tracks spawned processes and cleans up when they exit: +- Each spawned process is tracked by PID +- The daemon periodically checks for finished processes +- Exited processes are automatically removed from tracking + +### Status Query +Get the number of currently running processes: + +```rust +match IpcClient::send_message(&IpcMessage::GetStatus) { + Ok(IpcResponse::Status { uptime_secs, apps_running }) => { + println!("Daemon uptime: {}s, Processes running: {}", + uptime_secs, apps_running); + } + _ => {} +} +``` + +## Environment Inheritance + +Spawned processes inherit the daemon's environment, which includes: +- `WAYLAND_DISPLAY` - for Wayland session access +- `XDG_RUNTIME_DIR` - runtime directory +- `DISPLAY` - for X11 fallback (if available) +- All other environment variables from the daemon + +This ensures graphical applications can connect to the display server. + +## Examples + +### Spawn a terminal emulator +```rust +IpcClient::send_message(&IpcMessage::SpawnProcess { + command: "alacritty".to_string(), + args: vec![], +}) +``` + +### Spawn a browser with URL +```rust +IpcClient::send_message(&IpcMessage::SpawnProcess { + command: "firefox".to_string(), + args: vec!["https://github.com".to_string()], +}) +``` + +### Spawn with working directory (using sh wrapper) +```rust +IpcClient::send_message(&IpcMessage::SpawnProcess { + command: "sh".to_string(), + args: vec![ + "-c".to_string(), + "cd /path/to/project && code .".to_string() + ], +}) +``` + +## Response Format + +`ProcessSpawned` response contains: +- `success: bool` - Whether the spawn was successful +- `pid: Option` - Process ID if successful, None on failure +- `message: String` - Human-readable status message + +## Error Handling + +Common errors: +- Command not found: Returns `success: false` with error message +- Permission denied: Returns `success: false` with permission error +- Invalid arguments: Returns `success: false` with argument error + +Always check the `success` field before assuming the process started. diff --git a/src/daemon/README.md b/src/daemon/README.md new file mode 100644 index 0000000..144ff81 --- /dev/null +++ b/src/daemon/README.md @@ -0,0 +1,86 @@ +# Daemon and IPC Implementation + +This directory contains the daemon process and IPC (Inter-Process Communication) implementation for shepherd-launcher. + +## Architecture + +The application uses a multi-process architecture: +- **Main Process**: Spawns the daemon and runs the UI +- **Daemon Process**: Background service that handles application launching and state management +- **IPC**: Unix domain sockets for communication between processes + +## Files + +- `mod.rs`: Module exports +- `daemon.rs`: Daemon process implementation +- `ipc.rs`: IPC protocol, message types, client and server implementations + +## IPC Protocol + +Communication uses JSON-serialized messages over Unix domain sockets. + +### Message Types (UI → Daemon) +- `Ping`: Simple health check +- `GetStatus`: Request daemon status (uptime, running apps) +- `LaunchApp { name, command }`: Request to launch an application +- `Shutdown`: Request daemon shutdown + +### Response Types (Daemon → UI) +- `Pong`: Response to Ping +- `Status { uptime_secs, apps_running }`: Daemon status information +- `AppLaunched { success, message }`: Result of app launch request +- `ShuttingDown`: Acknowledgment of shutdown request +- `Error { message }`: Error response + +## Socket Location + +The IPC socket is created at: `$XDG_RUNTIME_DIR/shepherd-launcher.sock` (typically `/run/user/1000/shepherd-launcher.sock`) + +## Usage Example + +```rust +use crate::daemon::{IpcClient, IpcMessage, IpcResponse}; + +// Send a ping +match IpcClient::send_message(&IpcMessage::Ping) { + Ok(IpcResponse::Pong) => println!("Daemon is alive!"), + Ok(other) => println!("Unexpected response: {:?}", other), + Err(e) => eprintln!("IPC error: {}", e), +} + +// Get daemon status +match IpcClient::send_message(&IpcMessage::GetStatus) { + Ok(IpcResponse::Status { uptime_secs, apps_running }) => { + println!("Uptime: {}s, Apps: {}", uptime_secs, apps_running); + } + _ => {} +} + +// Launch an app +let msg = IpcMessage::LaunchApp { + name: "Firefox".to_string(), + command: "firefox".to_string(), +}; +match IpcClient::send_message(&msg) { + Ok(IpcResponse::AppLaunched { success, message }) => { + println!("Launch {}: {}", if success { "succeeded" } else { "failed" }, message); + } + _ => {} +} +``` + +## Current Functionality + +Currently this is a dummy implementation demonstrating the IPC pattern: +- The daemon process runs in the background +- The UI periodically queries the daemon status (every 5 seconds) +- Messages are printed to stdout for debugging +- App launching is simulated (doesn't actually launch apps yet) + +## Future Enhancements + +- Actual application launching logic +- App state tracking +- Bi-directional notifications (daemon → UI events) +- Multiple concurrent IPC connections +- Authentication/security diff --git a/src/daemon/daemon.rs b/src/daemon/daemon.rs new file mode 100644 index 0000000..9371217 --- /dev/null +++ b/src/daemon/daemon.rs @@ -0,0 +1,29 @@ +use super::ipc::IpcServer; +use std::time::Duration; + +/// Start the daemon process +pub fn start_daemon() -> Result<(), Box> { + println!("[Daemon] Starting shepherd-launcher daemon..."); + + let mut ipc_server = IpcServer::new()?; + println!("[Daemon] IPC server listening on socket"); + + loop { + // Handle incoming IPC connections + match ipc_server.accept_and_handle() { + Ok(should_shutdown) => { + if should_shutdown { + println!("[Daemon] Shutdown requested, exiting..."); + break; + } + } + Err(e) => eprintln!("[Daemon] Error handling client: {}", e), + } + + // Sleep briefly to avoid busy-waiting + std::thread::sleep(Duration::from_millis(10)); + } + + println!("[Daemon] Daemon shut down cleanly"); + Ok(()) +} diff --git a/src/daemon/ipc.rs b/src/daemon/ipc.rs new file mode 100644 index 0000000..8df309d --- /dev/null +++ b/src/daemon/ipc.rs @@ -0,0 +1,207 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::PathBuf; +use std::process::{Child, Command}; + +/// Messages that can be sent from the UI to the daemon +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IpcMessage { + Ping, + GetStatus, + LaunchApp { name: String, command: String }, + SpawnProcess { command: String, args: Vec }, + Shutdown, +} + +/// Responses sent from the daemon to the UI +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum IpcResponse { + Pong, + Status { uptime_secs: u64, apps_running: usize }, + AppLaunched { success: bool, message: String }, + ProcessSpawned { success: bool, pid: Option, message: String }, + ShuttingDown, + Error { message: String }, +} + +/// Get the IPC socket path +pub fn get_socket_path() -> PathBuf { + let runtime_dir = std::env::var("XDG_RUNTIME_DIR") + .unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(runtime_dir).join("shepherd-launcher.sock") +} + +/// Server-side IPC handler for the daemon +pub struct IpcServer { + listener: UnixListener, + start_time: std::time::Instant, + processes: HashMap, +} + +impl IpcServer { + pub fn new() -> std::io::Result { + let socket_path = get_socket_path(); + + // Remove old socket if it exists + let _ = std::fs::remove_file(&socket_path); + + let listener = UnixListener::bind(&socket_path)?; + listener.set_nonblocking(true)?; + + Ok(Self { + listener, + start_time: std::time::Instant::now(), + processes: HashMap::new(), + }) + } + + pub fn accept_and_handle(&mut self) -> std::io::Result { + // Clean up finished processes + self.cleanup_processes(); + + match self.listener.accept() { + Ok((stream, _)) => { + let should_shutdown = self.handle_client(stream)?; + Ok(should_shutdown) + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + Ok(false) + } + Err(e) => Err(e), + } + } + + fn handle_client(&mut self, mut stream: UnixStream) -> std::io::Result { + let mut reader = BufReader::new(stream.try_clone()?); + let mut line = String::new(); + + reader.read_line(&mut line)?; + + let message: IpcMessage = match serde_json::from_str(&line) { + Ok(msg) => msg, + Err(e) => { + let response = IpcResponse::Error { + message: format!("Failed to parse message: {}", e), + }; + let response_json = serde_json::to_string(&response)?; + writeln!(stream, "{}", response_json)?; + return Ok(false); + } + }; + + let should_shutdown = matches!(message, IpcMessage::Shutdown); + let response = self.process_message(message); + let response_json = serde_json::to_string(&response)?; + writeln!(stream, "{}", response_json)?; + + Ok(should_shutdown) + } + + fn process_message(&mut self, message: IpcMessage) -> IpcResponse { + match message { + IpcMessage::Ping => IpcResponse::Pong, + IpcMessage::GetStatus => { + let uptime_secs = self.start_time.elapsed().as_secs(); + IpcResponse::Status { + uptime_secs, + apps_running: self.processes.len(), + } + } + IpcMessage::LaunchApp { name, command } => { + println!("[Daemon] Launching app: {} ({})", name, command); + self.spawn_graphical_process(&command, &[]) + } + IpcMessage::SpawnProcess { command, args } => { + println!("[Daemon] Spawning process: {} {:?}", command, args); + self.spawn_graphical_process(&command, &args) + } + IpcMessage::Shutdown => IpcResponse::ShuttingDown, + } + } + + fn spawn_graphical_process(&mut self, command: &str, args: &[String]) -> IpcResponse { + // Parse command if it contains arguments and args is empty + let (cmd, cmd_args) = if args.is_empty() { + let parts: Vec<&str> = command.split_whitespace().collect(); + if parts.is_empty() { + return IpcResponse::ProcessSpawned { + success: false, + pid: None, + message: "Empty command".to_string(), + }; + } + (parts[0], parts[1..].iter().map(|s| s.to_string()).collect()) + } else { + (command, args.to_vec()) + }; + + match Command::new(cmd) + .args(&cmd_args) + .spawn() + { + Ok(child) => { + let pid = child.id(); + println!("[Daemon] Successfully spawned process PID: {}", pid); + self.processes.insert(pid, child); + IpcResponse::ProcessSpawned { + success: true, + pid: Some(pid), + message: format!("Process spawned with PID {}", pid), + } + } + Err(e) => { + eprintln!("[Daemon] Failed to spawn process '{}': {}", cmd, e); + IpcResponse::ProcessSpawned { + success: false, + pid: None, + message: format!("Failed to spawn: {}", e), + } + } + } + } + + fn cleanup_processes(&mut self) { + // Check for finished processes and remove them + let mut finished = Vec::new(); + for (pid, child) in self.processes.iter_mut() { + match child.try_wait() { + Ok(Some(status)) => { + println!("[Daemon] Process {} exited with status: {}", pid, status); + finished.push(*pid); + } + Ok(None) => {} + Err(e) => { + eprintln!("[Daemon] Error checking process {}: {}", pid, e); + finished.push(*pid); + } + } + } + for pid in finished { + self.processes.remove(&pid); + } + } +} + +/// Client-side IPC handler for the UI +pub struct IpcClient; + +impl IpcClient { + pub fn send_message(message: &IpcMessage) -> std::io::Result { + let socket_path = get_socket_path(); + let mut stream = UnixStream::connect(&socket_path)?; + + let message_json = serde_json::to_string(message)?; + writeln!(stream, "{}", message_json)?; + + let mut reader = BufReader::new(stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; + + let response: IpcResponse = serde_json::from_str(&response_line) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + + Ok(response) + } +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 0000000..21835d9 --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,5 @@ +mod daemon; +mod ipc; + +pub use daemon::start_daemon; +pub use ipc::{IpcClient, IpcMessage, IpcResponse}; diff --git a/src/main.rs b/src/main.rs index f26e39d..e6aef8b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,79 @@ +mod daemon; mod ui; +use std::env; +use std::process::{Command, Stdio}; + fn main() -> Result<(), Box> { - ui::run() + let args: Vec = env::args().collect(); + + // Check if we're running as the daemon + if args.len() > 1 && args[1] == "--daemon" { + return daemon::start_daemon(); + } + + // Spawn the daemon process + println!("[Main] Spawning daemon process..."); + let mut daemon_child = Command::new(&args[0]) + .arg("--daemon") + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?; + + let daemon_pid = daemon_child.id(); + println!("[Main] Daemon spawned with PID: {}", daemon_pid); + + // Give the daemon a moment to start up + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Test the IPC connection + println!("[Main] Testing IPC connection..."); + match daemon::IpcClient::send_message(&daemon::IpcMessage::Ping) { + Ok(daemon::IpcResponse::Pong) => println!("[Main] IPC connection successful!"), + Ok(response) => println!("[Main] Unexpected response: {:?}", response), + Err(e) => println!("[Main] IPC connection failed: {}", e), + } + + // Start the UI + println!("[Main] Starting UI..."); + let ui_result = ui::run(); + + // UI has exited, shut down the daemon + println!("[Main] UI exited, shutting down daemon..."); + match daemon::IpcClient::send_message(&daemon::IpcMessage::Shutdown) { + Ok(daemon::IpcResponse::ShuttingDown) => { + println!("[Main] Daemon acknowledged shutdown"); + } + Ok(response) => { + println!("[Main] Unexpected shutdown response: {:?}", response); + } + Err(e) => { + eprintln!("[Main] Failed to send shutdown to daemon: {}", e); + } + } + + // Wait for daemon to exit (with timeout) + let wait_start = std::time::Instant::now(); + loop { + match daemon_child.try_wait() { + Ok(Some(status)) => { + println!("[Main] Daemon exited with status: {}", status); + break; + } + Ok(None) => { + if wait_start.elapsed().as_secs() > 5 { + eprintln!("[Main] Daemon did not exit in time, killing it"); + let _ = daemon_child.kill(); + break; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Err(e) => { + eprintln!("[Main] Error waiting for daemon: {}", e); + break; + } + } + } + + ui_result } diff --git a/src/ui/ui.rs b/src/ui/ui.rs index 8d7f0cf..5348f96 100644 --- a/src/ui/ui.rs +++ b/src/ui/ui.rs @@ -1,4 +1,5 @@ use super::clock::ClockApp; +use crate::daemon::{IpcClient, IpcMessage, IpcResponse}; use smithay_client_toolkit::{ compositor::CompositorState, output::OutputState, @@ -49,11 +50,60 @@ pub fn run() -> Result<(), Box> { app.layer_surface = Some(layer_surface); + // Periodically query daemon status via IPC + let mut counter = 0; + + // Example: Spawn a test process after 2 seconds + let mut test_spawned = false; + loop { event_queue.blocking_dispatch(&mut app)?; if app.configured { app.draw(&qh)?; + + // Example: Spawn a simple graphical process after 2 seconds + if counter == 4 && !test_spawned { + println!("[UI] Testing process spawn API..."); + match IpcClient::send_message(&IpcMessage::SpawnProcess { + command: "echo".to_string(), + args: vec!["Hello from spawned process!".to_string()], + }) { + Ok(IpcResponse::ProcessSpawned { success, pid, message }) => { + if success { + println!("[UI] Process spawned successfully! PID: {:?}, Message: {}", + pid, message); + } else { + println!("[UI] Process spawn failed: {}", message); + } + } + Ok(response) => { + println!("[UI] Unexpected response: {:?}", response); + } + Err(e) => { + eprintln!("[UI] Failed to spawn process: {}", e); + } + } + test_spawned = true; + } + + // Every 10 iterations (5 seconds), query the daemon + if counter % 10 == 0 { + match IpcClient::send_message(&IpcMessage::GetStatus) { + Ok(IpcResponse::Status { uptime_secs, apps_running }) => { + println!("[UI] Daemon status - Uptime: {}s, Apps running: {}", + uptime_secs, apps_running); + } + Ok(response) => { + println!("[UI] Unexpected daemon response: {:?}", response); + } + Err(e) => { + eprintln!("[UI] Failed to communicate with daemon: {}", e); + } + } + } + counter += 1; + // Sleep briefly to reduce CPU usage std::thread::sleep(std::time::Duration::from_millis(500)); }