atagen

macltok

public
Star0
Watch0
3e5a785a7273c52abdc58fb07ceb86b59eb59727
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mod.rsRust1326 lines48.3 KB
1
mod commands;
2
pub(crate) mod context;
3
pub(crate) mod executor;
4
mod media;
5
mod mutations;
6
mod packs;
7
mod prepare;
8
mod repair;
9
mod room;
10
mod session;
11
mod thread_list;
12
mod timeline;
13
mod url_preview;
14
mod watchers;
15
16
use std::{
17
    collections::{HashMap, VecDeque},
18
    future::Future,
19
    panic::AssertUnwindSafe,
20
    sync::Arc,
21
};
22
23
use futures_util::FutureExt;
24
use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
25
use matrix_sdk_ui::sync_service::SyncService;
26
use ruma::{OwnedDeviceId, OwnedRoomId};
27
use thiserror::Error as ThisError;
28
use tokio::sync::{broadcast, mpsc, watch};
29
30
pub(crate) use media::MediaService;
31
pub(crate) use session::Persistence;
32
33
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
34
pub(crate) use crate::store_lock::StoreLock;
35
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
36
pub(crate) enum StoreLock {}
37
38
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
39
pub(crate) use crate::mentions::store::MentionsStore;
40
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
41
pub(crate) enum MentionsStore {}
42
43
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
44
pub(crate) use crate::search::store::SearchStore;
45
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
46
pub(crate) enum SearchStore {}
47
48
use self::{
49
    commands::Job,
50
    executor::{EngineBoxFuture, TaskSet},
51
    mutations::{Mutation, MutationJob},
52
    packs::PackEdit,
53
    room::{ActorNote, ActorRequest, RoomActor},
54
    timeline::{SessionContext, Supervisor},
55
};
56
use crate::{
57
    AccountCommand, ClientHandle, Command, CommandOutput, ConnectionState, EncryptionCommand,
58
    EngineErrorKind, EngineFailure, Error, ErrorKind, Event, EventStream, ImagePackSource,
59
    LifecycleCommand, MediaCommand, NotificationCommand, PackCommand, RoomCommand, Termination,
60
    ThreadListCommand, TimelineCommand,
61
    normalise::{ROOM_PACKS, ROOM_PREVIEW_URLS},
62
    transport::{DriverPort, Request, Responder, transport_pair},
63
};
64
65
#[derive(Clone, Debug, Eq, PartialEq)]
66
#[non_exhaustive]
67
pub struct EngineConfig {
68
    pub command_capacity: usize,
69
    pub event_capacity: usize,
70
    /// what a thread paginates as it opens, and a room if its recent load fails. 0 for neither
71
    pub initial_history_limit: u16,
72
    pub max_media_bytes: usize,
73
    /// media one event context session keeps registered, least recently used dropped first
74
    pub max_context_media: usize,
75
    pub max_mentions_bytes: usize,
76
    pub max_mentions_entries: usize,
77
    pub max_mention_context_events: usize,
78
    pub max_mention_candidates: usize,
79
    /// warm a room up for sending when it's opened or someone starts typing in it
80
    pub prepare_to_send: bool,
81
    /// `/messages` pages each joined room gets per engine run for search backfill. 0 for none
82
    pub search_backfill_pages_per_room: u16,
83
    pub search_backfill_page_size: u16,
84
    /// the least time between two backfill requests, across every room
85
    pub search_backfill_interval_ms: u64,
86
    /// undecryptable messages kept to index once their keys turn up, newest first
87
    pub max_search_undecrypted: usize,
88
}
89
90
impl EngineConfig {
91
    #[must_use]
92
    pub const fn with_max_mentions_bytes(mut self, bytes: usize) -> Self {
93
        self.max_mentions_bytes = bytes;
94
        self
95
    }
96
97
    #[must_use]
98
    pub const fn with_max_mentions_entries(mut self, entries: usize) -> Self {
99
        self.max_mentions_entries = entries;
100
        self
101
    }
102
103
    #[must_use]
104
    pub const fn with_max_mention_context_events(mut self, events: usize) -> Self {
105
        self.max_mention_context_events = events;
106
        self
107
    }
108
109
    #[must_use]
110
    pub const fn with_max_mention_candidates(mut self, candidates: usize) -> Self {
111
        self.max_mention_candidates = candidates;
112
        self
113
    }
114
115
    #[must_use]
116
    pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
117
        self.command_capacity = capacity;
118
        self
119
    }
120
121
    #[must_use]
122
    pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
123
        self.event_capacity = capacity;
124
        self
125
    }
126
127
    #[must_use]
128
    pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
129
        self.initial_history_limit = limit;
130
        self
131
    }
132
133
    #[must_use]
134
    pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
135
        self.max_media_bytes = bytes;
136
        self
137
    }
138
139
    #[must_use]
140
    pub const fn with_max_context_media(mut self, media: usize) -> Self {
141
        self.max_context_media = media;
142
        self
143
    }
144
145
    #[must_use]
146
    pub const fn with_prepare_to_send(mut self, prepare: bool) -> Self {
147
        self.prepare_to_send = prepare;
148
        self
149
    }
150
151
    #[must_use]
152
    pub const fn with_search_backfill_pages_per_room(mut self, pages: u16) -> Self {
153
        self.search_backfill_pages_per_room = pages;
154
        self
155
    }
156
157
    #[must_use]
158
    pub const fn with_search_backfill_page_size(mut self, events: u16) -> Self {
159
        self.search_backfill_page_size = events;
160
        self
161
    }
162
163
    #[must_use]
164
    pub const fn with_search_backfill_interval_ms(mut self, interval_ms: u64) -> Self {
165
        self.search_backfill_interval_ms = interval_ms;
166
        self
167
    }
