studio_worker/ui/
actions.rs1use std::path::PathBuf;
5use std::sync::atomic::Ordering;
6use std::sync::Arc;
7
8use chrono::{DateTime, Utc};
9use eframe::egui;
10use parking_lot::Mutex;
11
12use crate::daemon_link::{perform, Action, Replica};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct Feedback {
17 pub text: String,
18 pub ok: bool,
19 pub at: DateTime<Utc>,
20}
21
22#[derive(Clone)]
24pub struct ActionRunner {
25 config_path: PathBuf,
26 replica: Replica,
27 pub feedback: Arc<Mutex<Option<Feedback>>>,
28 ctx: Arc<Mutex<Option<egui::Context>>>,
29}
30
31impl ActionRunner {
32 pub fn new(config_path: PathBuf, replica: Replica) -> Self {
33 Self {
34 config_path,
35 replica,
36 feedback: Arc::default(),
37 ctx: Arc::default(),
38 }
39 }
40
41 pub fn attach(&self, ctx: egui::Context) {
43 *self.ctx.lock() = Some(ctx);
44 }
45
46 pub fn run(&self, action: Action) {
48 let runner = self.clone();
49 std::thread::spawn(move || runner.run_blocking(&action));
50 }
51
52 pub fn run_and_wait(&self, action: Action) -> bool {
56 let runner = self.clone();
57 std::thread::spawn(move || runner.run_blocking(&action))
58 .join()
59 .unwrap_or(false)
60 }
61
62 pub fn run_blocking(&self, action: &Action) -> bool {
64 let outcome = perform(&self.config_path, action);
65 if let (Ok(_), Action::SetPaused(paused)) = (&outcome, action) {
66 self.replica.paused.store(*paused, Ordering::SeqCst);
68 }
69 let ok = outcome.is_ok();
70 *self.feedback.lock() = Some(Feedback {
71 text: outcome.unwrap_or_else(|e| e),
72 ok,
73 at: Utc::now(),
74 });
75 if let Some(ctx) = self.ctx.lock().as_ref() {
76 ctx.request_repaint();
77 }
78 ok
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::test_support::DaemonHarness;
86
87 #[test]
88 fn an_action_records_its_feedback_and_mirrors_pause() {
89 let daemon = DaemonHarness::start();
90 let replica = Replica::default();
91 let runner = ActionRunner::new(daemon.config_path.clone(), replica.clone());
92 assert!(runner.run_blocking(&Action::SetPaused(true)));
93 assert!(replica.paused.load(Ordering::SeqCst));
94 let feedback = runner.feedback.lock().clone().unwrap();
95 assert!(feedback.ok);
96 assert_eq!(feedback.text, "paused");
97 }
98
99 #[test]
100 fn run_and_wait_works_from_inside_a_tokio_runtime() {
101 let daemon = DaemonHarness::start();
102 let runner = ActionRunner::new(daemon.config_path.clone(), Replica::default());
103 let rt = tokio::runtime::Runtime::new().unwrap();
104 let ok = rt.block_on(async { runner.run_and_wait(Action::SetPaused(true)) });
105 assert!(ok);
106 assert!(daemon.control.paused.load(Ordering::SeqCst));
107 }
108
109 #[test]
110 fn a_refusal_is_recorded_as_failed_feedback() {
111 let dir = tempfile::tempdir().unwrap();
112 let runner = ActionRunner::new(dir.path().join("config.toml"), Replica::default());
113 assert!(!runner.run_blocking(&Action::Shutdown));
114 let feedback = runner.feedback.lock().clone().unwrap();
115 assert!(!feedback.ok);
116 assert!(feedback.text.contains("not reachable"), "{}", feedback.text);
117 }
118}