use std::path::{Path, PathBuf};
use decdn_client::GaveUp;
use decdn_client::buyer_pool::{
EscrowUnconfirmed, OpenUnconfirmed, TopUpUnconfirmed, UnconfirmedEscrows, UntrackedEscrow,
};
#[derive(Debug)]
pub struct Interrupted;
impl std::fmt::Display for Interrupted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("interrupted")
}
}
impl std::error::Error for Interrupted {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResumeFrom(pub PathBuf);
impl std::fmt::Display for ResumeFrom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "resume from {}", self.0.display())
}
}
impl std::error::Error for ResumeFrom {}
pub(crate) fn name_resume_dir<T>(result: anyhow::Result<T>, dir: &Path) -> anyhow::Result<T> {
match result {
Err(err)
if err.downcast_ref::<Interrupted>().is_some()
|| err.downcast_ref::<GaveUp>().is_some() =>
{
Err(err.context(ResumeFrom(dir.to_path_buf())))
}
other => other,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnreconciledEscrows {
pending: Vec<EscrowUnconfirmed>,
untracked: Vec<UntrackedEscrow>,
}
impl std::fmt::Display for UnreconciledEscrows {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pending = self.pending.iter().map(ToString::to_string);
let untracked = self.untracked.iter().map(ToString::to_string);
for (i, escrow) in pending.chain(untracked).enumerate() {
if i > 0 {
f.write_str("\n")?;
}
f.write_str(&escrow)?;
}
Ok(())
}
}
impl std::error::Error for UnreconciledEscrows {}
pub(crate) fn name_held_escrows<T>(
result: anyhow::Result<T>,
escrows: &UnconfirmedEscrows,
) -> anyhow::Result<T> {
name_unconfirmed_escrows(result, &escrows.pending(), &escrows.untracked())
}
pub(crate) fn name_unconfirmed_escrows<T>(
result: anyhow::Result<T>,
escrows: &[EscrowUnconfirmed],
untracked: &[UntrackedEscrow],
) -> anyhow::Result<T> {
let err = result.as_ref().err();
let carried = err.and_then(carried);
let named = err
.filter(|err| err.downcast_ref::<Interrupted>().is_none())
.filter(|err| err.downcast_ref::<GaveUp>().is_none())
.map(|err| format!("{err:#}"));
let pending: Vec<EscrowUnconfirmed> = escrows
.iter()
.copied()
.filter(|escrow| Some(*escrow) != carried)
.filter(|escrow| {
named
.as_ref()
.is_none_or(|named| !named.contains(&escrow.to_string()))
})
.collect();
let untracked: Vec<UntrackedEscrow> = untracked
.iter()
.filter(|held| {
named
.as_ref()
.is_none_or(|named| !named.contains(&held.tx.to_string()))
})
.cloned()
.collect();
if pending.is_empty() && untracked.is_empty() {
return result;
}
let left = UnreconciledEscrows { pending, untracked };
match result {
Ok(_) => Err(anyhow::Error::new(left)),
Err(err) => Err(err.context(left)),
}
}
fn carried(err: &anyhow::Error) -> Option<EscrowUnconfirmed> {
err.downcast_ref::<OpenUnconfirmed>()
.copied()
.map(EscrowUnconfirmed::from)
.or_else(|| {
err.downcast_ref::<TopUpUnconfirmed>()
.copied()
.map(EscrowUnconfirmed::from)
})
}
fn in_flight(err: &anyhow::Error) -> Option<String> {
let lines: Vec<String> = carried(err)
.map(|escrow| escrow.to_string())
.into_iter()
.chain(
err.downcast_ref::<UnreconciledEscrows>()
.map(ToString::to_string),
)
.collect();
(!lines.is_empty()).then(|| lines.join("\n"))
}
#[must_use]
pub fn stop_message(err: &anyhow::Error) -> Option<String> {
let in_flight = in_flight(err);
if err.downcast_ref::<Interrupted>().is_some() {
return Some(match (in_flight, err.downcast_ref::<ResumeFrom>()) {
(Some(escrows), _) => format!("interrupted: {escrows}"),
(None, Some(ResumeFrom(dir))) => format!(
"interrupted; run the same command again from {} to resume",
dir.display()
),
(None, None) => "interrupted; run the same command again to resume".to_owned(),
});
}
let gave_up = err.downcast_ref::<GaveUp>()?;
Some(match (in_flight, err.downcast_ref::<ResumeFrom>()) {
(Some(escrows), _) => format!("no progress for {}s: {escrows}", gave_up.idle.as_secs()),
(None, Some(ResumeFrom(dir))) => resume_from(gave_up, dir),
(None, None) => gave_up.to_string(),
})
}
fn resume_from(gave_up: &GaveUp, dir: &Path) -> String {
let line = gave_up.to_string();
match line.strip_suffix("; rerun to resume") {
Some(head) => format!("{head}; rerun from {} to resume", dir.display()),
None => line,
}
}
#[must_use]
pub fn stop_exit(err: &anyhow::Error) -> Option<u8> {
if err.downcast_ref::<Interrupted>().is_some() {
return Some(INTERRUPTED_EXIT);
}
(err.downcast_ref::<GaveUp>().is_some() && in_flight(err).is_none()).then_some(GAVE_UP_EXIT)
}
fn exit_lines(escrows: &UnconfirmedEscrows) -> Vec<String> {
let pending = escrows
.pending()
.into_iter()
.map(|escrow| escrow.to_string());
let untracked = escrows
.untracked()
.into_iter()
.map(|escrow| escrow.to_string());
pending
.chain(untracked)
.map(|escrow| format!("interrupted: {escrow}"))
.collect()
}
pub const INTERRUPTED_EXIT: u8 = 130;
pub const GAVE_UP_EXIT: u8 = 75;
pub(crate) struct Interrupt {
rx: tokio::sync::oneshot::Receiver<()>,
fired: bool,
}
impl Interrupt {
pub(crate) fn watch(escrows: UnconfirmedEscrows) -> Self {
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let mut tx = Some(tx);
while tokio::signal::ctrl_c().await.is_ok() {
if let Some(tx) = tx.take()
&& tx.send(()).is_ok()
{
continue;
}
super::tab_progress::remove_now();
for line in exit_lines(&escrows) {
eprintln!("{line}");
}
std::process::exit(i32::from(INTERRUPTED_EXIT));
}
});
Self { rx, fired: false }
}
#[cfg(test)]
pub(crate) fn manual() -> (tokio::sync::oneshot::Sender<()>, Self) {
let (tx, rx) = tokio::sync::oneshot::channel();
(tx, Self { rx, fired: false })
}
pub(crate) async fn wait(&mut self) {
if self.fired {
return;
}
match (&mut self.rx).await {
Ok(()) => self.fired = true,
Err(_) => std::future::pending::<()>().await,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests;