use std::path::{Path, PathBuf};
pub struct Config {
pub dir: PathBuf,
pub build_slots: usize,
pub gpu_slots: usize,
pub now_slots: usize,
pub now_mem_gb: u32,
pub jobs: u32,
pub cpu_percent: u32,
pub mem_gb: u32,
pub min_free_gb: u32,
pub night_from: u32,
pub night_until: u32,
pub night_build_slots: usize,
pub night_mem_gb: u32,
pub night_min_free_gb: u32,
pub night: bool,
pub switch_clean: u32,
pub keep_bins: u32,
pub capture: u32,
pub solo_minutes: u32,
}
const KEYS: [&str; 17] = [
"build_slots",
"gpu_slots",
"now_slots",
"now_mem_gb",
"jobs",
"cpu_percent",
"mem_gb",
"min_free_gb",
"night_from",
"night_until",
"night_build_slots",
"night_mem_gb",
"night_min_free_gb",
"switch_clean",
"keep_bins",
"capture",
"solo_minutes",
];
impl Config {
pub fn load() -> Config {
let dir = std::env::var_os("FGATE_DIR")
.map(PathBuf::from)
.unwrap_or_else(crate::sys::settings_dir);
let total_gb = (crate::sys::memory().0 as f64 / (1u64 << 30) as f64).round() as u32;
let mut c = Config {
dir,
build_slots: 1,
gpu_slots: 1,
now_slots: 4,
now_mem_gb: 2,
jobs: 6,
cpu_percent: 40,
mem_gb: (total_gb * 2 / 5).max(8),
min_free_gb: 6,
night_from: 23,
night_until: 7,
night_build_slots: 1,
night_mem_gb: 10,
night_min_free_gb: 8,
night: false,
switch_clean: 1,
keep_bins: 1,
capture: 1,
solo_minutes: 10,
};
let file = c.file();
match std::fs::read_to_string(&file) {
Ok(text) => {
for line in text.lines() {
let line = line.split('#').next().unwrap_or("").trim();
if let Some((key, value)) = line.split_once('=') {
c.set(key.trim(), value.trim());
}
}
let mut more = String::new();
if !text.contains("night_from") {
more += &c.night_text();
}
if !text.contains("now_slots") {
more += &c.now_text();
}
if !text.contains("switch_clean") {
more += &c.switch_text();
}
if !text.contains("keep_bins") {
more += &c.keep_text();
}
if !text.contains("capture") {
more += &c.capture_text();
}
if !text.contains("solo_minutes") {
more += &c.solo_text();
}
if !more.is_empty() {
let gap = if text.ends_with('\n') { "" } else { "\n" };
let _ = std::fs::write(&file, format!("{text}{gap}{more}"));
}
}
Err(_) => c.write_default(&file),
}
for key in KEYS {
if let Ok(value) = std::env::var(format!("FGATE_{}", key.to_uppercase())) {
c.set(key, &value);
}
}
c.night = is_night(crate::sys::local_hour(), c.night_from, c.night_until);
if c.night {
c.build_slots = c.night_build_slots;
c.mem_gb = c.night_mem_gb;
c.min_free_gb = c.night_min_free_gb;
}
c.build_slots = c.build_slots.max(1);
c.gpu_slots = c.gpu_slots.max(1);
c.now_slots = c.now_slots.max(1);
c
}
pub fn mem_gb_for(&self, class: crate::gate::Class) -> u32 {
match class {
crate::gate::Class::Now => self.now_mem_gb,
_ => self.mem_gb,
}
}
pub fn file(&self) -> PathBuf {
self.dir.join("config.txt")
}
fn set(&mut self, key: &str, value: &str) {
let Ok(n) = value.parse::<u32>() else {
warn(&format!(
"ignoring setting {key} = {value} (not a whole number)"
));
return;
};
match key {
"build_slots" => self.build_slots = n as usize,
"gpu_slots" => self.gpu_slots = n as usize,
"now_slots" => self.now_slots = n as usize,
"now_mem_gb" => self.now_mem_gb = n,
"jobs" => self.jobs = n.max(1),
"cpu_percent" => self.cpu_percent = n.min(100),
"mem_gb" => self.mem_gb = n,
"min_free_gb" => self.min_free_gb = n,
"night_from" => self.night_from = n.min(24),
"night_until" => self.night_until = n.min(24),
"night_build_slots" => self.night_build_slots = n as usize,
"night_mem_gb" => self.night_mem_gb = n,
"night_min_free_gb" => self.night_min_free_gb = n,
"switch_clean" => self.switch_clean = n.min(1),
"keep_bins" => self.keep_bins = n.min(1),
"capture" => self.capture = n.min(1),
"solo_minutes" => self.solo_minutes = n,
_ => warn(&format!("unknown setting {key}")),
}
}
fn night_text(&self) -> String {
format!(
"# Night limits, local hours night_from until night_until (0 and 24: always). At night a command\n\
# also waits for free memory when it would run alone, so unattended agents can't run out of memory.\n\
night_from = {}\n\
night_until = {}\n\
night_build_slots = {}\n\
night_mem_gb = {}\n\
night_min_free_gb = {}\n",
self.night_from,
self.night_until,
self.night_build_slots,
self.night_mem_gb,
self.night_min_free_gb
)
}
fn now_text(&self) -> String {
format!(
"# Small commands that skip the line (`fgate --now`): how many at once, and each one's memory ceiling\n\
# in GB. One that needs more is stopped by the ceiling and belongs in the line.\n\
now_slots = {}\n\
now_mem_gb = {}\n",
self.now_slots, self.now_mem_gb
)
}
fn switch_text(&self) -> String {
format!(
"# Checkouts sharing one cargo target directory (CARGO_TARGET_DIR): before a build, clean this workspace's\n\
# own packages there when another checkout built that profile last, so cargo never reuses its crates\n\
# (1 = on, 0 = off):\n\
switch_clean = {}\n",
self.switch_clean
)
}
fn keep_text(&self) -> String {
format!(
"# After a build into a shared target directory, copy this workspace's executables into the checkout,\n\
# target/fgate-bin/<profile>/, where the next checkout's clean can't delete them (1 = on, 0 = off):\n\
keep_bins = {}\n",
self.keep_bins
)
}
fn capture_text(&self) -> String {
format!(
"# Keep the last lines of each gated command's output, for `fgate peek` and `fgate watch`, when it goes to\n\
# a pipe or file (an agent's shell) rather than a terminal (1 = on, 0 = off):\n\
capture = {}\n",
self.capture
)
}
fn solo_text(&self) -> String {
format!(
"# A solo run (`fgate --solo`, such as a performance measurement) has the machine to itself: nothing else\n\
# starts while it runs. After this many minutes it hands the machine back and others run beside it\n\
# (0 = never):\n\
solo_minutes = {}\n",
self.solo_minutes
)
}
fn write_default(&self, file: &Path) {
let text = format!(
"# fgate settings for this machine (https://github.com/alexander-frase/fgate). Every agent reads this file,\n\
# so all count the same slots.\n\
# Builds and test runs at once:\n\
build_slots = {}\n\
# GPU runs at once (games, renderers, screenshot scripts):\n\
gpu_slots = {}\n\
# Compiler jobs for each gated cargo or cmake command (CARGO_BUILD_JOBS, CMAKE_BUILD_PARALLEL_LEVEL) unless it sets its own:\n\
jobs = {}\n\
# Each gated command's hard CPU ceiling, percent of all threads (0 = none):\n\
cpu_percent = {}\n\
# Each gated command's memory ceiling in GB, everything it starts together (0 = none):\n\
mem_gb = {}\n\
# A second command waits while less than this many GB are free:\n\
min_free_gb = {}\n{}{}{}{}{}{}",
self.build_slots,
self.gpu_slots,
self.jobs,
self.cpu_percent,
self.mem_gb,
self.min_free_gb,
self.night_text(),
self.now_text(),
self.switch_text(),
self.keep_text(),
self.capture_text(),
self.solo_text()
);
let _ = std::fs::create_dir_all(&self.dir);
let _ = std::fs::write(file, text);
}
}
fn warn(text: &str) {
static SAID: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
if let Ok(mut said) = SAID.lock() {
if !said.iter().any(|s| s == text) {
eprintln!("fgate: {text}");
said.push(text.to_string());
}
}
}
fn is_night(hour: u32, from: u32, until: u32) -> bool {
if from == until {
false
} else if from < until {
(from..until).contains(&hour)
} else {
hour >= from || hour < until
}
}
#[cfg(test)]
mod tests {
use super::is_night;
#[test]
fn night_hours_wrap_past_midnight() {
assert!(is_night(23, 23, 7) && is_night(0, 23, 7) && is_night(6, 23, 7));
assert!(!is_night(7, 23, 7) && !is_night(22, 23, 7) && !is_night(12, 23, 7));
assert!(is_night(12, 0, 24) && !is_night(5, 5, 5));
}
}