Skip to content

Commit 9944957

Browse files
committed
systemd: move toplevel to separate scopes
1 parent 010a236 commit 9944957

File tree

2 files changed

+266
-1
lines changed

2 files changed

+266
-1
lines changed

src/handlers/compositor.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ impl CompositorHandler for State {
165165

166166
self.niri.queue_redraw(&output);
167167
}
168+
169+
for _ in 0..3 {
170+
let toplevel = window.toplevel().expect("no X11 support");
171+
if let Err(err) =
172+
crate::utils::spawning::test_scope(toplevel, &self.niri.display_handle)
173+
{
174+
tracing::warn!(?err, "failed to test scope");
175+
continue;
176+
};
177+
178+
break;
179+
}
180+
168181
return;
169182
}
170183

src/utils/spawning.rs

Lines changed: 253 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
use std::collections::HashMap;
12
use std::ffi::OsStr;
23
use std::os::unix::process::CommandExt;
34
use std::path::Path;
45
use std::process::{Child, Command, Stdio};
56
use std::sync::atomic::{AtomicBool, Ordering};
6-
use std::sync::RwLock;
7+
use std::sync::{OnceLock, RwLock};
78
use std::{io, thread};
89

10+
use anyhow::Context;
911
use atomic::Atomic;
1012
use libc::{getrlimit, rlim_t, rlimit, setrlimit, RLIMIT_NOFILE};
1113
use niri_config::Environment;
14+
use smithay::reexports::wayland_server::{DisplayHandle, Resource};
15+
use smithay::wayland::compositor;
16+
use smithay::wayland::shell::xdg::{ToplevelSurface, XdgToplevelSurfaceData};
17+
use zbus::zvariant::Value;
1218

1319
use crate::utils::expand_home;
1420

@@ -173,9 +179,12 @@ use systemd::do_spawn;
173179
mod systemd {
174180
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
175181

182+
use serde::{Deserialize, Serialize};
176183
use smithay::reexports::rustix;
177184
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
178185
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
186+
use zbus::dbus_proxy;
187+
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Type, Value};
179188

180189
use super::*;
181190

