Commit 56a853ebf254
tarball3 files changed+111-19
@@ -35,7 +35,7 @@ the plugin brings a skill and hooks. unwrapped builds get bounced back with inst
3535
3636
## config3737
38
-`~/.config/roundabout/config.toml`, falling back to `/etc/roundabout/config.toml`, or `$ROUNDABOUT_CONFIG` over both. everything's optional, these are the defaults38
+`~/.config/roundabout/config.toml`, falling back to `/etc/roundabout/config.toml`, or `$ROUNDABOUT_CONFIG` over both. the daemon picks up edits on save, on linux anyway. a broken config gets complained about and ignored rather than taking every agent down with it. everything's optional, these are the defaults3939
4040
```toml4141
[budget]@@ -70,24 +70,53 @@ fn lock(shared: &Shared) -> MutexGuard<'_, State> {
7070
7171
impl State {7272
fn new(cfg: Config) -> Self {73
- let (total, live) = sys::memory();74
- let cap = Capacity {75
- ram_budget: cfg.budget.ram.resolve(total),76
- ram_live: live,77
- ram_reserve: cfg.budget.ram_reserve,78
- cores: cfg.budget.cores.resolve(sys::cores()),79
- disks: BTreeMap::new(),80
- pressure: false,81
- };82
- Self {73
+ let mut st = Self {8374
cfg,8475
next: 1,8576
leases: BTreeMap::new(),86
- cap,77
+ cap: Capacity {78
+ ram_budget: Bytes::ZERO,79
+ ram_live: Bytes::ZERO,80
+ ram_reserve: Bytes::ZERO,81
+ cores: 1,82
+ disks: BTreeMap::new(),83
+ pressure: false,84
+ },8785
baselines: BTreeMap::new(),8886
conns: 0,8987
idle_since: Instant::now(),88
+ };89
+ st.apply_budget();90
+ st91
+ }92
+93
+ fn apply_budget(&mut self) {94
+ let (total, live) = sys::memory();95
+ self.cap.ram_budget = self.cfg.budget.ram.resolve(total);96
+ self.cap.ram_live = live;97
+ self.cap.ram_reserve = self.cfg.budget.ram_reserve;98
+ self.cap.cores = self.cfg.budget.cores.resolve(sys::cores());99
+ }100
+101
+ /// running leases keep the heaviness they started with, so a reload can't102
+ /// retroactively put two heavies side by side103
+ fn reload(&mut self, cfg: Config) {104
+ self.cfg = cfg;105
+ self.apply_budget();106
+ let queued: Vec<(LeaseId, Need)> = self107
+ .leases108
+ .iter()109
+ .filter(|(_, l)| l.run.is_none())110
+ .map(|(id, l)| (*id, l.req.need))111
+ .collect();112
+ for (id, need) in queued {113
+ let heavy = self.heavy(&need);114
+ if let Some(l) = self.leases.get_mut(&id) {115
+ l.heavy = heavy;116
+ }90117
}118
+ self.refresh();119
+ self.reschedule();91120
}92121
93122
fn heavy(&self, need: &Need) -> bool {@@ -470,6 +499,65 @@ fn tick(shared: &Shared) {
470499
}471500
}472501
502
+/// watches the directory, not the file. editors save by renaming over it,503
+/// which would orphan a watch on the old inode after the first edit504
+#[cfg(target_os = "linux")]505
+fn watch_config(shared: &Shared) {506
+ use std::os::unix::ffi::OsStrExt;507
+508
+ let path = paths::config_file();509
+ let (Some(dir), Some(name)) = (path.parent(), path.file_name()) else {510
+ return;511
+ };512
+ let _ = fs::create_dir_all(dir);513
+ let Ok(cdir) = std::ffi::CString::new(dir.as_os_str().as_bytes()) else {514
+ return;515
+ };516
+ let fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC) };517
+ if fd < 0 {518
+ return;519
+ }520
+ let mask = libc::IN_CLOSE_WRITE521
+ | libc::IN_MOVED_TO522
+ | libc::IN_MOVED_FROM523
+ | libc::IN_CREATE524
+ | libc::IN_DELETE;525
+ if unsafe { libc::inotify_add_watch(fd, cdir.as_ptr(), mask) } < 0 {526
+ return;527
+ }528
+529
+ let header = std::mem::size_of::<libc::inotify_event>();530
+ let mut buf = [0u8; 4096];531
+ loop {532
+ let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };533
+ let Ok(n) = usize::try_from(n) else {534
+ return;535
+ };536
+ let mut hit = false;537
+ let mut off = 0;538
+ while off + header <= n {539
+ let ev: libc::inotify_event =540
+ unsafe { std::ptr::read_unaligned(buf.as_ptr().add(off).cast()) };541
+ let len = ev.len as usize;542
+ let raw = &buf[off + header..(off + header + len).min(n)];543
+ let ev_name = raw.split(|b| *b == 0).next().unwrap_or_default();544
+ hit |= ev_name == name.as_bytes();545
+ off += header + len;546
+ }547
+ if !hit {548
+ continue;549
+ }550
+ thread::sleep(Duration::from_millis(100));551
+ match Config::load() {552
+ Ok(cfg) => {553
+ lock(shared).reload(cfg);554
+ eprintln!("roundabout: config reloaded");555
+ }556
+ Err(e) => eprintln!("roundabout: kept the old config, {e}"),557
+ }558
+ }559
+}560
+473561
fn try_spawn_lock() -> Option<client::Flock> {474562
let file = fs::OpenOptions::new()475563
.write(true)@@ -506,6 +594,11 @@ pub fn main(cfg: Config) -> ! {
506594
let shared = shared.clone();507595
thread::spawn(move || tick(&shared));508596
}597
+ #[cfg(target_os = "linux")]598
+ {599
+ let shared = shared.clone();600
+ thread::spawn(move || watch_config(&shared));601
+ }509602
510603
for stream in listener.incoming() {511604
let Ok(stream) = stream else { continue };@@ -16,12 +16,11 @@ mod units;
1616
use pound::Parse;1717
1818
fn main() {19
- let cfg = match config::Config::load() {20
- Ok(c) => c,21
- Err(e) => {22
- eprintln!("rabt: bad config, {e}");23
- std::process::exit(2);24
- }25
- };19
+ // a typo must not exit 2, that's a blocking pretooluse verdict and would20
+ // wall off every bash call in every session21
+ let cfg = config::Config::load().unwrap_or_else(|e| {22
+ eprintln!("rabt: bad config, using defaults. {e}");23
+ config::Config::default()24
+ });2625
std::process::exit(cli::dispatch(&cfg, cli::Rabt::parse()));2726
}