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
//! What:
//! The service's last line when a signal ends it:
//! the service manager stops a unit with `SIGTERM`,
//! and a terminal sends
//! `SIGINT` or `SIGHUP`;
//! the handler writes one line with the signal and the uptime,
//! then lets the signal end the process as
//! it would have without a handler.
//! Why:
//! The journal of the unit with the 17-second commit had no line for the first service's end,
//! and the manager's own lines
//! about the unit cannot be relied on.
//! A signal gives the process no chance to log from ordinary code,
//! so the line is
//! built and written with nothing but calls that are safe in a handler (`clock_gettime`,
//! `write`,
//! `signal`,
//! `raise`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! process.on('SIGTERM', () => { console.error(`cli-git-service: exiting reason=signal-15 uptime_ms=${uptime()}`); process.exit(143); });
//! ```
use super::service_timing::LOG_PREFIX;
use std::sync::atomic::{AtomicU64, Ordering};
/// The signals that end the service without a word otherwise.
pub const HANDLED: [libc::c_int; 3] = [libc::SIGTERM, libc::SIGINT, libc::SIGHUP];
/// The words before the signal number in the line.
const BEFORE_SIGNAL: &str = "exiting reason=signal-";
/// The words between the signal number and the uptime.
const BEFORE_UPTIME: &str = " uptime_ms=";
/// The words between the uptime and the wall-clock time of the event,
/// as every other line of the service states it.
const BEFORE_TIME: &str = " at_ms=";
/// The room the line needs:
/// the prefix,
/// the words,
/// three numbers of at most twenty digits,
/// and the newline.
const LINE_CAPACITY: usize = 192;
/// The monotonic clock,
/// in nanoseconds,
/// when the handlers were installed;
/// zero before that.
static STARTED_NANOS: AtomicU64 = AtomicU64::new(0);
/// The wall clock in whole milliseconds since the epoch,
/// zero when the clock cannot be read.
fn wall_millis_signal_safe() -> u64 {
let mut now: libc::timespec = libc::timespec { tv_sec: 0, tv_nsec: 0 };
// SAFETY: `now` is valid for the call to write one `timespec`, and `clock_gettime` is safe in a signal handler.
let status: libc::c_int = unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &raw mut now) };
if status != 0 {
return 0;
}
return u64::try_from(now.tv_sec).unwrap_or(0).saturating_mul(1_000).saturating_add(u64::try_from(now.tv_nsec).unwrap_or(0) / 1_000_000);
}
/// The monotonic clock in nanoseconds,
/// zero when the clock cannot be read.
fn monotonic_nanos() -> u64 {
let mut now: libc::timespec = libc::timespec { tv_sec: 0, tv_nsec: 0 };
// SAFETY: `now` is valid for the call to write one `timespec`, and `clock_gettime` is safe in a signal handler.
let status: libc::c_int = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &raw mut now) };
if status != 0 {
return 0;
}
return u64::try_from(now.tv_sec).unwrap_or(0).saturating_mul(1_000_000_000).saturating_add(u64::try_from(now.tv_nsec).unwrap_or(0));
}
/// What:
/// Appends the decimal digits of `value` to `line` at `used`,
/// and returns the new length.
/// Why:
/// Formatting in a signal handler may not allocate,
/// so the digits go into a fixed buffer from the least significant end and
/// are copied in order.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// line.write(String(value), used)
/// ```
fn push_number(line: &mut [u8; LINE_CAPACITY], used: usize, value: u64) -> usize {
let mut digits: [u8; 20] = [0; 20];
let mut count: usize = 0;
let mut rest: u64 = value;
loop {
digits[count] = b'0' + u8::try_from(rest % 10).unwrap_or(0);
count += 1;
rest /= 10;
if rest == 0 {
break;
}
}
let mut end: usize = used;
for index in (0..count).rev() {
line[end] = digits[index];
end += 1;
}
return end;
}
/// Appends `text` to `line` at `used`,
/// and returns the new length.
fn push_text(line: &mut [u8; LINE_CAPACITY], used: usize, text: &str) -> usize {
let bytes: &[u8] = text.as_bytes();
line[used..used + bytes.len()].copy_from_slice(bytes);
return used + bytes.len();
}
/// What:
/// The line the handler writes for `signal` after `uptime_nanos` of running,
/// and its length.
/// Why:
/// Kept apart from the handler so a test can read the bytes without sending a signal.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `cli-git-service: exiting reason=signal-${signal} uptime_ms=${uptimeMs} at_ms=${Date.now()}\n`
/// ```
pub fn signal_line(signal: libc::c_int, uptime_nanos: u64, at_ms: u64) -> ([u8; LINE_CAPACITY], usize) {
let mut line: [u8; LINE_CAPACITY] = [0; LINE_CAPACITY];
let mut used: usize = push_text(&mut line, 0, LOG_PREFIX);
used = push_text(&mut line, used, BEFORE_SIGNAL);
used = push_number(&mut line, used, u64::try_from(signal).unwrap_or(0));
used = push_text(&mut line, used, BEFORE_UPTIME);
used = push_number(&mut line, used, uptime_nanos / 1_000_000);
used = push_text(&mut line, used, BEFORE_TIME);
used = push_number(&mut line, used, at_ms);
line[used] = b'\n';
return (line, used + 1);
}
/// The handler:
/// one `write` of the line to standard error,
/// then the signal's default action.
extern "C" fn on_signal(signal: libc::c_int) {
let started: u64 = STARTED_NANOS.load(Ordering::Relaxed);
let uptime: u64 = monotonic_nanos().saturating_sub(started);
let (line, length) = signal_line(signal, uptime, wall_millis_signal_safe());
// SAFETY: `line` holds `length` initialized bytes, and `write`, `signal` and `raise` are safe in a signal handler. The
// write's result is dropped on purpose: a closed standard error is no reason to keep the process alive.
unsafe {
let _written = libc::write(libc::STDERR_FILENO, line.as_ptr().cast(), length);
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
}
/// What:
/// Installs the handler for the signals that end the service.
/// Why:
/// Without it the process dies from `SIGTERM` with no line in the journal.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// installExitLines()
/// ```
pub fn install() {
STARTED_NANOS.store(monotonic_nanos(), Ordering::Relaxed);
for signal in HANDLED {
// SAFETY: `on_signal` has the signature a handler needs and only calls functions that are safe in a handler.
unsafe {
libc::signal(signal, on_signal as *const () as libc::sighandler_t);
}
}
}
/// The line's bytes.
#[cfg(test)]
#[path = "service_signal_tests.rs"]
mod tests;