veloren_server_cli/
shutdown_coordinator.rs

1use crate::settings::Settings;
2use common::comp::{Content, chat::ChatType};
3use common_net::msg::ServerGeneral;
4use server::Server;
5use std::{
6    ops::Add,
7    sync::{
8        Arc,
9        atomic::{AtomicBool, Ordering},
10    },
11    time::{Duration, Instant},
12};
13use tracing::{error, info};
14
15/// Coordinates the shutdown procedure for the server, which can be initiated by
16/// either the TUI console interface or by sending the server the SIGUSR1 (or
17/// others) signal which indicates the server is restarting due to an update.
18pub(crate) struct ShutdownCoordinator {
19    /// The instant that the last shutdown message was sent, used for
20    /// calculating when to send the next shutdown message
21    last_shutdown_msg: Instant,
22    /// The interval that shutdown warning messages are sent at
23    msg_interval: Duration,
24    /// The instant that shudown was initiated at
25    shutdown_initiated_at: Option<Instant>,
26    /// The period to wait before shutting down after shutdown is initiated
27    shutdown_grace_period: Duration,
28    /// The message to use for the shutdown warning message that is sent to all
29    /// connected players
30    shutdown_message: String,
31    /// Provided by `signal_hook` to allow observation of a shutdown signal
32    shutdown_signal: Arc<AtomicBool>,
33}
34
35impl ShutdownCoordinator {
36    pub fn new(shutdown_signal: Arc<AtomicBool>) -> Self {
37        Self {
38            last_shutdown_msg: Instant::now(),
39            msg_interval: Duration::from_secs(30),
40            shutdown_initiated_at: None,
41            shutdown_grace_period: Duration::from_secs(0),
42            shutdown_message: String::new(),
43            shutdown_signal,
44        }
45    }
46
47    /// Initiates a graceful shutdown of the server using the specified grace
48    /// period and message. When the grace period expires, the server
49    /// process exits.
50    pub fn initiate_shutdown(
51        &mut self,
52        server: &mut Server,
53        grace_period: Duration,
54        message: String,
55    ) {
56        if self.shutdown_initiated_at.is_none() {
57            self.shutdown_grace_period = grace_period;
58            self.shutdown_initiated_at = Some(Instant::now());
59            self.shutdown_message = message;
60
61            // Send an initial shutdown warning message to all connected clients
62            self.send_shutdown_msg(server);
63        } else {
64            error!("Shutdown already in progress")
65        }
66    }
67
68    /// Aborts an in-progress shutdown and sends a message to all connected
69    /// clients.
70    pub fn abort_shutdown(&mut self, server: &mut Server) {
71        if self.shutdown_initiated_at.is_some() {
72            self.shutdown_initiated_at = None;
73            ShutdownCoordinator::send_msg(server, "The shutdown has been aborted".to_owned());
74        } else {
75            error!("There is no shutdown in progress");
76        }
77    }
78
79    /// Called once per tick to process any pending actions related to server
80    /// shutdown. If the grace period for an initiated shutdown has expired,
81    /// returns `true` which triggers the loop in `main.rs` to break and
82    /// exit the server process.
83    pub fn check(&mut self, server: &mut Server, settings: &Settings) -> bool {
84        // Check whether shutdown has been set
85        self.check_shutdown_signal(server, settings);
86
87        // If a shutdown is in progress, check whether it's time to send another warning
88        // message or shut down if the grace period has expired.
89        if let Some(shutdown_initiated_at) = self.shutdown_initiated_at {
90            if Instant::now() > shutdown_initiated_at.add(self.shutdown_grace_period) {
91                info!("Shutting down");
92                return true;
93            }
94
95            // In the last 10 seconds start sending messages every 1 second
96            if let Some(time_until_shutdown) = self.time_until_shutdown() {
97                if time_until_shutdown <= Duration::from_secs(10) {
98                    self.msg_interval = Duration::from_secs(1);
99                }
100            }
101
102            // Send another shutdown warning message to all connected clients if
103            // msg_interval has expired
104            if self.last_shutdown_msg + self.msg_interval <= Instant::now() {
105                self.send_shutdown_msg(server);
106            }
107        }
108
109        false
110    }
111
112    /// Checks whether a shutdown (SIGUSR1 by default) signal has been set,
113    /// which is used to trigger a graceful shutdown for an update. [Watchtower](https://containrrr.dev/watchtower/) is configured on the main
114    /// Veloren server to send SIGUSR1 instead of SIGTERM which allows us to
115    /// react specifically to shutdowns that are for an update.
116    /// NOTE: SIGUSR1 is not supported on Windows
117    fn check_shutdown_signal(&mut self, server: &mut Server, settings: &Settings) {
118        if self.shutdown_signal.load(Ordering::Relaxed) && self.shutdown_initiated_at.is_none() {
119            info!("Received shutdown signal, initiating graceful shutdown");
120            let grace_period =
121                Duration::from_secs(u64::from(settings.update_shutdown_grace_period_secs));
122            let shutdown_message = settings.update_shutdown_message.to_owned();
123            self.initiate_shutdown(server, grace_period, shutdown_message);
124
125            // Reset the SIGUSR1 signal indicator in case shutdown is aborted and we need to
126            // trigger shutdown again
127            self.shutdown_signal.store(false, Ordering::Relaxed);
128        }
129    }
130
131    /// Constructs a formatted shutdown message and sends it to all connected
132    /// clients
133    fn send_shutdown_msg(&mut self, server: &mut Server) {
134        if let Some(time_until_shutdown) = self.time_until_shutdown() {
135            let msg = format!(
136                "{} in {}",
137                self.shutdown_message,
138                ShutdownCoordinator::duration_to_text(time_until_shutdown)
139            );
140            ShutdownCoordinator::send_msg(server, msg);
141            self.last_shutdown_msg = Instant::now();
142        }
143    }
144
145    /// Calculates the remaining time before the shutdown grace period expires
146    fn time_until_shutdown(&self) -> Option<Duration> {
147        let shutdown_initiated_at = self.shutdown_initiated_at?;
148        let shutdown_time = shutdown_initiated_at + self.shutdown_grace_period;
149
150        // If we're somehow trying to calculate the time until shutdown after the
151        // shutdown time Instant::checked_duration_since will return None as
152        // negative durations are not supported.
153        shutdown_time.checked_duration_since(Instant::now())
154    }
155
156    /// Logs and sends a message to all connected clients
157    fn send_msg(server: &mut Server, msg: String) {
158        info!("{}", &msg);
159        server.notify_players(ServerGeneral::server_msg(
160            ChatType::CommandError,
161            Content::Plain(msg),
162        ));
163    }
164
165    /// Converts a `Duration` into text in the format XsXm for example 1 minute
166    /// 50 seconds would be converted to "1m50s", 2 minutes 0 seconds to
167    /// "2m" and 0 minutes 23 seconds to "23s".
168    fn duration_to_text(duration: Duration) -> String {
169        let secs = duration.as_secs_f32().round() as i32 % 60;
170        let mins = duration.as_secs_f32().round() as i32 / 60;
171
172        let mut text = String::new();
173        if mins > 0 {
174            text.push_str(format!("{}m", mins).as_str())
175        }
176        if secs > 0 {
177            text.push_str(format!("{}s", secs).as_str())
178        }
179        text
180    }
181}