168
169
    #[must_use]
170
    pub const fn with_max_search_undecrypted(mut self, events: usize) -> Self {
171
        self.max_search_undecrypted = events;
172
        self
173
    }
174
}
175
176
impl Default for EngineConfig {
177
    fn default() -> Self {
178
        Self {
179
            command_capacity: 64,
180
            event_capacity: 128,
181
            initial_history_limit: 30,
182
            max_media_bytes: 20 * 1024 * 1024,
183
            max_context_media: 1_024,
184
            max_mentions_bytes: 64 * 1024 * 1024,
185
            max_mentions_entries: 5_000,
186
            max_mention_context_events: 25_000,
187
            max_mention_candidates: 5_000,
188
            prepare_to_send: true,
189
            search_backfill_pages_per_room: 10,
190
            search_backfill_page_size: 100,
191
            search_backfill_interval_ms: 2_000,
192
            max_search_undecrypted: 2_000,
193
        }
194
    }
195
}
196
197
#[derive(Debug, ThisError)]
198
pub enum EngineError {
199
    #[error("the Matrix client is not authenticated")]
200
    Unauthenticated,
201
    #[error("command, event, media source, and mentions capacities must be nonzero")]
202
    ZeroCapacity,
203
    #[error("could not build the sync service")]
204
    SyncService(#[source] matrix_sdk_ui::sync_service::Error),
205
    #[error("the event receiver has closed")]
206
    EventReceiverClosed,
207
    #[error("engine task {task} panicked")]
208
    TaskPanicked { task: String },
209
}
210
211
impl EngineError {
212
    fn failure(&self) -> EngineFailure {
213
        EngineFailure {
214
            kind: match self {
215
                Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
216
                Self::TaskPanicked { .. } => EngineErrorKind::Watcher,
217
                Self::Unauthenticated | Self::ZeroCapacity | Self::SyncService(_) => {
218
                    EngineErrorKind::Other
219
                }
220
            },
221
            message: self.to_string(),
222
        }
223
    }
224
}
225
226
#[derive(Clone)]
227
enum TaskContext {
228
    /// a panic here costs the caller an `Interrupted` answer and nothing else
229
    Command(&'static str),
230
    /// a panic here takes the engine with it
231
    Watcher(&'static str),
232
    RoomActor {
233
        room_id: OwnedRoomId,
234
        generation: u64,
235
    },
236
}
237
238
impl TaskContext {
239
    fn label(&self) -> String {
240
        match self {
241
            Self::Command(name) | Self::Watcher(name) => (*name).to_owned(),
242
            Self::RoomActor {
243
                room_id,
244
                generation,
245
            } => format!("room actor {room_id} generation {generation}"),
246
        }
247
    }
248
}
249
250
/// tuwunel resends every room in range each time the room list grows, so the sdk's 100 a step
251
/// had a 399 room account hand most rooms to the event cache four or five times
252
const ROOM_LIST_GROWING_BATCH: u32 = 500;
253
254
/// how long shutdown waits on the mentions and search services before aborting them with the
255
/// other tasks. a sync response still being processed kept mentions busy for 2 to 3.4s, and the
256
/// next engine can't start until this one is gone
257
const SERVICE_STOP_GRACE: std::time::Duration = std::time::Duration::from_millis(500);
258
259
async fn stop_within(stop: impl Future<Output = ()>, service: &'static str) {
260
    if matrix_sdk::timeout::timeout(stop, SERVICE_STOP_GRACE)
261
        .await
262
        .is_err()
263
    {
264
        tracing::debug!(service, "a service didn't stop in time and is aborted");
265
    }
266
}
267
268
/// tasks catch their own panics so each comes back with its context, since joining
269
/// through the sdk's executor gives no task id to look one up by. wasm has nothing to catch
270
fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
271
    payload
272
        .downcast_ref::<&str>()
273
        .map(ToString::to_string)
274
        .or_else(|| payload.downcast_ref::<String>().cloned())
275
        .unwrap_or_else(|| "task panicked with a non-string payload".to_owned())
276
}
277
278
struct TaskRegistry {
279
    tasks: TaskSet<(TaskContext, Option<String>)>,
280
}
281
282
impl TaskRegistry {
283
    fn spawn(
284
        &mut self,
285
        context: TaskContext,
286
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
287
    ) {
288
        self.tasks.spawn(async move {
289
            let panic = AssertUnwindSafe(task)
290
                .catch_unwind()
291
                .await
292
                .err()
293
                .map(panic_message);
294
            (context, panic)
295
        });
296
    }
297
298
    fn is_empty(&self) -> bool {
299
        self.tasks.is_empty()
300
    }
301
302
    async fn join_next(&mut self) -> Option<(TaskContext, Option<String>)> {
303
        loop {
304
            if let Ok(done) = self.tasks.join_next().await? {
305
                return Some(done);
306
            }
307
        }
308
    }
309
310
    async fn shutdown(&mut self) {
311
        self.tasks.shutdown().await;
312
    }
313
}
314
315
enum RoomSlot {
316
    Active {
317
        generation: u64,
318
        sender: mpsc::Sender<ActorRequest>,
319
    },
320
    /// opens and closes that arrive while the actor winds down, replayed in order once it's gone
321
    Closing {
322
        generation: u64,
323
        pending: VecDeque<Deferred>,
324
    },
325
}
326
327
impl RoomSlot {
328
    const fn generation(&self) -> u64 {
329
        match self {
330
            Self::Active { generation, .. } | Self::Closing { generation, .. } => *generation,
331
        }
332
    }
333
}
334
335
enum Deferred {
336
    Open(Responder),
337
    Close(Responder),
338
}
339
340
/// logouts in flight, and a token rejection held back because one of them caused it
341
#[derive(Default)]
342
pub(super) struct Logouts {
343
    pub(super) in_flight: usize,
344
    pub(super) rejected: Option<Option<OwnedDeviceId>>,
345
}
346
347
struct Queues {
348
    mutations: mpsc::Receiver<MutationJob>,
349
    notifications: mpsc::Receiver<Job<NotificationCommand>>,
350
    encryption: mpsc::Receiver<Job<EncryptionCommand>>,
351
    thread_lists: mpsc::Receiver<Job<ThreadListCommand>>,
352
    session_changes: broadcast::Receiver<SessionChange>,
353
    persistence: Option<Persistence>,
354
}
355
356
/// requests other devices send us, handed from the sdk's event handler to the encryption worker
357
type IncomingVerifications =
358
    mpsc::UnboundedSender<matrix_sdk::encryption::verification::VerificationRequest>;
359
360
pub struct Engine {
361
    client: Client,
362
    sync: Arc<SyncService>,
363
    port: DriverPort,
364
    config: EngineConfig,
365
    media: Arc<MediaService>,
366
    contexts: Arc<context::Sessions>,
367
    tasks: TaskRegistry,
368
    rooms: HashMap<OwnedRoomId, RoomSlot>,
369
    next_generation: u64,
370
    notes: mpsc::UnboundedReceiver<ActorNote>,
371
    note_sender: mpsc::UnboundedSender<ActorNote>,
372
    /// the rooms sliding sync should follow, applied off the run loop since it touches the store
373
    subscriptions: watch::Sender<Vec<OwnedRoomId>>,
374
    mutations: mpsc::Sender<MutationJob>,
375
    notifications: mpsc::Sender<Job<NotificationCommand>>,
376
    encryption: mpsc::Sender<Job<EncryptionCommand>>,
377
    thread_lists: mpsc::Sender<Job<ThreadListCommand>>,
378
    queues: Option<Queues>,
379
    /// the client can outlive us, so the handler has to come off it when we stop
380
    verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
381
    stop: mpsc::UnboundedSender<Termination>,
382
    stops: mpsc::UnboundedReceiver<Termination>,
383
    logouts: Arc<watch::Sender<Logouts>>,
384
    preparations: prepare::Preparations,
385
    /// shared with every `ClientHandle`, so the store stays locked while the client is reachable
386
    _store_lock: Option<Arc<StoreLock>>,
387
    mentions: crate::mentions::service::Handle,
388
    mentions_service: Option<crate::mentions::service::Service>,
389
    mentions_updates: Option<watch::Receiver<crate::MentionsPage>>,
390
    search: crate::search::Handle,
391
    search_service: Option<crate::search::Service>,
392
}
393
394
impl Engine {
395
    /// the escape hatch for a client you built yourself. `Session` is the usual way in
396
    pub async fn from_client(
397
        client: Client,
398
        config: EngineConfig,
399
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
400
        let session_changes = client.subscribe_to_session_changes();
401
        Self::with_persistence(client, config, session_changes, None, None, None, None).await
402
    }
403
404
    /// `session_changes` is taken when the client is built, since the sdk can refresh
405
    /// before an engine exists
406
    pub(crate) async fn with_persistence(
407
        client: Client,
408
        config: EngineConfig,
409
        session_changes: broadcast::Receiver<SessionChange>,
410
        persistence: Option<Persistence>,
411
        store_lock: Option<StoreLock>,
412
        mentions: Option<Result<MentionsStore, crate::mentions::service::Unopened>>,
413
        search: Option<Result<SearchStore, crate::Error>>,
414
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
415
        if client.user_id().is_none() {
416
            return Err(EngineError::Unauthenticated);
417
        }
418
        if config.command_capacity == 0
419
            || config.event_capacity == 0
420
            || config.max_context_media == 0
421
            || config.max_mentions_entries == 0
422
            || config.max_mention_context_events == 0
423
            || config.max_mention_candidates == 0
424
        {
425
            return Err(EngineError::ZeroCapacity);
426
        }
427
        let store_lock = store_lock.map(Arc::new);
428
        let media = Arc::new(MediaService::new(
429
            client.clone(),
430
            config.max_media_bytes,
431
            config.max_context_media,
432
        ));
433
        let (mentions, mentions_service, mentions_updates) =
434
            crate::mentions::service::Service::new(
435
                client.clone(),
436
                media.clone(),
437
                mentions,
438
                config.clone(),
439
            )
440
            .await
441
            // a missing user id is all it still refuses, and that was turned away above
442
            .map_err(|_| EngineError::Unauthenticated)?;
443
        let sync = Arc::new(
444
            SyncService::builder(client.clone())
445
                .with_offline_mode()
446
                .with_room_list_extra_required_state(vec![
447
                    (ROOM_PACKS.into(), "*".to_owned()),
448
                    (ROOM_PREVIEW_URLS.into(), String::new()),
449
                ])
450
                .with_room_list_response_commit_hook(mentions.hook())
451
                .with_room_list_growing_batch_size(ROOM_LIST_GROWING_BATCH)
452
                .build()
453
                .await
454
                .map_err(EngineError::SyncService)?,
455
        );
456
        let (handle, port, events) = transport_pair(
457
            client.clone(),
458
            sync.clone(),
459
            media.clone(),
460
            store_lock.clone(),
461
            config.command_capacity,
462
            config.event_capacity,
463
        );
464
        let (search, search_service) = crate::search::Service::new(
465
            client.clone(),
466
            media.clone(),
467
            search,
468
            port.event_sender(),
469
            &config,
470
        );
471
        client.send_queue().enable_upload_progress(true);
472
        client.send_queue().enable_global_upload_progress(true);
473
        let (note_sender, notes) = mpsc::unbounded_channel();
474
        let (stop, stops) = mpsc::unbounded_channel();
475
        let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
476
        let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
477
        let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
478
        let (thread_lists, thread_list_queue) = mpsc::channel(config.command_capacity);
479
        let engine = Self {
480
            contexts: Arc::new(context::Sessions::new(port.event_sender())),
481
            client,
482
            sync,
483
            port,
484
            config,
485
            media,
486
            tasks: TaskRegistry {
487
                tasks: TaskSet::new(),
488
            },
489
            rooms: HashMap::new(),
490
            next_generation: 0,
491
            notes,
492
            note_sender,
493
            subscriptions: watch::Sender::new(Vec::new()),
494
            mutations,
495
            notifications,
496
            encryption,
497
            thread_lists,
498
            queues: Some(Queues {
499
                mutations: mutation_queue,
500
                notifications: notification_queue,
501
                encryption: encryption_queue,
502
                thread_lists: thread_list_queue,
503
                session_changes,
504
                persistence,
505
            }),
506
            verification_handler: None,
507
            preparations: prepare::Preparations::default(),
508
            stop,
509
            stops,
510
            logouts: Arc::new(watch::Sender::new(Logouts::default())),
511
            _store_lock: store_lock,
512
            mentions,
513
            mentions_service: Some(mentions_service),
514
            mentions_updates: Some(mentions_updates),
515
            search,
516
            search_service: Some(search_service),
517
        };
518
        Ok((engine, handle, events))
519
    }
520
521
    /// boxed because the engine's future is deep enough to blow the compiler's query depth
522
    /// in every crate that spawns it, which isn't something a client should have to fix
523
    pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {
524
        Box::pin(self.run_inner())
525
    }
526
527
    async fn run_inner(mut self) -> Result<Termination, EngineError> {
528
        let events = self.port.event_sender();
529
        let user_id = self
530
            .client
531
            .user_id()
532
            .expect("authentication was checked during construction")
533
            .to_owned();
534
        self.port
535
            .connection_sender()
536
            .send_replace(ConnectionState::Connecting);
537
        for event in [
538
            Event::Authenticated { user_id },
539
            Event::ConnectionChanged(ConnectionState::Connecting),
540
        ] {
541
            if events.send(event).await.is_err() {
542
                self.shutdown().await;
543
                return Err(EngineError::EventReceiverClosed);
544
            }
545
        }
546
        self.start_tasks();
547
        self.sync.start().await;
548
549
        let mut shutdown_reply = None;
550
        let mut handles_dropped = false;
551
        let mut logouts = self.logouts.subscribe();
552
        let ending = loop {
553
            tokio::select! {
554
                biased;
555
                Some(termination) = self.stops.recv() => break Ok(termination),
556
                () = events.closed() => break Err(EngineError::EventReceiverClosed),
557
                () = settled(&mut logouts), if handles_dropped => {
558
                    break Ok(Termination::HandlesDropped);
559
                }
560
                Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
561
                    let Some(message) = panic else { continue };
562
                    if let TaskContext::Command(label) = context {
563
                        tracing::error!(label, panic = %message, "command task panicked");
564
                        continue;
565
                    }
566
                    break Err(EngineError::TaskPanicked { task: context.label() });
567
                }
568
                Some(note) = self.notes.recv() => self.actor_note(note).await,
569
                request = self.port.recv(), if !handles_dropped => {
570
                    // nobody's left to ask, but a logout they asked for still deserves to land
571
                    let Some(request) = request else {
572
                        handles_dropped = true;
573
                        continue;
574
                    };
575
                    if let Some(reply) = self.dispatch(request).await {
576
                        shutdown_reply = Some(reply);
577
                        break Ok(Termination::ShutDown);
578
                    }
579
                }
580
            }
581
        };
582
583
        if let Err(error) = &ending {
584
            let _ = events.send(Event::Failed(error.failure())).await;
585
        }
586
        self.shutdown().await;
587
        let _ = events.send(Event::Stopped).await;
588
        if let Some(reply) = shutdown_reply {
589
            reply.done(Ok(()));
590
        }
591
        // everything else still queued answers `Interrupted` as it drops, but a shutdown did happen
592
        while let Some(Request { command, reply }) = self.port.try_recv() {
593
            if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
594
                reply.done(Ok(()));
595
            }
596
        }
597
        ending
598
    }
599
600
    fn start_tasks(&mut self) {
601
        let queues = self.queues.take().expect("engine tasks start once");
602
        if let Some(service) = self.mentions_service.take() {
603
            self.tasks
604
                .spawn(TaskContext::Watcher("mentions service"), service.run());
605
        }
606
        if let Some(service) = self.search_service.take() {
607
            self.tasks
608
                .spawn(TaskContext::Watcher("search service"), service.run());
609
        }
610
        if let Some(updates) = self.mentions_updates.take() {
611
            let events = self.port.event_sender();
612
            self.tasks.spawn(
613
                TaskContext::Watcher("mentions updates"),
614
                crate::mentions::service::watch_updates(updates, events),
615
            );
616
        }
617
        let (incoming, incoming_verifications) = mpsc::unbounded_channel();
618
        self.verification_handler = Some(self.verification_handler(incoming));
619
        let client = &self.client;
620
        let events = || self.port.event_sender();
621
        let (notices, notice_queue) = mpsc::unbounded_channel();
622
        let (enabling_recovery, recovery_enabling) = watch::channel(false);
623
        let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 15] = [
624
            (
625
                "upload tracker",
626
                Box::pin(watchers::uploads(
627
                    client.clone(),
628
                    client.send_queue().subscribe(),
629
                    events(),
630
                )),
631
            ),
632
            (
633
                "room subscription watcher",
634
                Box::pin(watchers::room_subscriptions(
635
                    self.sync.clone(),
636
                    self.subscriptions.subscribe(),
637
                )),
638
            ),
639
            (
640
                "session change watcher",
641
                Box::pin(session::session(
642
                    client.clone(),
643
                    queues.session_changes,
644
                    queues.persistence,
645
                    notices,
646
                    client
647
                        .device_id()
648
                        .expect("authentication was checked during construction")
649
                        .to_owned(),
650
                    self.logouts.clone(),
651
                )),
652
            ),
653
            (
654
                "session notice watcher",
655
                Box::pin(session::session_notices(
656
                    notice_queue,
657
                    events(),
658
                    self.stop.clone(),
659
                )),
660
            ),
661
            (
662
                "connection state watcher",
663
                Box::pin(watchers::connection(
664
                    self.sync.state(),
665
                    self.sync.clone(),
666
                    client.clone(),
667
                    events(),
668
                    self.port.connection_sender(),
669
                )),
670
            ),
671
            (
672
                "send queue watcher",
673
                Box::pin(watchers::send_queue_errors(
674
                    client.clone(),
675
                    client.send_queue().subscribe_errors(),
676
                    self.port.connection_sender().subscribe(),
677
                )),
678
            ),
679
            (
680
                "encryption state watcher",
681
                Box::pin(watchers::encryption(
682
                    client.clone(),
683
                    events(),
684
                    recovery_enabling,
685
                )),
686
            ),
687
            (
688
                "room list watcher",
689
                Box::pin(watchers::rooms(client.clone(), events())),
690
            ),
691
            (
692
                "event context watcher",
693
                Box::pin(context::close_on_leave(
694
                    client.clone(),
695
                    self.media.clone(),
696
                    self.contexts.clone(),
697
                )),
698
            ),
699
            (
700
                "image pack watcher",
701
                Box::pin(watchers::image_packs(client.clone(), events())),
702
            ),
703
            (
704
                "notification settings watcher",
705
                Box::pin(watchers::notification_settings(client.clone(), events())),
706
            ),
707
            (
708
                "room command worker",
709
                Box::pin(mutations::run_scheduler(
710
                    client.clone(),
711
                    queues.mutations,
712
                    self.config.command_capacity,
713
                )),
714
            ),
715
            (
716
                "notification command worker",
717
                Box::pin(commands::run_notifications(
718
                    client.clone(),
719
                    queues.notifications,
720
                )),
721
            ),
722
            (
723
                "encryption command worker",
724
                Box::pin(commands::run_encryption(
725
                    client.clone(),
726
                    events(),
727
                    queues.encryption,
728
                    incoming_verifications,
729
                    enabling_recovery,
730
                )),
731
            ),
732
            (
733
                "thread list worker",
734
                Box::pin(thread_list::run(
735
                    client.clone(),
736
                    events(),
737
                    self.media.clone(),
738
                    queues.thread_lists,
739
                )),
740
            ),
741
        ];
742
        for (label, task) in watchers {
743
            self.tasks.spawn(TaskContext::Watcher(label), task);
744
        }
745
    }
