diff --git a/Cargo.toml b/Cargo.toml index d04e482..f4fd297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ daemonize = "0.5.0" num-rational = "0.4" num-traits = "0.2" libc = "0.2" +which = "6" [[bin]] name = "ptp_probe" diff --git a/src/ptp.rs b/src/ptp.rs index df36ae9..bdd1a27 100644 --- a/src/ptp.rs +++ b/src/ptp.rs @@ -121,3 +121,166 @@ pub use linux::{is_supported, PtpClock}; #[cfg(not(target_os = "linux"))] pub use non_linux::{is_supported, PtpClock}; + +// ----------------------------------------------------------------------------- +// Statime daemon integration +// This provides a thin wrapper to spawn and manage the 'statime' PTP daemon +// as a child process from within this application (Linux only). +// On non-Linux targets, it returns PtpError::Unsupported. +// ----------------------------------------------------------------------------- + +#[cfg(target_os = "linux")] +mod statime_linux_mod { + use std::io; + use std::path::PathBuf; + use std::process::{Child, Command, Stdio}; + + use super::PtpError; + + #[derive(Debug, Clone)] + pub struct StatimeOptions { + pub interface: String, + pub phc_index: Option, + pub extra_args: Vec, + } + + impl StatimeOptions { + pub fn new>(interface: S) -> Self { + Self { + interface: interface.into(), + phc_index: None, + extra_args: Vec::new(), + } + } + pub fn with_phc_index(mut self, idx: u32) -> Self { + self.phc_index = Some(idx); + self + } + pub fn with_args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.extra_args = args.into_iter().map(|s| s.into()).collect(); + self + } + } + + pub struct StatimeDaemon { + child: Child, + } + + impl StatimeDaemon { + pub fn is_running(&mut self) -> bool { + self.child.try_wait().ok().flatten().is_none() + } + pub fn pid(&self) -> u32 { + self.child.id() + } + pub fn stop(&mut self) -> Result<(), PtpError> { + self.child.kill().map_err(PtpError::Io)?; + let _ = self.child.wait(); + Ok(()) + } + } + + impl Drop for StatimeDaemon { + fn drop(&mut self) { + let _ = self.stop(); + } + } + + fn find_statime_binary() -> Result { + which::which("statime").or_else(|_| { + let fallback = PathBuf::from("/usr/local/bin/statime"); + if fallback.exists() { + Ok(fallback) + } else { + Err(io::Error::new( + io::ErrorKind::NotFound, + "statime binary not found in PATH or /usr/local/bin", + )) + } + }) + } + + pub fn spawn_statime(opts: StatimeOptions) -> Result { + let bin = find_statime_binary().map_err(PtpError::Io)?; + + let mut cmd = Command::new(bin); + cmd.arg("-i").arg(&opts.interface); + + if let Some(idx) = opts.phc_index { + // Prefer --phc-index if supported by the installed statime version. + cmd.arg("--phc-index").arg(idx.to_string()); + } + + for a in &opts.extra_args { + cmd.arg(a); + } + + // Inherit stdio for visibility; detach stdin. + cmd.stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + let child = cmd.spawn().map_err(PtpError::Io)?; + Ok(StatimeDaemon { child }) + } +} + +#[cfg(not(target_os = "linux"))] +mod statime_nonlinux_mod { + use super::PtpError; + + #[derive(Debug, Clone)] + pub struct StatimeOptions { + pub interface: String, + pub phc_index: Option, + pub extra_args: Vec, + } + + impl StatimeOptions { + pub fn new>(interface: S) -> Self { + Self { + interface: interface.into(), + phc_index: None, + extra_args: Vec::new(), + } + } + pub fn with_phc_index(self, _idx: u32) -> Self { + self + } + pub fn with_args(self, _args: I) -> Self + where + I: IntoIterator, + S: Into, + { + self + } + } + + pub struct StatimeDaemon; + + impl StatimeDaemon { + pub fn is_running(&mut self) -> bool { + false + } + pub fn pid(&self) -> u32 { + 0 + } + pub fn stop(&mut self) -> Result<(), PtpError> { + Err(PtpError::Unsupported) + } + } + + pub fn spawn_statime(_opts: StatimeOptions) -> Result { + Err(PtpError::Unsupported) + } +} + +#[cfg(target_os = "linux")] +pub use statime_linux_mod::{spawn_statime, StatimeDaemon, StatimeOptions}; + +#[cfg(not(target_os = "linux"))] +pub use statime_nonlinux_mod::{spawn_statime, StatimeDaemon, StatimeOptions};