1use dbus::blocking::Connection;
11use std::sync::{Arc, Mutex};
12use std::time::Duration;
13use thiserror::Error;
14use tracing::{debug, instrument};
15
16mod dbus_generated;
18use dbus_generated::OrgFreedesktopSystemd1Manager;
19
20#[derive(Error, Debug)]
21pub enum Error {
22 #[error("dbus error")]
23 Dbus(#[from] dbus::Error),
24
25 #[error("invalid scope name: {0:?}")]
26 InvalidScopeName(String),
27}
28
29#[derive(Debug, Clone)]
31pub struct ScopeParameters {
32 pub unit_name: String,
35 pub description: Option<String>,
36 pub cpu_weight: Option<u64>,
37 pub memory_high: Option<u64>,
38 pub memory_max: Option<u64>,
39 pub tasks_max: Option<u64>,
40}
41
42impl ScopeParameters {
43 pub fn with_unique_name() -> Self {
44 Self {
45 unit_name: unique_scope_name(),
46 description: None,
47 cpu_weight: None,
48 memory_high: None,
49 memory_max: None,
50 tasks_max: None,
51 }
52 }
53}
54
55#[derive(Clone, Debug)]
56pub struct ScopeHandle {
57 unit_name: String,
58}
59
60impl ScopeHandle {
61 pub fn unit_name(&self) -> &str {
62 self.unit_name.as_str()
63 }
64}
65
66#[instrument(level = "debug", skip_all, fields(unit = %params.unit_name))]
69pub fn start_transient_unit(pid: u32, params: ScopeParameters) -> Result<ScopeHandle, Error> {
70 let conn = Connection::new_session()?;
72
73 let proxy = conn.with_proxy(
76 "org.freedesktop.systemd1",
77 "/org/freedesktop/systemd1",
78 Duration::from_millis(5000),
79 );
80
81 if !params
83 .unit_name
84 .chars()
85 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
86 || !params.unit_name.ends_with(".scope")
87 {
88 return Err(Error::InvalidScopeName(params.unit_name));
89 }
90
91 let pid_list = dbus::arg::Variant(Box::new(vec![pid]) as Box<dyn dbus::arg::RefArg>);
93 let var_str =
94 |s: &str| dbus::arg::Variant(Box::new(s.to_string()) as Box<dyn dbus::arg::RefArg>);
95 let var_u64 = |u: u64| dbus::arg::Variant(Box::new(u) as Box<dyn dbus::arg::RefArg>);
96 let mut props = vec![
97 ("PIDs", pid_list),
98 ("Delegate", dbus::arg::Variant(Box::new(true))),
99 ];
100 if let Some(desc) = ¶ms.description {
101 props.push(("Description", var_str(desc)));
102 }
103 if let Some(cpu_weight) = params.cpu_weight {
104 props.push(("CPUWeight", var_u64(cpu_weight)));
105 }
106 if let Some(memory_high) = params.memory_high {
107 props.push(("MemoryHigh", var_u64(memory_high)));
108 }
109 if let Some(memory_max) = params.memory_max {
110 props.push(("MemoryMax", var_u64(memory_max)));
111 }
112 if let Some(tasks_max) = params.tasks_max {
113 props.push(("TasksMax", var_u64(tasks_max)));
114 }
115
116 let job_path = proxy.start_transient_unit(¶ms.unit_name, "fail", props, vec![])?;
118 debug!("requested unit start");
119
120 let _cgroup_wait_start = std::time::Instant::now();
121
122 let job_done = Arc::new(Mutex::new(false));
124 proxy.match_signal({
125 let job_done = job_done.clone();
126 move |msg: dbus_generated::OrgFreedesktopSystemd1ManagerJobRemoved,
127 _: &Connection,
128 _: &dbus::Message| {
129 if msg.job != job_path {
130 return true;
131 }
132 *job_done.lock().unwrap() = true;
133 false
134 }
135 })?;
136
137 while !*job_done.lock().unwrap() {
139 conn.process(Duration::from_millis(1000))?;
140 }
141
142 Ok(ScopeHandle {
143 unit_name: params.unit_name,
144 })
145}
146
147pub fn unique_scope_name() -> String {
152 use rand::seq::IteratorRandom;
153
154 const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz0123456789";
155
156 let mut rng = rand::thread_rng();
157 let mut name = String::from("cordon-");
158 for _ in 0..16 {
159 name.push(ALPHABET.chars().choose(&mut rng).unwrap());
160 }
161 name.push_str(".scope");
162
163 name
164}