746
747
    fn verification_handler(
748
        &self,
749
        incoming: IncomingVerifications,
750
    ) -> matrix_sdk::event_handler::EventHandlerHandle {
751
        use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
752
        self.client.add_event_handler(
753
            move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
754
                let incoming = incoming.clone();
755
                async move {
756
                    let request = client
757
                        .encryption()
758
                        .get_verification_request(
759
                            &event.sender,
760
                            event.content.transaction_id.as_str(),
761
                        )
762
                        .await;
763
                    if let Some(request) = request {
764
                        let _ = incoming.send(request);
765
                    }
766
                }
767
            },
768
        )
769
    }
770
771
    /// hands back the shutdown request's responder, which is answered once we've stopped
772
    async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
773
        tracing::debug!(command = %executor::label(&command), "engine command");
774
        let client = self.client.clone();
775
        match command {
776
            Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
777
            Command::Account(AccountCommand::Logout { remove_pushers }) => {
778
                let events = self.port.event_sender();
779
                let stop = self.stop.clone();
780
                let logouts = self.logouts.clone();
781
                logouts.send_modify(|logouts| logouts.in_flight += 1);
782
                self.spawn_command("logout", reply, async move {
783
                    for pusher in remove_pushers {
784
                        if let Err(error) = crate::transport::remove_pusher(&client, pusher).await {
785
                            tracing::debug!(%error, "removing a pusher before logout failed");
786
                        }
787
                    }
788
                    let result = client.logout().await;
789
                    if result.is_ok() {
790
                        let _ = events.send(Event::LoggedOut).await;
791
                        let _ = stop.send(Termination::LoggedOut);
792
                    }
793
                    let mut rejected = None;
794
                    logouts.send_if_modified(|logouts| {
795
                        if result.is_err() && logouts.in_flight == 1 {
796
                            rejected = logouts.rejected.take();
797
                        }
798
                        false
799
                    });
800
                    if let Some(soft_logout) = rejected {
801
                        session::reject_session(&events, &stop, soft_logout).await;
802
                    }
803
                    logouts.send_modify(|logouts| logouts.in_flight -= 1);
804
                    result.map_err(|source| Error::from_sdk("logout failed", source))
805
                });
806
            }
807
            Command::Account(AccountCommand::SetAccountData {
808
                event_type,
809
                content,
810
            }) => enqueue(
811
                &self.mutations,
812
                Job {
813
                    command: Mutation::GlobalAccountData {
814
                        event_type,
815
                        content,
816
                    },
817
                    reply,
818
                },
819
            ),
820
            Command::Account(AccountCommand::SetDisplayName(name)) => {
821
                self.spawn_command("set display name", reply, async move {
822
                    client
823
                        .account()
824
                        .set_display_name(name.as_deref())
825
                        .await
826
                        .map_err(|source| Error::from_sdk("setting display name failed", source))
827
                });
828
            }
829
            Command::Account(AccountCommand::SetAvatar(avatar)) => {
830
                self.spawn_command("set avatar", reply, async move {
831
                    client
832
                        .account()
833
                        .set_avatar_url(avatar.as_deref())
834
                        .await
835
                        .map_err(|source| Error::from_sdk("setting avatar failed", source))
836
                });
837
            }
838
            Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
839
                let max_bytes = self.config.max_media_bytes;
840
                self.spawn_command("upload avatar", reply, async move {
841
                    commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
842
                });
843
            }
