atagen

roundabout

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git
src/systemd.rsRust302 lines9.3 KB
1
use std::{
2
    env, io,
3
    os::fd::{AsRawFd, FromRawFd, OwnedFd},
4
    os::unix::process::CommandExt,
5
    process::{Child, Command},
6
    time::Duration,
7
};
8
9
use futures_lite::StreamExt;
10
use zbus::{
11
    blocking::{Connection, Proxy},
12
    proxy::SignalStream,
13
    zvariant::{OwnedObjectPath, Value},
14
};
15
16
use crate::{
17
    config::{Overrun, RamOverrun},
18
    units::Bytes,
19
};
20
21
const EXEC: &str = "__roundabout_exec";
22
const MANAGER: &str = "org.freedesktop.systemd1.Manager";
23
const BUS_NAME: &str = "org.freedesktop.systemd1";
24
const BUS_PATH: &str = "/org/freedesktop/systemd1";
25
26
pub struct Scope {
27
    connection: Connection,
28
    name: String,
29
}
30
31
fn properties(
32
    pid: u32,
33
    cores: u32,
34
    ram: Bytes,
35
    overrun: &Overrun,
36
) -> zbus::Result<Vec<(&'static str, Value<'static>)>> {
37
    let max = ram.0 as f64 * overrun.ram_slack.max(1.0);
38
    if !overrun.ram_slack.is_finite() || max >= u64::MAX as f64 {
39
        return Err(zbus::Error::Failure("RAM limit must be finite".into()));
40
    }
41
    let mut properties = vec![
42
        ("Description", Value::from("roundabout build")),
43
        ("PIDs", Value::from(vec![pid])),
44
        ("CollectMode", Value::from("inactive-or-failed")),
45
        ("CPUAccounting", Value::from(true)),
46
        ("MemoryAccounting", Value::from(true)),
47
        (
48
            "CPUQuotaPerSecUSec",
49
            Value::from(u64::from(cores.max(1)) * 1_000_000),
50
        ),
51
        ("MemoryMax", Value::from((max as u64).max(1))),
52
        ("OOMPolicy", Value::from("kill")),
53
        ("TimeoutStopUSec", Value::from(10_000_000_u64)),
54
    ];
55
    if overrun.ram == RamOverrun::Throttle {
56
        properties.push(("MemoryHigh", Value::from(ram.0.max(1))));
57
    }
58
    Ok(properties)
59
}
60
61
fn wait_job(mut signals: SignalStream<'_>, job: &OwnedObjectPath) -> zbus::Result<()> {
62
    async_io::block_on(futures_lite::future::race(
63
        async {
64
            while let Some(signal) = signals.next().await {
65
                let (_, path, _, result): (u32, OwnedObjectPath, String, String) =
66
                    signal.body().deserialize()?;
67
                if path == *job {
68
                    return if result == "done" {
69
                        Ok(())
70
                    } else {
71
                        Err(zbus::Error::Failure(format!("systemd job {result}")))
72
                    };
73
                }
74
            }
75
            Err(zbus::Error::Failure(
76
                "systemd disconnected before the job finished".into(),
77
            ))
78
        },
79
        async {
80
            async_io::Timer::after(Duration::from_secs(15)).await;
81
            Err(zbus::Error::Failure("systemd job timed out".into()))
82
        },
83
    ))
84
}
85
86
impl Scope {
87
    fn manager(&self) -> zbus::Result<Proxy<'_>> {
88
        Proxy::new(&self.connection, BUS_NAME, BUS_PATH, MANAGER)
89
    }
