Commit fe4133dbcd47
tarball13 files changed+106-50
@@ -38,11 +38,13 @@ separately from its encrypted store. On `SessionTokensChanged`, obtain a new rec
3838
`ClientHandle::session_record()` and replace the saved record atomically in3939
protected storage. Restore with the same store location and passphrase.4040
41
-`LoggedOut` and `SessionAccessRejected` both stop the engine. after a logout or a42
-hard rejection the record and the store are dead, bin them. a soft logout wants43
-the store kept and a fresh login with `PasswordLogin::device_id` set to the old44
-device, or the e2ee keys go with it. a bare `Stopped` means keep everything. one45
-store directory per login, the sdk won't share a crypto store between devices.41
+`Engine::run` resolves to a `Termination` saying why it stopped, and42
+`Termination::retain` says what to do about it. after `LoggedOut` or43
+`AccessRevoked` the record and the store are dead, bin them. `SoftLoggedOut`44
+wants the store kept and a fresh login with `PasswordLogin::device_id` set to the45
+device it carries, or the e2ee keys go with it. `ShutDown` and `HandlesDropped`46
+keep everything. an `Err` is a real failure and kills nothing. one store47
+directory per login, the sdk won't share a crypto store between devices.4648
The headless example is4749
login-only; it does not persist sessions across runs.4850
@@ -41,7 +41,9 @@ the send queues, since the sdk parks a room's queue on any send failure and
4141
never unparks it. a recoverable failure while online gets the room another go4242
after a few seconds4343
44
-logout and a rejected token both stop the engine. `Shutdown` answers once44
+logout and a rejected token both stop the engine. `run` resolves to a45
+`Termination` naming which of those it was, or a shutdown, or every handle46
+dropping, so the host never works it out from the events. `Shutdown` answers once4547
`Stopped` has gone out, every one of them if several were queued4648
4749
`Engine::run` hands back a boxed future. the real one nests deep enough that@@ -1,6 +1,6 @@
11
use macltok::{2
- EngineConfig, Event, PasswordLogin, RoomMembership, Session, StoreConfig, TimelineContent,3
- TimelineId, TimelineItem,2
+ EngineConfig, Event, PasswordLogin, Retain, RoomMembership, Session, StoreConfig,3
+ TimelineContent, TimelineId, TimelineItem,44
};55
66
#[tokio::main]@@ -77,9 +77,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7777
Event::SessionTokensChanged => {7878
std::fs::write(&record_path, serde_json::to_vec(&handle.session_record()?)?)?;7979
}80
- Event::LoggedOut | Event::SessionAccessRejected { soft_logout: false } => {81
- std::fs::remove_dir_all(&store_path)?;82
- }8380
Event::Stopped => break,8481
_ => {}8582
}@@ -91,7 +88,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9188
}9289
}9390
}94
- running.await??;91
+ match running.await??.retain() {92
+ Retain::RecordAndStore => {}93
+ Retain::Store => std::fs::remove_file(&record_path)?,94
+ Retain::Nothing => std::fs::remove_dir_all(&store_path)?,95
+ }9596
Ok(())9697
}9798
@@ -18,7 +18,7 @@ use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
1818
use matrix_sdk_ui::sync_service::SyncService;1919
use ruma::OwnedRoomId;2020
use thiserror::Error as ThisError;21
-use tokio::sync::{Notify, broadcast, mpsc, watch};21
+use tokio::sync::{broadcast, mpsc, watch};2222
2323
pub(crate) use media::MediaService;2424
@@ -32,7 +32,7 @@ use self::{
3232
use crate::{3333
AccountCommand, ClientHandle, Command, ConnectionState, EncryptionCommand, EngineErrorKind,3434
EngineFailure, Error, ErrorKind, Event, EventStream, LifecycleCommand, MediaCommand,35
- NotificationCommand, RoomCommand, TimelineCommand,35
+ NotificationCommand, RoomCommand, Termination, TimelineCommand,3636
transport::{DriverPort, Request, Responder, transport_pair},3737
};3838
@@ -246,7 +246,8 @@ pub struct Engine {
246246
uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,247247
/// the client can outlive us, so the handler has to come off it when we stop248248
verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,249
- stop: Arc<Notify>,249
+ stop: mpsc::UnboundedSender<Termination>,250
+ stops: mpsc::UnboundedReceiver<Termination>,250251
}251252
252253
impl Engine {@@ -286,6 +287,7 @@ impl Engine {
286287
);287288
client.send_queue().enable_upload_progress(true);288289
let (note_sender, notes) = mpsc::unbounded_channel();290
+ let (stop, stops) = mpsc::unbounded_channel();289291
let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);290292
let (notifications, notification_queue) = mpsc::channel(config.command_capacity);291293
let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);@@ -314,18 +316,19 @@ impl Engine {
314316
}),315317
uploads: HashMap::new(),316318
verification_handler: None,317
- stop: Arc::new(Notify::new()),319
+ stop,320
+ stops,318321
};319322
Ok((engine, handle, events))320323
}321324
322325
/// boxed because the engine's future is deep enough to blow the compiler's query depth323326
/// in every crate that spawns it, which isn't something a client should have to fix324
- pub fn run(self) -> EngineBoxFuture<'static, Result<(), EngineError>> {327
+ pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {325328
Box::pin(self.run_inner())326329
}327330
328
- async fn run_inner(mut self) -> Result<(), EngineError> {331
+ async fn run_inner(mut self) -> Result<Termination, EngineError> {329332
let events = self.port.event_sender();330333
let user_id = self331334
.client@@ -348,37 +351,32 @@ impl Engine {
348351
self.resume_upload_forwarders().await;349352
self.sync.start().await;350353
351
- let mut failure = None;352354
let mut shutdown_reply = None;353
- loop {355
+ let ending = loop {354356
tokio::select! {355357
biased;356
- () = events.closed() => {357
- failure = Some(EngineError::EventReceiverClosed);358
- break;359
- }360
- () = self.stop.notified() => break,358
+ () = events.closed() => break Err(EngineError::EventReceiverClosed),359
+ Some(termination) = self.stops.recv() => break Ok(termination),361360
Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {362361
let Some(message) = panic else { continue };363362
if let TaskContext::Command(label) = context {364363
tracing::error!(label, panic = %message, "command task panicked");365364
continue;366365
}367
- failure = Some(EngineError::TaskPanicked { task: context.label() });368
- break;366
+ break Err(EngineError::TaskPanicked { task: context.label() });369367
}370368
Some(note) = self.notes.recv() => self.actor_note(note).await,371369
request = self.port.recv() => {372
- let Some(request) = request else { break };370
+ let Some(request) = request else { break Ok(Termination::HandlesDropped) };373371
if let Some(reply) = self.dispatch(request).await {374372
shutdown_reply = Some(reply);375
- break;373
+ break Ok(Termination::ShutDown);376374
}377375
}378376
}379
- }377
+ };380378
381
- if let Some(error) = &failure {379
+ if let Err(error) = &ending {382380
let _ = events.send(Event::Failed(error.failure())).await;383381
}384382
self.shutdown().await;@@ -392,7 +390,7 @@ impl Engine {
392390
reply.done(Ok(()));393391
}394392
}395
- failure.map_or(Ok(()), Err)393
+ ending396394
}397395
398396
fn start_tasks(&mut self) {@@ -422,6 +420,10 @@ impl Engine {
422420
Box::pin(watchers::session(423421
queues.session_changes,424422
events(),423
+ client424
+ .device_id()425
+ .expect("authentication was checked during construction")426
+ .to_owned(),425427
self.stop.clone(),426428
)),427429
),@@ -523,7 +525,7 @@ impl Engine {
523525
.await524526
.map_err(|source| Error::from_sdk("logout failed", source))?;525527
let _ = events.send(Event::LoggedOut).await;526
- stop.notify_one();528
+ let _ = stop.send(Termination::LoggedOut);527529
Ok(())528530
});529531
}@@ -7,12 +7,12 @@ use matrix_sdk::{
77
send_queue::{LocalEchoContent, RoomSendQueueUpdate},88
};99
use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};10
-use ruma::time::Instant;11
-use tokio::sync::{Notify, broadcast::error::RecvError, watch};10
+use ruma::{OwnedDeviceId, time::Instant};11
+use tokio::sync::{broadcast::error::RecvError, mpsc, watch};1212
1313
use super::executor::TaskSet;1414
use crate::{15
- ConnectionState, Event, encryption::encryption_state, error::classify_sdk_error,15
+ ConnectionState, Event, Termination, encryption::encryption_state, error::classify_sdk_error,1616
transport::EventSender,1717
};1818
@@ -35,7 +35,8 @@ pub(super) async fn room_subscriptions(
3535
pub(super) async fn session(3636
mut changes: tokio::sync::broadcast::Receiver<SessionChange>,3737
events: EventSender,38
- stop: Arc<Notify>,38
+ device_id: OwnedDeviceId,39
+ stop: mpsc::UnboundedSender<Termination>,3940
) {4041
loop {4142
let event = match changes.recv().await {@@ -46,7 +47,11 @@ pub(super) async fn session(
4647
soft_logout: data.soft_logout,4748
})4849
.await;49
- stop.notify_one();50
+ let _ = stop.send(if data.soft_logout {51
+ Termination::SoftLoggedOut { device_id }52
+ } else {53
+ Termination::AccessRevoked54
+ });5055
return;5156
}5257
// a missed refresh costs one needless save, a missed rejection comes round again@@ -33,10 +33,10 @@ pub use model::{
3333
MembershipChange, Message, MessageContent, MessageEditContent, MessageKind, MessageMentions,3434
NotificationCommand, OwnDevice, PowerLevelChanges, Profile, PushNotification,3535
PushNotificationEvent, PushNotificationRoom, PushNotificationSender, PushNotificationStatus,36
- Reaction, RecoveryState, ReplyDetails, ReplyReference, RichText, RoomCategory, RoomCommand,37
- RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,36
+ Reaction, RecoveryState, ReplyDetails, ReplyReference, Retain, RichText, RoomCategory,37
+ RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,3838
RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,39
- Sender, SpaceChild, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,39
+ Sender, SpaceChild, Termination, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,4040
TimelineDiffError, TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState,4141
UploadDetails, UploadThumbnail, UtdCause, VerificationAction, VerificationFailure,4242
VerificationState, VerificationTarget,@@ -682,6 +682,43 @@ pub enum Event {
682682
Stopped,683683
}684684
685
+/// why `Engine::run` came back, when nothing went wrong686
+#[derive(Clone, Debug, Eq, PartialEq)]687
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]688
+#[non_exhaustive]689
+pub enum Termination {690
+ ShutDown,691
+ /// every `ClientHandle` was dropped, so nobody could have asked for anything else692
+ HandlesDropped,693
+ LoggedOut,694
+ /// the server rejected our token for good695
+ AccessRevoked,696
+ /// the server wants this device to log in again, and its e2ee keys live in the store697
+ SoftLoggedOut {698
+ device_id: OwnedDeviceId,699
+ },700
+}701
+702
+impl Termination {703
+ #[must_use]704
+ pub const fn retain(&self) -> Retain {705
+ match self {706
+ Self::ShutDown | Self::HandlesDropped => Retain::RecordAndStore,707
+ Self::SoftLoggedOut { .. } => Retain::Store,708
+ Self::LoggedOut | Self::AccessRevoked => Retain::Nothing,709
+ }710
+ }711
+}712
+713
+/// what's still worth keeping of the saved record and the store714
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]715
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]716
+pub enum Retain {717
+ RecordAndStore,718
+ Store,719
+ Nothing,720
+}721
+685722
#[derive(Clone, Copy, Debug, Eq, PartialEq)]686723
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]687724
#[non_exhaustive]@@ -130,7 +130,7 @@ async fn start_engine(
130130
MatrixMockServer,131131
ClientHandle,132132
EventStream,133
- JoinHandle<Result<(), EngineError>>,133
+ JoinHandle<Result<macltok::Termination, EngineError>>,134134
) {135135
let server = MatrixMockServer::new().await;136136
server@@ -35,7 +35,7 @@ struct MediaHarness {
3535
client: matrix_sdk::Client,3636
handle: ClientHandle,3737
events: EventStream,38
- running: tokio::task::JoinHandle<Result<(), EngineError>>,38
+ running: tokio::task::JoinHandle<Result<macltok::Termination, EngineError>>,3939
media_id: MediaId,4040
}4141
@@ -132,7 +132,7 @@ async fn setup() -> (
132132
Client,133133
ClientHandle,134134
EventStream,135
- tokio::task::JoinHandle<Result<(), macltok::EngineError>>,135
+ tokio::task::JoinHandle<Result<macltok::Termination, macltok::EngineError>>,136136
) {137137
let server = MatrixMockServer::new().await;138138
server@@ -2,7 +2,9 @@ mod support;
22
33
use std::time::Duration;44
5
-use macltok::{ConnectionState, Engine, EngineConfig, EngineError, Event, RoomMembership};5
+use macltok::{6
+ ConnectionState, Engine, EngineConfig, EngineError, Event, Retain, RoomMembership, Termination,7
+};68
use matrix_sdk::{79
ruma::{api::client::sync::sync_events::v5, room_id},810
test_utils::mocks::MatrixMockServer,@@ -92,7 +94,9 @@ async fn soft_logout_says_so_and_stops_the_engine() {
9294
})9395
.await;9496
support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;95
- running.await.unwrap().unwrap();97
+ let termination = running.await.unwrap().unwrap();98
+ assert!(matches!(termination, Termination::SoftLoggedOut { .. }));99
+ assert_eq!(termination.retain(), Retain::Store);96100
}97101
98102
#[tokio::test]@@ -24,13 +24,16 @@ pub async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event)
2424
pub async fn shut_down(2525
handle: &ClientHandle,2626
events: &mut EventStream,27
- running: tokio::task::JoinHandle<Result<(), macltok::EngineError>>,27
+ running: tokio::task::JoinHandle<Result<macltok::Termination, macltok::EngineError>>,2828
) {2929
let (answer, _) = tokio::join!(handle.shutdown(), async {3030
receive_until(events, |event| matches!(event, Event::Stopped)).await3131
});3232
answer.unwrap();33
- running.await.unwrap().unwrap();33
+ assert_eq!(34
+ running.await.unwrap().unwrap(),35
+ macltok::Termination::ShutDown36
+ );3437
}3538
3639
pub fn event_messages(event: &Event) -> Vec<&TimelineEvent> {@@ -330,7 +330,7 @@ async fn start_engine() -> (
330330
MatrixMockServer,331331
ClientHandle,332332
EventStream,333
- JoinHandle<Result<(), EngineError>>,333
+ JoinHandle<Result<macltok::Termination, EngineError>>,334334
) {335335
let (server, client) = configured_client().await;336336
let (handle, events, running) = run_client(client).await;@@ -342,7 +342,7 @@ async fn run_client(
342342
) -> (343343
ClientHandle,344344
EventStream,345
- JoinHandle<Result<(), EngineError>>,345
+ JoinHandle<Result<macltok::Termination, EngineError>>,346346
) {347347
let config = EngineConfig::default().with_initial_history_limit(0);348348
let (engine, handle, events) = Engine::from_client(client, config).await.unwrap();