844
            Command::Account(AccountCommand::UploadMedia { mimetype, bytes }) => {
845
                let max_bytes = self.config.max_media_bytes;
846
                self.tasks
847
                    .spawn(TaskContext::Command("upload media"), async move {
848
                        let uploaded =
849
                            commands::upload_media(&client, &mimetype, bytes, max_bytes).await;
850
                        reply.send(uploaded.map(CommandOutput::Mxc));
851
                    });
852
            }
853
            Command::Account(AccountCommand::IgnoreUser(user_id)) => enqueue(
854
                &self.mutations,
855
                Job {
856
                    command: Mutation::Ignore {
857
                        user_id,
858
                        ignored: true,
859
                    },
860
                    reply,
861
                },
862
            ),
863
            Command::Account(AccountCommand::UnignoreUser(user_id)) => enqueue(
864
                &self.mutations,
865
                Job {
866
                    command: Mutation::Ignore {
867
                        user_id,
868
                        ignored: false,
869
                    },
870
                    reply,
871
                },
872
            ),
873
            Command::Account(AccountCommand::ReorderSpaces(order)) => enqueue(
874
                &self.mutations,
875
                Job {
876
                    command: Mutation::ReorderSpaces(order),
877
                    reply,
878
                },
879
            ),
880
            Command::Account(AccountCommand::RenameDevice {
881
                device_id,
882
                display_name,
883
            }) => self.spawn_command("rename device", reply, async move {
884
                commands::rename_device(&client, &device_id, &display_name).await
885
            }),
