1#[cfg(any(all(feature = "test", feature = "kobo"), doc))]
40mod dbus_monitor;
41pub mod dictionary_index;
42#[cfg(any(feature = "test", doc))]
43mod hello_world;
44pub mod import;
45pub mod thumbnail;
46#[cfg(any(feature = "kobo", doc))]
47mod wifi_status_monitor;
48
49use std::collections::{HashMap, VecDeque};
50use std::sync::atomic::{AtomicBool, Ordering};
51use std::sync::mpsc::{self, Receiver, Sender};
52use std::thread::{self, JoinHandle};
53use std::time::Duration;
54
55use thiserror::Error;
56
57use crate::db::Database;
58use crate::settings::Settings;
59use crate::view::Event;
60
61#[derive(Error, Debug)]
63pub enum TaskError {
64 #[error("task '{0}' is already running")]
66 AlreadyRunning(TaskId),
67
68 #[error("task '{0}' is not running")]
70 NotRunning(TaskId),
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub enum TaskId {
76 Placeholder,
78 Import,
80 ThumbnailExtraction,
82 DictionaryIndex,
84 #[cfg(any(feature = "test", doc))]
86 HelloWorld,
87 #[cfg(any(all(feature = "test", feature = "kobo"), doc))]
89 DbusMonitor,
90 #[cfg(any(feature = "kobo", doc))]
92 WifiStatusMonitor,
93 #[cfg(test)]
95 TestTask,
96 #[cfg(test)]
98 TestTask2,
99}
100
101impl std::fmt::Display for TaskId {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 TaskId::Placeholder => write!(f, "placeholder"),
105 TaskId::Import => write!(f, "import"),
106 TaskId::ThumbnailExtraction => write!(f, "thumbnail_extraction"),
107 TaskId::DictionaryIndex => write!(f, "dictionary_index"),
108 #[cfg(feature = "test")]
109 TaskId::HelloWorld => write!(f, "hello_world"),
110 #[cfg(all(feature = "test", feature = "kobo"))]
111 TaskId::DbusMonitor => write!(f, "dbus_monitor"),
112 #[cfg(feature = "kobo")]
113 TaskId::WifiStatusMonitor => write!(f, "wifi_status_monitor"),
114 #[cfg(test)]
115 TaskId::TestTask => write!(f, "test_task"),
116 #[cfg(test)]
117 TaskId::TestTask2 => write!(f, "test_task_2"),
118 }
119 }
120}
121
122pub struct ShutdownSignal {
127 receiver: Receiver<()>,
128 _sender_anchor: Option<Sender<()>>,
131 stopped: AtomicBool,
132}
133
134impl ShutdownSignal {
135 fn new(receiver: Receiver<()>) -> Self {
136 Self {
137 receiver,
138 _sender_anchor: None,
139 stopped: AtomicBool::new(false),
140 }
141 }
142
143 pub fn never() -> Self {
148 let (tx, rx) = mpsc::channel();
149 Self {
150 receiver: rx,
151 _sender_anchor: Some(tx),
152 stopped: AtomicBool::new(false),
153 }
154 }
155
156 #[cfg(test)]
162 pub fn new_for_test(receiver: Receiver<()>) -> Self {
163 Self::new(receiver)
164 }
165
166 pub fn should_stop(&self) -> bool {
172 if self.stopped.load(Ordering::Acquire) {
173 return true;
174 }
175 if self.receiver.try_recv().is_ok() {
176 self.stopped.store(true, Ordering::Release);
177 return true;
178 }
179 false
180 }
181
182 pub fn wait(&self, duration: Duration) -> bool {
188 if self.stopped.load(Ordering::Acquire) {
189 return true;
190 }
191 match self.receiver.recv_timeout(duration) {
192 Ok(()) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
193 self.stopped.store(true, Ordering::Release);
194 true
195 }
196 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => false,
197 }
198 }
199}
200
201pub trait BackgroundTask: Send {
207 fn id(&self) -> TaskId;
209
210 fn run(&mut self, hub: &Sender<Event>, shutdown: &ShutdownSignal);
215
216 fn stop(&mut self) {}
220
221 fn finished_event(&self) -> Option<Event> {
226 None
227 }
228}
229
230struct RunningTask {
231 handle: JoinHandle<()>,
232 shutdown: Sender<()>,
233 finished_event: Option<Event>,
235}
236
237pub struct TaskManager {
242 tasks: HashMap<TaskId, RunningTask>,
243 pending_import_indices: VecDeque<(Option<usize>, bool)>,
245 pending_thumbnail_indices: VecDeque<Option<usize>>,
247 buffered_events: Vec<Event>,
249}
250
251impl TaskManager {
252 pub fn new() -> Self {
254 Self {
255 tasks: HashMap::new(),
256 pending_import_indices: VecDeque::new(),
257 pending_thumbnail_indices: VecDeque::new(),
258 buffered_events: Vec::new(),
259 }
260 }
261
262 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, task, hub), fields(task_id = tracing::field::Empty), ret))]
269 pub fn start(
270 &mut self,
271 task: Box<dyn BackgroundTask>,
272 hub: Sender<Event>,
273 ) -> Result<TaskId, TaskError> {
274 let id = task.id();
275
276 #[cfg(feature = "tracing")]
277 tracing::Span::current().record("task_id", tracing::field::display(&id));
278
279 if self.is_running(&id) {
280 return Err(TaskError::AlreadyRunning(id));
281 }
282
283 let (shutdown_tx, shutdown_rx) = mpsc::channel();
284 let shutdown_signal = ShutdownSignal::new(shutdown_rx);
285
286 let finished_event = task.finished_event();
287
288 let handle = thread::spawn(move || {
289 let mut task = task;
290 tracing::info!("task started");
291 task.run(&hub, &shutdown_signal);
292 task.stop();
293 tracing::info!("task stopped");
294 });
295
296 self.tasks.insert(
297 id.clone(),
298 RunningTask {
299 handle,
300 shutdown: shutdown_tx,
301 finished_event,
302 },
303 );
304
305 tracing::info!("task registered");
306 Ok(id)
307 }
308
309 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(task_id = %id), ret))]
314 pub fn stop(&mut self, id: &TaskId) -> Result<(), TaskError> {
315 self.cleanup_finished();
316 if let Some(task) = self.tasks.remove(id) {
317 tracing::info!("sending shutdown signal");
318 if let Err(e) = task.shutdown.send(()) {
319 tracing::error!(error = %e, "failed to send shutdown signal");
320 }
321 if task.handle.join().is_err() {
322 tracing::error!("task thread panicked");
323 }
324 Ok(())
325 } else {
326 Err(TaskError::NotRunning(id.clone()))
327 }
328 }
329
330 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(task_count = tracing::field::Empty)))]
334 pub fn stop_all(&mut self) {
335 let tasks: Vec<_> = self.tasks.drain().collect();
336
337 #[cfg(feature = "tracing")]
338 tracing::Span::current().record("task_count", tasks.len());
339
340 if !tasks.is_empty() {
341 tracing::info!("stopping all tasks");
342 }
343 for (_, task) in &tasks {
344 if let Err(e) = task.shutdown.send(()) {
345 tracing::error!(error = %e, "failed to send shutdown signal");
346 }
347 }
348 for (_, task) in tasks {
349 if task.handle.join().is_err() {
350 tracing::error!("task thread panicked");
351 }
352 }
353 }
354
355 fn cleanup_finished(&mut self) {
358 let finished: Vec<TaskId> = self
359 .tasks
360 .iter()
361 .filter(|(_, task)| task.handle.is_finished())
362 .map(|(id, _)| id.clone())
363 .collect();
364
365 for id in finished {
366 if let Some(task) = self.tasks.remove(&id) {
367 if task.handle.join().is_ok() {
368 if let Some(evt) = task.finished_event {
369 self.buffered_events.push(evt);
370 }
371 } else {
372 tracing::error!(task_id = %id, "task thread panicked");
373 }
374 }
375 }
376 }
377
378 fn flush_buffered_events(&mut self, hub: &Sender<Event>) {
380 for evt in self.buffered_events.drain(..) {
381 hub.send(evt).ok();
382 }
383 }
384
385 #[cfg_attr(
390 feature = "tracing",
391 tracing::instrument(skip(self, hub, database, settings))
392 )]
393 pub fn handle_event(
394 &mut self,
395 evt: &Event,
396 hub: &Sender<Event>,
397 database: &Database,
398 settings: &Settings,
399 ) -> bool {
400 self.cleanup_finished();
401 self.flush_buffered_events(hub);
402
403 match evt {
404 Event::ImportLibrary {
405 library_index,
406 force,
407 } => {
408 self.schedule_import(*library_index, *force, hub, database, settings);
409 }
410 Event::ImportFinished { library_index } => {
411 self.drain_pending_imports(hub, database, settings);
412 self.schedule_thumbnail_extraction(*library_index, hub, database, settings);
413 }
414 Event::ThumbnailExtractionFinished { .. } => {
415 self.drain_pending_thumbnails(hub, database, settings);
416 }
417 Event::ReindexDictionaries => {
418 self.schedule_dictionary_index(hub, database);
419 }
420 _ => {}
421 }
422 false
423 }
424
425 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
427 fn schedule_import(
428 &mut self,
429 library_index: Option<usize>,
430 force: bool,
431 hub: &Sender<Event>,
432 database: &Database,
433 settings: &Settings,
434 ) {
435 if self.is_running(&TaskId::Import) {
436 tracing::info!(library_index = ?library_index, force, "import already running, queueing");
437 self.pending_import_indices
438 .push_back((library_index, force));
439 return;
440 }
441
442 self.flush_buffered_events(hub);
443
444 let task = Box::new(import::ImportTask::new(
445 database.clone(),
446 settings.clone(),
447 library_index,
448 force,
449 ));
450
451 if let Err(e) = self.start(task, hub.clone()) {
452 tracing::warn!(error = %e, "failed to start import task");
453 }
454 }
455
456 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
458 fn drain_pending_imports(
459 &mut self,
460 hub: &Sender<Event>,
461 database: &Database,
462 settings: &Settings,
463 ) {
464 if self.is_running(&TaskId::Import) || self.pending_import_indices.is_empty() {
465 return;
466 }
467
468 let Some((next, force)) = self.pending_import_indices.pop_front() else {
469 return;
470 };
471 self.schedule_import(next, force, hub, database, settings);
472 }
473
474 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
476 fn schedule_dictionary_index(&mut self, hub: &Sender<Event>, database: &Database) {
477 if self.is_running(&TaskId::DictionaryIndex) {
478 tracing::debug!("stopping running dictionary index task for restart");
479 if let Err(e) = self.stop(&TaskId::DictionaryIndex) {
480 tracing::warn!(error = %e, "failed to stop dictionary_index task for restart");
481 }
482 }
483
484 self.flush_buffered_events(hub);
485
486 let task = Box::new(dictionary_index::DictionaryIndexTask::new(database.clone()));
487
488 if let Err(e) = self.start(task, hub.clone()) {
489 tracing::warn!(error = %e, "failed to start dictionary_index task");
490 }
491 }
492
493 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
495 pub fn schedule_thumbnail_extraction(
496 &mut self,
497 library_index: Option<usize>,
498 hub: &Sender<Event>,
499 database: &Database,
500 settings: &Settings,
501 ) {
502 if self.is_running(&TaskId::ThumbnailExtraction) {
503 tracing::info!(library_index = ?library_index, "thumbnail extraction already running, queueing");
504 self.pending_thumbnail_indices.push_back(library_index);
505 return;
506 }
507
508 self.flush_buffered_events(hub);
509
510 let task = Box::new(thumbnail::ThumbnailExtractionTask::new(
511 database.clone(),
512 settings.clone(),
513 library_index,
514 ));
515
516 if let Err(e) = self.start(task, hub.clone()) {
517 tracing::warn!(error = %e, "failed to start thumbnail extraction task");
518 }
519 }
520
521 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
523 fn drain_pending_thumbnails(
524 &mut self,
525 hub: &Sender<Event>,
526 database: &Database,
527 settings: &Settings,
528 ) {
529 if self.is_running(&TaskId::ThumbnailExtraction)
530 || self.pending_thumbnail_indices.is_empty()
531 {
532 return;
533 }
534
535 let Some(next) = self.pending_thumbnail_indices.pop_front() else {
536 return;
537 };
538 self.schedule_thumbnail_extraction(next, hub, database, settings);
539 }
540
541 pub fn is_running(&mut self, id: &TaskId) -> bool {
543 self.cleanup_finished();
544 self.tasks.contains_key(id)
545 }
546
547 pub fn running_tasks(&mut self) -> Vec<TaskId> {
549 self.cleanup_finished();
550 self.tasks.keys().cloned().collect()
551 }
552}
553
554impl Default for TaskManager {
555 fn default() -> Self {
556 Self::new()
557 }
558}
559
560impl Drop for TaskManager {
561 fn drop(&mut self) {
562 self.stop_all();
563 }
564}
565
566pub fn register_startup_tasks(
576 manager: &mut TaskManager,
577 hub: Sender<Event>,
578 settings: &Settings,
579 database: &Database,
580) {
581 #[cfg(feature = "kobo")]
582 {
583 let task = Box::new(wifi_status_monitor::WifiStatusMonitorTask);
584 if let Err(e) = manager.start(task, hub.clone()) {
585 tracing::warn!(error = %e, "failed to start wifi_status_monitor task");
586 }
587 }
588
589 #[cfg(feature = "test")]
590 {
591 let task = Box::new(hello_world::HelloWorldTask);
592 if let Err(e) = manager.start(task, hub.clone()) {
593 tracing::warn!(error = %e, "failed to start hello_world task");
594 }
595
596 #[cfg(feature = "kobo")]
597 if settings.logging.enable_dbus_log {
598 let task = Box::new(dbus_monitor::DbusMonitorTask);
599 if let Err(e) = manager.start(task, hub.clone()) {
600 tracing::warn!(error = %e, "failed to start dbus_monitor task");
601 }
602 }
603 }
604
605 manager.schedule_import(None, false, &hub, database, settings);
606
607 let task = Box::new(dictionary_index::DictionaryIndexTask::new(database.clone()));
608 if let Err(e) = manager.start(task, hub.clone()) {
609 tracing::warn!(error = %e, "failed to start dictionary_index task");
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use std::sync::mpsc;
617 use std::time::{Duration, Instant};
618
619 fn wait_until_not_running(manager: &mut TaskManager, id: &TaskId) {
620 let deadline = Instant::now() + Duration::from_secs(5);
621 while Instant::now() < deadline {
622 if !manager.is_running(id) {
623 return;
624 }
625 std::thread::sleep(Duration::from_millis(1));
626 }
627 panic!("task '{id}' did not finish within timeout");
628 }
629
630 struct InstantTask;
631
632 impl BackgroundTask for InstantTask {
633 fn id(&self) -> TaskId {
634 TaskId::TestTask2
635 }
636
637 fn run(&mut self, _hub: &Sender<Event>, _shutdown: &ShutdownSignal) {}
638 }
639
640 struct WaitingTask;
641
642 impl BackgroundTask for WaitingTask {
643 fn id(&self) -> TaskId {
644 TaskId::TestTask
645 }
646
647 fn run(&mut self, _hub: &Sender<Event>, shutdown: &ShutdownSignal) {
648 shutdown.wait(Duration::from_secs(60));
649 }
650 }
651
652 #[test]
653 fn start_and_stop() {
654 let mut manager = TaskManager::new();
655 let (hub, _rx) = mpsc::channel();
656
657 let id = manager.start(Box::new(WaitingTask), hub).unwrap();
658 assert!(manager.is_running(&id));
659
660 manager.stop(&id).unwrap();
661 assert!(!manager.is_running(&id));
662 }
663
664 #[test]
665 fn duplicate_start_returns_error() {
666 let mut manager = TaskManager::new();
667 let (hub, _rx) = mpsc::channel();
668
669 manager.start(Box::new(WaitingTask), hub.clone()).unwrap();
670 let err = manager.start(Box::new(WaitingTask), hub).unwrap_err();
671
672 assert!(matches!(err, TaskError::AlreadyRunning(TaskId::TestTask)));
673 }
674
675 #[test]
676 fn finished_task_is_cleaned_up() {
677 let mut manager = TaskManager::new();
678 let (hub, _rx) = mpsc::channel();
679
680 let id = manager.start(Box::new(InstantTask), hub).unwrap();
681
682 wait_until_not_running(&mut manager, &id);
683 assert!(!manager.is_running(&id));
684 }
685
686 #[test]
687 fn stop_finished_task_returns_not_running() {
688 let mut manager = TaskManager::new();
689 let (hub, _rx) = mpsc::channel();
690
691 let id = manager.start(Box::new(InstantTask), hub).unwrap();
692
693 wait_until_not_running(&mut manager, &id);
694 let err = manager.stop(&id).unwrap_err();
695
696 assert!(matches!(err, TaskError::NotRunning(TaskId::TestTask2)));
697 }
698
699 #[test]
700 fn running_tasks_excludes_finished() {
701 let mut manager = TaskManager::new();
702 let (hub, _rx) = mpsc::channel();
703
704 manager.start(Box::new(WaitingTask), hub.clone()).unwrap();
705 let instant_id = manager.start(Box::new(InstantTask), hub).unwrap();
706
707 wait_until_not_running(&mut manager, &instant_id);
708 let running = manager.running_tasks();
709
710 assert_eq!(running.len(), 1);
711 assert_eq!(running[0], TaskId::TestTask);
712
713 manager.stop_all();
714 }
715
716 #[test]
717 fn stop_all_stops_everything() {
718 let mut manager = TaskManager::new();
719 let (hub, _rx) = mpsc::channel();
720
721 manager.start(Box::new(WaitingTask), hub).unwrap();
722 manager.stop_all();
723
724 assert!(!manager.is_running(&TaskId::TestTask));
725 }
726
727 #[test]
728 fn test_thumbnail_extraction_task_lifecycle() {
729 let mut manager = TaskManager::new();
730 let (hub, _rx) = mpsc::channel();
731 let database = Database::new(":memory:").unwrap();
732 database.migrate().unwrap();
733 let settings = Settings::default();
734
735 manager.schedule_thumbnail_extraction(None, &hub, &database, &settings);
736
737 wait_until_not_running(&mut manager, &TaskId::ThumbnailExtraction);
740 assert!(!manager.is_running(&TaskId::ThumbnailExtraction));
741
742 let err = manager.stop(&TaskId::ThumbnailExtraction).unwrap_err();
743 assert!(matches!(
744 err,
745 TaskError::NotRunning(TaskId::ThumbnailExtraction)
746 ));
747 }
748
749 #[test]
750 fn thumbnail_extraction_queues_when_running() {
751 let mut manager = TaskManager::new();
752 let (hub, _rx) = mpsc::channel();
753
754 let (shutdown_tx, shutdown_rx) = mpsc::channel();
756 let blocking_handle = thread::spawn(move || {
757 let _ = shutdown_rx.recv();
758 });
759 manager.tasks.insert(
760 TaskId::ThumbnailExtraction,
761 RunningTask {
762 handle: blocking_handle,
763 shutdown: shutdown_tx,
764 finished_event: None,
765 },
766 );
767
768 let database = Database::new(":memory:").unwrap();
769 database.migrate().unwrap();
770 let settings = Settings::default();
771
772 manager.schedule_thumbnail_extraction(Some(0), &hub, &database, &settings);
773 manager.schedule_thumbnail_extraction(Some(1), &hub, &database, &settings);
774
775 assert_eq!(manager.pending_thumbnail_indices.len(), 2);
776
777 manager.stop(&TaskId::ThumbnailExtraction).unwrap();
778
779 manager.drain_pending_thumbnails(&hub, &database, &settings);
780 assert_eq!(manager.pending_thumbnail_indices.len(), 1);
781
782 wait_until_not_running(&mut manager, &TaskId::ThumbnailExtraction);
783
784 manager.drain_pending_thumbnails(&hub, &database, &settings);
785 assert!(manager.pending_thumbnail_indices.is_empty());
786
787 wait_until_not_running(&mut manager, &TaskId::ThumbnailExtraction);
788 }
789
790 #[test]
791 fn import_queue_preserves_force_flag() {
792 let mut manager = TaskManager::new();
793 let (hub, _rx) = mpsc::channel();
794 let database = Database::new(":memory:").unwrap();
795 database.migrate().unwrap();
796 let settings = Settings::default();
797
798 let (shutdown_tx, shutdown_rx) = mpsc::channel();
800 let blocking_handle = thread::spawn(move || {
801 let _ = shutdown_rx.recv();
802 });
803 manager.tasks.insert(
804 TaskId::Import,
805 RunningTask {
806 handle: blocking_handle,
807 shutdown: shutdown_tx,
808 finished_event: None,
809 },
810 );
811
812 manager.handle_event(
813 &Event::ImportLibrary {
814 library_index: Some(0),
815 force: true,
816 },
817 &hub,
818 &database,
819 &settings,
820 );
821
822 assert_eq!(
823 manager.pending_import_indices.front(),
824 Some(&(Some(0), true))
825 );
826
827 manager.stop(&TaskId::Import).unwrap();
828 }
829
830 #[test]
831 fn import_queue_preserves_force_false_flag() {
832 let mut manager = TaskManager::new();
833 let (hub, _rx) = mpsc::channel();
834 let database = Database::new(":memory:").unwrap();
835 database.migrate().unwrap();
836 let settings = Settings::default();
837
838 let (shutdown_tx, shutdown_rx) = mpsc::channel();
839 let blocking_handle = thread::spawn(move || {
840 let _ = shutdown_rx.recv();
841 });
842 manager.tasks.insert(
843 TaskId::Import,
844 RunningTask {
845 handle: blocking_handle,
846 shutdown: shutdown_tx,
847 finished_event: None,
848 },
849 );
850
851 manager.handle_event(
852 &Event::ImportLibrary {
853 library_index: None,
854 force: false,
855 },
856 &hub,
857 &database,
858 &settings,
859 );
860
861 assert_eq!(manager.pending_import_indices.front(), Some(&(None, false)));
862
863 manager.stop(&TaskId::Import).unwrap();
864 }
865}