From b7c367fef972fd66ccc64967bbbd73857104bfd5 Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 26 Jun 2024 00:35:30 -0400 Subject: [PATCH] Handle cli interrupt gracefully Print an informative message on how to proceed when any `send`, `receive`, or `resume` command is interrupted. Use `tokio::signal::ctrl_c` to do this without introducing any new dependency. --- Cargo.lock | 88 +++++++++++++++++++++++++-------------- payjoin-cli/src/app/v2.rs | 80 +++++++++++++++++++++++++---------- 2 files changed, 116 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acfb886d..edc4a828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -357,9 +357,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" [[package]] name = "cfg-if" @@ -597,7 +597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -631,15 +631,27 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", + "cfg-if", + "cpufeatures 0.2.12", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", "subtle", - "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", ] [[package]] @@ -711,9 +723,9 @@ checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "env_logger" @@ -750,6 +762,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.23" @@ -1049,9 +1067,9 @@ dependencies = [ [[package]] name = "hpke" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf39e5461bfdc6ad0fbc97067519fcaf96a7a2e67f24cc0eb8a1e7c0c45af792" +checksum = "e04a5933a381bb81f00b083fce6b4528e16d735dbeecbb2bdb45e0dbbf3f7e17" dependencies = [ "aead 0.5.2", "aes-gcm 0.10.3", @@ -1061,7 +1079,7 @@ dependencies = [ "generic-array", "hkdf 0.12.4", "hmac 0.12.1", - "rand_core 0.6.4", + "rand_core", "sha2 0.10.8", "subtle", "x25519-dalek", @@ -1524,9 +1542,9 @@ dependencies = [ [[package]] name = "ohttp" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578cb11a3fb5c85697ed8bb850d5ad1cbf819d3eea05c2b253cf1d240fbb10c5" +checksum = "4ea87785aa505c77b1447f9092b1bd583402bc5d986f8f36c7963df17eef9c8f" dependencies = [ "aead 0.4.3", "aes-gcm 0.9.2", @@ -1947,7 +1965,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1957,15 +1975,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - [[package]] name = "rand_core" version = "0.6.4" @@ -2178,6 +2190,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -2363,6 +2384,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.203" @@ -2985,9 +3012,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3318,13 +3345,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", - "zeroize", + "rand_core", ] [[package]] diff --git a/payjoin-cli/src/app/v2.rs b/payjoin-cli/src/app/v2.rs index fc7a7929..a34c6170 100644 --- a/payjoin-cli/src/app/v2.rs +++ b/payjoin-cli/src/app/v2.rs @@ -9,6 +9,8 @@ use payjoin::bitcoin::Amount; use payjoin::receive::v2::ActiveSession; use payjoin::send::RequestContext; use payjoin::{base64, bitcoin, Error, Uri}; +use tokio::signal; +use tokio::sync::watch; use super::config::AppConfig; use super::App as AppTrait; @@ -19,13 +21,16 @@ use crate::db::Database; pub(crate) struct App { config: AppConfig, db: Arc, + interrupt: watch::Receiver<()>, } #[async_trait::async_trait] impl AppTrait for App { fn new(config: AppConfig) -> Result { let db = Arc::new(Database::create(&config.db_path)?); - let app = Self { config, db }; + let (interrupt_tx, interrupt_rx) = watch::channel(()); + tokio::spawn(handle_interrupt(interrupt_tx)); + let app = Self { config, db, interrupt: interrupt_rx }; app.bitcoind()? .get_blockchain_info() .context("Failed to connect to bitcoind. Check config RPC connection.")?; @@ -102,9 +107,16 @@ impl AppTrait for App { impl App { async fn spawn_payjoin_sender(&self, mut req_ctx: RequestContext) -> Result<()> { - let res = self.long_poll_post(&mut req_ctx).await?; - self.process_pj_response(res)?; - self.db.clear_send_session(req_ctx.endpoint())?; + let mut interrupt = self.interrupt.clone(); + tokio::select! { + res = self.long_poll_post(&mut req_ctx) => { + self.process_pj_response(res?)?; + self.db.clear_send_session(req_ctx.endpoint())?; + } + _ = interrupt.changed() => { + println!("Interrupted. Call `send` with the same arguments to resume this session or `resume` to resume all sessions."); + } + } Ok(()) } @@ -123,7 +135,15 @@ impl App { println!("Request Payjoin by sharing this Payjoin Uri:"); println!("{}", pj_uri); - let res = self.long_poll_fallback(&mut session).await?; + let mut interrupt = self.interrupt.clone(); + let res = tokio::select! { + res = self.long_poll_fallback(&mut session) => res, + _ = interrupt.changed() => { + println!("Interrupted. Call the `resume` command to resume all sessions."); + return Ok(()); + } + }?; + println!("Fallback transaction received. Consider broadcasting this to get paid if the Payjoin fails:"); println!("{}", serialize_hex(&res.extract_tx_to_schedule_broadcast())); let mut payjoin_proposal = self @@ -154,29 +174,40 @@ impl App { } pub async fn resume_payjoins(&self) -> Result<()> { + let recv_sessions = self.db.get_recv_sessions()?; + let send_sessions = self.db.get_send_sessions()?; + + if recv_sessions.is_empty() && send_sessions.is_empty() { + println!("No sessions to resume."); + return Ok(()); + } + let mut tasks = Vec::new(); - let recv_sessions = self.db.get_recv_sessions()?; - for recv_session in recv_sessions { + for session in recv_sessions { let self_clone = self.clone(); - tasks.push(tokio::task::spawn(async move { - self_clone.spawn_payjoin_receiver(recv_session, None).await + tasks.push(tokio::spawn(async move { + self_clone.spawn_payjoin_receiver(session, None).await })); } - let send_sessions = self.db.get_send_sessions()?; - for send_session in send_sessions { + + for session in send_sessions { let self_clone = self.clone(); - tasks.push(tokio::task::spawn(async move { - self_clone.spawn_payjoin_sender(send_session).await - })); + tasks.push(tokio::spawn(async move { self_clone.spawn_payjoin_sender(session).await })); } - if tasks.is_empty() { - println!("No sessions to resume."); - } else { - for task in tasks { - let _ = task.await?; + + let mut interrupt = self.interrupt.clone(); + tokio::select! { + _ = async { + for task in tasks { + let _ = task.await; + } + } => { + println!("All resumed sessions completed."); + } + _ = interrupt.changed() => { + println!("Resumed sessions were interrupted."); } - println!("All resumed sessions completed."); } Ok(()) } @@ -199,7 +230,7 @@ impl App { Ok(Some(psbt)) => return Ok(psbt), Ok(None) => { println!("No response yet."); - std::thread::sleep(std::time::Duration::from_secs(5)) + tokio::time::sleep(std::time::Duration::from_secs(5)).await; } Err(re) => { println!("{}", re); @@ -351,6 +382,13 @@ async fn unwrap_ohttp_keys_or_else_fetch(config: &AppConfig) -> Result) { + if let Err(e) = signal::ctrl_c().await { + eprintln!("Error setting up Ctrl-C handler: {}", e); + } + let _ = tx.send(()); +} + fn map_reqwest_err(e: reqwest::Error) -> anyhow::Error { match e.status() { Some(status_code) => anyhow!("HTTP request failed: {} {}", status_code, e),