886
            Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
887
                self.spawn_command("delete devices", reply, async move {
888
                    commands::delete_devices(&client, &device_ids, auth).await
889
                });
890
            }
891
            Command::Room(RoomCommand::Open(room_id)) => {
892
                if self.config.prepare_to_send {
893
                    self.prepare_quietly(room_id.clone());
894
                }
895
                self.open_room(room_id, reply).await;
896
            }
897
            Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
898
            Command::Room(RoomCommand::PrepareToSend(room_id)) => {
899
                let flight = self.preparations.join(&client, room_id);
900
                self.spawn_command("prepare to send", reply, flight);
901
            }
902
            Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
903
                if typing && self.config.prepare_to_send {
904
                    self.prepare_quietly(room_id.clone());
905
                }
906
                self.spawn_command("typing", reply, async move {
907
                    commands::set_typing(&client, &room_id, typing).await
908
                });
909
            }
910
            Command::Room(RoomCommand::CancelSend {
911
                room_id,
912
                transaction_id,
913
            }) => self.spawn_command("cancel send", reply, async move {
914
                commands::cancel_send(&client, &room_id, &transaction_id).await
915
            }),
916
            Command::Room(RoomCommand::RetrySend {
917
                room_id,
918
                transaction_id,
919
            }) => self.spawn_command("retry send", reply, async move {
920
                commands::retry_send(&client, &room_id, &transaction_id).await
921
            }),
