desktop: add systemd service spawn option alongside scope
Per systemd's desktop environment recommendations [1], transient .service units are preferred over .scope units when launching applications. This ensures the systemd user session is the direct parent of launched processes. The previous approach (desktop-systemd-scope) spawned processes via double-fork (orphaning them to PID 1), then moved them into a scope. Security tools like 1Password that verify parent process lineage rejected these processes because their ancestor chain led to PID 1 rather than systemd --user. This commit adds a new 'desktop-systemd-service' feature that uses StartTransientUnit with ExecStart to let systemd spawn the process directly, giving launched applications a proper parent lineage. Feature behavior: - desktop-systemd-service only: Uses transient .service units - desktop-systemd-scope only: Uses transient .scope units (existing behavior) - Both enabled: Tries .service first, falls back to .scope, then double-fork - Neither enabled: Uses double-fork directly Also fixes typo: SystemdManger -> SystemdManager [1] https://systemd.io/DESKTOP_ENVIRONMENTS/
This commit is contained in:
parent
3e78eb2381
commit
2af4cff958
2 changed files with 192 additions and 41 deletions
|
|
@ -49,6 +49,11 @@ desktop = [
|
||||||
]
|
]
|
||||||
# Enables launching desktop files inside systemd scopes
|
# Enables launching desktop files inside systemd scopes
|
||||||
desktop-systemd-scope = ["desktop", "dep:zbus"]
|
desktop-systemd-scope = ["desktop", "dep:zbus"]
|
||||||
|
# Enables launching desktop files via systemd transient services.
|
||||||
|
# This is preferred over scopes as systemd becomes the direct parent,
|
||||||
|
# which satisfies security tools that verify process lineage.
|
||||||
|
# If both this and desktop-systemd-scope are enabled, service is tried first.
|
||||||
|
desktop-systemd-service = ["desktop", "dep:zbus"]
|
||||||
# Enables keycode serialization
|
# Enables keycode serialization
|
||||||
serde-keycode = ["iced_core/serde"]
|
serde-keycode = ["iced_core/serde"]
|
||||||
# Prevents multiple separate process instances.
|
# Prevents multiple separate process instances.
|
||||||
|
|
|
||||||
228
src/desktop.rs
228
src/desktop.rs
|
|
@ -792,71 +792,217 @@ pub async fn spawn_desktop_exec<S, I, K, V>(
|
||||||
exec.as_ref()
|
exec.as_ref()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut exec = shlex::Shlex::new(exec_str);
|
let mut args = shlex::Shlex::new(exec_str);
|
||||||
|
|
||||||
let executable = match exec.next() {
|
let executable = match args.next() {
|
||||||
Some(executable) if !executable.contains('=') => executable,
|
Some(executable) if !executable.contains('=') => executable,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = std::process::Command::new(&executable);
|
let mut argv: Vec<String> = vec![executable.clone()];
|
||||||
|
for arg in args {
|
||||||
for arg in exec {
|
|
||||||
// TODO handle "%" args here if necessary?
|
// TODO handle "%" args here if necessary?
|
||||||
if !arg.starts_with('%') {
|
if !arg.starts_with('%') {
|
||||||
cmd.arg(arg);
|
argv.push(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.envs(env_vars);
|
// Collect environment variables for systemd-based spawning
|
||||||
|
#[cfg(any(feature = "desktop-systemd-service", feature = "desktop-systemd-scope"))]
|
||||||
|
let env_vars: Vec<(String, String)> = env_vars
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
(
|
||||||
|
k.as_ref().to_string_lossy().into_owned(),
|
||||||
|
v.as_ref().to_string_lossy().into_owned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// https://systemd.io/DESKTOP_ENVIRONMENTS
|
// https://systemd.io/DESKTOP_ENVIRONMENTS
|
||||||
//
|
//
|
||||||
// Similar to what Gnome sets, for now.
|
// Per systemd recommendations, prefer .service units over .scope units
|
||||||
if let Some(pid) = crate::process::spawn(cmd).await {
|
// so that systemd is the direct parent of the process. This ensures proper
|
||||||
#[cfg(feature = "desktop-systemd-scope")]
|
// process lineage for security tools that verify parent process chains.
|
||||||
if let Ok(session) = zbus::Connection::session().await {
|
|
||||||
if let Ok(systemd_manager) = SystemdMangerProxy::new(&session).await {
|
// Try systemd service first if enabled
|
||||||
let _ = systemd_manager
|
#[cfg(feature = "desktop-systemd-service")]
|
||||||
.start_transient_unit(
|
{
|
||||||
&format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(&executable), pid),
|
if spawn_via_systemd_service(&executable, &argv, &env_vars, app_id).await {
|
||||||
"fail",
|
return;
|
||||||
&[
|
}
|
||||||
(
|
}
|
||||||
"Description".to_string(),
|
|
||||||
zbus::zvariant::Value::from("Application launched by COSMIC")
|
// Fall back to systemd scope if enabled (or if service failed)
|
||||||
.try_to_owned()
|
#[cfg(feature = "desktop-systemd-scope")]
|
||||||
.unwrap(),
|
{
|
||||||
),
|
#[cfg(feature = "desktop-systemd-service")]
|
||||||
(
|
tracing::debug!("Falling back to systemd scope after service spawn failed");
|
||||||
"PIDs".to_string(),
|
|
||||||
zbus::zvariant::Value::from(vec![pid])
|
if spawn_via_systemd_scope(&executable, &argv, &env_vars, app_id).await {
|
||||||
.try_to_owned()
|
return;
|
||||||
.unwrap(),
|
}
|
||||||
),
|
}
|
||||||
(
|
|
||||||
"CollectMode".to_string(),
|
#[cfg(any(feature = "desktop-systemd-service", feature = "desktop-systemd-scope"))]
|
||||||
zbus::zvariant::Value::from("inactive-or-failed")
|
tracing::debug!("Falling back to direct spawn");
|
||||||
.try_to_owned()
|
|
||||||
.unwrap(),
|
let mut cmd = std::process::Command::new(&executable);
|
||||||
),
|
cmd.args(&argv[1..]);
|
||||||
],
|
cmd.envs(env_vars);
|
||||||
&[],
|
let _ = crate::process::spawn(cmd).await;
|
||||||
)
|
}
|
||||||
.await;
|
|
||||||
}
|
/// Spawn an application via a transient systemd .service unit.
|
||||||
|
/// Returns true if the service was started successfully.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[cfg(feature = "desktop-systemd-service")]
|
||||||
|
async fn spawn_via_systemd_service(
|
||||||
|
executable: &str,
|
||||||
|
argv: &[String],
|
||||||
|
env_vars: &[(String, String)],
|
||||||
|
app_id: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let unit_name = format!(
|
||||||
|
"app-cosmic-{}@{}.service",
|
||||||
|
app_id.unwrap_or(executable),
|
||||||
|
std::process::id()
|
||||||
|
);
|
||||||
|
|
||||||
|
let Ok(session) = zbus::Connection::session().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(systemd_manager) = SystemdManagerProxy::new(&session).await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build ExecStart property: (path, argv, is_ignore_failure)
|
||||||
|
let exec_start_value = vec![(executable.to_string(), argv.to_vec(), false)];
|
||||||
|
|
||||||
|
// Build Environment property: array of "KEY=VALUE" strings
|
||||||
|
let environment: Vec<String> = env_vars
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{}={}", k, v))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut properties: Vec<(String, zbus::zvariant::OwnedValue)> = vec![
|
||||||
|
(
|
||||||
|
"Description".to_string(),
|
||||||
|
zbus::zvariant::Value::from("Application launched by COSMIC")
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ExecStart".to_string(),
|
||||||
|
zbus::zvariant::Value::from(exec_start_value)
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Type".to_string(),
|
||||||
|
zbus::zvariant::Value::from("simple")
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CollectMode".to_string(),
|
||||||
|
zbus::zvariant::Value::from("inactive-or-failed")
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !environment.is_empty() {
|
||||||
|
properties.push((
|
||||||
|
"Environment".to_string(),
|
||||||
|
zbus::zvariant::Value::from(environment)
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
match systemd_manager
|
||||||
|
.start_transient_unit(&unit_name, "fail", &properties, &[])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Failed to start transient service unit: {}", err);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn an application and move it into a transient systemd .scope unit.
|
||||||
|
/// Returns true if the process was spawned and moved into a scope successfully.
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
#[cfg(feature = "desktop-systemd-scope")]
|
#[cfg(feature = "desktop-systemd-scope")]
|
||||||
|
async fn spawn_via_systemd_scope(
|
||||||
|
executable: &str,
|
||||||
|
argv: &[String],
|
||||||
|
env_vars: &[(String, String)],
|
||||||
|
app_id: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let mut cmd = std::process::Command::new(executable);
|
||||||
|
for arg in &argv[1..] {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
for (k, v) in env_vars {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pid) = crate::process::spawn(cmd).await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(session) = zbus::Connection::session().await else {
|
||||||
|
return true; // Process spawned, just couldn't create scope
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(systemd_manager) = SystemdManagerProxy::new(&session).await else {
|
||||||
|
return true; // Process spawned, just couldn't create scope
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = systemd_manager
|
||||||
|
.start_transient_unit(
|
||||||
|
&format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(executable), pid),
|
||||||
|
"fail",
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
"Description".to_string(),
|
||||||
|
zbus::zvariant::Value::from("Application launched by COSMIC")
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"PIDs".to_string(),
|
||||||
|
zbus::zvariant::Value::from(vec![pid])
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CollectMode".to_string(),
|
||||||
|
zbus::zvariant::Value::from("inactive-or-failed")
|
||||||
|
.try_to_owned()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[cfg(any(feature = "desktop-systemd-service", feature = "desktop-systemd-scope"))]
|
||||||
#[zbus::proxy(
|
#[zbus::proxy(
|
||||||
interface = "org.freedesktop.systemd1.Manager",
|
interface = "org.freedesktop.systemd1.Manager",
|
||||||
default_service = "org.freedesktop.systemd1",
|
default_service = "org.freedesktop.systemd1",
|
||||||
default_path = "/org/freedesktop/systemd1"
|
default_path = "/org/freedesktop/systemd1"
|
||||||
)]
|
)]
|
||||||
trait SystemdManger {
|
trait SystemdManager {
|
||||||
async fn start_transient_unit(
|
async fn start_transient_unit(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue