diff --git a/core/archipelago/src/container/quadlet.rs b/core/archipelago/src/container/quadlet.rs index 2f6d4e06..0734e978 100644 --- a/core/archipelago/src/container/quadlet.rs +++ b/core/archipelago/src/container/quadlet.rs @@ -222,7 +222,7 @@ impl QuadletUnit { for env in &self.environment { // env entries already arrive shaped as "KEY=VALUE"; quadlet // accepts that form on a single Environment= line per pair. - let _ = writeln!(s, "Environment={env}"); + let _ = writeln!(s, "Environment={}", quote_environment(env)); } for dev in &self.devices { let _ = writeln!(s, "AddDevice={dev}"); @@ -296,6 +296,19 @@ fn shell_join(parts: &[String]) -> String { .join(" ") } +fn quote_environment(env: &str) -> String { + let env = env.replace(['\r', '\n'], " "); + if env.is_empty() || env.chars().any(|c| c.is_whitespace() || "\"\\$`".contains(c)) { + let escaped = env + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('$', "$$"); + format!("\"{escaped}\"") + } else { + env + } +} + impl QuadletUnit { /// Build a backend-flavour QuadletUnit from a parsed AppManifest. /// Wired through `prod_orchestrator::install_via_quadlet`, gated by @@ -778,6 +791,19 @@ mod tests { ); } + #[test] + fn quote_environment_quotes_values_with_spaces() { + assert_eq!(quote_environment("BITCOIN_RPC_PASS=secret"), "BITCOIN_RPC_PASS=secret"); + assert_eq!( + quote_environment("RELAY_NAME=Archipelago Nostr Relay"), + "\"RELAY_NAME=Archipelago Nostr Relay\"" + ); + assert_eq!( + quote_environment("GREETING=say \"hi\""), + "\"GREETING=say \\\"hi\\\"\"" + ); + } + #[test] fn restart_policy_emits_correct_systemd_string() { assert_eq!(RestartPolicy::Always.as_systemd(), "always"); @@ -797,6 +823,7 @@ mod tests { environment: vec![ "BITCOIN_RPC_USER=archipelago".into(), "BITCOIN_RPC_PASS=secret".into(), + "RELAY_NAME=Archipelago Nostr Relay".into(), ], devices: vec!["/dev/kvm".into()], add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())], @@ -813,6 +840,7 @@ mod tests { assert!(s.contains("PublishPort=8333:8333/tcp")); assert!(s.contains("Environment=BITCOIN_RPC_USER=archipelago")); assert!(s.contains("Environment=BITCOIN_RPC_PASS=secret")); + assert!(s.contains("Environment=\"RELAY_NAME=Archipelago Nostr Relay\"")); assert!(s.contains("AddDevice=/dev/kvm")); assert!(s.contains("AddHost=host.archipelago:10.89.0.1")); assert!(s.contains("ReadOnly=true")); diff --git a/neode-ui/src/views/appDetails/appDetailsData.ts b/neode-ui/src/views/appDetails/appDetailsData.ts index b7872b98..3f3fcc83 100644 --- a/neode-ui/src/views/appDetails/appDetailsData.ts +++ b/neode-ui/src/views/appDetails/appDetailsData.ts @@ -160,7 +160,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy' if (state === PackageState.Running && health === 'healthy') return 'healthy' if (state === PackageState.Exited) { - if (exitCode === 137) return 'killed (OOM)' + if (exitCode === 137) return 'killed (SIGKILL)' if (exitCode != null && exitCode !== 0) return 'crashed' return 'stopped' } diff --git a/neode-ui/src/views/apps/appsConfig.ts b/neode-ui/src/views/apps/appsConfig.ts index 9ab1d91e..a2337333 100644 --- a/neode-ui/src/views/apps/appsConfig.ts +++ b/neode-ui/src/views/apps/appsConfig.ts @@ -184,7 +184,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit if (state === PackageState.Updating) return 'updating...' if (state === PackageState.Running) return 'running' if (state === PackageState.Exited || state === PackageState.Stopped) { - if (exitCode === 137) return 'killed (OOM)' + if (exitCode === 137) return 'killed (SIGKILL)' if (exitCode != null && exitCode !== 0) return 'crashed' return 'stopped' }