use anyhow::Result;
use scv_client::{ControlError, Layout};
use scv_protocol::{ConfirmState, DaemonCommand, ErrorCode};
use std::time::Duration;
use super::control;
const NO: i32 = 1;
const UNASKED: i32 = 2;
const POLL: Duration = Duration::from_secs(2);
const LATE_SECONDS: u64 = 120;
pub(crate) async fn confirm(layout: &Layout, question: String, timeout: u64) -> Result<()> {
match ask(layout, question, timeout).await {
0 => Ok(()),
code => std::process::exit(code),
}
}
async fn ask(layout: &Layout, question: String, timeout: u64) -> i32 {
let parent = std::env::var(scv_tools::delegation::PARENT_VARIABLE)
.ok()
.filter(|chain| !chain.trim().is_empty());
let asked = control(
layout,
DaemonCommand::ConfirmAsk {
question,
parent,
timeout_seconds: Some(timeout),
},
)
.await;
let mut info = match asked.map(|status| status.confirm) {
Ok(Some(info)) => info,
Ok(None) => {
eprintln!("scv confirm: the daemon did not take the question");
return UNASKED;
}
Err(error) => {
eprintln!("scv confirm: {}", not_asked(&error));
return UNASKED;
}
};
eprintln!(
"Asked the owner on {}; waiting up to {} for yes or no.",
info.chat,
minutes(timeout)
);
let give_up = info.deadline_unix_seconds + LATE_SECONDS;
loop {
if let Some(code) = exit_code(info.state) {
match info.state {
ConfirmState::Yes => println!("The owner said yes."),
ConfirmState::No => println!("The owner said no."),
ConfirmState::Expired => println!("No answer in time, which counts as no."),
ConfirmState::Failed => eprintln!(
"scv confirm: the question never reached the owner on {}, or the answer was \
lost; nothing was decided",
info.chat
),
_ => eprintln!(
"scv confirm: the question on {} ended without an answer ({:?})",
info.chat, info.state
),
}
return code;
}
tokio::time::sleep(POLL).await;
let followed = control(
layout,
DaemonCommand::ConfirmStatus {
id: info.id.clone(),
},
)
.await;
match followed.map(|status| status.confirm) {
Ok(Some(next)) => info = next,
Ok(None) => {
eprintln!("scv confirm: the daemon no longer reports the question");
return UNASKED;
}
Err(error) => {
let busy = matches!(
error.downcast_ref::<ControlError>(),
Some(ControlError::TimedOut | ControlError::Protocol(_))
);
if busy && unix_now() < give_up {
continue;
}
eprintln!("scv confirm: lost the question while waiting: {error:#}");
return UNASKED;
}
}
}
}
fn exit_code(state: ConfirmState) -> Option<i32> {
match state {
ConfirmState::Pending => None,
ConfirmState::Yes => Some(0),
ConfirmState::No | ConfirmState::Expired => Some(NO),
ConfirmState::Withdrawn | ConfirmState::Failed | ConfirmState::Unknown => Some(UNASKED),
}
}
fn not_asked(error: &anyhow::Error) -> String {
match error.downcast_ref::<ControlError>() {
Some(ControlError::Unavailable(_)) => {
"no SCV daemon is running to ask the owner; nothing was asked".into()
}
Some(ControlError::Server {
code: ErrorCode::InvalidJson,
..
}) => "the running SCV daemon is too old to ask the owner; nothing was asked".into(),
_ => format!("{error:#}"),
}
}
fn minutes(seconds: u64) -> String {
match seconds.div_ceil(60) {
1 => "1 minute".into(),
minutes => format!("{minutes} minutes"),
}
}
fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
#[cfg(test)]
mod tests;