@@ -382,6 +391,20 @@ mod systemd {
382391

383392
let _ = write!(scope_name, "-{child_pid}.scope");
384393

394+
let mut slice_name = format!("app-niri-");
395+
396+
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
397+
// systemd source.
398+
for &c in name.as_bytes() {
399+
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
400+
slice_name.push(char::from(c));
401+
} else {
402+
let _ = write!(slice_name, "\\x{c:02x}");
403+
}
404+
}
405+
406+
let _ = write!(slice_name, ".slice");
407+
385408
// Ask systemd to start a transient scope.
386409
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
387410
let conn = CONNECTION
@@ -405,6 +428,7 @@ mod systemd {
405428
let properties: &[_] = &[
406429
("PIDs", Value::new(pids)),
407430
("CollectMode", Value::new("inactive-or-failed")),
431+
("Slice", Value::new(&slice_name)),
408432
];
409433
let aux: &[(&str, &[(&str, Value)])] = &[];
410434

@@ -425,4 +449,232 @@ mod systemd {
425449

426450
Ok(())
427451
}
452+
453+
#[dbus_proxy(
454+
interface = "org.freedesktop.systemd1.Manager",
455+
default_service = "org.freedesktop.systemd1",
456+
default_path = "/org/freedesktop/systemd1"
457+
)]
458+
trait Manager {
459+
#[dbus_proxy(name = "GetUnitByPID")]
460+
fn get_unit_by_pid(&self, pid: u32) -> zbus::Result<OwnedObjectPath>;
461+
462+
#[dbus_proxy(name = "StartTransientUnit")]
463+
fn start_transient_unit(
464+
&self,
465+
name: &str,
466+
mode: &str,
467+
properties: &[(&str, Value<'_>)],
468+
aux: &[(&str, &[(&str, Value<'_>)])],
469+
) -> zbus::Result<OwnedObjectPath>;
470+
471+
#[dbus_proxy(signal)]
472+
fn job_removed(
473+
&self,
474+
id: u32,
475+
job: zbus::zvariant::ObjectPath<'_>,
476+
unit: &str,
477+
result: &str,
478+
) -> zbus::Result<()>;
479+
}
480+
481+
/// A process spawned by systemd for a unit.
482+
#[derive(Debug, PartialEq, Eq, Clone, Type, Serialize, Deserialize, Value, OwnedValue)]
483+
pub struct Process {
484+
/// The cgroup controller of the process.
485+
pub cgroup_controller: String,
486+
487+
/// The PID of the process.
488+
pub pid: u32,
489+
490+
/// The command line of the process.
491+
pub command_line: String,
492+
}
493+
494+
#[dbus_proxy(
495+
interface = "org.freedesktop.systemd1.Scope",
496+
default_service = "org.freedesktop.systemd1"
497+
)]
498+
trait Scope {
499+
#[dbus_proxy(property)]
500+
fn control_group(&self) -> zbus::Result<String>;
501+
502+
fn get_processes(&self) -> zbus::Result<Vec<Process>>;
503+
}
504+
505+
#[dbus_proxy(
506+
interface = "org.freedesktop.systemd1.Unit",
507+
default_service = "org.freedesktop.systemd1",
508+
default_path = "/org/freedesktop/systemd1/unit"
509+
)]
510+
trait Unit {
511+
fn freeze(&self) -> zbus::Result<()>;
512+
fn thaw(&self) -> zbus::Result<()>;
513+
}
514+
}
515+
516+
pub fn test_scope(toplevel: &ToplevelSurface, dh: &DisplayHandle) -> anyhow::Result<()> {
517+
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
518+
let conn = CONNECTION
519+
.get_or_init(zbus::blocking::Connection::session)
520+
.clone()
521+
.context("error connecting to session bus")?;
522+
523+
let manager = systemd::ManagerProxyBlocking::new(&conn).context("error creating a Proxy")?;
524+
525+
let wl_surface = toplevel.wl_surface();
526+
let Some(client) = wl_surface.client() else {
527+
return Ok(());
528+
};
529+
530+
let credentials = client.get_credentials(dh)?;
531+
let pid = credentials.pid as u32;
532+
533+
let Some(app_id) = compositor::with_states(&wl_surface, |states| {
534+
states
535+
.data_map
536+
.get::<XdgToplevelSurfaceData>()
537+
.and_then(|surface_data| surface_data.lock().unwrap().app_id.clone())
538+
}) else {
539+
return Ok(());
540+
};
541+
542+
let unit_path = manager.get_unit_by_pid(pid)?;
543+
544+
use std::fmt::Write;
545+
let mut expected_scope_name = format!("app-niri-");
546+
547+
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
548+
// systemd source.
549+
for &c in app_id.as_bytes() {
550+
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
551+
expected_scope_name.push(char::from(c));
552+
} else {
553+
let _ = write!(expected_scope_name, "\\x{c:02x}");
554+
}
555+
}
556+
557+
let _ = write!(expected_scope_name, "-{pid}.scope");
558+
559+
let scope = systemd::ScopeProxyBlocking::builder(&conn)
560+
.path(&unit_path)?
561+
.build()
562+
.with_context(|| format!("failed to get scope for: {unit_path:?}"))?;
563+
let control_group = scope.control_group()?;
564+
565+
let existing_scope_name = control_group.split_terminator('/').last().unwrap();
566+
567+
if existing_scope_name.eq_ignore_ascii_case(&expected_scope_name)
568+
|| !existing_scope_name.starts_with("app-niri-")
569+
{
570+
return Ok(());
571+
}
572+
573+
let unit = systemd::UnitProxyBlocking::builder(&conn)
574+
.path(&unit_path)?
575+
.build()?;
576+
577+
let frozen = match unit.freeze() {
578+
Ok(_) => true,
579+
Err(err) => {
580+
tracing::warn!(?unit_path, ?err, "failed to freeze unit");
581+
false
582+
}
583+
};
584+
585+
let apply = || {
586+
let processes = scope.get_processes()?;
587+
588+
let mut pids = processes
589+
.iter()
590+
.map(|process| process.pid)
591+
.collect::<Vec<_>>();
592+
593+
let mut ppid_map: HashMap<u32, u32> = HashMap::new();
594+
595+
let mut i = 0;
596+
while i < pids.len() {
597+
// self check
598+
if pids[i] == pid {
599+
i += 1;
600+
continue;
601+
}
602+
603+
let pid: u32 = pids[i];
604+
605+
let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid))
606+
.context("failed to parse stat")?;
607+
let ppid_start = stat.rfind(')').unwrap_or_default() + 4;
608+
let ppid_end = ppid_start + stat[ppid_start..].find(' ').unwrap_or(0);
609+
let ppid = &stat[ppid_start..ppid_end];
610+
let ppid = ppid
611+
.parse::<u32>()
612+
.with_context(|| format!("failed to parse ppid from stat: {stat}"))?;
613+
614+
if !pids.contains(&ppid) {
615+
pids.remove(i);
616+
} else {
617+
ppid_map.insert(pid, ppid);
618+
i += 1;
619+
}
620+
}
621+
622+
let mut i = 0;
623+
while i < pids.len() {
624+
// self check
625+
if pids[i] == pid {
626+
i += 1;
627+
continue;
628+
}
629+
630+
let mut root_pid = pids[i];
631+
while let Some(&ppid) = ppid_map.get(&root_pid) {
632+
root_pid = ppid;
633+
}
634+
635+
if root_pid != pid {
636+
pids.remove(i);
637+
} else {
638+
i += 1;
639+
}
640+
}
641+
642+
let mut slice_name = format!("app-niri-");
643+
644+
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
645+
// systemd source.
646+
for &c in app_id.as_bytes() {
647+
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
648+
slice_name.push(char::from(c));
649+
} else {
650+
let _ = write!(slice_name, "\\x{c:02x}");
651+
}
652+
}
653+
654+
let _ = write!(slice_name, ".slice");
655+
656+
let properties: &[_] = &[
657+
("PIDs", Value::new(pids)),
658+
("CollectMode", Value::new("inactive-or-failed")),
659+
("Slice", Value::new(&slice_name)),
660+
];
661+
662+
tracing::info!(
663+
?expected_scope_name,
664+
?existing_scope_name,
665+
"trying to move to different scope"
666+
);
667+
668+
manager.start_transient_unit(&expected_scope_name, "fail", properties, &[])?;
669+
Result::<(), anyhow::Error>::Ok(())
670+
};
671+
672+
let res = apply();
673+
674+
if frozen {
675+
let _ = unit.thaw();
676+
}
677+
678+
res?;
679+
Ok(())
428680
}

0 commit comments

Comments
 (0)