use std::path::{Path, PathBuf};
pub struct Config {
pub dir: PathBuf,
pub build_slots: usize,
pub gpu_slots: usize,
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,
}
const KEYS: [&str; 11] = [
"build_slots",
"gpu_slots",
"jobs",
"cpu_percent",
"mem_gb",
"min_free_gb",
"night_from",
"night_until",
"night_build_slots",
"night_mem_gb",
"night_min_free_gb",
];
impl Config {
pub fn load() -> Config {
let dir = std::env::var_os("FGATE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
let base = std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
base.join("fgate")
});
let total_gb = (crate::job::memory().0 as f64 / (1u64 << 30) as f64).round() as u32;
let mut c = Config {
dir,
build_slots: 1,
gpu_slots: 1,
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,
};
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());
}
}
if !text.contains("night_from") {
c.append_night(&file, &text);
}
}
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::job::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
}
pub fn file(&self) -> PathBuf {
self.dir.join("config.txt")
}
pub fn mem_bytes(&self) -> u64 {
self.mem_gb as u64 * (1u64 << 30)
}
fn set(&mut self, key: &str, value: &str) {
let Ok(n) = value.parse::<u32>() else {
eprintln!("fgate: 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,
"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,
_ => eprintln!("fgate: 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 append_night(&self, file: &Path, text: &str) {
let gap = if text.ends_with('\n') { "" } else { "\n" };
let _ = std::fs::write(file, format!("{text}{gap}{}", self.night_text()));
}
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 command (CARGO_BUILD_JOBS) 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()
);
let _ = std::fs::create_dir_all(&self.dir);
let _ = std::fs::write(file, text);
}
}
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));
}
}