WIP: process spawn API

This commit is contained in:
Albert Armea 2025-12-26 10:44:17 -05:00
parent bdc1083b04
commit f3e62c43ea
9 changed files with 614 additions and 1 deletions

37
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<u32>` - 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.

86
src/daemon/README.md Normal file
View file

@ -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

29
src/daemon/daemon.rs Normal file
View file

@ -0,0 +1,29 @@
use super::ipc::IpcServer;
use std::time::Duration;
/// Start the daemon process
pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
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(())
}

207
src/daemon/ipc.rs Normal file
View file

@ -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<String> },
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<u32>, 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<u32, Child>,
}
impl IpcServer {
pub fn new() -> std::io::Result<Self> {
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<bool> {
// 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<bool> {
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<IpcResponse> {
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)
}
}

5
src/daemon/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod daemon;
mod ipc;
pub use daemon::start_daemon;
pub use ipc::{IpcClient, IpcMessage, IpcResponse};

View file

@ -1,5 +1,79 @@
mod daemon;
mod ui;
use std::env;
use std::process::{Command, Stdio};
fn main() -> Result<(), Box<dyn std::error::Error>> {
ui::run()
let args: Vec<String> = 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
}

View file

@ -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<dyn std::error::Error>> {
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));
}