diff --git a/Cargo.lock b/Cargo.lock index a74aa49..aa53fc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,6 +1187,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "shepherd-api" version = "0.1.0" @@ -1252,6 +1258,7 @@ dependencies = [ "dirs", "nix", "serde", + "shell-escape", "shepherd-api", "shepherd-host-api", "shepherd-util", diff --git a/config.example.toml b/config.example.toml index 5c8d346..9b61966 100644 --- a/config.example.toml +++ b/config.example.toml @@ -13,7 +13,7 @@ config_version = 1 # data_dir = "~/.local/share/shepherdd" # Capture stdout/stderr from child applications to log files -# Logs are written to child_log_dir (or log_dir/sessions if not set)` to view logs. +# Logs are written to child_log_dir (or log_dir/sessions if not set) # File format: _.log # capture_child_output = true # child_log_dir = "~/.local/state/shepherdd/sessions" diff --git a/crates/shepherd-host-linux/Cargo.toml b/crates/shepherd-host-linux/Cargo.toml index 51f3048..4459e39 100644 --- a/crates/shepherd-host-linux/Cargo.toml +++ b/crates/shepherd-host-linux/Cargo.toml @@ -16,6 +16,7 @@ tokio = { workspace = true } nix = { workspace = true } async-trait = "0.1" dirs = "5.0" +shell-escape = "0.1" [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/shepherd-host-linux/src/process.rs b/crates/shepherd-host-linux/src/process.rs index 41bd87a..b7643f7 100644 --- a/crates/shepherd-host-linux/src/process.rs +++ b/crates/shepherd-host-linux/src/process.rs @@ -130,6 +130,8 @@ impl ManagedProcess { /// systemd scope-based killing instead of signal-based killing. /// /// If `log_path` is provided, stdout and stderr will be redirected to that file. + /// For snap apps, we use `script` to capture output from all child processes + /// via a pseudo-terminal, since snap child processes don't inherit file descriptors. pub fn spawn( argv: &[String], env: &HashMap, @@ -141,8 +143,42 @@ impl ManagedProcess { return Err(HostError::SpawnFailed("Empty argv".into())); } - let program = &argv[0]; - let args = &argv[1..]; + // For snap apps with log capture, wrap with `script` to capture all child output + // via a pseudo-terminal. Snap child processes don't inherit file descriptors, + // but they do write to the controlling terminal. + let (actual_argv, actual_log_path) = match (&snap_name, &log_path) { + (Some(_), Some(log_file)) => { + // Create parent directory if it doesn't exist + if let Some(parent) = log_file.parent() + && let Err(e) = std::fs::create_dir_all(parent) + { + warn!(path = %parent.display(), error = %e, "Failed to create log directory"); + } + + // Build command: script -q -c "original command" logfile + // -q: quiet mode (no start/done messages) + // -c: command to run + let original_cmd = argv.iter() + .map(|arg| shell_escape::escape(std::borrow::Cow::Borrowed(arg))) + .collect::>() + .join(" "); + + let script_argv = vec![ + "script".to_string(), + "-q".to_string(), + "-c".to_string(), + original_cmd, + log_file.to_string_lossy().to_string(), + ]; + + info!(log_path = %log_file.display(), "Using script to capture snap output via pty"); + (script_argv, None) // script handles the log file itself + } + _ => (argv.to_vec(), log_path), + }; + + let program = &actual_argv[0]; + let args = &actual_argv[1..]; let mut cmd = Command::new(program); cmd.args(args); @@ -248,9 +284,10 @@ impl ManagedProcess { } // Configure output handling - // If log_path is provided, redirect stdout/stderr to the log file + // If actual_log_path is provided, redirect stdout/stderr to the log file + // (For snap apps, we already wrapped with `script` which handles logging) // Otherwise, inherit from parent so we can see child output for debugging - if let Some(ref path) = log_path { + if let Some(ref path) = actual_log_path { // Create parent directory if it doesn't exist if let Some(parent) = path.parent() && let Err(e) = std::fs::create_dir_all(parent)