diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 9e16fd3..2eb0cce 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -22,7 +22,7 @@ use thiserror::Error; use crate::{ config::{Config, ConfigError}, instance::{Instance, InstanceClient, InstanceError}, - util::Path, + path::PrefixedPath, }; /// One virtio-scsi IOThread-to-virtqueue mapping. @@ -159,7 +159,7 @@ pub struct BackendRegistration { /// Stable backend name used in logs and diagnostics. pub name: &'static str, /// Build the backend from `Config::backend_config_dir`. - pub build: fn(&Path) -> Result, BackendClientError>, + pub build: fn(&PrefixedPath) -> Result, BackendClientError>, } /// Linked-in backend factories. Feature-gated modules append themselves here. @@ -180,7 +180,7 @@ pub trait Backend: Send + Sync { /// Inotify identifies the changed entry and event kind, but it does not /// describe the backend-level inventory delta. The daemon therefore uses /// any event only as a prompt to run a complete discovery pass. - fn watch_paths(&self) -> Vec { + fn watch_paths(&self) -> Vec { Vec::new() } @@ -224,12 +224,12 @@ pub fn registered_backends(cfg: &Config) -> Result>, Backen /// supplied factory keeps the control protocol and concrete client /// backend-owned. pub async fn discover_via_control_socket( - socket_dir: impl AsRef, + socket_dir: impl AsRef, socket_name: &str, make_client: F, ) -> io::Result>> where - F: Fn(Path) -> Box, + F: Fn(PrefixedPath) -> Box, { let socket_dir = socket_dir.as_ref(); tracing::debug!("discovering in {}", socket_dir.display()); @@ -262,7 +262,8 @@ where let Ok(id) = entry.file_name().into_string() else { continue; }; - let sock_path = Path::new(&socket_dir.join(entry.path().join(socket_name))); + // FIXME double prefix bug + let sock_path = PrefixedPath::new(&socket_dir.join(entry.path().join(socket_name))); let pid = match cached_control_socket_peer_pid(&sock_path).await { Ok(pid) => pid, Err(error) if error.kind() == io::ErrorKind::WouldBlock => { @@ -320,7 +321,7 @@ static PEER_CACHE: LazyLock>> = LazyLock::new /// Return the control-socket peer PID, connecting only when the socket file /// or the previously identified peer process changed. -async fn cached_control_socket_peer_pid(sock_path: &Path) -> io::Result { +async fn cached_control_socket_peer_pid(sock_path: &PrefixedPath) -> io::Result { let key: &std::path::Path = sock_path; let metadata = tokio::fs::metadata(sock_path).await?; let cached = PEER_CACHE.lock().expect("lock poisoned").get(key).copied(); @@ -348,7 +349,7 @@ async fn cached_control_socket_peer_pid(sock_path: &Path) -> io::Result { /// Return the start time of `pid`, or `None` if it no longer exists. fn process_start_time(pid: i32) -> Option { - let path = Path::new(&format!("/proc/{pid}")); + let path = PrefixedPath::new(&format!("/proc/{pid}")); let path = std::path::PathBuf::from(std::path::Path::as_os_str(&path)); procfs::process::Process::new_with_root(path) .and_then(|process| process.stat()) @@ -357,7 +358,7 @@ fn process_start_time(pid: i32) -> Option { } /// Return the process ID at the far end of a UNIX control socket. -async fn control_socket_peer_pid(sock_path: &Path) -> io::Result { +async fn control_socket_peer_pid(sock_path: &PrefixedPath) -> io::Result { let stream = tokio::net::UnixStream::connect(sock_path).await?; stream.peer_cred()?.pid().ok_or_else(|| { io::Error::new( @@ -392,7 +393,7 @@ mod tests { } async fn discover(socket_dir: &std::path::Path) -> Vec> { - discover_via_control_socket(Path::new(socket_dir), "ctl", |_| { + discover_via_control_socket(PrefixedPath::new(socket_dir), "ctl", |_| { Box::new(NullClient) as Box }) .await diff --git a/src/backends/qemu/client.rs b/src/backends/qemu/client.rs index b185ed4..2022b27 100644 --- a/src/backends/qemu/client.rs +++ b/src/backends/qemu/client.rs @@ -26,6 +26,7 @@ use tokio::sync::Mutex; use uuid::Uuid; use crate::{ + ProcessExt, backends::{ BackendClientError, IoThreadProperties, VqMapping, qemu::{ @@ -36,7 +37,6 @@ use crate::{ }, }, instance::{InstanceClient, ThreadPoolSnapshot}, - util::ProcessExt, }; /// Wire client for an QEMU-managed VM. diff --git a/src/backends/qemu/helpers.rs b/src/backends/qemu/helpers.rs index 776e32c..eda816a 100644 --- a/src/backends/qemu/helpers.rs +++ b/src/backends/qemu/helpers.rs @@ -30,8 +30,8 @@ use rustix::param; use thiserror::Error; use crate::{ + ProcessExt, backends::{BackendClientError, VqMapping}, - util::ProcessExt, }; /// Errors produced while querying or reconfiguring a QEMU vm. diff --git a/src/backends/qemu/mod.rs b/src/backends/qemu/mod.rs index 34e98a1..ba00f9a 100644 --- a/src/backends/qemu/mod.rs +++ b/src/backends/qemu/mod.rs @@ -33,7 +33,7 @@ use crate::{ backends::{Backend, BackendClientError, BackendRegistration}, config::{self, ConfigError}, instance::{Instance, InstanceClient}, - util::Path, + path::PrefixedPath, }; pub mod client; @@ -42,7 +42,7 @@ pub mod libvirt; const BACKEND_NAME: &str = "qemu"; -fn build_qemu_backend(dir: &Path) -> Result, BackendClientError> { +fn build_qemu_backend(dir: &PrefixedPath) -> Result, BackendClientError> { Ok(Box::new(QemuBackend::from_config_dir(dir)?)) } @@ -81,8 +81,8 @@ impl Default for QemuConfig { impl QemuConfig { /// Read `/qemu.json` if present, otherwise return the /// built-in defaults. - pub fn from_dir(dir: &Path) -> Result { - let path = Path::new(&dir.join("qemu.json")); + pub fn from_dir(dir: &PrefixedPath) -> Result { + let path = PrefixedPath::new(&dir.join("qemu.json")); config::load_or_default(&path) } } @@ -109,7 +109,7 @@ impl QemuBackend { /// Convenience: read `/qemu.json` (or fall back to /// defaults) and construct. - pub fn from_config_dir(dir: &Path) -> Result { + pub fn from_config_dir(dir: &PrefixedPath) -> Result { Ok(Self::new(QemuConfig::from_dir(dir)?)) } @@ -207,7 +207,7 @@ impl Backend for QemuBackend { // `Instance` model; for the libvirt-backed backend // it is purely informational (log context), so we // stuff the libvirt URI in there. - let sock_path = Path::new(&self.cfg.libvirt_uri); + let sock_path = PrefixedPath::new(&self.cfg.libvirt_uri); out.push(Arc::new(Instance::new( uuid.to_string(), sock_path, diff --git a/src/config.rs b/src/config.rs index 0e4cf37..5843f85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use std::{io, time::Duration}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; -use crate::util::Path; +use crate::path::PrefixedPath; #[derive(Error, Debug)] pub enum ConfigError { @@ -39,14 +39,14 @@ pub struct Config { pub engine: String, /// Directory to look for `.json` engine-specific /// config files. - pub engine_config_dir: Path, + pub engine_config_dir: PrefixedPath, /// Directory containing backend-owned configuration files. - pub backend_config_dir: Path, + pub backend_config_dir: PrefixedPath, /// JSON file containing the managed and unmanaged VM lists. /// /// The default is below `/run`, so state survives daemon restarts but not a /// host reboot. - pub vm_state_path: Path, + pub vm_state_path: PrefixedPath, /// Lowest worker count accepted from a scaling engine. #[serde(default = "default_min_thread_count")] pub min_thread_count: u32, @@ -235,15 +235,15 @@ impl Default for Config { fn default() -> Self { Self { engine: "threshold".to_string(), - engine_config_dir: Path::new("/etc/io-thread-controller.d/engines"), - backend_config_dir: Path::new("/etc/io-thread-controller.d/backends"), + engine_config_dir: PrefixedPath::new("/etc/io-thread-controller.d/engines"), + backend_config_dir: PrefixedPath::new("/etc/io-thread-controller.d/backends"), scale_poll_secs: 10, round_up_durations: false, min_thread_count: 2, max_thread_count: 8, host_cpu_scale_up_ceiling: 0.9, cooldown_secs: default_cooldown_secs(), - vm_state_path: Path::new("/run/io-thread-controller/vm-ownership.json"), + vm_state_path: PrefixedPath::new("/run/io-thread-controller/vm-ownership.json"), enable_per_vm_status_line: false, enable_per_vm_main_log: false, enable_aggregate_status_line: true, @@ -490,7 +490,7 @@ mod tests { ) .unwrap(); - let cfg: Config = load_or_default(Path::new(path.to_str().unwrap())).unwrap(); + let cfg: Config = load_or_default(PrefixedPath::new(path.to_str().unwrap())).unwrap(); assert_eq!(cfg.engine, "threshold"); assert_eq!(cfg.scale_poll_secs, 7); assert_eq!( @@ -517,7 +517,7 @@ mod tests { ) .unwrap(); - let err = load_or_default::(Path::new(path.to_str().unwrap())).unwrap_err(); + let err = load_or_default::(PrefixedPath::new(path.to_str().unwrap())).unwrap_err(); assert!(matches!(err, ConfigError::SerdeJson(_))); } diff --git a/src/controller.rs b/src/controller.rs index 668fb40..5fcd0ce 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -27,9 +27,9 @@ use crate::{ AppliedOutcome, BlockedReason, EngineTickContext, PsiSample, ScaleAction, ScalingEngine, }, instance::{CgroupError, Instance, InstanceStatus, SnapshotLatency}, + path::PrefixedPath, per_vm_log, state::{StateError, VmOwnership, VmStateStore}, - util::Path, }; /// Wire-facing container for the D-Bus `GetSnapshot` reply. #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -1012,7 +1012,7 @@ fn read_psi_sample() -> PsiSample { /// Read aggregate host CPU counters. fn read_host_cpu_sample() -> Result { let current_si = procfs::current_system_info(); - let total = KernelStats::from_file(Path::new("/proc/stat"), current_si)?.total; + let total = KernelStats::from_file(PrefixedPath::new("/proc/stat"), current_si)?.total; let idle_ticks = total.idle.saturating_add(total.iowait.unwrap_or(0)); let total_ticks = total @@ -1086,7 +1086,6 @@ mod tests { engines::{EngineTickContext, ScalingEngine}, instance::{InstanceClient, ThreadPoolSnapshot}, per_vm_log::PerVmLog, - util::Path, }; use tracing_subscriber::layer::SubscriberExt; @@ -1147,14 +1146,14 @@ mod tests { async fn actuation_enforces_vcpu_cap() { let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), cooldown_secs: 0, ..Default::default() }; let client = Arc::new(VcpuLimitedClient::new(2)); let instance = Arc::new(Instance::new( "some-vcpu-limited-test-instance".to_string(), - Path::new(""), + PrefixedPath::new(""), 0, Arc::clone(&client), )); @@ -1210,7 +1209,7 @@ mod tests { async fn actuation_enforces_controller_bounds_and_host_ceiling() { let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), min_thread_count: 2, max_thread_count: 3, host_cpu_scale_up_ceiling: 0.5, @@ -1220,7 +1219,7 @@ mod tests { let client = Arc::new(VcpuLimitedClient::new(2)); let instance = Arc::new(Instance::new( "policy-guarded".to_string(), - Path::new(""), + PrefixedPath::new(""), 0, Arc::clone(&client), )); @@ -1280,7 +1279,7 @@ mod tests { async fn revert_arms_cooldown_blocking_immediate_retry() { let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), cooldown_secs: 30, min_thread_count: 1, max_thread_count: 8, @@ -1289,7 +1288,7 @@ mod tests { let client = Arc::new(VcpuLimitedClient::new(4)); let instance = Arc::new(Instance::new( "revert-cooldown".to_string(), - Path::new(""), + PrefixedPath::new(""), 0, Arc::clone(&client), )); @@ -1337,7 +1336,7 @@ mod tests { async fn revert_bypasses_existing_cooldown() { let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), cooldown_secs: 60, min_thread_count: 1, max_thread_count: 8, @@ -1346,7 +1345,7 @@ mod tests { let client = Arc::new(VcpuLimitedClient::new(3)); let instance = Arc::new(Instance::new( "revert-bypass".to_string(), - Path::new(""), + PrefixedPath::new(""), 0, Arc::clone(&client), )); @@ -1414,7 +1413,7 @@ mod tests { async fn test_set_all_attempts_every_vm_and_reports_partial_failure() { let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), ..Default::default() }; let engine = Box::new(default_threshold_engine()); @@ -1429,8 +1428,18 @@ mod tests { fail: true, target: Arc::clone(&bad_target), }; - let good_instance = Arc::new(Instance::new("visible".to_string(), Path::new(""), 0, good)); - let bad_instance = Arc::new(Instance::new("hidden".to_string(), Path::new(""), 0, bad)); + let good_instance = Arc::new(Instance::new( + "visible".to_string(), + PrefixedPath::new(""), + 0, + good, + )); + let bad_instance = Arc::new(Instance::new( + "hidden".to_string(), + PrefixedPath::new(""), + 0, + bad, + )); good_instance.status.write().await.vcpu_count = 16; bad_instance.status.write().await.vcpu_count = 16; controller @@ -1529,7 +1538,7 @@ mod tests { fn instance(id: &str, threads: u32, closed: Arc) -> Arc { Arc::new(Instance::new( id.to_string(), - Path::new(""), + PrefixedPath::new(""), 1, SnapshotClient { threads, closed }, )) @@ -1564,7 +1573,7 @@ mod tests { let evaluated = Arc::new(AtomicUsize::new(0)); let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), ..Default::default() }; let mut controller = Controller::new( @@ -1613,7 +1622,7 @@ mod tests { ) { let dir = tempfile::tempdir().unwrap(); let per_vm_log = PerVmLog::default(); - per_vm_log.set_dir(Some(Path::new(dir.path()))); + per_vm_log.set_dir(Some(PrefixedPath::new(dir.path()))); let guard = tracing::subscriber::set_default( tracing_subscriber::registry().with(per_vm_log.clone()), ); @@ -1641,7 +1650,7 @@ mod tests { let evaluated = Arc::new(AtomicUsize::new(0)); let state_dir = tempfile::tempdir().unwrap(); let cfg = Config { - vm_state_path: Path::new(&state_dir.path().join("ownership.json")), + vm_state_path: PrefixedPath::new(&state_dir.path().join("ownership.json")), ..Default::default() }; let mut controller = Controller::new( @@ -1659,7 +1668,7 @@ mod tests { let ok = instance("ok", 4, Arc::clone(&closed)); let bad = Arc::new(Instance::new( "bad".to_string(), - Path::new(""), + PrefixedPath::new(""), 2, FailingClient, )); diff --git a/src/daemon.rs b/src/daemon.rs index a733e72..762c2ee 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -24,7 +24,7 @@ use crate::{ dbus, engines::ScalingEngine, instance::Instance, - util::Path, + path::PrefixedPath, }; #[derive(Error, Debug)] @@ -169,7 +169,7 @@ async fn discover_all(backends: &[Box]) -> Vec> { fn setup_inotify( backends: &[Box], ) -> Result, DaemonError> { - let paths: Vec = backends + let paths: Vec = backends .iter() .flat_map(|backends| backends.watch_paths()) .collect(); diff --git a/src/engines/mod.rs b/src/engines/mod.rs index 286c867..2535c05 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -16,7 +16,7 @@ use crate::{ backends::BackendClientError, config::{ConfigError, PollConverter}, instance::Instance, - util::Path, + path::PrefixedPath, }; #[cfg(feature = "threshold-engine")] @@ -26,7 +26,7 @@ pub mod threshold; /// converted with the supplied converter, which reports invalid durations /// together with the controller's after the engine is built. pub type EngineBuilder = - fn(&Path, &mut PollConverter) -> Result, EngineError>; + fn(&PrefixedPath, &mut PollConverter) -> Result, EngineError>; /// One in-tree (or out-of-tree) engine factory registered on [`ENGINES`]. pub struct EngineRegistration { @@ -280,7 +280,7 @@ pub trait ScalingEngine: Send + Sync { /// Load the selected built-in engine by name. pub fn load_registered_engine( - engine_config_dir: &Path, + engine_config_dir: &PrefixedPath, name: &str, durations: &mut PollConverter, ) -> Result, EngineError> { @@ -324,7 +324,7 @@ mod tests { /// on unknown engines. #[test] fn load_registered_engine_finds_threshold_and_rejects_unknown() { - let dir = Path::new("/tmp/missing-engines"); + let dir = PrefixedPath::new("/tmp/missing-engines"); let mut durations = crate::config::Config::default().poll_period().converter(); let engine = load_registered_engine(&dir, "threshold", &mut durations).unwrap(); assert_eq!(engine.name(), "threshold"); @@ -390,7 +390,7 @@ mod tests { fn test_instance(id: &str) -> Arc { Arc::new(Instance::new( id.to_string(), - Path::new(""), + PrefixedPath::new(""), 0, SnapshotClient, )) diff --git a/src/engines/threshold.rs b/src/engines/threshold.rs index 6458335..3d0a9e4 100644 --- a/src/engines/threshold.rs +++ b/src/engines/threshold.rs @@ -22,14 +22,14 @@ use crate::{ ScalingEngine, }, instance::Instance, - util::Path, + path::PrefixedPath, }; /// Registry and configuration name of the threshold engine. pub const ENGINE_NAME: &str = "threshold"; fn build_threshold_engine( - dir: &Path, + dir: &PrefixedPath, durations: &mut PollConverter, ) -> Result, EngineError> { Ok(Box::new(ThresholdEngine::from_config_dir(dir, durations)?)) @@ -475,8 +475,11 @@ impl ThresholdEngine { } /// Load `threshold.json`, falling back to built-in defaults when absent. - pub fn from_config_dir(dir: &Path, durations: &mut PollConverter) -> Result { - let path = Path::new(&dir.join(format!("{ENGINE_NAME}.json"))); + pub fn from_config_dir( + dir: &PrefixedPath, + durations: &mut PollConverter, + ) -> Result { + let path = PrefixedPath::new(&dir.join(format!("{ENGINE_NAME}.json"))); let cfg: ThresholdConfig = config::load_or_default(&path)?; cfg.validate()?; Ok(Self::new(cfg, durations)?) @@ -926,10 +929,7 @@ mod tests { use rstest::{fixture, rstest}; use test_log::test; - use super::{ - InstanceState, PostRevertUpHold, ThresholdConfig, ThresholdEngine, backoff_polls, - log_performance_revert, - }; + use super::*; use crate::{ backends::BackendClientError, config::PollPeriod, @@ -937,7 +937,6 @@ mod tests { instance::{ Instance, InstanceClient, InstancePerfSample, InstanceStatus, ThreadPoolSnapshot, }, - util::Path, }; #[derive(Clone)] @@ -978,7 +977,7 @@ mod tests { async fn instance() -> Arc { let instance = Arc::new(Instance::new( "vm-1".to_string(), - Path::new(""), + PrefixedPath::new(""), 0, SnapshotClient, )); @@ -1759,7 +1758,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let mut durations = PollPeriod::new(POLL_SECS, false).converter(); let engine = ThresholdEngine::from_config_dir( - &Path::new(dir.path().to_str().unwrap()), + &PrefixedPath::new(dir.path().to_str().unwrap()), &mut durations, ) .unwrap(); diff --git a/src/instance.rs b/src/instance.rs index 4a44322..e8befc5 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -25,9 +25,10 @@ use thiserror::Error; use tokio::sync::RwLock; use crate::{ + ProcessExt, backends::{BackendClientError, IoThreadProperties, VqMapping}, + path::PrefixedPath, rolling::RollingMetrics, - util::{Path, ProcessExt}, }; #[derive(Debug, Error)] @@ -122,7 +123,7 @@ pub struct Instance { /// Stable identifier used by inventory, logs, and engine plans. pub id: String, /// Backend-owned path or URI identifying this VM's transport. - pub sock_path: Path, + pub sock_path: PrefixedPath, /// Backend process ID. pub pid: i32, /// Backend-selected task names included in fallback CPU sampling. @@ -179,7 +180,7 @@ impl Instance { /// client: the client handling this instance pub fn new( id: String, - sock_path: Path, + sock_path: PrefixedPath, pid: i32, client: impl InstanceClient + 'static, ) -> Self { @@ -710,7 +711,7 @@ fn read_cgroup_sample(pid: i32) -> Result { .find(|entry| entry.hierarchy == 0 && entry.controllers.is_empty()) .map(|entry| entry.pathname) .ok_or(CgroupError::MissingFile("cgroup path".to_string()))?; - let base = Path::new(&format!("/sys/fs/cgroup{path}")); + let base = PrefixedPath::new(&format!("/sys/fs/cgroup{path}")); let cpu_max = std::fs::read_to_string(format!("{base}/cpu.max"))?; let cpu_stat = std::fs::read_to_string(format!("{base}/cpu.stat"))?; @@ -812,7 +813,7 @@ mod tests { async fn refresh_state_applies_successful_snapshot() { let instance = Instance::new( "vm-ok".to_string(), - Path::new(""), + PrefixedPath::new(""), 7, SnapshotClient { threads: 3 }, ); @@ -829,7 +830,7 @@ mod tests { let closed = Arc::new(AtomicBool::new(false)); let instance = Instance::new( "vm-bad".to_string(), - Path::new(""), + PrefixedPath::new(""), 9, FailingClient { closed: Arc::clone(&closed), @@ -846,7 +847,7 @@ mod tests { fn display_trims_instance_id() { let instance = Instance::new( " vm-1 ".to_string(), - Path::new(""), + PrefixedPath::new(""), 1, SnapshotClient { threads: 1 }, ); diff --git a/src/lib.rs b/src/lib.rs index 252818b..395cf68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,23 @@ pub mod daemon; pub mod dbus; pub mod engines; pub mod instance; +pub mod path; pub mod per_vm_log; pub mod rolling; pub mod state; -pub mod util; + +pub(crate) trait ProcessExt { + fn from_pid(pid: i32) -> procfs::ProcResult; +} + +impl ProcessExt for procfs::process::Process { + /// `/proc/` after applying [`IO_THREAD_CONTROLLER_ROOT_PATH`]. + /// + /// `procfs::process::Process::new` always opens the real `/proc`. Callers + /// that should honor the mock root pass this to create the process. + fn from_pid(pid: i32) -> procfs::ProcResult { + let path = crate::path::PrefixedPath::new(&format!("/proc/{pid}")); + let path = std::path::PathBuf::from(std::path::Path::as_os_str(&path)); + Self::new_with_root(path) + } +} diff --git a/src/main.rs b/src/main.rs index 0d55b6d..43166a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,8 @@ use io_thread_controller::{ config::{Config, ConfigError, dump_default_config, load_or_default, validate_config}, daemon::{DaemonError, VERSION, run}, engines::{EngineError, load_registered_engine}, + path::PrefixedPath, per_vm_log::{self, PER_VM_LOG_DIR, PerVmLog}, - util::Path, }; use thiserror::Error; use tracing::{debug, info}; @@ -54,7 +54,7 @@ enum IoThreadControllerError { struct Cli { /// Top-level controller configuration. #[arg(long, default_value = "/etc/io-thread-controller.json")] - config: Path, + config: PrefixedPath, /// Print built-in defaults and exit. #[arg(long)] @@ -165,7 +165,7 @@ async fn run_daemon() -> Result<(), IoThreadControllerError> { durations.finish()?; per_vm_log.set_dir( (cfg.enable_per_vm_status_line && !cfg.enable_per_vm_main_log) - .then(|| Path::from(PER_VM_LOG_DIR)), + .then(|| PrefixedPath::from(PER_VM_LOG_DIR)), ); let backends = registered_backends(&cfg)?; run(cfg, engine, backends).await?; diff --git a/src/util.rs b/src/path.rs similarity index 63% rename from src/util.rs rename to src/path.rs index 9ff211b..d93cf62 100644 --- a/src/util.rs +++ b/src/path.rs @@ -20,11 +20,11 @@ use glob; use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[derive(Debug, Clone)] -pub struct Path { +pub struct PrefixedPath { path: std::path::PathBuf, } -impl Serialize for Path { +impl Serialize for PrefixedPath { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -33,7 +33,7 @@ impl Serialize for Path { } } -impl<'de> Deserialize<'de> for Path { +impl<'de> Deserialize<'de> for PrefixedPath { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -43,7 +43,7 @@ impl<'de> Deserialize<'de> for Path { } } -impl Deref for Path { +impl Deref for PrefixedPath { type Target = std::path::Path; fn deref(&self) -> &Self::Target { @@ -51,19 +51,19 @@ impl Deref for Path { } } -impl AsRef for Path { +impl AsRef for PrefixedPath { fn as_ref(&self) -> &Self { self } } -impl AsRef for Path { +impl AsRef for PrefixedPath { fn as_ref(&self) -> &std::path::Path { &self.path } } -impl fmt::Display for Path { +impl fmt::Display for PrefixedPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.path.fmt(f) } @@ -75,7 +75,7 @@ static IO_THREAD_CONTROLLER_ROOT_PATH: LazyLock = LazyLock:: ) }); -impl Path { +impl PrefixedPath { pub fn new(s: &S) -> Self where S: AsRef + ?Sized, @@ -92,31 +92,31 @@ impl Path { } } -impl From<&str> for Path { +impl From<&str> for PrefixedPath { fn from(s: &str) -> Self { Self::new(s) } } -impl From for Path { +impl From for PrefixedPath { fn from(s: String) -> Self { Self::new(&s) } } -impl From<&OsStr> for Path { +impl From<&OsStr> for PrefixedPath { fn from(s: &OsStr) -> Self { Self::new(s) } } -impl From for Path { +impl From for PrefixedPath { fn from(s: OsString) -> Self { Self::new(&s) } } -impl FromStr for Path { +impl FromStr for PrefixedPath { type Err = Infallible; fn from_str(s: &str) -> Result { @@ -124,40 +124,6 @@ impl FromStr for Path { } } -#[macro_export] -macro_rules! path { - ( $x: ident, $p: expr) => { - pub static $x: std::sync::LazyLock<$crate::util::Path> = - std::sync::LazyLock::new(|| $crate::util::Path::new($p)); - impl AsRef for $x { - fn as_ref(&self) -> &std::path::Path { - &self - } - } - impl AsRef for $x { - fn as_ref(&self) -> &std::ffi::OsStr { - self.as_os_str() - } - } - }; -} - -pub(crate) trait ProcessExt { - fn from_pid(pid: i32) -> procfs::ProcResult; -} - -impl ProcessExt for procfs::process::Process { - /// `/proc/` after applying [`IO_THREAD_CONTROLLER_ROOT_PATH`]. - /// - /// `procfs::process::Process::new` always opens the real `/proc`. Callers - /// that should honor the mock root pass this to create the process. - fn from_pid(pid: i32) -> procfs::ProcResult { - let path = Path::new(&format!("/proc/{pid}")); - let path = std::path::PathBuf::from(std::path::Path::as_os_str(&path)); - Self::new_with_root(path) - } -} - #[cfg(test)] mod tests { use super::*; @@ -165,7 +131,7 @@ mod tests { /// Test that relative `Path` values are stored unchanged. #[test] fn relative_paths_are_unchanged() { - let path = Path::new("relative/sock"); + let path = PrefixedPath::new("relative/sock"); assert_eq!(path.as_os_str(), OsStr::new("relative/sock")); } @@ -173,14 +139,14 @@ mod tests { /// root. #[test] fn absolute_paths_keep_absolute_form_under_default_root() { - let path = Path::new("/var/run/example.sock"); + let path = PrefixedPath::new("/var/run/example.sock"); assert_eq!(path.as_os_str(), OsStr::new("/var/run/example.sock")); } /// Test that `FromStr` builds a `Path` from a string. #[test] fn from_str_builds_path() { - let path: Path = "tmp/example".parse().unwrap(); + let path: PrefixedPath = "tmp/example".parse().unwrap(); assert_eq!(path.as_os_str(), OsStr::new("tmp/example")); } } diff --git a/src/per_vm_log.rs b/src/per_vm_log.rs index e2ffb26..86b7cbd 100644 --- a/src/per_vm_log.rs +++ b/src/per_vm_log.rs @@ -26,7 +26,7 @@ use tracing::{ }; use tracing_subscriber::{EnvFilter, Layer, layer::Context}; -use crate::util::Path; +use crate::path::PrefixedPath; /// Default per-VM log directory. pub const PER_VM_LOG_DIR: &str = "/var/log/io-thread-controller/vms"; @@ -52,14 +52,14 @@ pub struct PerVmLog { #[derive(Default)] struct Inner { /// Destination directory; `None` disables per-VM files. - dir: RwLock>, + dir: RwLock>, /// VM IDs for which per-VM log I/O failures were already reported. warned: Mutex>, } impl PerVmLog { /// Write per-VM files under `dir`, or stop writing them with `None`. - pub fn set_dir(&self, dir: Option) { + pub fn set_dir(&self, dir: Option) { *self.inner.dir.write().expect("lock poisoned") = dir; } @@ -180,7 +180,7 @@ mod tests { fn test_writes_vm_tagged_events_without_vm_field() { let dir = tempfile::tempdir().unwrap(); let log = PerVmLog::default(); - log.set_dir(Some(Path::new(dir.path()))); + log.set_dir(Some(PrefixedPath::new(dir.path()))); let subscriber = tracing_subscriber::registry().with(log.clone()); tracing::subscriber::with_default(subscriber, || { tracing::info!(target: VM, vm = "vm-a", "status=1 2 3"); @@ -225,7 +225,7 @@ mod tests { fn test_per_vm_target_bypasses_main_log() { let dir = tempfile::tempdir().unwrap(); let log = PerVmLog::default(); - log.set_dir(Some(Path::new(dir.path()))); + log.set_dir(Some(PrefixedPath::new(dir.path()))); let main_log = Buffer::default(); let writer = main_log.clone(); let subscriber = tracing_subscriber::registry() diff --git a/src/state.rs b/src/state.rs index 29dfcab..f1a187d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -17,7 +17,7 @@ use std::{ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::util::Path; +use crate::path::PrefixedPath; #[derive(Debug, Error)] pub enum StateError { @@ -84,12 +84,12 @@ impl VmOwnership { /// Atomic JSON-file store for [`VmOwnership`]. #[derive(Debug, Clone)] pub struct VmStateStore { - path: Path, + path: PrefixedPath, } impl VmStateStore { /// Address the ownership registry at `path`. - pub fn new(path: impl AsRef) -> Self { + pub fn new(path: impl AsRef) -> Self { Self { path: path.as_ref().clone(), } @@ -114,7 +114,7 @@ impl VmStateStore { /// Atomically replace the complete ownership registry. pub fn save(&self, state: &VmOwnership) -> Result<(), StateError> { state.validate()?; - let path = Path::new("."); + let path = PrefixedPath::new("."); let parent = self.path.parent().unwrap_or_else(|| &path); std::fs::create_dir_all(parent)?; let tmp = self @@ -136,7 +136,7 @@ mod tests { #[test] fn ownership_round_trips_in_one_file() { let dir = tempfile::tempdir().unwrap(); - let store = VmStateStore::new(Path::new(&dir.path().join("ownership.json"))); + let store = VmStateStore::new(PrefixedPath::new(&dir.path().join("ownership.json"))); let mut expected = VmOwnership::default(); expected.record("managed", true).unwrap(); expected.record("../unmanaged", false).unwrap(); @@ -152,7 +152,7 @@ mod tests { fn missing_state_uses_defaults() { let dir = tempfile::tempdir().unwrap(); assert_eq!( - VmStateStore::new(Path::new(&dir.path().join("missing.json"))) + VmStateStore::new(PrefixedPath::new(&dir.path().join("missing.json"))) .load() .unwrap(), VmOwnership::default() @@ -170,6 +170,6 @@ mod tests { r#"{"managed_vms":["same"],"unmanaged_vms":["same"]}"#, ) .unwrap(); - assert!(VmStateStore::new(Path::new(&path)).load().is_err()); + assert!(VmStateStore::new(PrefixedPath::new(&path)).load().is_err()); } }