use std::{
io::{IsTerminal, Write},
path::PathBuf,
process::ExitCode,
time::{SystemTime, UNIX_EPOCH},
};
use algorithm::{SingleShotController, SingleShotControllerConfig};
use ntp_proto::{NtpClock, NtpDuration};
use tokio::runtime::Builder;
use crate::daemon::{
config, initialize_logging_parse_config, nts_key_provider, spawn, tracing::LogLevel,
};
mod algorithm;
fn human_readable_duration(abs_offset: f64) -> String {
let mut offset = abs_offset;
let mut res = String::new();
if offset >= 86400.0 {
let days = (offset / 86400.0).floor() as u64;
offset -= days as f64 * 86400.0;
res.push_str(&format!("{days} day(s) "));
}
if offset >= 3600.0 {
let hours = (offset / 3600.0).floor() as u64;
offset -= hours as f64 * 3600.0;
res.push_str(&format!("{hours} hour(s) "));
}
if offset >= 60.0 {
let minutes = (offset / 60.0).floor() as u64;
offset -= minutes as f64 * 60.0;
res.push_str(&format!("{minutes} minute(s) "));
}
if offset >= 1.0 {
res.push_str(&format!("{offset:.0} second(s)"));
}
res
}
fn try_date_display(offset: NtpDuration) -> Option<String> {
let time = SystemTime::now();
let since_epoch = time
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let ts = if offset < NtpDuration::ZERO {
since_epoch - ((-offset.to_seconds()) as u64)
} else {
since_epoch + ((offset.to_seconds()) as u64)
};
std::process::Command::new("date")
.arg("-d")
.arg(format!("@{ts}"))
.arg("+%c")
.output()
.ok()
.and_then(|output| {
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
})
}
impl<C: NtpClock> SingleShotController<C> {
fn offer_clock_change(&self, offset: NtpDuration) {
let offset_ms = offset.to_seconds();
if offset.abs() < NtpDuration::from_seconds(1.0) {
println!("Your clock is already within 1s of the correct time");
return;
}
if let Some(s) = try_date_display(NtpDuration::ZERO) {
println!("The current local time is: {s}");
}
if let Some(s) = try_date_display(offset) {
println!("It looks like the time should be: {s}");
}
if offset < NtpDuration::ZERO {
println!(
"It looks like your clock is ahead by {}",
human_readable_duration(-offset_ms)
);
} else {
println!(
"It looks like your clock is behind by {}",
human_readable_duration(offset_ms)
);
}
println!("Please validate externally that this offset is correct");
print!("Do you want to update your local clock? [y/N] ");
std::io::stdout().flush().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
if input.trim().to_lowercase() == "y" || input.trim().to_lowercase() == "yes" {
match self.clock.step_clock(offset) {
Ok(_) => println!("Time updated successfully"),
Err(_) => println!("Could not update clock, do you have the right permissions?"),
}
} else {
println!("Time not updated");
}
}
}
pub(crate) fn force_sync(config: Option<PathBuf>) -> std::io::Result<ExitCode> {
let (config, _) = initialize_logging_parse_config(
Some(LogLevel::Warn),
config,
crate::daemon::Application::Ctl,
);
config.check();
if !std::io::stdin().is_terminal() {
eprintln!("This command must be run interactively");
return Ok(ExitCode::FAILURE);
}
println!("Determining current time...");
Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
let mut total_sources = 0;
for source in &config.sources {
match source {
config::NtpSourceConfig::Standard(_)
| config::NtpSourceConfig::Nts(_)
| config::NtpSourceConfig::Sock(_) => total_sources += 1,
#[cfg(feature = "pps")]
config::NtpSourceConfig::Pps(_) => {} config::NtpSourceConfig::Pool(cfg) => total_sources += cfg.first.count,
config::NtpSourceConfig::NtsPool(cfg) => total_sources += cfg.first.count,
}
}
let keyset = nts_key_provider::spawn(config.keyset).await;
#[cfg(feature = "hardware-timestamping")]
let clock_config = config.clock;
#[cfg(not(feature = "hardware-timestamping"))]
let clock_config = config::ClockConfig::default();
::tracing::debug!("Configuration loaded, spawning daemon jobs");
let (main_loop_handle, _) = spawn::<SingleShotController<_>>(
config.synchronization.synchronization_base,
SingleShotControllerConfig {
expected_sources: total_sources,
},
config.source_defaults,
clock_config,
&config.sources,
&[], keyset.clone(),
)
.await?;
let _ = main_loop_handle.await;
Ok(ExitCode::SUCCESS)
})
}