Skip to main content

workflow_egui/runtime/
runtime.rs

1use super::*;
2use crate::imports::*;
3use workflow_wasm::callback::CallbackMap;
4
5pub use payload::Payload;
6use repaint::RepaintService;
7pub use service::{Service, ServiceResult};
8
9/// Shared inner state of the [`Runtime`], held behind an `Arc` so that
10/// runtime handles can be cloned cheaply.
11pub struct Inner {
12    egui_ctx: egui::Context,
13    events: ApplicationEventsChannel,
14    services: RwLock<AHashMap<String, Arc<dyn Service>>>,
15    repaint_service: Arc<RepaintService>,
16    is_running: Arc<AtomicBool>,
17    start_time: Instant,
18    #[allow(dead_code)]
19    callbacks: CallbackMap,
20}
21
22/// Cheaply cloneable handle to the application runtime, providing access to
23/// services, the egui context, and the application events channel.
24#[derive(Clone)]
25pub struct Runtime {
26    inner: Arc<Inner>,
27}
28
29impl Runtime {
30    /// Create a new runtime bound to the given egui context, registering it
31    /// as the global runtime. An optional application events channel may be
32    /// supplied, otherwise an unbounded channel is created.
33    pub fn new(egui_ctx: &egui::Context, events: Option<ApplicationEventsChannel>) -> Self {
34        let events = events.unwrap_or_else(channel::Channel::unbounded);
35
36        let repaint_service = Arc::new(RepaintService::default());
37
38        let runtime = Self {
39            inner: Arc::new(Inner {
40                services: Default::default(),
41                events,
42                repaint_service: repaint_service.clone(),
43                egui_ctx: egui_ctx.clone(),
44                is_running: Arc::new(AtomicBool::new(false)),
45                start_time: Instant::now(),
46                callbacks: Default::default(),
47            }),
48        };
49        register_global(Some(runtime.clone()));
50
51        runtime.bind(repaint_service);
52
53        #[cfg(target_arch = "wasm32")]
54        runtime.register_visibility_handler();
55
56        runtime
57    }
58
59    /// Register a service with the runtime under its name and immediately
60    /// spawn its async task.
61    pub fn bind(&self, service: Arc<dyn Service>) {
62        self.inner
63            .services
64            .write()
65            .unwrap()
66            .insert(service.name().to_string(), service.clone());
67        let runtime = self.clone();
68        spawn(async move { service.spawn(runtime).await });
69    }
70
71    /// Returns the elapsed time since the runtime was created.
72    pub fn uptime(&self) -> Duration {
73        self.inner.start_time.elapsed()
74    }
75
76    /// Spawn the async task for each bound service.
77    pub fn start_services(&self) {
78        let services = self.services();
79        for service in services {
80            let runtime = self.clone();
81            // service.spawn().await?;
82            spawn(async move { service.spawn(runtime).await });
83        }
84    }
85
86    /// Returns a snapshot of all currently bound services.
87    pub fn services(&self) -> Vec<Arc<dyn Service>> {
88        self.inner
89            .services
90            .read()
91            .unwrap()
92            .values()
93            .cloned()
94            .collect()
95    }
96
97    /// Request termination of all bound services.
98    pub fn stop_services(&self) {
99        self.services()
100            .into_iter()
101            .for_each(|service| service.terminate());
102    }
103
104    /// Await the completion of all services' join futures.
105    pub async fn join_services(&self) {
106        // for service in self.services() {
107        //  let name = service.name();
108        //  println!("⚡ {name}");
109        //  service.join().await.expect("service join failure");
110        //  println!("💀 {name}");
111        // }
112
113        let futures = self
114            .services()
115            .into_iter()
116            .map(|service| service.join())
117            .collect::<Vec<_>>();
118        join_all(futures).await;
119    }
120
121    /// Unregister this runtime from the global slot.
122    pub fn drop(&self) {
123        register_global(None);
124    }
125
126    // / Start the runtime runtime.
127    /// Mark the runtime as running and start all bound services.
128    pub fn start(&self) {
129        self.inner.is_running.store(true, Ordering::SeqCst);
130        self.start_services();
131    }
132
133    /// Shutdown runtime runtime.
134    pub async fn shutdown(&self) {
135        if self.inner.is_running.load(Ordering::SeqCst) {
136            self.inner.is_running.store(false, Ordering::SeqCst);
137            self.stop_services();
138            self.join_services().await;
139            register_global(None);
140        }
141    }
142
143    /// Returns the reference to the repaint service driving egui redraws.
144    pub fn repaint_service(&self) -> &Arc<RepaintService> {
145        &self.inner.repaint_service
146    }
147
148    /// Returns the reference to the application events channel.
149    pub fn events(&self) -> &ApplicationEventsChannel {
150        &self.inner.events
151    }
152
153    /// Send an application even to the UI asynchronously.
154    pub async fn send<T>(&self, msg: T) -> Result<()>
155    where
156        T: Any + Send + Sync + 'static,
157    {
158        self.inner.events.send(RuntimeEvent::new(msg)).await?;
159        Ok(())
160    }
161
162    /// Send a [`RuntimeEvent`] to the UI asynchronously.
163    pub async fn send_runtime_event(&self, msg: RuntimeEvent) -> Result<()> {
164        self.inner.events.send(msg).await?;
165        Ok(())
166    }
167
168    /// Send an application event to the UI synchronously.
169    pub fn try_send<T>(&self, msg: T) -> Result<()>
170    where
171        T: Any + Send + Sync + 'static,
172    {
173        self.inner.events.sender.try_send(RuntimeEvent::new(msg))?;
174        Ok(())
175    }
176
177    /// Send a [`RuntimeEvent`] to the UI synchronously without blocking.
178    pub fn try_send_runtime_event(&self, msg: RuntimeEvent) -> Result<()> {
179        self.inner.events.sender.try_send(msg)?;
180        Ok(())
181    }
182
183    /// Spawn an async task on the runtime; if the task returns an error,
184    /// it is forwarded to the UI as a [`RuntimeEvent::Error`].
185    pub fn spawn_task<F>(&self, task: F)
186    where
187        F: Future<Output = Result<()>> + Send + 'static,
188    {
189        let sender = self.events().sender.clone();
190        workflow_core::task::spawn(async move {
191            if let Err(err) = task.await {
192                sender
193                    .send(RuntimeEvent::Error(err.to_string()))
194                    .await
195                    .unwrap();
196            }
197        });
198    }
199
200    /// Spawn an async task whose result is stored into the supplied
201    /// [`Payload`]. The payload is marked pending while the task runs and
202    /// then populated with the `Ok` or `Err` outcome on completion.
203    pub fn spawn_task_with_result<R, F>(
204        &self,
205        payload: &Payload<std::result::Result<R, Error>>,
206        task: F,
207    ) where
208        R: Clone + Send + 'static,
209        F: Future<Output = Result<R>> + Send + 'static,
210    {
211        let payload = (*payload).clone();
212        payload.mark_pending();
213        workflow_core::task::spawn(async move {
214            let result = task.await;
215            match result {
216                Ok(r) => payload.store(Ok(r)),
217                Err(err) => {
218                    payload.store(Err(err));
219                }
220            }
221        });
222    }
223
224    /// Returns the reference to the egui context bound to this runtime.
225    pub fn egui_ctx(&self) -> &egui::Context {
226        &self.inner.egui_ctx
227    }
228
229    /// Request an egui repaint via the repaint service.
230    pub fn request_repaint(&self) {
231        self.repaint_service().trigger();
232    }
233
234    #[cfg(target_arch = "wasm32")]
235    pub fn register_visibility_handler(&self) {
236        use workflow_dom::utils::*;
237        use workflow_wasm::callback::*;
238
239        let sender = self.events().sender.clone();
240        let callback = callback!(move || {
241            let visibility_state = document().visibility_state();
242            sender
243                .try_send(RuntimeEvent::VisibilityState(visibility_state))
244                .unwrap();
245            runtime().egui_ctx().request_repaint();
246        });
247
248        document().set_onvisibilitychange(Some(callback.as_ref()));
249        self.inner.callbacks.retain(callback).unwrap();
250    }
251}
252
253static RUNTIME: Mutex<Option<Runtime>> = Mutex::new(None);
254
255/// Returns a clone of the globally registered [`Runtime`].
256///
257/// Panics if the runtime has not been initialized.
258pub fn runtime() -> Runtime {
259    if let Some(runtime) = RUNTIME.lock().unwrap().as_ref() {
260        runtime.clone()
261    } else {
262        panic!("Error: `Runtime` is not initialized")
263    }
264}
265
266/// Returns a clone of the global [`Runtime`] if it has been initialized,
267/// or `None` otherwise.
268pub fn try_runtime() -> Option<Runtime> {
269    RUNTIME.lock().unwrap().clone()
270}
271
272fn register_global(runtime: Option<Runtime>) {
273    match runtime {
274        Some(runtime) => {
275            let mut global = RUNTIME.lock().unwrap();
276            if global.is_some() {
277                panic!("runtime already initialized");
278            }
279            global.replace(runtime);
280        }
281        None => {
282            RUNTIME.lock().unwrap().take();
283        }
284    };
285}
286
287/// Spawn an async task that will result in
288/// egui redraw upon its completion.
289pub fn spawn<F>(task: F)
290where
291    F: Future<Output = Result<()>> + Send + 'static,
292{
293    runtime().spawn_task(task);
294}
295
296/// Spawn an async task that will result in
297/// egui redraw upon its completion. Upon
298/// the task completion, the supplied [`Payload`]
299/// will be populated with the task result.
300pub fn spawn_with_result<R, F>(payload: &Payload<std::result::Result<R, Error>>, task: F)
301where
302    R: Clone + Send + 'static,
303    F: Future<Output = Result<R>> + Send + 'static,
304{
305    runtime().spawn_task_with_result(payload, task);
306}
307
308/// Gracefully halt the runtime runtime. This is used
309/// to shutdown kaspad when the kaspa-ng process exit
310/// is an inevitable eventuality.
311#[cfg(not(target_arch = "wasm32"))]
312impl Runtime {
313    /// Trigger a graceful shutdown of the runtime, sending an exit event
314    /// and blocking until all services have completed their shutdown.
315    pub fn halt() {
316        if let Some(runtime) = try_runtime() {
317            runtime.try_send(RuntimeEvent::Exit).ok();
318            // runtime.kaspa_service().clone().terminate();
319
320            let handle = tokio::spawn(async move { runtime.shutdown().await });
321
322            while !handle.is_finished() {
323                std::thread::sleep(std::time::Duration::from_millis(50));
324            }
325        }
326    }
327
328    /// Attempt to halt the runtime runtime but exit the process
329    /// if it takes too long. This is used in attempt to shutdown
330    /// kaspad if the kaspa-ng process panics, which can result
331    /// in a still functioning zombie child process on unix systems.
332    pub fn abort() {
333        const TIMEOUT: u128 = 5000;
334        let flag = Arc::new(AtomicBool::new(false));
335        let flag_ = flag.clone();
336        let thread = std::thread::Builder::new()
337            .name("halt".to_string())
338            .spawn(move || {
339                let start = std::time::Instant::now();
340                while !flag_.load(Ordering::SeqCst) {
341                    if start.elapsed().as_millis() > TIMEOUT {
342                        println!("halting...");
343                        std::process::exit(1);
344                    }
345                    std::thread::sleep(std::time::Duration::from_millis(50));
346                }
347            })
348            .ok();
349
350        Self::halt();
351
352        flag.store(true, Ordering::SeqCst);
353        if let Some(thread) = thread {
354            thread.join().unwrap();
355        }
356
357        #[cfg(feature = "console")]
358        {
359            println!("Press Enter to exit...");
360            let mut input = String::new();
361            let _ = std::io::stdin().read_line(&mut input);
362        }
363
364        std::process::exit(1);
365    }
366}