Capture logs for snap apps as well

This commit is contained in:
Albert Armea 2026-01-02 19:26:26 -05:00
parent 1e584f2907
commit 15697465c5
4 changed files with 50 additions and 5 deletions

7
Cargo.lock generated
View file

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

View file

@ -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: <entry_id>_<timestamp>.log
# capture_child_output = true
# child_log_dir = "~/.local/state/shepherdd/sessions"

View file

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

View file

@ -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<String, String>,
@ -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::<Vec<_>>()
.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)