Merge pull request #17 from aarmea/u/aarmea/fix/mc-launcher-snap
Merging now, Minecraft specific issue is tracked in #18
This commit is contained in:
commit
51b81c2a5b
11 changed files with 178 additions and 25 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -1187,6 +1187,12 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-escape"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shepherd-api"
|
name = "shepherd-api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1252,6 +1258,7 @@ dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"nix",
|
"nix",
|
||||||
"serde",
|
"serde",
|
||||||
|
"shell-escape",
|
||||||
"shepherd-api",
|
"shepherd-api",
|
||||||
"shepherd-host-api",
|
"shepherd-host-api",
|
||||||
"shepherd-util",
|
"shepherd-util",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# shepherd-launcher
|
# shepherd-launcher
|
||||||
|
|
||||||
A child-friendly, parent-guided launcher for Wayland, allowing supervised
|
A child-friendly, parent-guided desktop environment *alternative* for Wayland,
|
||||||
access to applications and content that you define.
|
allowing supervised access to applications and content that you define.
|
||||||
|
|
||||||
Its primary goal is to return control of child-focused computing to parents,
|
Its primary goal is to return control of child-focused computing to parents,
|
||||||
not software or hardware vendors, by providing:
|
not software or hardware vendors, by providing:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ config_version = 1
|
||||||
# log_dir = "~/.local/state/shepherdd"
|
# log_dir = "~/.local/state/shepherdd"
|
||||||
# data_dir = "~/.local/share/shepherdd"
|
# 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)
|
||||||
|
# File format: <entry_id>_<timestamp>.log
|
||||||
|
# capture_child_output = true
|
||||||
|
# child_log_dir = "~/.local/state/shepherdd/sessions"
|
||||||
|
|
||||||
# Default max run duration if not specified per entry (1 hour)
|
# Default max run duration if not specified per entry (1 hour)
|
||||||
# Set to 0 for unlimited (no time limit)
|
# Set to 0 for unlimited (no time limit)
|
||||||
default_max_run_seconds = 3600
|
default_max_run_seconds = 3600
|
||||||
|
|
@ -308,13 +314,17 @@ max_run_seconds = 0 # Unlimited: sleep/study aid
|
||||||
daily_quota_seconds = 0 # Unlimited
|
daily_quota_seconds = 0 # Unlimited
|
||||||
cooldown_seconds = 0 # No cooldown
|
cooldown_seconds = 0 # No cooldown
|
||||||
|
|
||||||
# Example: Disabled entry
|
# Terminal for debugging only
|
||||||
[[entries]]
|
[[entries]]
|
||||||
id = "disabled-game"
|
id = "terminal"
|
||||||
label = "Game Under Maintenance"
|
label = "Terminal"
|
||||||
disabled = true
|
icon = "utilities-terminal"
|
||||||
disabled_reason = "This game is being updated"
|
disabled = true # Typically disabled, since it's:
|
||||||
|
disabled_reason = "For debugging only"
|
||||||
|
|
||||||
[entries.kind]
|
[entries.kind]
|
||||||
type = "process"
|
type = "process"
|
||||||
command = "/bin/false"
|
command = "ptyxis"
|
||||||
|
|
||||||
|
[entries.availability]
|
||||||
|
always = true
|
||||||
|
|
|
||||||
|
|
@ -77,17 +77,28 @@ pub struct ServiceConfig {
|
||||||
pub socket_path: PathBuf,
|
pub socket_path: PathBuf,
|
||||||
pub log_dir: PathBuf,
|
pub log_dir: PathBuf,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
|
/// Whether to capture stdout/stderr from child applications
|
||||||
|
pub capture_child_output: bool,
|
||||||
|
/// Directory for child application logs
|
||||||
|
pub child_log_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceConfig {
|
impl ServiceConfig {
|
||||||
fn from_raw(raw: RawServiceConfig) -> Self {
|
fn from_raw(raw: RawServiceConfig) -> Self {
|
||||||
|
let log_dir = raw
|
||||||
|
.log_dir
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(default_log_dir);
|
||||||
|
let child_log_dir = raw
|
||||||
|
.child_log_dir
|
||||||
|
.unwrap_or_else(|| log_dir.join("sessions"));
|
||||||
Self {
|
Self {
|
||||||
socket_path: raw
|
socket_path: raw
|
||||||
.socket_path
|
.socket_path
|
||||||
.unwrap_or_else(socket_path_without_env),
|
.unwrap_or_else(socket_path_without_env),
|
||||||
log_dir: raw
|
log_dir,
|
||||||
.log_dir
|
capture_child_output: raw.capture_child_output,
|
||||||
.unwrap_or_else(default_log_dir),
|
child_log_dir,
|
||||||
data_dir: raw
|
data_dir: raw
|
||||||
.data_dir
|
.data_dir
|
||||||
.unwrap_or_else(default_data_dir),
|
.unwrap_or_else(default_data_dir),
|
||||||
|
|
@ -97,10 +108,13 @@ impl ServiceConfig {
|
||||||
|
|
||||||
impl Default for ServiceConfig {
|
impl Default for ServiceConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let log_dir = default_log_dir();
|
||||||
Self {
|
Self {
|
||||||
socket_path: socket_path_without_env(),
|
socket_path: socket_path_without_env(),
|
||||||
log_dir: default_log_dir(),
|
child_log_dir: log_dir.join("sessions"),
|
||||||
|
log_dir,
|
||||||
data_dir: default_data_dir(),
|
data_dir: default_data_dir(),
|
||||||
|
capture_child_output: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ pub struct RawServiceConfig {
|
||||||
/// Data directory for store (default: $XDG_DATA_HOME/shepherdd)
|
/// Data directory for store (default: $XDG_DATA_HOME/shepherdd)
|
||||||
pub data_dir: Option<PathBuf>,
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Capture stdout/stderr from child applications to log files
|
||||||
|
/// Files are written to child_log_dir (or log_dir/sessions if not set)
|
||||||
|
#[serde(default)]
|
||||||
|
pub capture_child_output: bool,
|
||||||
|
|
||||||
|
/// Directory for child application logs (default: log_dir/sessions)
|
||||||
|
pub child_log_dir: Option<PathBuf>,
|
||||||
|
|
||||||
/// Default warning thresholds (can be overridden per entry)
|
/// Default warning thresholds (can be overridden per entry)
|
||||||
pub default_warnings: Option<Vec<RawWarningThreshold>>,
|
pub default_warnings: Option<Vec<RawWarningThreshold>>,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ tokio = { workspace = true }
|
||||||
nix = { workspace = true }
|
nix = { workspace = true }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
shell-escape = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ impl HostAdapter for LinuxHost {
|
||||||
&argv,
|
&argv,
|
||||||
&env,
|
&env,
|
||||||
cwd.as_ref(),
|
cwd.as_ref(),
|
||||||
options.capture_stdout || options.capture_stderr,
|
options.log_path.clone(),
|
||||||
snap_name.clone(),
|
snap_name.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
use nix::sys::signal::{self, Signal};
|
use nix::sys::signal::{self, Signal};
|
||||||
use nix::unistd::Pid;
|
use nix::unistd::Pid;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
|
@ -126,19 +128,57 @@ impl ManagedProcess {
|
||||||
///
|
///
|
||||||
/// If `snap_name` is provided, the process is treated as a snap app and will use
|
/// If `snap_name` is provided, the process is treated as a snap app and will use
|
||||||
/// systemd scope-based killing instead of signal-based killing.
|
/// 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(
|
pub fn spawn(
|
||||||
argv: &[String],
|
argv: &[String],
|
||||||
env: &HashMap<String, String>,
|
env: &HashMap<String, String>,
|
||||||
cwd: Option<&std::path::PathBuf>,
|
cwd: Option<&std::path::PathBuf>,
|
||||||
capture_output: bool,
|
log_path: Option<PathBuf>,
|
||||||
snap_name: Option<String>,
|
snap_name: Option<String>,
|
||||||
) -> HostResult<Self> {
|
) -> HostResult<Self> {
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
return Err(HostError::SpawnFailed("Empty argv".into()));
|
return Err(HostError::SpawnFailed("Empty argv".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let program = &argv[0];
|
// For snap apps with log capture, wrap with `script` to capture all child output
|
||||||
let args = &argv[1..];
|
// 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);
|
let mut cmd = Command::new(program);
|
||||||
cmd.args(args);
|
cmd.args(args);
|
||||||
|
|
@ -233,6 +273,9 @@ impl ManagedProcess {
|
||||||
cmd.env("WAYLAND_DISPLAY", shepherd_display);
|
cmd.env("WAYLAND_DISPLAY", shepherd_display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chromium-based browsers and Electron apps need this to use the correct password store.
|
||||||
|
cmd.env("PASSWORD_STORE", "gnome");
|
||||||
|
|
||||||
// Add custom environment (these can override inherited vars)
|
// Add custom environment (these can override inherited vars)
|
||||||
for (k, v) in env {
|
for (k, v) in env {
|
||||||
cmd.env(k, v);
|
cmd.env(k, v);
|
||||||
|
|
@ -243,11 +286,43 @@ impl ManagedProcess {
|
||||||
cmd.current_dir(dir);
|
cmd.current_dir(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure output capture
|
// Configure output handling
|
||||||
// For debugging, inherit stdout/stderr so we can see errors
|
// If actual_log_path is provided, redirect stdout/stderr to the log file
|
||||||
if capture_output {
|
// (For snap apps, we already wrapped with `script` which handles logging)
|
||||||
cmd.stdout(Stdio::piped());
|
// Otherwise, inherit from parent so we can see child output for debugging
|
||||||
cmd.stderr(Stdio::piped());
|
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)
|
||||||
|
{
|
||||||
|
warn!(path = %parent.display(), error = %e, "Failed to create log directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open log file for appending (create if doesn't exist)
|
||||||
|
match File::create(path) {
|
||||||
|
Ok(file) => {
|
||||||
|
// Clone file handle for stderr (both point to same file)
|
||||||
|
let stderr_file = match file.try_clone() {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(path = %path.display(), error = %e, "Failed to clone log file handle");
|
||||||
|
cmd.stdout(Stdio::inherit());
|
||||||
|
cmd.stderr(Stdio::inherit());
|
||||||
|
cmd.stdin(Stdio::null());
|
||||||
|
// Skip to spawn
|
||||||
|
return Self::spawn_with_cmd(cmd, program, snap_name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cmd.stdout(Stdio::from(file));
|
||||||
|
cmd.stderr(Stdio::from(stderr_file));
|
||||||
|
info!(path = %path.display(), "Redirecting child output to log file");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(path = %path.display(), error = %e, "Failed to open log file, inheriting output");
|
||||||
|
cmd.stdout(Stdio::inherit());
|
||||||
|
cmd.stderr(Stdio::inherit());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Inherit from parent so we can see child output for debugging
|
// Inherit from parent so we can see child output for debugging
|
||||||
cmd.stdout(Stdio::inherit());
|
cmd.stdout(Stdio::inherit());
|
||||||
|
|
@ -256,6 +331,15 @@ impl ManagedProcess {
|
||||||
|
|
||||||
cmd.stdin(Stdio::null());
|
cmd.stdin(Stdio::null());
|
||||||
|
|
||||||
|
Self::spawn_with_cmd(cmd, program, snap_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete the spawn process with the configured command
|
||||||
|
fn spawn_with_cmd(
|
||||||
|
mut cmd: Command,
|
||||||
|
program: &str,
|
||||||
|
snap_name: Option<String>,
|
||||||
|
) -> HostResult<Self> {
|
||||||
// Store the command name for later use in killing
|
// Store the command name for later use in killing
|
||||||
let command_name = program.to_string();
|
let command_name = program.to_string();
|
||||||
|
|
||||||
|
|
@ -467,7 +551,7 @@ mod tests {
|
||||||
let argv = vec!["true".to_string()];
|
let argv = vec!["true".to_string()];
|
||||||
let env = HashMap::new();
|
let env = HashMap::new();
|
||||||
|
|
||||||
let mut proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap();
|
let mut proc = ManagedProcess::spawn(&argv, &env, None, None, None).unwrap();
|
||||||
|
|
||||||
// Wait for it to complete
|
// Wait for it to complete
|
||||||
let status = proc.wait().unwrap();
|
let status = proc.wait().unwrap();
|
||||||
|
|
@ -479,7 +563,7 @@ mod tests {
|
||||||
let argv = vec!["echo".to_string(), "hello".to_string()];
|
let argv = vec!["echo".to_string(), "hello".to_string()];
|
||||||
let env = HashMap::new();
|
let env = HashMap::new();
|
||||||
|
|
||||||
let mut proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap();
|
let mut proc = ManagedProcess::spawn(&argv, &env, None, None, None).unwrap();
|
||||||
let status = proc.wait().unwrap();
|
let status = proc.wait().unwrap();
|
||||||
assert!(status.is_success());
|
assert!(status.is_success());
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +573,7 @@ mod tests {
|
||||||
let argv = vec!["sleep".to_string(), "60".to_string()];
|
let argv = vec!["sleep".to_string(), "60".to_string()];
|
||||||
let env = HashMap::new();
|
let env = HashMap::new();
|
||||||
|
|
||||||
let proc = ManagedProcess::spawn(&argv, &env, None, false, None).unwrap();
|
let proc = ManagedProcess::spawn(&argv, &env, None, None, None).unwrap();
|
||||||
|
|
||||||
// Give it a moment to start
|
// Give it a moment to start
|
||||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
|
|
||||||
|
|
@ -537,6 +537,27 @@ impl Service {
|
||||||
.get_entry(&entry_id)
|
.get_entry(&entry_id)
|
||||||
.map(|e| e.kind.clone());
|
.map(|e| e.kind.clone());
|
||||||
|
|
||||||
|
// Build spawn options with log path if capture_child_output is enabled
|
||||||
|
let spawn_options = if eng.policy().service.capture_child_output {
|
||||||
|
let log_dir = &eng.policy().service.child_log_dir;
|
||||||
|
// Create log filename: <entry_id>_<session_id>_<timestamp>.log
|
||||||
|
let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
|
||||||
|
let log_filename = format!(
|
||||||
|
"{}_{}.log",
|
||||||
|
entry_id.as_str().replace(['/', '\\', ' '], "_"),
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
let log_path = log_dir.join(log_filename);
|
||||||
|
shepherd_host_api::SpawnOptions {
|
||||||
|
capture_stdout: true,
|
||||||
|
capture_stderr: true,
|
||||||
|
log_path: Some(log_path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shepherd_host_api::SpawnOptions::default()
|
||||||
|
};
|
||||||
|
|
||||||
drop(eng); // Release lock before spawning
|
drop(eng); // Release lock before spawning
|
||||||
|
|
||||||
if let Some(kind) = entry_kind {
|
if let Some(kind) = entry_kind {
|
||||||
|
|
@ -544,7 +565,7 @@ impl Service {
|
||||||
.spawn(
|
.spawn(
|
||||||
plan.session_id.clone(),
|
plan.session_id.clone(),
|
||||||
&kind,
|
&kind,
|
||||||
shepherd_host_api::SpawnOptions::default(),
|
spawn_options,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ sway
|
||||||
|
|
||||||
# Wayland session utilities
|
# Wayland session utilities
|
||||||
swayidle
|
swayidle
|
||||||
|
xdg-desktop-portal-wlr
|
||||||
|
|
||||||
# Runtime libraries (may be pulled in automatically, but explicit is safer)
|
# Runtime libraries (may be pulled in automatically, but explicit is safer)
|
||||||
libgtk-4-1
|
libgtk-4-1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
# Shepherd Launcher - Kiosk Mode Sway Configuration
|
# Shepherd Launcher - Kiosk Mode Sway Configuration
|
||||||
# This config makes Sway act as a kiosk environment with a custom launcher
|
# This config makes Sway act as a kiosk environment with a custom launcher
|
||||||
|
|
||||||
|
# Set up GNOME Keyring for secrets management if available
|
||||||
|
exec_always sh -lc 'command -v gnome-keyring-daemon >/dev/null && gnome-keyring-daemon --start --components=secrets >/dev/null 2>&1 || true'
|
||||||
|
|
||||||
|
# Update environment variables for D-Bus and GNOME Keyring
|
||||||
|
exec_always dbus-update-activation-environment --systemd \
|
||||||
|
SSH_AUTH_SOCK GNOME_KEYRING_CONTROL WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE XDG_SESSION_DESKTOP XDG_RUNTIME_DIR
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
set $launcher ./target/debug/shepherd-launcher
|
set $launcher ./target/debug/shepherd-launcher
|
||||||
set $hud ./target/debug/shepherd-hud
|
set $hud ./target/debug/shepherd-hud
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue