| 1 | #![cfg(target_os = "linux")] |
| 2 | |
| 3 | use std::{ |
| 4 | fs, |
| 5 | io::Write, |
| 6 | os::fd::{AsRawFd, FromRawFd, OwnedFd}, |
| 7 | os::unix::process::CommandExt, |
| 8 | process::{Child, Command, Stdio}, |
| 9 | time::Duration, |
| 10 | }; |
| 11 | |
| 12 | fn gated_command(program: &str, arguments: &[&str]) -> (Child, OwnedFd) { |
| 13 | let mut descriptors = [-1; 2]; |
| 14 | assert_eq!( |
| 15 | unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) }, |
| 16 | 0 |
| 17 | ); |
| 18 | let read = unsafe { OwnedFd::from_raw_fd(descriptors[0]) }; |
| 19 | let write = unsafe { OwnedFd::from_raw_fd(descriptors[1]) }; |
| 20 | let descriptor = read.as_raw_fd(); |
| 21 | let mut command = Command::new(env!("CARGO_BIN_EXE_rabt")); |
| 22 | command |
| 23 | .args(["__roundabout_exec", &descriptor.to_string(), program]) |
| 24 | .args(arguments) |
| 25 | .stdout(Stdio::piped()) |
| 26 | .stderr(Stdio::piped()); |
| 27 | unsafe { |
| 28 | command.pre_exec(move || { |
| 29 | if libc::fcntl(descriptor, libc::F_SETFD, 0) == -1 { |
| 30 | return Err(std::io::Error::last_os_error()); |
| 31 | } |
| 32 | Ok(()) |
| 33 | }); |
| 34 | } |
| 35 | let child = command.spawn().unwrap(); |
| 36 | drop(read); |
| 37 | (child, write) |
| 38 | } |
| 39 | |
| 40 | #[test] |
| 41 | fn command_waits_for_scope_setup() { |
| 42 | let (mut child, gate) = gated_command(env!("CARGO_BIN_EXE_rabt"), &["--version"]); |
| 43 | let mut output = libc::pollfd { |
| 44 | fd: child.stdout.as_ref().unwrap().as_raw_fd(), |
| 45 | events: libc::POLLIN, |
| 46 | revents: 0, |
| 47 | }; |
| 48 | assert_eq!(unsafe { libc::poll(&raw mut output, 1, 100) }, 0); |
| 49 | assert!(child.try_wait().unwrap().is_none()); |
| 50 | fs::File::from(gate).write_all(b"1").unwrap(); |
| 51 | let output = child.wait_with_output().unwrap(); |
| 52 | assert!(output.status.success()); |
| 53 | assert!( |
| 54 | String::from_utf8(output.stdout) |
| 55 | .unwrap() |
| 56 | .starts_with("rabt ") |
| 57 | ); |
| 58 | } |
| 59 | |
| 60 | #[test] |
| 61 | fn failed_scope_setup_never_executes_the_command() { |
| 62 | let (child, gate) = gated_command(env!("CARGO_BIN_EXE_rabt"), &["--version"]); |
| 63 | drop(gate); |
| 64 | let output = child.wait_with_output().unwrap(); |
| 65 | assert_eq!(output.status.code(), Some(1)); |
| 66 | assert!(output.stdout.is_empty()); |
| 67 | } |
| 68 | |
| 69 | #[test] |
| 70 | fn missing_commands_keep_exit_127() { |
| 71 | let (child, gate) = gated_command("/roundabout-missing-command", &[]); |
| 72 | fs::File::from(gate).write_all(b"1").unwrap(); |
| 73 | assert_eq!(child.wait_with_output().unwrap().status.code(), Some(127)); |
| 74 | } |
| 75 | |
| 76 | #[test] |
| 77 | #[ignore = "requires a systemd user manager and cgroup v2"] |
| 78 | fn transient_scope_enforces_the_grant() { |
| 79 | if std::env::var_os("ROUNDABOUT_SCOPE_PROBE").is_some() { |
| 80 | let membership = fs::read_to_string("/proc/self/cgroup").unwrap(); |
| 81 | let path = membership |
| 82 | .lines() |
| 83 | .find_map(|line| line.strip_prefix("0::")) |
| 84 | .unwrap(); |
| 85 | assert!(path.contains("roundabout-")); |
| 86 | let cgroup = std::path::Path::new("/sys/fs/cgroup").join(path.trim_start_matches('/')); |
| 87 | let cpu = fs::read_to_string(cgroup.join("cpu.max")).unwrap(); |
| 88 | let limits: Vec<_> = cpu.split_whitespace().collect(); |
| 89 | assert_eq!(limits[0], limits[1]); |
| 90 | assert_eq!( |
| 91 | fs::read_to_string(cgroup.join("memory.max")) |
| 92 | .unwrap() |
| 93 | .trim(), |
| 94 | (80 * 1024 * 1024).to_string() |
| 95 | ); |
| 96 | assert_eq!( |
| 97 | fs::read_to_string(cgroup.join("memory.high")) |
| 98 | .unwrap() |
| 99 | .trim(), |
| 100 | (64 * 1024 * 1024).to_string() |
| 101 | ); |
| 102 | assert_eq!( |
| 103 | fs::read_to_string(cgroup.join("memory.oom.group")) |
| 104 | .unwrap() |
| 105 | .trim(), |
| 106 | "1" |
| 107 | ); |
| 108 | assert_eq!(std::env::var("CARGO_BUILD_JOBS").unwrap(), "1"); |
| 109 | assert!(std::env::var("ROUNDABOUT_LEASE").is_ok()); |
| 110 | return; |
| 111 | } |
| 112 | let directory = |
| 113 | std::env::temp_dir().join(format!("roundabout-scope-test-{}", std::process::id())); |
| 114 | fs::create_dir_all(&directory).unwrap(); |
| 115 | let config = directory.join("config.toml"); |
| 116 | fs::write( |
| 117 | &config, |
| 118 | "[overrun]\nram = \"throttle\"\n[queue]\nlinger = \"0s\"\n", |
| 119 | ) |
| 120 | .unwrap(); |
| 121 | let output = Command::new(env!("CARGO_BIN_EXE_rabt")) |
| 122 | .args([ |
| 123 | "run", "--ram", "64M", "--disk", "0", "--cores", "1", "--time", "5s", "--exact", "--", |
| 124 | ]) |
| 125 | .arg(std::env::current_exe().unwrap()) |
| 126 | .args([ |
| 127 | "--exact", |
| 128 | "transient_scope_enforces_the_grant", |
| 129 | "--ignored", |
| 130 | "--nocapture", |
| 131 | ]) |
| 132 | .current_dir(&directory) |
| 133 | .env("ROUNDABOUT_CONFIG", &config) |
| 134 | .env("ROUNDABOUT_RUNTIME_DIR", directory.join("runtime")) |
| 135 | .env("ROUNDABOUT_STATE_DIR", directory.join("state")) |
| 136 | .env("ROUNDABOUT_SCOPE_PROBE", "1") |
| 137 | .output() |
| 138 | .unwrap(); |
| 139 | std::thread::sleep(Duration::from_secs(2)); |
| 140 | fs::remove_dir_all(directory).unwrap(); |
| 141 | assert!( |
| 142 | output.status.success(), |
| 143 | "{}\n{}", |
| 144 | String::from_utf8_lossy(&output.stdout), |
| 145 | String::from_utf8_lossy(&output.stderr) |
| 146 | ); |
| 147 | } |