Skip to content

Commit 0ceb3a8

Browse files
Handle interrupts across the whole receive session
Ctrl-C was only honored while the receiver polled for the original proposal, because the interrupt select lived inside read_from_directory. Since tokio::signal::ctrl_c replaces the default terminate handler for the life of the process, an interrupt during any other receiver phase (for example a transient retry against an unresponsive relay) was swallowed entirely and the process could only be stopped by an external kill. Wrap process_receiver_session in a select against the interrupt channel in receive_payjoin, mirroring how send_payjoin guards the sender driver. This is safe at any phase: transitions persist to the event log with no await between transition and save, so an interrupted session resumes from its last saved state via the resume command. The interrupt message now includes the session id, and read_from_directory becomes a plain single-step function like the other state handlers.
1 parent abedef7 commit 0ceb3a8

1 file changed

Lines changed: 29 additions & 33 deletions

File tree

payjoin-cli/src/app/v2/mod.rs

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,17 @@ impl AppTrait for App {
293293
println!("Request Payjoin by sharing this Payjoin Uri:");
294294
println!("{pj_uri}");
295295

296-
self.process_receiver_session(ReceiveSession::Initialized(session.clone()), &persister)
297-
.await?;
298-
Ok(())
296+
let mut interrupt = self.interrupt.clone();
297+
tokio::select! {
298+
res = self.process_receiver_session(ReceiveSession::Initialized(session.clone()), &persister) => res,
299+
_ = interrupt.changed() => {
300+
let id = persister.session_id();
301+
println!(
302+
"Session {id} interrupted. Call the `resume` command to resume all sessions, or `payjoin-cli cancel {id}` to cancel the session."
303+
);
304+
Err(anyhow!("Interrupted"))
305+
}
306+
}
299307
}
300308

301309
async fn resume_payjoins(&self, session_id: Option<SessionId>) -> Result<()> {
@@ -806,43 +814,31 @@ impl App {
806814
}
807815
}
808816

809-
#[allow(clippy::incompatible_msrv)]
810817
async fn read_from_directory(
811818
&self,
812819
session: Receiver<Initialized>,
813820
persister: &ReceiverPersister,
814821
) -> Result<ReceiveSession> {
815-
let poll_once = async {
816-
println!("Polling receive request...");
817-
let (ohttp_response, context) =
818-
self.post_via_relay(|relay| session.create_poll_request(relay)).await?;
819-
let state_transition = session
820-
.process_response(ohttp_response.bytes().await?.to_vec().as_slice(), context)
821-
.save(persister);
822-
match state_transition {
823-
Ok(OptionalTransitionOutcome::Progress(next_state)) => {
824-
println!("Got a request from the sender. Responding with a Payjoin proposal.");
825-
Ok(ReceiveSession::UncheckedOriginalPayload(next_state))
826-
}
827-
Ok(OptionalTransitionOutcome::Stasis(current_state)) =>
828-
Ok(ReceiveSession::Initialized(current_state)),
829-
Err(e) if e.is_transient() => {
830-
tracing::debug!("Transient error polling for request, retrying: {e:?}");
831-
let session =
832-
e.transient_state().expect("transient error carries current state");
833-
tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
834-
Ok(ReceiveSession::Initialized(session))
835-
}
836-
Err(e) => Err(e.into()),
822+
println!("Polling receive request...");
823+
let (ohttp_response, context) =
824+
self.post_via_relay(|relay| session.create_poll_request(relay)).await?;
825+
let state_transition = session
826+
.process_response(ohttp_response.bytes().await?.to_vec().as_slice(), context)
827+
.save(persister);
828+
match state_transition {
829+
Ok(OptionalTransitionOutcome::Progress(next_state)) => {
830+
println!("Got a request from the sender. Responding with a Payjoin proposal.");
831+
Ok(ReceiveSession::UncheckedOriginalPayload(next_state))
837832
}
838-
};
839-
let mut interrupt = self.interrupt.clone();
840-
tokio::select! {
841-
res = poll_once => res,
842-
_ = interrupt.changed() => {
843-
println!("Interrupted. Call the `resume` command to resume all sessions.");
844-
Err(anyhow!("Interrupted"))
833+
Ok(OptionalTransitionOutcome::Stasis(current_state)) =>
834+
Ok(ReceiveSession::Initialized(current_state)),
835+
Err(e) if e.is_transient() => {
836+
tracing::debug!("Transient error polling for request, retrying: {e:?}");
837+
let session = e.transient_state().expect("transient error carries current state");
838+
tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
839+
Ok(ReceiveSession::Initialized(session))
845840
}
841+
Err(e) => Err(e.into()),
846842
}
847843
}
848844

0 commit comments

Comments
 (0)