| 1 | use std::{ |
| 2 | env, |
| 3 | fmt::Write as _, |
| 4 | os::unix::{net::UnixStream, process::CommandExt}, |
| 5 | process::{Command, ExitStatus}, |
| 6 | sync::{ |
| 7 | Arc, |
| 8 | atomic::{AtomicBool, AtomicI32, Ordering}, |
| 9 | }, |
| 10 | thread, |
| 11 | time::{Duration, Instant}, |
| 12 | }; |
| 13 | |
| 14 | use crate::{ |
| 15 | artifacts, client, |
| 16 | config::{Config, OverCap, RamOverrun}, |
| 17 | history, paths, |
| 18 | proto::{self, Acquire, Event, Reader, Request}, |
| 19 | sched::Need, |
| 20 | sys, |
| 21 | units::{Amount, Bytes, Dur}, |
| 22 | }; |
| 23 | |
| 24 | pub const EXIT_TIMEOUT: i32 = 124; |
| 25 | pub const EXIT_GAVE_UP: i32 = 75; |
| 26 | |
| 27 | pub struct Args { |
| 28 | pub size: Option<String>, |
| 29 | pub ram: Option<Bytes>, |
| 30 | pub disk: Option<Bytes>, |
| 31 | pub cores: Option<Amount<u32>>, |
| 32 | pub time: Option<Dur>, |
| 33 | pub deadline: Option<Dur>, |
| 34 | pub max_wait: Option<Dur>, |
| 35 | pub session: Option<String>, |
| 36 | pub cmd: Vec<String>, |
| 37 | } |
| 38 | |
| 39 | static PGID: AtomicI32 = AtomicI32::new(0); |
| 40 | |
| 41 | extern "C" fn forward(sig: libc::c_int) { |
| 42 | let pgid = PGID.load(Ordering::Relaxed); |
| 43 | if pgid > 1 { |
| 44 | unsafe { libc::kill(-pgid, sig) }; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | fn say(msg: &str) { |
| 49 | eprintln!("rabt: {msg}"); |
| 50 | } |
| 51 | |
| 52 | fn need(cfg: &Config, a: &Args) -> Result<Need, String> { |
| 53 | let base = cfg.preset(a.size.as_deref().unwrap_or(&cfg.enforce.default_size))?; |
| 54 | Ok(Need { |
| 55 | ram: a.ram.unwrap_or(base.ram), |
| 56 | disk: a.disk.unwrap_or(base.disk), |
| 57 | cores: a.cores.unwrap_or(base.cores).resolve(sys::cores()), |
| 58 | time: a.time.unwrap_or(base.time).0, |
| 59 | }) |
| 60 | } |
| 61 | |
| 62 | pub fn base(cfg: &Config) -> Result<Need, String> { |
| 63 | let p = cfg.preset(&cfg.enforce.default_size)?; |
| 64 | Ok(Need { |
| 65 | ram: p.ram, |
| 66 | disk: p.disk, |
| 67 | cores: p.cores.resolve(sys::cores()), |
| 68 | time: p.time.0, |
| 69 | }) |
| 70 | } |
| 71 | |
| 72 | struct Grant { |
| 73 | stream: UnixStream, |
| 74 | lease: u64, |
| 75 | cores: u32, |
| 76 | } |
| 77 | |
| 78 | fn acquire(req: &Acquire, max_wait: Option<Dur>) -> Result<Grant, i32> { |
| 79 | let started = Instant::now(); |
| 80 | // a daemon on its way out can accept us then hang up, so go round again |
| 81 | for _ in 0..3 { |
| 82 | let mut stream = client::connect_or_spawn().map_err(|e| { |
| 83 | say(&format!("can't reach the daemon: {e}")); |
| 84 | 1 |
| 85 | })?; |
| 86 | if proto::send(&mut stream, &Request::Acquire(req.clone())).is_err() { |
| 87 | continue; |
| 88 | } |
| 89 | let _ = stream.set_read_timeout(Some(Duration::from_secs(1))); |
| 90 | let mut reader = Reader::new(&stream).map_err(|_| 1)?; |
| 91 | loop { |
| 92 | match reader.recv::<Event>() { |
| 93 | Ok(Some(Event::Granted { |
| 94 | lease, |
| 95 | cores, |
| 96 | heavy, |
| 97 | })) => { |
| 98 | say(&format!( |
| 99 | "lease {lease} go, cores {cores}{}", |
| 100 | if heavy { ", heavy" } else { "" } |
| 101 | )); |
| 102 | let _ = stream.set_read_timeout(None); |
| 103 | return Ok(Grant { |
| 104 | stream, |
| 105 | lease, |
| 106 | cores, |
| 107 | }); |
| 108 | } |
| 109 | Ok(Some(Event::Queued { |
| 110 | lease, |
| 111 | position, |
| 112 | reason, |
| 113 | })) => { |
| 114 | let why = reason.map(|r| format!(", {r}")).unwrap_or_default(); |
| 115 | say(&format!("lease {lease} queued at #{position}{why}")); |
| 116 | } |
| 117 | Ok(Some(Event::Kill)) => { |
| 118 | say("killed while queued"); |
| 119 | return Err(143); |
| 120 | } |
| 121 | Ok(Some(_)) => {} |
| 122 | Err(e) |
| 123 | if matches!( |
| 124 | e.kind(), |
| 125 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut |
| 126 | ) => |
| 127 | { |
| 128 | if max_wait.is_some_and(|m| started.elapsed() > m.0) { |
| 129 | say("gave up waiting"); |
| 130 | return Err(EXIT_GAVE_UP); |
| 131 | } |
| 132 | } |
| 133 | Ok(None) | Err(_) => break, |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | say("daemon keeps hanging up"); |
| 138 | Err(1) |
| 139 | } |
| 140 | |
| 141 | fn has_systemd_run() -> bool { |
| 142 | env::var_os("PATH") |
| 143 | .is_some_and(|p| env::split_paths(&p).any(|d| d.join("systemd-run").exists())) |
| 144 | } |
| 145 | |
| 146 | fn command(cfg: &Config, a: &Args, need: &Need, grant: &Grant) -> Command { |
| 147 | let mut argv = a.cmd.clone(); |
| 148 | if cfg.overrun.ram == RamOverrun::Throttle && cfg!(target_os = "linux") && has_systemd_run() { |
| 149 | let high = Bytes((need.ram.0 as f64 * cfg.overrun.ram_slack) as u64); |
| 150 | let mut wrapped: Vec<String> = [ |
| 151 | "systemd-run", |
| 152 | "--user", |
| 153 | "--scope", |
| 154 | "--quiet", |
| 155 | "--collect", |
| 156 | "-p", |
| 157 | ] |
| 158 | .map(Into::into) |
| 159 | .into(); |
| 160 | wrapped.push(format!("MemoryHigh={}", high.0)); |
| 161 | wrapped.push("--".into()); |
| 162 | wrapped.append(&mut argv); |
| 163 | argv = wrapped; |
| 164 | } |
| 165 | |
| 166 | let n = grant.cores.to_string(); |
| 167 | let mut cmd = Command::new(&argv[0]); |
| 168 | cmd.args(&argv[1..]) |
| 169 | .process_group(0) |
| 170 | .env("ROUNDABOUT_LEASE", grant.lease.to_string()) |
| 171 | .env("CARGO_BUILD_JOBS", &n) |
| 172 | .env("CMAKE_BUILD_PARALLEL_LEVEL", &n); |
| 173 | |
| 174 | let make = env::var("MAKEFLAGS").unwrap_or_default(); |
| 175 | if !make.contains("-j") { |
| 176 | cmd.env("MAKEFLAGS", format!("-j{n} {make}").trim_end()); |
| 177 | } |
| 178 | |
| 179 | if cfg.nix.inject { |
| 180 | let per = cfg.nix.cores_per_job.max(1); |
| 181 | let jobs = (grant.cores / per).max(1); |
| 182 | let cores = grant.cores.min(per); |
| 183 | let mut nix = env::var("NIX_CONFIG").unwrap_or_default(); |
| 184 | let _ = write!(nix, "\ncores = {cores}\nmax-jobs = {jobs}\n"); |
| 185 | cmd.env("NIX_CONFIG", nix); |
| 186 | } |
| 187 | |
| 188 | #[cfg(target_os = "linux")] |
| 189 | unsafe { |
| 190 | cmd.pre_exec(|| { |
| 191 | libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM); |
| 192 | Ok(()) |
| 193 | }); |
| 194 | } |
| 195 | cmd |
| 196 | } |
| 197 | |
| 198 | fn cpu_time() -> Duration { |
| 199 | let mut ru = std::mem::MaybeUninit::<libc::rusage>::uninit(); |
| 200 | unsafe { |
| 201 | if libc::getrusage(libc::RUSAGE_CHILDREN, ru.as_mut_ptr()) != 0 { |
| 202 | return Duration::ZERO; |
| 203 | } |
| 204 | let ru = ru.assume_init(); |
| 205 | let tv = |t: libc::timeval| { |
| 206 | Duration::from_secs(t.tv_sec as u64) + Duration::from_micros(t.tv_usec as u64) |
| 207 | }; |
| 208 | tv(ru.ru_utime) + tv(ru.ru_stime) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | fn code(status: ExitStatus) -> i32 { |
| 213 | use std::os::unix::process::ExitStatusExt; |
| 214 | status |
| 215 | .code() |
| 216 | .or_else(|| status.signal().map(|s| 128 + s)) |
| 217 | .unwrap_or(1) |
| 218 | } |
| 219 | |
| 220 | pub fn run(cfg: &Config, a: &Args) -> i32 { |
| 221 | if a.cmd.is_empty() { |
| 222 | say("nothing to run, put the command after --"); |
| 223 | return 2; |
| 224 | } |
| 225 | if a.size.is_none() && a.ram.is_none() { |
| 226 | let sizes: Vec<_> = cfg.sizes.keys().map(String::as_str).collect(); |
| 227 | say(&format!( |
| 228 | "how big is it? pass --size {} or --ram and friends. `rabt suggest -- <cmd>` has a guess", |
| 229 | sizes.join("|") |
| 230 | )); |
| 231 | return 2; |
| 232 | } |
| 233 | let need = match need(cfg, a) { |
| 234 | Ok(n) => n, |
| 235 | Err(e) => { |
| 236 | say(&e); |
| 237 | return 2; |
| 238 | } |
| 239 | }; |
| 240 | let cwd = env::current_dir().unwrap_or_else(|_| "/".into()); |
| 241 | let root = paths::project_root(&cwd); |
| 242 | |
| 243 | let report = artifacts::check(cfg, Some(&root), false); |
| 244 | if report.over() { |
| 245 | let msg = report.describe(cfg); |
| 246 | if cfg.artifacts.over_cap == OverCap::Block { |
| 247 | say(&format!("{msg}. clean up first, `rabt artifacts gc --cap`")); |
| 248 | return EXIT_GAVE_UP; |
| 249 | } |
| 250 | say(&format!( |
| 251 | "{msg}. `rabt artifacts gc --cap` when you get a sec" |
| 252 | )); |
| 253 | } |
| 254 | |
| 255 | let req = Acquire { |
| 256 | need, |
| 257 | cwd, |
| 258 | cmd: a.cmd.clone(), |
| 259 | session: a.session.clone(), |
| 260 | deadline: a.deadline, |
| 261 | }; |
| 262 | |
| 263 | let grant = match acquire(&req, a.max_wait.or(cfg.queue.max_wait)) { |
| 264 | Ok(g) => g, |
| 265 | Err(code) => return code, |
| 266 | }; |
| 267 | |
| 268 | let mut child = match command(cfg, a, &need, &grant).spawn() { |
| 269 | Ok(c) => c, |
| 270 | Err(e) => { |
| 271 | say(&format!("couldn't start {}: {e}", a.cmd[0])); |
| 272 | return 127; |
| 273 | } |
| 274 | }; |
| 275 | let pgid = i32::try_from(child.id()).expect("pid fits in pid_t"); |
| 276 | PGID.store(pgid, Ordering::Relaxed); |
| 277 | for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] { |
| 278 | unsafe { libc::signal(sig, forward as *const () as libc::sighandler_t) }; |
| 279 | } |
| 280 | |
| 281 | let mut out = grant.stream.try_clone().ok(); |
| 282 | if let Some(s) = &mut out { |
| 283 | let _ = proto::send(s, &Request::Started { pid: pgid }); |
| 284 | } |
| 285 | |
| 286 | let killed = Arc::new(AtomicBool::new(false)); |
| 287 | if let Ok(mut reader) = Reader::new(&grant.stream) { |
| 288 | let killed = killed.clone(); |
| 289 | thread::spawn(move || { |
| 290 | while let Ok(Some(ev)) = reader.recv::<Event>() { |
| 291 | if matches!(ev, Event::Kill) { |
| 292 | killed.store(true, Ordering::Relaxed); |
| 293 | } |
| 294 | } |
| 295 | }); |
| 296 | } |
| 297 | |
| 298 | let started = Instant::now(); |
| 299 | let grace = need.time.mul_f64(cfg.overrun.time_grace); |
| 300 | let loud_ram = Bytes((need.ram.0 as f64 * cfg.overrun.ram_slack) as u64); |
| 301 | let mut peak = Bytes::ZERO; |
| 302 | let mut last_sample: Option<Instant> = None; |
| 303 | let mut term_sent: Option<Instant> = None; |
| 304 | let mut timed_out = false; |
| 305 | let mut warned_ram = false; |
| 306 | let mut warned_time = false; |
| 307 | |
| 308 | let status = loop { |
| 309 | match child.try_wait() { |
| 310 | Ok(Some(status)) => break status, |
| 311 | Ok(None) => {} |
| 312 | Err(e) => { |
| 313 | say(&format!("lost track of the child: {e}")); |
| 314 | return 1; |
| 315 | } |
| 316 | } |
| 317 | let elapsed = started.elapsed(); |
| 318 | |
| 319 | if term_sent.is_none() { |
| 320 | if killed.load(Ordering::Relaxed) { |
| 321 | say("killed on request"); |
| 322 | term_sent = Some(Instant::now()); |
| 323 | } else if a.deadline.is_some_and(|d| elapsed > d.0) { |
| 324 | say("deadline hit, killing it"); |
| 325 | timed_out = true; |
| 326 | term_sent = Some(Instant::now()); |
| 327 | } |
| 328 | if term_sent.is_some() { |
| 329 | unsafe { libc::kill(-pgid, libc::SIGTERM) }; |
| 330 | } |
| 331 | } else if term_sent.is_some_and(|t| t.elapsed() > Duration::from_secs(10)) { |
| 332 | unsafe { libc::kill(-pgid, libc::SIGKILL) }; |
| 333 | } |
| 334 | |
| 335 | if last_sample.is_none_or(|t| t.elapsed() >= Duration::from_secs(2)) { |
| 336 | last_sample = Some(Instant::now()); |
| 337 | let rss = sys::group_rss(pgid); |
| 338 | peak = peak.max(rss); |
| 339 | if let Some(s) = &mut out { |
| 340 | let _ = proto::send(s, &Request::Usage { rss }); |
| 341 | } |
| 342 | if !warned_ram && rss > loud_ram { |
| 343 | warned_ram = true; |
| 344 | say(&format!("using {rss} against a {} estimate", need.ram)); |
| 345 | } |
| 346 | } |
| 347 | if !warned_time && elapsed > grace { |
| 348 | warned_time = true; |
| 349 | say(&format!( |
| 350 | "{} past its {} estimate and still going", |
| 351 | Dur(elapsed), |
| 352 | Dur(need.time) |
| 353 | )); |
| 354 | } |
| 355 | thread::sleep(Duration::from_millis(250)); |
| 356 | }; |
| 357 | |
| 358 | let wall = started.elapsed(); |
| 359 | if let Some(s) = &mut out { |
| 360 | let _ = proto::send(s, &Request::Done); |
| 361 | } |
| 362 | history::record( |
| 363 | history::key(&root, &a.cmd), |
| 364 | peak, |
| 365 | wall, |
| 366 | cpu_time(), |
| 367 | grant.cores, |
| 368 | ); |
| 369 | artifacts::register(cfg, &root, true); |
| 370 | if peak > loud_ram || wall > grace { |
| 371 | say(&format!( |
| 372 | "took {} and peaked at {}, asked for {} and {}. ask for more next time", |
| 373 | Dur(wall), |
| 374 | peak, |
| 375 | Dur(need.time), |
| 376 | need.ram |
| 377 | )); |
| 378 | } |
| 379 | if timed_out { |
| 380 | EXIT_TIMEOUT |
| 381 | } else { |
| 382 | code(status) |
| 383 | } |
| 384 | } |