Skip to content

Commit 9a63acb

Browse files
fix: Add graceful shutdown for web server and tray (#26)
1 parent 97916b4 commit 9a63acb

File tree

8 files changed

+1252
-14
lines changed

8 files changed

+1252
-14
lines changed

.cargo/config.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ linker = "arm-linux-gnueabihf-gcc"
2929
linker = "arm-linux-gnueabihf-gcc"
3030

3131
[target.x86_64-pc-windows-gnu]
32-
linker = "C:\\msys2\\ucrt64\\bin\\gcc.exe"
33-
ar = "C:\\msys2\\ucrt64\\bin\\ar.exe"
32+
# TODO: compilation/linking fails
33+
linker = "C:\\msys64\\mingw64\\bin\\gcc.exe"
34+
ar = "C:\\msys64\\mingw64\\bin\\ar.exe"

crates/server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ schemars = "0.8.1"
6969
serde = "1.0.217"
7070
serde_json = "1.0.138"
7171
tao = "0.31.1"
72+
tokio = { version = "1.0", features = ["full"] }
7273
tray-icon = "0.21.1"
7374
webbrowser = "1.0.3"
7475
# common = { path = "../common" }

crates/server/src/lib.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,39 @@ pub mod db;
1111
pub mod dependencies;
1212
pub mod globals;
1313
mod logging;
14+
pub mod signal_handler;
1415
pub mod tray;
1516
pub mod web;
1617

17-
// standard imports
18-
use std::thread;
19-
2018
/// Main entry point for the application.
2119
/// Initializes logging, the web server, and tray icon.
2220
#[cfg(not(tarpaulin_include))]
2321
pub fn main() {
2422
logging::init().expect("Failed to initialize logging");
2523

26-
let web_handle = thread::spawn(|| {
27-
web::launch();
24+
// Create a shutdown coordinator to manage all threads
25+
let mut coordinator = signal_handler::ShutdownCoordinator::new();
26+
27+
// Register the web server thread
28+
coordinator.register_async_thread("web-server", |shutdown_signal| async move {
29+
web::launch_with_shutdown(shutdown_signal).await;
30+
log::info!("Web server thread completed");
2831
});
2932

30-
tray::launch();
33+
// Start the monitoring system
34+
coordinator.start_monitor();
35+
36+
// Run tray on main thread - this will block until tray exits
37+
// The tray gets the main shutdown signal to coordinate with other threads
38+
tray::launch_with_shutdown(coordinator.signal());
39+
40+
log::info!("Tray has exited, initiating coordinated shutdown");
41+
42+
// Trigger shutdown of all threads
43+
coordinator.shutdown();
44+
45+
// Wait for all threads to complete
46+
coordinator.wait_for_completion();
3147

32-
web_handle.join().expect("Web server thread panicked");
48+
log::info!("Application shutdown complete");
3349
}
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
//! Signal handling utilities for graceful shutdown.
2+
3+
use std::sync::Arc;
4+
use std::sync::atomic::{AtomicBool, Ordering};
5+
use std::thread::JoinHandle;
6+
use std::time::Duration;
7+
8+
/// A thread-safe shutdown signal that can be shared across threads.
9+
#[derive(Clone)]
10+
pub struct ShutdownSignal {
11+
/// Atomic boolean indicating whether shutdown has been requested.
12+
shutdown: Arc<AtomicBool>,
13+
}
14+
15+
impl ShutdownSignal {
16+
/// Create a new shutdown signal.
17+
pub fn new() -> Self {
18+
Self {
19+
shutdown: Arc::new(AtomicBool::new(false)),
20+
}
21+
}
22+
23+
/// Signal that shutdown has been requested.
24+
pub fn shutdown(&self) {
25+
self.shutdown.store(true, Ordering::Relaxed);
26+
}
27+
28+
/// Check if shutdown has been requested.
29+
pub fn is_shutdown(&self) -> bool {
30+
self.shutdown.load(Ordering::Relaxed)
31+
}
32+
33+
/// Wait for shutdown signal.
34+
pub fn wait(&self) {
35+
while !self.is_shutdown() {
36+
std::thread::sleep(Duration::from_millis(100));
37+
}
38+
}
39+
}
40+
41+
impl Default for ShutdownSignal {
42+
fn default() -> Self {
43+
Self::new()
44+
}
45+
}
46+
47+
/// Represents a managed thread that can be gracefully shut down.
48+
pub struct ManagedThread {
49+
name: String,
50+
handle: JoinHandle<()>,
51+
shutdown_signal: ShutdownSignal,
52+
}
53+
54+
impl ManagedThread {
55+
/// Create a new managed thread.
56+
pub fn new(
57+
name: String,
58+
handle: JoinHandle<()>,
59+
shutdown_signal: ShutdownSignal,
60+
) -> Self {
61+
Self {
62+
name,
63+
handle,
64+
shutdown_signal,
65+
}
66+
}
67+
68+
/// Get the thread name.
69+
pub fn name(&self) -> &str {
70+
&self.name
71+
}
72+
73+
/// Signal this thread to shut down.
74+
pub fn shutdown(&self) {
75+
self.shutdown_signal.shutdown();
76+
}
77+
78+
/// Check if this thread is finished.
79+
pub fn is_finished(&self) -> bool {
80+
self.handle.is_finished()
81+
}
82+
83+
/// Wait for this thread to complete.
84+
pub fn join(self) -> Result<(), Box<dyn std::any::Any + Send + 'static>> {
85+
log::info!("Waiting for {} thread to complete", self.name);
86+
self.handle.join()
87+
}
88+
}
89+
90+
/// Coordinates graceful shutdown across multiple threads.
91+
pub struct ShutdownCoordinator {
92+
main_signal: ShutdownSignal,
93+
threads: Vec<ManagedThread>,
94+
timeout: Duration,
95+
exit_fn: Arc<dyn Fn() + Send + Sync>,
96+
}
97+
98+
impl ShutdownCoordinator {
99+
/// Create a new shutdown coordinator with default 5-second timeout.
100+
pub fn new() -> Self {
101+
Self::with_timeout(Duration::from_secs(5))
102+
}
103+
104+
/// Create a new shutdown coordinator with custom timeout.
105+
pub fn with_timeout(timeout: Duration) -> Self {
106+
Self {
107+
main_signal: ShutdownSignal::new(),
108+
threads: Vec::new(),
109+
timeout,
110+
exit_fn: Arc::new(|| std::process::exit(0)),
111+
}
112+
}
113+
114+
/// Create a new shutdown coordinator with custom timeout and exit function.
115+
/// This is primarily used for testing to avoid calling std::process::exit.
116+
pub fn with_timeout_and_exit_fn<F>(
117+
timeout: Duration,
118+
exit_fn: F,
119+
) -> Self
120+
where
121+
F: Fn() + Send + Sync + 'static,
122+
{
123+
Self {
124+
main_signal: ShutdownSignal::new(),
125+
threads: Vec::new(),
126+
timeout,
127+
exit_fn: Arc::new(exit_fn),
128+
}
129+
}
130+
131+
/// Get the main shutdown signal.
132+
pub fn signal(&self) -> ShutdownSignal {
133+
self.main_signal.clone()
134+
}
135+
136+
/// Register a new thread for shutdown coordination.
137+
pub fn register_thread<F>(
138+
&mut self,
139+
name: &str,
140+
thread_fn: F,
141+
) -> ShutdownSignal
142+
where
143+
F: FnOnce(ShutdownSignal) + Send + 'static,
144+
{
145+
let shutdown_signal = ShutdownSignal::new();
146+
let signal_clone = shutdown_signal.clone();
147+
148+
let handle = std::thread::Builder::new()
149+
.name(name.to_string())
150+
.spawn(move || {
151+
thread_fn(signal_clone);
152+
})
153+
.unwrap_or_else(|_| panic!("Failed to spawn {} thread", name));
154+
155+
let managed_thread = ManagedThread::new(name.to_string(), handle, shutdown_signal.clone());
156+
self.threads.push(managed_thread);
157+
158+
shutdown_signal
159+
}
160+
161+
/// Register an async thread for shutdown coordination.
162+
pub fn register_async_thread<F, Fut>(
163+
&mut self,
164+
name: &str,
165+
thread_fn: F,
166+
) -> ShutdownSignal
167+
where
168+
F: FnOnce(ShutdownSignal) -> Fut + Send + 'static,
169+
Fut: std::future::Future<Output = ()> + Send + 'static,
170+
{
171+
let shutdown_signal = ShutdownSignal::new();
172+
let signal_clone = shutdown_signal.clone();
173+
let name_owned = name.to_string(); // Convert to owned string
174+
175+
let handle = std::thread::Builder::new()
176+
.name(name.to_string())
177+
.spawn(move || {
178+
let rt = tokio::runtime::Runtime::new().unwrap_or_else(|_| {
179+
panic!("Failed to create tokio runtime for {}", name_owned)
180+
});
181+
rt.block_on(thread_fn(signal_clone));
182+
})
183+
.unwrap_or_else(|_| panic!("Failed to spawn {} async thread", name));
184+
185+
let managed_thread = ManagedThread::new(name.to_string(), handle, shutdown_signal.clone());
186+
self.threads.push(managed_thread);
187+
188+
shutdown_signal
189+
}
190+
191+
/// Start a monitor thread that watches for thread completion or external shutdown.
192+
pub fn start_monitor(&mut self) {
193+
let main_signal = self.main_signal.clone();
194+
let timeout = self.timeout;
195+
196+
// Create signals for the monitor to watch
197+
let thread_signals: Vec<_> = self
198+
.threads
199+
.iter()
200+
.map(|t| (t.name().to_string(), t.shutdown_signal.clone()))
201+
.collect();
202+
203+
self.register_thread("monitor", move |monitor_signal| {
204+
loop {
205+
// Check if main shutdown was signaled
206+
if main_signal.is_shutdown() {
207+
log::info!(
208+
"Monitor detected main shutdown signal, signaling all threads to exit"
209+
);
210+
for (name, signal) in &thread_signals {
211+
log::debug!("Signaling {} thread to shutdown", name);
212+
signal.shutdown();
213+
}
214+
break;
215+
}
216+
217+
// Check if any thread completed (which should trigger shutdown)
218+
for (name, signal) in &thread_signals {
219+
if signal.is_shutdown() && !main_signal.is_shutdown() {
220+
log::info!(
221+
"Monitor detected {} thread completed, initiating global shutdown",
222+
name
223+
);
224+
main_signal.shutdown();
225+
break;
226+
}
227+
}
228+
229+
// Check if monitor itself should shut down
230+
if monitor_signal.is_shutdown() {
231+
break;
232+
}
233+
234+
std::thread::sleep(Duration::from_millis(50));
235+
}
236+
});
237+
238+
// Start timeout thread
239+
let timeout_signal = self.main_signal.clone();
240+
let exit_fn = Arc::clone(&self.exit_fn);
241+
self.register_thread("timeout", move |_| {
242+
// Wait for shutdown signal to be received first
243+
while !timeout_signal.is_shutdown() {
244+
std::thread::sleep(Duration::from_millis(100));
245+
}
246+
247+
log::info!(
248+
"Timeout thread: shutdown signal received, starting {:?} timeout",
249+
timeout
250+
);
251+
let timeout_start = std::time::Instant::now();
252+
253+
loop {
254+
let elapsed = timeout_start.elapsed();
255+
if elapsed > timeout {
256+
log::warn!(
257+
"Application did not exit within {:?}, forcing exit",
258+
timeout
259+
);
260+
261+
// Use the configurable exit function
262+
exit_fn();
263+
break;
264+
}
265+
std::thread::sleep(Duration::from_millis(100));
266+
}
267+
});
268+
}
269+
270+
/// Trigger shutdown of all threads.
271+
pub fn shutdown(&self) {
272+
log::info!("Initiating coordinated shutdown of all threads");
273+
self.main_signal.shutdown();
274+
}
275+
276+
/// Wait for all threads to complete.
277+
pub fn wait_for_completion(self) {
278+
log::info!("Waiting for all threads to complete");
279+
280+
let mut failed_threads = Vec::new();
281+
for thread in self.threads {
282+
let thread_name = thread.name().to_string();
283+
match thread.join() {
284+
Ok(_) => log::debug!("{} thread completed successfully", thread_name),
285+
Err(_) => {
286+
log::warn!("{} thread completed with error", thread_name);
287+
failed_threads.push(thread_name);
288+
}
289+
}
290+
}
291+
292+
if failed_threads.is_empty() {
293+
log::info!("All threads completed successfully");
294+
} else {
295+
log::warn!("Some threads failed: {:?}", failed_threads);
296+
}
297+
}
298+
299+
/// Get the number of registered threads.
300+
pub fn thread_count(&self) -> usize {
301+
self.threads.len()
302+
}
303+
}
304+
305+
impl Default for ShutdownCoordinator {
306+
fn default() -> Self {
307+
Self::new()
308+
}
309+
}

0 commit comments

Comments
 (0)