1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use crate::settings::Settings;
use common::comp::{chat::ChatType, Content};
use common_net::msg::ServerGeneral;
use server::Server;
use std::{
    ops::Add,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
    time::{Duration, Instant},
};
use tracing::{error, info};

/// Coordinates the shutdown procedure for the server, which can be initiated by
/// either the TUI console interface or by sending the server the SIGUSR1 (or
/// others) signal which indicates the server is restarting due to an update.
pub(crate) struct ShutdownCoordinator {
    /// The instant that the last shutdown message was sent, used for
    /// calculating when to send the next shutdown message
    last_shutdown_msg: Instant,
    /// The interval that shutdown warning messages are sent at
    msg_interval: Duration,
    /// The instant that shudown was initiated at
    shutdown_initiated_at: Option<Instant>,
    /// The period to wait before shutting down after shutdown is initiated
    shutdown_grace_period: Duration,
    /// The message to use for the shutdown warning message that is sent to all
    /// connected players
    shutdown_message: String,
    /// Provided by `signal_hook` to allow observation of a shutdown signal
    shutdown_signal: Arc<AtomicBool>,
}

impl ShutdownCoordinator {
    pub fn new(shutdown_signal: Arc<AtomicBool>) -> Self {
        Self {
            last_shutdown_msg: Instant::now(),
            msg_interval: Duration::from_secs(30),
            shutdown_initiated_at: None,
            shutdown_grace_period: Duration::from_secs(0),
            shutdown_message: String::new(),
            shutdown_signal,
        }
    }

    /// Initiates a graceful shutdown of the server using the specified grace
    /// period and message. When the grace period expires, the server
    /// process exits.
    pub fn initiate_shutdown(
        &mut self,
        server: &mut Server,
        grace_period: Duration,
        message: String,
    ) {
        if self.shutdown_initiated_at.is_none() {
            self.shutdown_grace_period = grace_period;
            self.shutdown_initiated_at = Some(Instant::now());
            self.shutdown_message = message;

            // Send an initial shutdown warning message to all connected clients
            self.send_shutdown_msg(server);
        } else {
            error!("Shutdown already in progress")
        }
    }

    /// Aborts an in-progress shutdown and sends a message to all connected
    /// clients.
    pub fn abort_shutdown(&mut self, server: &mut Server) {
        if self.shutdown_initiated_at.is_some() {
            self.shutdown_initiated_at = None;
            ShutdownCoordinator::send_msg(server, "The shutdown has been aborted".to_owned());
        } else {
            error!("There is no shutdown in progress");
        }
    }

    /// Called once per tick to process any pending actions related to server
    /// shutdown. If the grace period for an initiated shutdown has expired,
    /// returns `true` which triggers the loop in `main.rs` to break and
    /// exit the server process.
    pub fn check(&mut self, server: &mut Server, settings: &Settings) -> bool {
        // Check whether shutdown has been set
        self.check_shutdown_signal(server, settings);

        // If a shutdown is in progress, check whether it's time to send another warning
        // message or shut down if the grace period has expired.
        if let Some(shutdown_initiated_at) = self.shutdown_initiated_at {
            if Instant::now() > shutdown_initiated_at.add(self.shutdown_grace_period) {
                info!("Shutting down");
                return true;
            }

            // In the last 10 seconds start sending messages every 1 second
            if let Some(time_until_shutdown) = self.time_until_shutdown() {
                if time_until_shutdown <= Duration::from_secs(10) {
                    self.msg_interval = Duration::from_secs(1);
                }
            }

            // Send another shutdown warning message to all connected clients if
            // msg_interval has expired
            if self.last_shutdown_msg + self.msg_interval <= Instant::now() {
                self.send_shutdown_msg(server);
            }
        }

        false
    }

    /// Checks whether a shutdown (SIGUSR1 by default) signal has been set,
    /// which is used to trigger a graceful shutdown for an update. [Watchtower](https://containrrr.dev/watchtower/) is configured on the main
    /// Veloren server to send SIGUSR1 instead of SIGTERM which allows us to
    /// react specifically to shutdowns that are for an update.
    /// NOTE: SIGUSR1 is not supported on Windows
    fn check_shutdown_signal(&mut self, server: &mut Server, settings: &Settings) {
        if self.shutdown_signal.load(Ordering::Relaxed) && self.shutdown_initiated_at.is_none() {
            info!("Received shutdown signal, initiating graceful shutdown");
            let grace_period =
                Duration::from_secs(u64::from(settings.update_shutdown_grace_period_secs));
            let shutdown_message = settings.update_shutdown_message.to_owned();
            self.initiate_shutdown(server, grace_period, shutdown_message);

            // Reset the SIGUSR1 signal indicator in case shutdown is aborted and we need to
            // trigger shutdown again
            self.shutdown_signal.store(false, Ordering::Relaxed);
        }
    }

    /// Constructs a formatted shutdown message and sends it to all connected
    /// clients
    fn send_shutdown_msg(&mut self, server: &mut Server) {
        if let Some(time_until_shutdown) = self.time_until_shutdown() {
            let msg = format!(
                "{} in {}",
                self.shutdown_message,
                ShutdownCoordinator::duration_to_text(time_until_shutdown)
            );
            ShutdownCoordinator::send_msg(server, msg);
            self.last_shutdown_msg = Instant::now();
        }
    }

    /// Calculates the remaining time before the shutdown grace period expires
    fn time_until_shutdown(&self) -> Option<Duration> {
        let shutdown_initiated_at = self.shutdown_initiated_at?;
        let shutdown_time = shutdown_initiated_at + self.shutdown_grace_period;

        // If we're somehow trying to calculate the time until shutdown after the
        // shutdown time Instant::checked_duration_since will return None as
        // negative durations are not supported.
        shutdown_time.checked_duration_since(Instant::now())
    }

    /// Logs and sends a message to all connected clients
    fn send_msg(server: &mut Server, msg: String) {
        info!("{}", &msg);
        server.notify_players(ServerGeneral::server_msg(
            ChatType::CommandError,
            Content::Plain(msg),
        ));
    }

    /// Converts a `Duration` into text in the format XsXm for example 1 minute
    /// 50 seconds would be converted to "1m50s", 2 minutes 0 seconds to
    /// "2m" and 0 minutes 23 seconds to "23s".
    fn duration_to_text(duration: Duration) -> String {
        let secs = duration.as_secs_f32().round() as i32 % 60;
        let mins = duration.as_secs_f32().round() as i32 / 60;

        let mut text = String::new();
        if mins > 0 {
            text.push_str(format!("{}m", mins).as_str())
        }
        if secs > 0 {
            text.push_str(format!("{}s", secs).as_str())
        }
        text
    }
}