use std::io::IsTerminal;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::time::timeout;
use crate::ui;
pub async fn sync_clock(target: &str, non_interactive: bool) {
ui::section("CLOCK SYNC");
if !non_interactive && std::io::stdin().is_terminal() {
let confirm = dialoguer::Confirm::new()
.with_prompt(" Sync local clock with target DC?")
.default(true)
.interact_opt()
.unwrap_or(Some(false))
.unwrap_or(false);
if !confirm {
ui::stage_skip("CLOCK", "user declined");
return;
}
}
let spin = ui::spinner("CLOCK");
spin.set_message(format!("syncing with {} ...", target));
let is_root = is_elevated();
if is_root {
if try_sync_direct(target, &spin).await {
return;
}
ui::finish_spinner_fail(&spin, "clock sync failed — no compatible tool found");
ui::info("Install ntpdate or rdate");
return;
}
if try_sync_direct(target, &spin).await {
return;
}
spin.finish_and_clear();
let password = match rpassword::prompt_password(" [sudo] password: ") {
Ok(p) if !p.is_empty() => p,
_ => {
ui::warning("No password provided — skipping clock sync");
ui::info("Kerberos may fail due to clock skew (>5 min difference)");
return;
}
};
let spin = ui::spinner("CLOCK");
spin.set_message(format!("syncing with {} (sudo) ...", target));
if try_sync_sudo(target, &password, &spin).await {
return;
}
ui::finish_spinner_fail(&spin, "clock sync failed");
ui::warning("Kerberos may fail due to clock skew");
ui::info("Try manually: sudo ntpdate -u <target>");
}
async fn try_sync_direct(target: &str, spin: &indicatif::ProgressBar) -> bool {
let candidates: &[(&str, &[&str])] = &[
("ntpdate", &["-u", target]),
("net", &["time", "set", "-S", target]),
("rdate", &["-n", "-s", target]),
];
for (bin, args) in candidates {
ui::verbose(&format!("trying: {} {}", bin, args.join(" ")));
let out = timeout(
Duration::from_secs(10),
Command::new(bin).args(*args).output(),
)
.await;
match out {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
ui::verbose_output(bin, &stdout);
ui::verbose_output(bin, &stderr);
if output.status.success() {
let detail = stdout.trim().lines().next().unwrap_or("ok");
ui::finish_spinner(spin, &format!("synced via {}: {}", bin, truncate(detail, 60)));
return true;
}
}
Ok(Err(_)) => continue, Err(_) => continue, }
}
false
}
async fn try_sync_sudo(target: &str, password: &str, spin: &indicatif::ProgressBar) -> bool {
let candidates: &[(&str, &[&str])] = &[
("ntpdate", &["-u", target]),
("net", &["time", "set", "-S", target]),
("rdate", &["-n", "-s", target]),
];
for (bin, args) in candidates {
ui::verbose(&format!("trying sudo: {} {}", bin, args.join(" ")));
let mut cmd = Command::new("sudo");
cmd.arg("-S")
.arg("-p")
.arg("") .arg(bin)
.args(*args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = match cmd.spawn() {
Ok(c) => c,
Err(_) => continue,
};
let mut child = child;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(format!("{}\n", password).as_bytes()).await;
drop(stdin);
}
let out = timeout(Duration::from_secs(15), child.wait_with_output()).await;
match out {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
ui::verbose_output(&format!("sudo {}", bin), &stdout);
ui::verbose_output(&format!("sudo {}", bin), &stderr);
if stderr.to_lowercase().contains("incorrect password")
|| stderr.to_lowercase().contains("sorry, try again")
{
ui::finish_spinner_fail(spin, "wrong sudo password");
return false;
}
if output.status.success() {
let detail = stdout.trim().lines().next().unwrap_or("ok");
ui::finish_spinner(
spin,
&format!("synced via sudo {}: {}", bin, truncate(detail, 60)),
);
return true;
}
}
Ok(Err(_)) => continue,
Err(_) => continue,
}
}
false
}
fn is_elevated() -> bool {
#[cfg(unix)]
{
unsafe { libc::geteuid() == 0 }
}
#[cfg(not(unix))]
{
false
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
format!("{}...", s.chars().take(max).collect::<String>())
}
}