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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! What a running smon says about itself, and how it is told to stand down.
//!
//! `smon update` replaces the binary on disk, but a process that is already
//! running keeps the old code until it goes. This is the channel it goes
//! through, so an update leaves nothing behind on the previous version.
use std::{
env::{args_os, current_exe},
ffi::OsString,
path::{Path, PathBuf},
process::{Command, id},
sync::{
Mutex,
atomic::{AtomicBool, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
/// What a process is, which decides what standing down means. A daemon owns
/// consoles named in a config file, so it comes back on the new binary. A TUI
/// has nothing to come back to and just goes.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Daemon,
Tui,
}
impl Role {
pub const fn label(self) -> &'static str {
match self {
Self::Daemon => "daemon",
Self::Tui => "tui",
}
}
}
/// What `smon update` needs to know about a process before it replaces the
/// binary underneath it.
#[derive(Debug, Serialize, Deserialize)]
pub struct Info {
pub version: String,
pub role: Role,
pub pid: u32,
/// When this process started, in nanoseconds since the unix epoch. It is
/// what tells a replacement apart from the process it replaced. The pid
/// cannot: a unix exec keeps it, and so does the version when a daemon
/// comes back on a binary the update did not touch.
pub started: u128,
/// The binary this process runs. Captured at startup because reading it
/// later would name a deleted inode, an update having renamed the file away.
pub exe: Option<PathBuf>,
pub consoles: usize,
}
pub struct Control {
role: Role,
exe: Option<PathBuf>,
args: Vec<OsString>,
started: u128,
asked: AtomicBool,
/// Ends the server so its port is free before the process is replaced.
release: Mutex<Option<oneshot::Sender<()>>>,
}
impl Control {
pub fn new(role: Role) -> Self {
Self {
role,
exe: current_exe().ok(),
args: args_os().skip(1).collect(),
started: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| since.as_nanos()),
asked: AtomicBool::new(false),
release: Mutex::new(None),
}
}
/// Hand over the signal that ends the server. Firing it before the process
/// is replaced is what frees the port for the new one to bind.
pub fn arm(&self, release: oneshot::Sender<()>) {
*self.release.lock().unwrap_or_else(|e| e.into_inner()) = Some(release);
}
/// Stop the server so its port is free. Returns whether it was still there
/// to be told. This is the only way the server is ever stopped, whether a
/// session ended or an update asked for it.
pub fn release(&self) -> bool {
let Some(release) = self.release.lock().unwrap_or_else(|e| e.into_inner()).take() else {
return false;
};
release.send(()).is_ok()
}
pub fn info(&self, consoles: usize) -> Info {
Info {
version: env!("CARGO_PKG_VERSION").to_string(),
role: self.role,
pid: id(),
started: self.started,
exe: self.exe.clone(),
consoles,
}
}
/// An update asked this process to stand down. The flag outlives the
/// server, because a TUI session loop reads it to know why it is ending.
pub fn request_stop(&self) {
self.asked.store(true, Ordering::SeqCst);
self.release();
}
pub fn stopping(&self) -> bool {
self.asked.load(Ordering::SeqCst)
}
pub fn exe(&self) -> Option<&Path> {
self.exe.as_deref()
}
/// Start over on whatever binary now sits at this process's own path,
/// carrying the same arguments. Never returns on success.
///
/// # Errors
/// Returns an error if the path was never known or the new process cannot
/// be started, leaving this one running the old code.
pub fn relaunch(&self) -> Result<()> {
let exe = self
.exe
.as_deref()
.context("this process does not know its own binary, restart it by hand")?;
replace(exe, &self.args)
}
}
/// Unix keeps the pid across an exec, so a daemon under systemd or launchd
/// restarts without the supervisor seeing anything happen at all.
#[cfg(unix)]
fn replace(exe: &Path, args: &[OsString]) -> Result<()> {
use std::os::unix::process::CommandExt;
Err(Command::new(exe).args(args).exec())
.with_context(|| format!("could not restart {}", exe.display()))
}
/// Windows has no exec, so the replacement is a fresh process and this one
/// ends once it is up.
#[cfg(windows)]
fn replace(exe: &Path, args: &[OsString]) -> Result<()> {
use std::process::exit;
Command::new(exe)
.args(args)
.spawn()
.with_context(|| format!("could not restart {}", exe.display()))?;
exit(0);
}
#[cfg(test)]
mod tests {
use tokio::sync::oneshot;
use super::{Control, Role};
#[test]
fn a_fresh_control_is_not_stopping() {
let control = Control::new(Role::Daemon);
assert!(!control.stopping());
assert_eq!(control.info(3).consoles, 3);
assert_eq!(control.info(0).role, Role::Daemon);
}
#[test]
fn a_stop_request_fires_the_release_and_sets_the_flag() {
let control = Control::new(Role::Daemon);
let (tx, mut rx) = oneshot::channel();
control.arm(tx);
control.request_stop();
assert!(control.stopping());
assert!(rx.try_recv().is_ok());
}
// A unix exec keeps the pid, so a restarted daemon reports the very same
// one. The start time is what tells the replacement apart from the process
// it replaced, and an update waits on exactly that.
#[test]
fn a_restart_is_recognisable_even_though_the_pid_is_not() {
let before = Control::new(Role::Daemon).info(0);
let after = Control::new(Role::Daemon).info(0);
assert_eq!(before.pid, after.pid);
assert_ne!(before.started, after.started);
}
#[test]
fn a_stop_request_works_with_nothing_armed() {
let control = Control::new(Role::Tui);
control.request_stop();
assert!(control.stopping());
assert_eq!(control.info(0).role, Role::Tui);
}
#[test]
fn a_second_stop_request_is_harmless() {
let control = Control::new(Role::Daemon);
let (tx, _rx) = oneshot::channel();
control.arm(tx);
control.request_stop();
control.request_stop();
assert!(control.stopping());
}
// A session ending is not an update, so quitting must not leave the process
// claiming it was stopped by one.
#[test]
fn releasing_the_server_does_not_look_like_an_update() {
let control = Control::new(Role::Tui);
let (tx, _rx) = oneshot::channel();
control.arm(tx);
assert!(control.release());
assert!(!control.stopping());
assert!(!control.release());
}
}