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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
//! 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 `signal-safety(7)` lists (`clock_gettime`,
//! `sendto`,
//! `send`,
//! `poll`,
//! `write`,
//! `signal`,
//! `raise`),
//! and the write never waits for room: a standard error that is a full pipe or stream loses the line, because a service
//! that cannot end for want of room to say so is worse than a missing line.
//!
//! 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::journald_native::{IDENTIFIER, Priority, raw_address, send_without_waiting};
use super::service_timing::{LOG_PREFIX, sink};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicI32, 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 = 256;
/// The monotonic clock,
/// in nanoseconds,
/// when the handlers were installed;
/// zero before that.
static STARTED_NANOS: AtomicU64 = AtomicU64::new(0);
/// The native journal socket's descriptor, or -1 when the service's entries go to standard error.
static NATIVE_DESCRIPTOR: AtomicI32 = AtomicI32::new(-1);
/// The journal socket's address, made before any signal so the handler only reads it.
static NATIVE_ADDRESS: OnceLock<(libc::sockaddr_un, libc::socklen_t)> = OnceLock::new();
/// Whether standard error is the journal's stream, decided before any signal: the fallback line then carries the priority
/// prefix.
static ON_STREAM: AtomicBool = AtomicBool::new(false);
/// The words before the message in a native entry: the priority and the identifier.
const NATIVE_HEAD: &str = "SYSLOG_IDENTIFIER=";
/// The field that carries the time of the event beside the message.
const NATIVE_TIME_FIELD: &str = "\nCLI_GIT_AT_MS=";
/// 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, on_stream: bool) -> ([u8; LINE_CAPACITY], usize) {
let mut line: [u8; LINE_CAPACITY] = [0; LINE_CAPACITY];
// On the journal's stream a `<6>` prefix keeps the `info` level the line has on the native socket.
let mut used: usize = push_text(&mut line, 0, if on_stream { "<6>" } else { "" });
used = push_text(&mut line, used, 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);
}
/// What:
/// The native journal entry the handler sends for `signal`, and its length: the priority, the identifier, the message
/// (the line without its prefix and newline) and the time of the event as a field.
/// Why:
/// The kernel stamps a datagram when it is sent, so the exit of a service that is stopped is dated by the stop and not by
/// the moment the journal gets round to reading standard error. Built in a fixed buffer like `signal_line`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `PRIORITY=6\nSYSLOG_IDENTIFIER=cli-git-service\nMESSAGE=exiting reason=signal-${signal} uptime_ms=${u} at_ms=${t}\nCLI_GIT_AT_MS=${t}\n`
/// ```
pub fn signal_datagram(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, "PRIORITY=");
used = push_number(&mut line, used, u64::from(Priority::Info.number()));
used = push_text(&mut line, used, "\n");
used = push_text(&mut line, used, NATIVE_HEAD);
used = push_text(&mut line, used, IDENTIFIER);
used = push_text(&mut line, used, "\nMESSAGE=");
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);
used = push_text(&mut line, used, NATIVE_TIME_FIELD);
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 at_ms: u64 = wall_millis_signal_safe();
let on_stream: bool = ON_STREAM.load(Ordering::Relaxed);
let (line, length) = signal_line(signal, uptime, at_ms, on_stream);
let (entry, entry_length) = signal_datagram(signal, uptime, at_ms);
let descriptor: libc::c_int = NATIVE_DESCRIPTOR.load(Ordering::Relaxed);
// SAFETY: the buffers hold `length` and `entry_length` initialized bytes, the address was made before any signal and is
// only read, and `sendto`, `write`, `signal` and `raise` are safe in a signal handler. The results are dropped on
// purpose: a closed standard error is no reason to keep the process alive. The datagram goes first and the line is
// written only when it could not be sent, so the exit is in the journal once.
unsafe {
let sent: bool = match NATIVE_ADDRESS.get() {
Some((address, address_length)) if descriptor >= 0 => {
libc::sendto(descriptor, entry.as_ptr().cast(), entry_length, libc::MSG_DONTWAIT, std::ptr::from_ref(address).cast(), *address_length) >= 0
}
_ => false,
};
if !sent {
// Without waiting for room (a full pipe or stream loses the line): `send_without_waiting` calls only `send`,
// `poll` and `write`.
let _written = send_without_waiting(libc::STDERR_FILENO, &line[..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);
ON_STREAM.store(sink().on_stream(), Ordering::Relaxed);
// The sink is decided here, before any signal, and the handler sends on its descriptor when the entries are native.
if let Some((descriptor, address)) = sink().signal_target()
&& let Some(raw) = raw_address(address)
&& NATIVE_ADDRESS.set(raw).is_ok()
{
NATIVE_DESCRIPTOR.store(descriptor, 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;