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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
//! What:
//! The service's way into the journal: the native journal protocol over a datagram socket when the service's standard
//! error is the journal, and a plain line on standard error otherwise.
//! Why:
//! A line on a journal stream socket is stamped by journald when it processes it, so under load the times are seconds
//! late and out of order with the service manager's own lines. A datagram on the native socket carries the kernel's
//! time from the moment it was sent (measured in `doc/handover/cli-git-logging.md`), and the entry can carry a
//! priority and fields a reader filters on (`journalctl CLI_GIT_REQUEST_ID=<id>`). The protocol is at
//! <https://systemd.io/JOURNAL_NATIVE_PROTOCOL/>, and `$JOURNAL_STREAM` is described in `systemd.exec(5)`.
//! No lock is taken: the socket is unconnected and non-blocking, one `sendto` per entry, and an entry that cannot be
//! sent (journald restarting, a full queue) goes to standard error instead, so a commit never waits for the journal.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! if (sameFile(process.env.JOURNAL_STREAM, stderr)) socket.send(`PRIORITY=6\nMESSAGE=${line}\n`); else console.error(line);
//! ```
use std::io::Write;
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixDatagram};
/// The socket journald reads native entries from.
pub const JOURNAL_SOCKET: &str = "/run/systemd/journal/socket";
/// Test-only: names another socket to send entries to, as a path or, after an `@`, as an abstract name (a fixture path
/// can be longer than a socket path may be). Detection by `$JOURNAL_STREAM` still applies, so a test points the
/// variable at the file it gave the process as standard error.
pub const TEST_SOCKET_VARIABLE: &str = "CLI_GIT_TEST_ONLY_JOURNAL_SOCKET";
/// The variable the service manager sets to the device and inode of the stream it connected to the journal.
pub const STREAM_VARIABLE: &str = "JOURNAL_STREAM";
/// The `SYSLOG_IDENTIFIER` of every entry, the name `journalctl` shows after the host.
pub const IDENTIFIER: &str = "cli-git-service";
/// The most characters of a message an entry carries; longer text is cut. The limits count characters, not encoded bytes,
/// and bound no total, so an entry made of very many or very wide fields can still exceed what a socket takes; the send then
/// fails with `EMSGSIZE` and the entry goes to standard error, as every entry that cannot be sent does.
pub const MESSAGE_LIMIT: usize = 16 * 1024;
/// The most characters of a field value an entry carries.
pub const FIELD_LIMIT: usize = 1024;
/// What is appended to a cut message.
const CUT_MARK: &str = " [cut]";
/// How serious an entry is, as journald's `PRIORITY` (syslog levels).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Priority {
/// Something the service needed failed; `journalctl --priority=err` shows it.
Error,
/// Something did not go as intended and the service went on.
Warning,
/// An event of the service's ordinary life.
Info,
}
impl Priority {
/// The syslog number journald reads.
pub fn number(self) -> u8 {
return match self {
Priority::Error => 3,
Priority::Warning => 4,
Priority::Info => 6,
};
}
}
/// What:
/// The text with every control character (a newline included) replaced by a space, and cut to `limit` characters.
/// Why:
/// One entry is one line whatever a client or an error message held: a newline in a message could otherwise start a
/// field of its own in the datagram, or a second line in the standard error stream.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// text.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, limit)
/// ```
pub fn one_line(text: &str, limit: usize) -> String {
let mut single: String = String::with_capacity(text.len().min(limit + CUT_MARK.len()));
for (count, character) in text.chars().enumerate() {
if count >= limit {
single.push_str(CUT_MARK);
break;
}
single.push(if character.is_control() { ' ' } else { character });
}
return single;
}
/// Whether a field name is one journald accepts from a client: upper-case letters, digits and underscores, not starting
/// with an underscore (those are the journal's own) or a digit.
pub fn valid_field_name(name: &str) -> bool {
let mut characters = name.chars();
return characters.next().is_some_and(|first| { return first.is_ascii_uppercase(); })
&& characters.all(|character| { return character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_'; });
}
/// What:
/// The bytes of one native entry: `PRIORITY`, `SYSLOG_IDENTIFIER`, `MESSAGE`, then each field as `NAME=value` and a newline.
/// Why:
/// Every value is a single line (`one_line`), so the simple form of the protocol is enough and no value can start a
/// field. A field whose name journald would refuse is left out.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `PRIORITY=${n}\nSYSLOG_IDENTIFIER=cli-git-service\nMESSAGE=${message}\n${fields.map(([k, v]) => `${k}=${v}\n`).join('')}`
/// ```
pub fn datagram(priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
let mut text: String = format!("PRIORITY={}\nSYSLOG_IDENTIFIER={IDENTIFIER}\nMESSAGE={}\n", priority.number(), one_line(message, MESSAGE_LIMIT));
for (name, value) in fields {
if valid_field_name(name) {
text.push_str(&format!("{name}={}\n", one_line(value, FIELD_LIMIT)));
}
}
return text.into_bytes();
}
/// Whether `$JOURNAL_STREAM` names the file with this device and inode, which is how `systemd.exec(5)` says to tell that
/// standard error is still the journal: the variable alone is not enough, because a redirection leaves it set.
pub fn stream_matches(variable: Option<&str>, device: u64, inode: u64) -> bool {
let Some((named_device, named_inode)) = variable.and_then(|text| { return text.split_once(':'); }) else {
return false;
};
return named_device.parse::<u64>().ok() == Some(device) && named_inode.parse::<u64>().ok() == Some(inode);
}
/// The device and inode of standard error, or nothing when it cannot be read.
pub fn stderr_identity() -> Option<(u64, u64)> {
let mut status: libc::stat = unsafe { std::mem::zeroed() };
// SAFETY: `status` is valid for the call to write one `stat`.
let result: libc::c_int = unsafe { libc::fstat(libc::STDERR_FILENO, &raw mut status) };
if result != 0 {
return None;
}
return Some((u64::from(status.st_dev), u64::from(status.st_ino)));
}
/// What:
/// The address as the C structure `sendto` takes, and its length, or nothing when the name does not fit.
/// Why:
/// A signal handler may not build it (the standard library allocates), so it is made once, ahead of the signal.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// rawAddress(address) // { family: AF_UNIX, path: '\0name' }
/// ```
pub fn raw_address(address: &SocketAddr) -> Option<(libc::sockaddr_un, libc::socklen_t)> {
use std::os::unix::ffi::OsStrExt;
// SAFETY: `sockaddr_un` is a plain C structure for which all zero bytes is a valid value.
let mut raw: libc::sockaddr_un = unsafe { std::mem::zeroed() };
raw.sun_family = libc::sa_family_t::try_from(libc::AF_UNIX).ok()?;
let (offset, name): (usize, &[u8]) = match (address.as_pathname(), address.as_abstract_name()) {
(Some(path), _) => (0, path.as_os_str().as_bytes()),
(None, Some(name)) => (1, name),
(None, None) => return None,
};
// A path needs room for the zero byte that ends it; an abstract name starts with one, so it may fill the whole field.
if offset + name.len() + usize::from(offset == 0) > raw.sun_path.len() {
return None;
}
for (slot, byte) in raw.sun_path[offset..].iter_mut().zip(name) {
*slot = libc::c_char::from_ne_bytes([*byte]);
}
let length: usize = std::mem::size_of::<libc::sa_family_t>() + offset + name.len() + usize::from(offset == 0);
return Some((raw, libc::socklen_t::try_from(length).ok()?));
}
/// Where an entry went.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Delivery {
/// As a datagram on the native socket.
Native,
/// As a line on standard error.
Stderr,
}
/// Where the journal listens.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Target {
/// A socket file.
Path(String),
/// An abstract name, which a test uses.
Abstract(String),
}
impl Target {
/// The journal's own socket.
pub fn journal() -> Self {
return Self::Path(String::from(JOURNAL_SOCKET));
}
/// Reads a test-only setting: `@name` is abstract, anything else a path.
pub fn parse(text: &str) -> Self {
return text.strip_prefix('@').map_or_else(|| { return Self::Path(text.to_string()); }, |name| { return Self::Abstract(name.to_string()); });
}
/// The socket address, or nothing when the name does not fit one.
pub fn address(&self) -> Option<SocketAddr> {
return match self {
Self::Path(path) => SocketAddr::from_pathname(path).ok(),
Self::Abstract(name) => SocketAddr::from_abstract_name(name.as_bytes()).ok(),
};
}
}
/// Where the service's entries go, decided once.
#[derive(Debug)]
pub struct Sink {
/// The unconnected, non-blocking socket, when standard error is the journal and a socket could be made.
native: Option<UnixDatagram>,
/// The journal's socket.
address: Option<SocketAddr>,
/// Whether standard error is the journal's stream, so that a line there may carry a `<priority>` prefix.
stream: bool,
}
impl Sink {
/// What:
/// The sink for a process whose `$JOURNAL_STREAM` is `variable`, whose standard error is the file `stderr`, and
/// whose journal socket is at `socket`.
/// Why:
/// The native socket is used only when standard error is the journal; a terminal, a file or a pipe keeps its plain
/// lines, and a service run by hand reads as before.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// Sink.choose(process.env.JOURNAL_STREAM, stderrIdentity(), '/run/systemd/journal/socket')
/// ```
pub fn choose(variable: Option<&str>, stderr: Option<(u64, u64)>, socket: &Target) -> Self {
let stream: bool = stderr.is_some_and(|(device, inode)| { return stream_matches(variable, device, inode); });
let native: Option<UnixDatagram> = if stream { Self::open() } else { None };
return Self { native, address: socket.address(), stream };
}
/// An unconnected datagram socket that never blocks, or nothing.
fn open() -> Option<UnixDatagram> {
let socket: UnixDatagram = UnixDatagram::unbound().ok()?;
socket.set_nonblocking(true).ok()?;
return Some(socket);
}
/// The sink of this process: the environment's `$JOURNAL_STREAM`, the real standard error, and the real socket or the
/// test-only one.
pub fn from_environment() -> Self {
let variable: Option<String> = std::env::var(STREAM_VARIABLE).ok();
let socket: Target = std::env::var(TEST_SOCKET_VARIABLE).map_or_else(|_| { return Target::journal(); }, |text| { return Target::parse(text.as_str()); });
return Self::choose(variable.as_deref(), stderr_identity(), &socket);
}
/// Whether standard error is the journal's stream, so that a write there may carry a `<priority>` prefix and must not wait.
pub fn on_stream(&self) -> bool {
return self.stream;
}
/// Whether this sink sends datagrams.
pub fn is_native(&self) -> bool {
return self.native.is_some();
}
/// The raw descriptor and socket address a signal handler sends its entry with, when this sink is native.
pub fn signal_target(&self) -> Option<(std::os::fd::RawFd, &SocketAddr)> {
use std::os::fd::AsRawFd;
let socket: &UnixDatagram = self.native.as_ref()?;
return self.address.as_ref().map(|address| { return (socket.as_raw_fd(), address); });
}
/// What:
/// Sends one entry, and gives where it went: a datagram when the sink is native and the socket takes it, otherwise
/// `line` on `stderr`.
/// Why:
/// A journal that is restarting, absent or full is no reason for the service to wait or to lose the entry. Standard
/// error is the journal's stream when the sink was chosen for it, and a `<priority>` prefix there keeps the level.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// try { socket.send(datagram) } catch { stderr.write(prefix + line) }
/// ```
pub fn deliver(&self, priority: Priority, message: &str, fields: &[(&str, String)], line: &str, stderr: &mut dyn Write) -> Delivery {
if let Some(socket) = self.native.as_ref()
&& let Some(address) = self.address.as_ref()
&& socket.send_to_addr(datagram(priority, message, fields).as_slice(), address).is_ok()
{
return Delivery::Native;
}
let prefix: String = if self.stream { format!("<{}>", priority.number()) } else { String::new() };
// The result is dropped on purpose: a closed standard error is no reason to fail the work the entry describes.
let _written = stderr.write_all(format!("{prefix}{line}").as_bytes());
return Delivery::Stderr;
}
}
/// What:
/// A writer to the journal's stream (standard error as the service manager connected it) that never waits for room.
/// Why:
/// `O_NONBLOCK` on the datagram socket does not make standard error non-blocking, and a journal that stops reading would
/// fill the stream and block the service, and so the commits that wait for it. Each line is sent with `MSG_DONTWAIT`: a
/// line that finds no room at all is dropped whole. A line the socket took only in part is finished within a short bound
/// (three waits of at most 100 milliseconds), because the stream would otherwise carry half a line into the next one.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// try { socket.sendNonBlocking(line) } catch (error) { if (error.code === 'EAGAIN') drop(line) }
/// ```
#[derive(Clone, Copy, Debug)]
pub struct StreamWriter {
/// The descriptor of the stream.
descriptor: std::os::fd::RawFd,
}
impl StreamWriter {
/// A writer to standard error.
pub fn stderr() -> Self {
return Self { descriptor: libc::STDERR_FILENO };
}
/// A writer to a stream socket descriptor.
pub fn on(descriptor: std::os::fd::RawFd) -> Self {
return Self { descriptor };
}
/// Sends bytes without waiting; the count sent, or the error.
fn send_now(&self, bytes: &[u8]) -> std::io::Result<usize> {
// SAFETY: `bytes` is valid for its length for the call.
let sent: isize = unsafe { libc::send(self.descriptor, bytes.as_ptr().cast(), bytes.len(), libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL) };
let Err(error) = usize::try_from(sent).map_err(|_| { return std::io::Error::last_os_error(); }) else {
return Ok(usize::try_from(sent).unwrap_or(0));
};
if error.raw_os_error() != Some(libc::ENOTSOCK) {
return Err(error);
}
// Not a socket (a file or a pipe a test or a redirection put there): an ordinary write.
// SAFETY: `bytes` is valid for its length for the call.
let written: isize = unsafe { libc::write(self.descriptor, bytes.as_ptr().cast(), bytes.len()) };
return usize::try_from(written).map_err(|_| { return std::io::Error::last_os_error(); });
}
/// Waits at most 100 milliseconds for room.
fn wait_for_room(&self) {
let mut request: libc::pollfd = libc::pollfd { fd: self.descriptor, events: libc::POLLOUT, revents: 0 };
// SAFETY: `request` is valid for the call, which reads and writes one `pollfd`.
unsafe { libc::poll(&raw mut request, 1, 100) };
}
}
impl Write for StreamWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
return self.send_now(bytes);
}
fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
let mut rest: &[u8] = bytes;
let mut waits: u32 = 0;
while !rest.is_empty() {
match self.send_now(rest) {
Ok(count) => rest = &rest[count..],
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock && rest.len() == bytes.len() => return Err(error),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock && waits < 3 => {
waits += 1;
self.wait_for_room();
}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(error) => return Err(error),
}
}
return Ok(());
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
/// The datagram, the detection, the fallback.
#[cfg(test)]
#[path = "journald_native_tests.rs"]
mod tests;