use crate::config::Config;
use crate::sys;
use serde::{Deserialize, Serialize};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Peer {
pub uid: u32,
pub pid: i32,
pub boot_id: String,
pub cpu: u64,
pub mem: u64,
pub updated_at: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Claims {
pub cpu: u64,
pub mem: u64,
pub count: usize,
}
fn peer_dir(cfg: &Config) -> Option<PathBuf> {
let dir = PathBuf::from(&cfg.peers.dir);
match std::fs::symlink_metadata(&dir) {
Ok(meta) => {
if !meta.is_dir() {
return None;
}
let mode = meta.mode();
if mode & 0o1000 == 0 {
return None;
}
let world_writable = mode & 0o002 != 0;
let owner = meta.uid();
if !world_writable && owner != 0 && owner != current_uid() {
return None;
}
Some(dir)
}
Err(_) => {
std::fs::create_dir_all(&dir).ok()?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).ok()?;
Some(dir)
}
}
}
pub fn describe(cfg: &Config) -> String {
if !cfg.peers.enabled {
return "off; the config file sets [peers] enabled = false".to_string();
}
match peer_dir(cfg) {
Some(dir) => {
let c = claims(cfg);
format!(
"on, in {} ({} other coordinator(s), {} cores and {} claimed)",
dir.display(),
c.count,
c.cpu,
crate::units::format_size(c.mem)
)
}
None => format!(
"NOT ACTIVE; qex cannot use the directory {}. It must be a directory with the \
sticky bit. qex uses the budget of this user only.",
cfg.peers.dir
),
}
}
pub fn current_uid() -> u32 {
unsafe { libc::getuid() }
}
fn user_dir_name(uid: u32) -> String {
format!("u{uid}")
}
fn peer_file_name(pid: i32) -> String {
format!("peer-{pid}.json")
}
pub fn publish(cfg: &Config, cpu: u64, mem: u64) {
if !cfg.peers.enabled {
return;
}
let Some(dir) = peer_dir(cfg) else { return };
let uid = current_uid();
let mine = dir.join(user_dir_name(uid));
if std::fs::create_dir_all(&mine).is_err() {
return;
}
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&mine, std::fs::Permissions::from_mode(0o755)).ok();
let peer = Peer {
uid,
pid: std::process::id() as i32,
boot_id: sys::boot_id(),
cpu,
mem,
updated_at: sys::now_secs(),
};
if let Ok(bytes) = serde_json::to_vec(&peer) {
crate::job::write_atomic(&mine.join(peer_file_name(peer.pid)), &bytes, 0o644).ok();
}
}
pub fn withdraw(cfg: &Config) {
if let Some(dir) = peer_dir(cfg) {
let mine = dir.join(user_dir_name(current_uid()));
std::fs::remove_file(mine.join(peer_file_name(std::process::id() as i32))).ok();
}
}
pub fn claims(cfg: &Config) -> Claims {
let mut total = Claims::default();
if !cfg.peers.enabled {
return total;
}
let Some(dir) = peer_dir(cfg) else {
return total;
};
let stale = cfg
.peer_stale_after()
.unwrap_or(std::time::Duration::from_secs(30))
.as_secs();
let now = sys::now_secs();
let boot = sys::boot_id();
let me = current_uid();
let Ok(entries) = std::fs::read_dir(&dir) else {
return total;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
let Some(claimed_uid) = name.strip_prefix('u').and_then(|n| n.parse::<u32>().ok()) else {
continue;
};
let Ok(files) = std::fs::read_dir(entry.path()) else {
continue;
};
let my_pid = std::process::id() as i32;
for file in files.flatten() {
let fname = file.file_name();
let Some(fname) = fname.to_str() else {
continue;
};
if !fname.starts_with("peer-") || !fname.ends_with(".json") {
continue;
}
let Some(peer) = read_peer(&file.path(), claimed_uid) else {
continue;
};
if claimed_uid == me && peer.pid == my_pid {
continue;
}
if peer.boot_id != boot {
std::fs::remove_file(file.path()).ok();
continue;
}
if now.saturating_sub(peer.updated_at) > stale {
std::fs::remove_file(file.path()).ok();
continue;
}
if !sys::pid_alive(peer.pid) {
std::fs::remove_file(file.path()).ok();
continue;
}
total.cpu += peer.cpu;
total.mem += peer.mem;
total.count += 1;
}
}
total
}
fn read_peer(path: &Path, expected_uid: u32) -> Option<Peer> {
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.ok()?;
let meta = file.metadata().ok()?;
if !meta.is_file() {
return None;
}
if meta.uid() != expected_uid {
return None;
}
if meta.len() > 64 * 1024 {
return None;
}
let text = std::io::read_to_string(file).ok()?;
let peer: Peer = serde_json::from_str(&text).ok()?;
if peer.uid != expected_uid {
return None;
}
Some(peer)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn tmpdir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("qex-peers-{tag}-{}", std::process::id()));
std::fs::remove_dir_all(&d).ok();
std::fs::create_dir_all(&d).unwrap();
d
}
fn cfg_for(dir: &Path) -> Config {
toml::from_str(&format!(
"[peers]\nenabled = true\ndir = \"{}\"\nstale_after = \"30s\"\n",
dir.display()
))
.unwrap()
}
#[test]
fn a_record_of_this_user_does_not_count() {
let dir = tmpdir("self");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let cfg = cfg_for(&dir);
publish(&cfg, 4, 8 << 30);
assert_eq!(claims(&cfg), Claims::default());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_directory_without_the_sticky_bit_is_refused() {
let dir = tmpdir("nosticky");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o777)).unwrap();
let cfg = cfg_for(&dir);
assert!(peer_dir(&cfg).is_none());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_file_in_place_of_the_directory_is_refused() {
let dir = tmpdir("isfile");
std::fs::remove_dir_all(&dir).ok();
std::fs::write(&dir, b"not a directory").unwrap();
let cfg = cfg_for(&dir);
assert!(peer_dir(&cfg).is_none());
std::fs::remove_file(&dir).ok();
}
#[test]
fn a_record_with_the_wrong_owner_is_refused() {
let dir = tmpdir("wrongowner");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let other = dir.join("u99999");
std::fs::create_dir_all(&other).unwrap();
let peer = Peer {
uid: 99999,
pid: std::process::id() as i32,
boot_id: sys::boot_id(),
cpu: 64,
mem: 64 << 30,
updated_at: sys::now_secs(),
};
std::fs::write(other.join("peer.json"), serde_json::to_vec(&peer).unwrap()).unwrap();
let cfg = cfg_for(&dir);
assert_eq!(
claims(&cfg),
Claims::default(),
"a record with an incorrect owner must not count"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_old_record_and_a_dead_process_do_not_count() {
let dir = tmpdir("stale");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let cfg = cfg_for(&dir);
let me = current_uid();
let other = dir.join(user_dir_name(me + 1));
std::fs::create_dir_all(&other).unwrap();
let old = Peer {
uid: me + 1,
pid: std::process::id() as i32,
boot_id: sys::boot_id(),
cpu: 64,
mem: 64 << 30,
updated_at: sys::now_secs().saturating_sub(3600),
};
std::fs::write(other.join("peer.json"), serde_json::to_vec(&old).unwrap()).unwrap();
assert_eq!(claims(&cfg), Claims::default());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_damaged_file_does_not_stop_the_reader() {
let dir = tmpdir("garbage");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let other = dir.join("u12345");
std::fs::create_dir_all(&other).unwrap();
std::fs::write(other.join("peer.json"), b"{not json").unwrap();
let cfg = cfg_for(&dir);
assert_eq!(claims(&cfg), Claims::default());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_symbolic_link_is_not_read() {
let dir = tmpdir("symlink");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let target = dir.join("real.json");
let peer = Peer {
uid: current_uid(),
pid: std::process::id() as i32,
boot_id: sys::boot_id(),
cpu: 64,
mem: 64 << 30,
updated_at: sys::now_secs(),
};
std::fs::write(&target, serde_json::to_vec(&peer).unwrap()).unwrap();
let other = dir.join("u54321");
std::fs::create_dir_all(&other).unwrap();
std::os::unix::fs::symlink(&target, other.join("peer.json")).unwrap();
assert!(
read_peer(&other.join("peer.json"), 54321).is_none(),
"the reader must not follow a symbolic link"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn the_peer_system_is_off_when_the_config_says_so() {
let dir = tmpdir("disabled");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let mut cfg = cfg_for(&dir);
cfg.peers.enabled = false;
assert_eq!(claims(&cfg), Claims::default());
std::fs::remove_dir_all(&dir).ok();
}
}