922
            Command::Room(command) => enqueue(
923
                &self.mutations,
924
                Job {
925
                    command: Mutation::Room(command),
926
                    reply,
927
                },
928
            ),
929
            Command::Timeline(command) => self.route_timeline(command, reply),
930
            Command::EventContext(command) => {
931
                let (client, media, contexts) =
932
                    (client.clone(), self.media.clone(), self.contexts.clone());
933
                self.tasks
934
                    .spawn(TaskContext::Command("event context"), async move {
935
                        reply.send(context::execute(client, media, contexts, command).await);
936
                    });
937
            }
938
            Command::Mentions(command) => {
939
                let mentions = self.mentions.clone();
940
                self.tasks
941
                    .spawn(TaskContext::Command("mentions command"), async move {
942
                        reply.send(mentions.execute(command).await);
943
                    });
944
            }
945
            Command::Search(command) => {
946
                let search = self.search.clone();
947
                self.tasks
948
                    .spawn(TaskContext::Command("search command"), async move {
949
                        reply.send(search.execute(command).await);
950
                    });
951
            }
952
            Command::HistoryRepair(command) => self.route_repair(command, reply),
953
            Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
954
            Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
955
            Command::Notification(command) => {
956
                enqueue(&self.notifications, Job { command, reply });
957
            }
958
            Command::Pack(command) => self.pack(command, reply),
959
            Command::ThreadList(command) => enqueue(&self.thread_lists, Job { command, reply }),
960
        }
961
        None
962
    }
963
964
    fn spawn_command(
965
        &mut self,
966
        label: &'static str,
967
        reply: Responder,
968
        work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
969
    ) {
970
        self.tasks.spawn(TaskContext::Command(label), async move {
971
            reply.done(work.await);
972
        });
973
    }
974
975
    fn prepare_quietly(&mut self, room_id: OwnedRoomId) {
976
        let flight = self.preparations.join(&self.client, room_id);
977
        self.tasks
978
            .spawn(TaskContext::Command("prepare to send"), async move {
979
                let _ = flight.await;
980
            });
981
    }