90
91
    fn start(
92
        connection: Connection,
93
        pid: u32,
94
        lease: u64,
95
        cores: u32,
96
        ram: Bytes,
97
        overrun: &Overrun,
98
    ) -> zbus::Result<Self> {
99
        let scope = Self {
100
            connection,
101
            name: format!("roundabout-{}-{lease}-{pid}.scope", std::process::id()),
102
        };
103
        let manager = scope.manager()?;
104
        let signals = async_io::block_on(
105
            manager
106
                .inner()
107
                .receive_signal_with_args("JobRemoved", &[(2, &scope.name)]),
108
        )?;
109
        let _: () = manager.call("Subscribe", &())?;
110
        let auxiliary: Vec<(&str, Vec<(&str, Value<'_>)>)> = Vec::new();
111
        let job: OwnedObjectPath = manager.call(
112
            "StartTransientUnit",
113
            &(
114
                &scope.name,
115
                "fail",
116
                properties(pid, cores, ram, overrun)?,
117
                auxiliary,
118
            ),
119
        )?;
120
        wait_job(signals, &job)?;
121
        drop(manager);
122
        Ok(scope)
123
    }
124
125
    pub fn signal(&self, signal: i32) -> zbus::Result<()> {
126
        self.manager()?
127
            .call("KillUnit", &(&self.name, "all", signal))
128
    }
129
130
    fn stop(&self) -> zbus::Result<()> {
131
        let manager = self.manager()?;
132
        let signals = async_io::block_on(
133
            manager
134
                .inner()
135
                .receive_signal_with_args("JobRemoved", &[(2, &self.name)]),
136
        )?;
137
        let job: OwnedObjectPath = manager.call("StopUnit", &(&self.name, "replace"))?;
138
        wait_job(signals, &job)
139
    }
140
}
141
142
impl Drop for Scope {
143
    fn drop(&mut self) {
144
        let _ = self.stop();
145
    }
146
}
147
148
fn gate() -> io::Result<(OwnedFd, OwnedFd)> {
149
    let mut descriptors = [-1; 2];
150
    if unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
151
        return Err(io::Error::last_os_error());
152
    }
153
    Ok(unsafe {
154
        (
155
            OwnedFd::from_raw_fd(descriptors[0]),
156
            OwnedFd::from_raw_fd(descriptors[1]),
157
        )
158
    })
159
}
160
161
pub fn spawn(
162
    command: &Command,
163
    lease: u64,
164
    cores: u32,
165
    ram: Bytes,
166
    overrun: &Overrun,
167
) -> Result<(Child, Scope), String> {
168
    let connection =
169
        Connection::session().map_err(|error| format!("can't connect to the user bus, {error}"))?;
170
    let (read, write) = gate().map_err(|error| error.to_string())?;
171
    let descriptor = read.as_raw_fd();
172
    let mut runner = Command::new(env::current_exe().map_err(|error| error.to_string())?);
173
    runner
174
        .arg(EXEC)
175
        .arg(descriptor.to_string())
176
        .arg(command.get_program())
177
        .args(command.get_args())
178
        .process_group(0);
179
    for (name, value) in command.get_envs() {
180
        if let Some(value) = value {
181
            runner.env(name, value);
182
        } else {
183
            runner.env_remove(name);
184
        }
185
    }
186
    let parent = unsafe { libc::getpid() };
187
    unsafe {
188
        runner.pre_exec(move || {
189
            if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0
190
                || libc::fcntl(descriptor, libc::F_SETFD, 0) != 0
191
            {
192
                return Err(io::Error::last_os_error());
193
            }
194
            if libc::getppid() != parent {
195
                return Err(io::Error::other("parent exited before launch"));
196
            }
197
            Ok(())
198
        });
199
    }
200
    let mut child = runner.spawn().map_err(|error| error.to_string())?;
201
    drop(read);
202
    let result = Scope::start(connection, child.id(), lease, cores, ram, overrun)
203
        .map_err(|error| format!("can't start the transient scope, {error}"))
204
        .and_then(|scope| {
205
            if unsafe { libc::write(write.as_raw_fd(), b"1".as_ptr().cast(), 1) } != 1 {
206
                return Err(io::Error::last_os_error().to_string());
207
            }
208
            Ok(scope)
209
        });
210
    drop(write);
211
    match result {
212
        Ok(scope) => Ok((child, scope)),
213
        Err(error) => {
214
            let _ = child.kill();
215
            let _ = child.wait();
216
            Err(error)
217
        }
218
    }
219
}
220
221
pub fn exec() -> Option<i32> {
222
    let mut arguments = env::args_os().skip(1);
223
    if arguments.next()?.as_encoded_bytes() != EXEC.as_bytes() {
224
        return None;
225
    }
226
    let Some(descriptor) = arguments
227
        .next()
228
        .and_then(|arg| arg.to_str()?.parse::<i32>().ok())
229
        .filter(|fd| *fd >= 3)
230
    else {
231
        return Some(1);
232
    };
233
    let descriptor = unsafe { OwnedFd::from_raw_fd(descriptor) };
234
    let mut byte = [0_u8];
235
    loop {
236
        let count = unsafe { libc::read(descriptor.as_raw_fd(), byte.as_mut_ptr().cast(), 1) };
237
        if count == 1 && byte[0] == b'1' {
238
            break;
239
        }
240
        if count != -1 || io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
241
            return Some(1);
242
        }
243
    }
244
    drop(descriptor);
245
    let Some(program) = arguments.next() else {
246
        return Some(1);
247
    };
248
    let error = Command::new(&program).args(arguments).exec();
249
    eprintln!(
250
        "roundabout: couldn't start {}: {error}",
251
        program.to_string_lossy()
252
    );
253
    Some(127)
254
}
255
256
#[cfg(test)]
257
mod tests {
258
    use super::*;
259
260
    #[test]
261
    fn limits_follow_the_grant_with_memory_headroom() {
262
        let overrun = Overrun::default();
263
        let properties = properties(123, 8, Bytes::gib(4), &overrun).unwrap();
264
        assert!(properties.contains(&("PIDs", Value::from(vec![123_u32]))));
265
        assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(8_000_000_u64))));
266
        assert!(properties.contains(&("MemoryMax", Value::from(Bytes::gib(5).0))));
267
        assert!(!properties.iter().any(|(name, _)| *name == "MemoryHigh"));
268
    }
269
270
    #[test]
271
    fn throttle_starts_at_the_granted_memory() {
272
        let overrun = Overrun {
273
            ram: RamOverrun::Throttle,
274
            ..Overrun::default()
275
        };
276
        let properties = properties(123, 2, Bytes::gib(2), &overrun).unwrap();
277
        assert!(properties.contains(&("MemoryHigh", Value::from(Bytes::gib(2).0))));
278
        assert!(properties.contains(&("MemoryMax", Value::from(Bytes::mib(2560).0))));
279
    }
280
281
    #[test]
282
    fn small_grants_never_become_unlimited() {
283
        let overrun = Overrun {
284
            ram_slack: 0.5,
285
            ..Overrun::default()
286
        };
287
        let properties = properties(123, 0, Bytes::ZERO, &overrun).unwrap();
288
        assert!(properties.contains(&("CPUQuotaPerSecUSec", Value::from(1_000_000_u64))));
289
        assert!(properties.contains(&("MemoryMax", Value::from(1_u64))));
290
    }
291
292
    #[test]
293
    fn nonfinite_limits_are_rejected() {
294
        for ram_slack in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, f64::MAX] {
295
            let overrun = Overrun {
296
                ram_slack,
297
                ..Overrun::default()
298
            };
299
            assert!(properties(123, 1, Bytes::gib(2), &overrun).is_err());
300
        }
301
    }
302
}