fix: handle operation results before the busy guard so they clear it

This commit is contained in:
randogoth 2026-10-01 23:45:24 +03:00
parent e9e40bca36
commit 96f3748086

View file

@ -389,6 +389,80 @@ impl Carrier {
} }
Task::none() Task::none()
} }
// Results pass regardless of the busy flag: each one clears the
// operation it belongs to, so swallowing it would deadlock.
Message::PinOffered(result) => {
self.done();
match result {
Ok(Some((addr, key))) => {
self.pin_offer = Some((addr, key));
self.note(
"The server presented this key. Pin it only if the fingerprint \
matches what the operator published."
.into(),
);
Task::none()
}
Ok(None) => {
self.note("This server's key is already pinned.".into());
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::Pinned(result) => {
self.done();
match result {
Ok(()) => self.note("server key pinned".into()),
Err(e) => self.fail(e),
}
Task::none()
}
Message::Exported(result) => {
self.done();
match result {
Ok((note, text)) => {
self.backup_text = Some(text);
self.note(note);
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::SnapshotLoaded(result) => {
self.done();
match result {
Ok(snap) => {
self.share_qr = qr_code::Data::new(
format!("smol://{}/{}", snap.who.address, snap.who.key).as_bytes(),
)
.ok();
self.lists = snap.lists.clone();
self.snapshot = Some(snap);
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::Generic(result) => {
self.done();
match result {
Ok(note) => self.refresh_after(note),
Err(e) => {
self.fail(e);
Task::none()
}
}
}
_ if self.busy.is_some() => Task::none(), _ if self.busy.is_some() => Task::none(),
Message::OnboardCreate => { Message::OnboardCreate => {
let master = ops::new_master(); let master = ops::new_master();
@ -477,28 +551,6 @@ impl Carrier {
self.pin_offer = None; self.pin_offer = None;
Task::none() Task::none()
} }
Message::PinOffered(result) => {
self.done();
match result {
Ok(Some((addr, key))) => {
self.pin_offer = Some((addr, key));
self.note(
"The server presented this key. Pin it only if the fingerprint \
matches what the operator published."
.into(),
);
Task::none()
}
Ok(None) => {
self.note("This server's key is already pinned.".into());
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::PinAccept => { Message::PinAccept => {
let Some((addr, key)) = self.pin_offer.take() else { let Some((addr, key)) = self.pin_offer.take() else {
return Task::none(); return Task::none();
@ -507,14 +559,6 @@ impl Carrier {
ops::pin(&store, &addr, &key) ops::pin(&store, &addr, &key)
}) })
} }
Message::Pinned(result) => {
self.done();
match result {
Ok(()) => self.note("server key pinned".into()),
Err(e) => self.fail(e),
}
Task::none()
}
Message::SignupRegister => { Message::SignupRegister => {
let Some(addr) = self.signup_address() else { let Some(addr) = self.signup_address() else {
self.fail("enter user and host".into()); self.fail("enter user and host".into());
@ -878,20 +922,6 @@ impl Carrier {
.map(|(path, text)| (format!("backup written to {}", path.display()), text)) .map(|(path, text)| (format!("backup written to {}", path.display()), text))
}) })
} }
Message::Exported(result) => {
self.done();
match result {
Ok((note, text)) => {
self.backup_text = Some(text);
self.note(note);
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::ImportPressed => { Message::ImportPressed => {
let text = self.import_body.text(); let text = self.import_body.text();
if text.trim().is_empty() { if text.trim().is_empty() {
@ -901,34 +931,46 @@ impl Carrier {
ops::import_backup(&store, &master, &text) ops::import_backup(&store, &master, &text)
}) })
} }
Message::SnapshotLoaded(result) => {
self.done();
match result {
Ok(snap) => {
self.share_qr = qr_code::Data::new(
format!("smol://{}/{}", snap.who.address, snap.who.key).as_bytes(),
)
.ok();
self.lists = snap.lists.clone();
self.snapshot = Some(snap);
Task::none()
}
Err(e) => {
self.fail(e);
Task::none()
}
}
}
Message::Generic(result) => {
self.done();
match result {
Ok(note) => self.refresh_after(note),
Err(e) => {
self.fail(e);
Task::none()
}
}
}
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn app() -> Carrier {
let dir = tempfile::tempdir().unwrap().keep();
let (app, _task) = Carrier::boot(dir);
app
}
#[test]
fn results_clear_the_busy_flag_instead_of_deadlocking() {
let mut app = app();
app.busy = Some("registering".into());
let _ = app.update(Message::Generic(Err("boom".into())));
assert!(app.busy.is_none());
assert!(app.status_error);
app.busy = Some("registering".into());
let _ = app.update(Message::SnapshotLoaded(Err("boom".into())));
assert!(app.busy.is_none());
app.busy = Some("registering".into());
let _ = app.update(Message::Generic(Ok("registered".into())));
// The result was handled: the note landed and the refresh it
// triggers is the new busy operation.
assert_eq!(app.status.as_deref(), Some("registered"));
assert_eq!(app.busy.as_deref(), Some("refreshing"));
}
#[test]
fn actions_are_swallowed_while_busy() {
let mut app = app();
app.master = Some([0u8; KEY_LEN]);
app.busy = Some("fetching".into());
let _ = app.update(Message::FetchPressed);
assert_eq!(app.busy.as_deref(), Some("fetching"));
}
}