982
983
    fn pack(&mut self, command: PackCommand, reply: Responder) {
984
        let edit = |pack, edit| Mutation::Pack { pack, edit };
985
        let mutation = match command {
986
            PackCommand::CreateRoomPack {
987
                room_id,
988
                state_key,
989
                display_name,
990
            } => edit(
991
                ImagePackSource::Room { room_id, state_key },
992
                PackEdit::Create { display_name },
993
            ),
994
            PackCommand::Delete(pack) => edit(pack, PackEdit::Delete),
995
            PackCommand::SetMeta {
996
                pack,
997
                display_name,
998
                avatar,
999
                usage,
1000
            } => edit(
1001
                pack,
1002
                PackEdit::Meta {
1003
                    display_name,
1004
                    avatar,
1005
                    usage,
1006
                },
1007
            ),
1008
            PackCommand::PutImage {
1009
                pack,
1010
                shortcode,
1011
                mxc,
1012
                body,
1013
                usage,
1014
                info,
1015
            } => edit(
1016
                pack,
1017
                PackEdit::Put {
1018
                    shortcode,
1019
                    mxc,
1020
                    body,
1021
                    usage,
1022
                    info,
1023
                },
1024
            ),
1025
            PackCommand::RenameImage { pack, from, to } => {
1026
                edit(pack, PackEdit::Rename { from, to })
1027
            }
1028
            PackCommand::RemoveImage { pack, shortcode } => {
1029
                edit(pack, PackEdit::Remove { shortcode })
1030
            }
1031
            PackCommand::SetEnabledEverywhere {
1032
                room_id,
1033
                state_key,
1034
                enabled,
1035
            } => Mutation::PackEnabledEverywhere {
1036
                room_id,
1037
                state_key,
1038
                enabled,
1039
            },
1040
            command => {
1041
                let client = self.client.clone();
1042
                let mutations = self.mutations.clone();
1043
                let max_bytes = self.config.max_media_bytes;
1044
                self.tasks.spawn(TaskContext::Command("pack"), async move {
1045
                    reply.send(packs::run_detached(client, mutations, max_bytes, command).await);
1046
                });
1047
                return;
1048
            }
1049
        };
1050
        enqueue(
1051
            &self.mutations,
1052
            Job {
1053
                command: mutation,
1054
                reply,
1055
            },
1056
        );
1057
    }
1058
1059
    fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
1060
        let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
1061
            return reply.done(Err(Error::room_unknown()));
1062
        };
1063
        let max_bytes = self.config.max_media_bytes;
1064
        self.spawn_command("upload", reply, async move {
1065
            media::queue_upload(room, upload, max_bytes).await
1066
        });
1067
    }
1068
1069
    fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
1070
        let sender = match self.rooms.get(command.room_id()) {
1071
            Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
1072
            _ => None,
1073
        };
1074
        match (sender, command) {
1075
            (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
1076
            (
1077
                None,
1078
                TimelineCommand::MarkRead {
1079
                    timeline_id,
1080
                    event_id,
1081
                },
1082
            ) => {
1083
                let client = self.client.clone();
1084
                self.spawn_command("mark read", reply, async move {
1085
                    commands::mark_read(&client, &timeline_id, event_id).await
1086
                });
1087
            }
1088
            (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1089
        }
1090
    }
1091
1092
    fn route_repair(&mut self, command: crate::HistoryRepairCommand, reply: Responder) {
1093
        use crate::HistoryRepairCommand;
1094
        let room_id = match &command {
1095
            HistoryRepairCommand::Cancel { room_id }
1096
            | HistoryRepairCommand::RetryRecent { room_id }
1097
            | HistoryRepairCommand::FillGap { room_id, .. } => room_id,
1098
        };
1099
        match self.rooms.get(room_id) {
1100
            Some(RoomSlot::Active { sender, .. }) => {
1101
                send_actor(sender, ActorRequest::Repair(command, reply))
1102
            }
1103
            Some(RoomSlot::Closing { .. }) => {
1104
                reply.done(Err(Error::new(ErrorKind::RoomBusy, "room is closing")))
1105
            }
1106
            None => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1107
        }
1108
    }
1109
1110
    async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1111
        match self.rooms.get_mut(&room_id) {
1112
            Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
1113
            Some(RoomSlot::Closing { pending, .. }) => {
1114
                defer(pending, Deferred::Open(reply), self.config.command_capacity);
1115
            }
1116
            None => self.start_actor(room_id, reply).await,
1117
        }
1118
    }
1119
1120
    async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1121
        let (generation, sender) = match self.rooms.get_mut(&room_id) {
1122
            None => return reply.done(Ok(())),
1123
            Some(RoomSlot::Closing { pending, .. }) => {
1124
                return defer(
1125
                    pending,
1126
                    Deferred::Close(reply),
1127
                    self.config.command_capacity,
1128
                );
1129
            }
1130
            Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
1131
        };
1132
        match sender.try_send(ActorRequest::Close(reply)) {
1133
            Ok(()) => {}
1134
            // the actor already left on its own, which is closed enough
1135
            Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
1136
            Err(mpsc::error::TrySendError::Full(request)) => {
1137
                return request.into_reply().done(Err(Error::new(
1138
                    ErrorKind::RoomBusy,
1139
                    "room command queue is full",
1140
                )));
1141
            }
1142
        }
1143
        self.rooms.insert(
1144
            room_id,
1145
            RoomSlot::Closing {
1146
                generation,
1147
                pending: VecDeque::new(),
1148
            },
1149
        );
1150
        self.update_room_subscriptions();
1151
    }
1152
1153
    async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
1154
        let Some(room) = self.client.get_room(&room_id) else {
1155
            return reply.done(Err(Error::room_unknown()));
1156
        };
1157
        self.next_generation = self.next_generation.wrapping_add(1);
1158
        let generation = self.next_generation;
1159
        let (sender, requests) = mpsc::channel(self.config.command_capacity);
1160
        let (supervisor, panics) = Supervisor::new();
1161
        let actor = RoomActor {
1162
            room_id: room_id.clone(),
1163
            generation,
1164
            room,
1165
            requests,
1166
            notes: self.note_sender.clone(),
1167
            context: SessionContext {
1168
                events: self.port.event_sender(),
1169
                media: self.media.clone(),
1170
                current_user_id: self.client.user_id().map(ToOwned::to_owned),
1171
                initial_history_limit: self.config.initial_history_limit,
1172
                supervisor,
1173
            },
1174
            panics,
1175
            opening: vec![reply],
1176
        };
1177
        self.rooms
1178
            .insert(room_id.clone(), RoomSlot::Active { generation, sender });
1179
        self.tasks.spawn(
1180
            TaskContext::RoomActor {
1181
                room_id,
1182
                generation,
1183
            },
1184
            actor.run(),
1185
        );
1186
        self.update_room_subscriptions();
1187
    }
1188
1189
    async fn actor_note(&mut self, note: ActorNote) {
1190
        let (room_id, generation) = match &note {
1191
            ActorNote::Leaving {
1192
                room_id,
1193
                generation,
1194
            }
1195
            | ActorNote::Exited {
1196
                room_id,
1197
                generation,
1198
                ..
1199
            } => (room_id.clone(), *generation),
1200
        };
1201
        if self
1202
            .rooms
1203
            .get(&room_id)
1204
            .is_none_or(|slot| slot.generation() != generation)
1205
        {
1206
            return;
1207
        }
1208
        let ActorNote::Exited { leftovers, .. } = note else {
1209
            if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
1210
                *slot = RoomSlot::Closing {
1211
                    generation,
1212
                    pending: VecDeque::new(),
1213
                };
1214
                self.update_room_subscriptions();
1215
            }
1216
            return;
1217
        };
1218
        let pending = match self.rooms.remove(&room_id) {
1219
            Some(RoomSlot::Closing { pending, .. }) => pending,
1220
            _ => VecDeque::new(),
1221
        };
1222
        // leftovers were sent before anything deferred, so they go first
1223
        for request in leftovers {
1224
            match request {
1225
                ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
1226
                ActorRequest::Close(reply) => reply.done(Ok(())),
1227
                ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
1228
                ActorRequest::Repair(command, reply) => self.route_repair(command, reply),
1229
            }
1230
        }
1231
        for deferred in pending {
1232
            match deferred {
1233
                Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
1234
                Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
1235
            }
1236
        }
1237
        self.update_room_subscriptions();
1238
    }
1239
1240
    fn update_room_subscriptions(&self) {
1241
        let room_ids = self
1242
            .rooms
1243
            .iter()
1244
            .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
1245
            .map(|(room_id, _)| room_id.clone())
1246
            .collect();
1247
        self.subscriptions.send_replace(room_ids);
1248
    }
1249
1250
    async fn shutdown(&mut self) {
1251
        if let Some(handler) = self.verification_handler.take() {
1252
            self.client.remove_event_handler(handler);
1253
        }
1254
        self.port.close();
1255
        tracing::debug!(step = "sync", "engine shutdown step");
1256
        self.sync.stop().await;
1257
        if self.mentions_service.is_none() {
1258
            tracing::debug!(step = "mentions", "engine shutdown step");
1259
            stop_within(self.mentions.stop(), "mentions").await;
1260
        }
1261
        if self.search_service.is_none() {
1262
            tracing::debug!(step = "search", "engine shutdown step");
1263
            stop_within(self.search.stop(), "search").await;
1264
        }
1265
        tracing::debug!(step = "tasks", "engine shutdown step");
1266
        self.tasks.shutdown().await;
1267
        self.rooms.clear();
1268
        tracing::debug!(step = "contexts", "engine shutdown step");
1269
        self.contexts.clear().await;
1270
        tracing::debug!(step = "media", "engine shutdown step");
1271
        self.media.clear().await;
1272
        tracing::debug!(step = "done", "engine shutdown step");
1273
        self.port
1274
            .connection_sender()
1275
            .send_replace(ConnectionState::Stopped);
1276
        let _ = self
1277
            .port
1278
            .event_sender()
1279
            .send(Event::ConnectionChanged(ConnectionState::Stopped))
1280
            .await;
1281
    }
1282
}
1283
1284
fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
1285
    if let Err(error) = sender.try_send(request) {
1286
        let (request, error) = match error {
1287
            mpsc::error::TrySendError::Full(request) => (
1288
                request,
1289
                Error::new(ErrorKind::RoomBusy, "room command queue is full"),
1290
            ),
1291
            mpsc::error::TrySendError::Closed(request) => (
1292
                request,
1293
                Error::new(ErrorKind::RoomNotOpen, "room is not open"),
1294
            ),
1295
        };
1296
        request.into_reply().done(Err(error));
1297
    }
1298
}
1299
1300
fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
1301
    if pending.len() < capacity {
1302
        return pending.push_back(deferred);
1303
    }
1304
    let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
1305
    reply.done(Err(Error::new(
1306
        ErrorKind::RoomBusy,
1307
        "room lifecycle queue is full",
1308
    )));
1309
}
1310
1311
async fn settled(logouts: &mut watch::Receiver<Logouts>) {
1312
    let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
1313
}
1314
1315
fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
1316
    if let Err(error) = sender.try_send(job) {
1317
        let (job, error) = match error {
1318
            mpsc::error::TrySendError::Full(job) => (
1319
                job,
1320
                Error::new(ErrorKind::QueueFull, "command queue is full"),
1321
            ),
1322
            mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
1323
        };
1324
        job.reply.done(Err(error));
1325
    }
1326
}