git_policy_cli/journald_native.rs
1//! What:
2//! The service's way into the journal: the native journal protocol over a datagram socket when the service's standard
3//! error is the journal, and a plain line on standard error otherwise.
4//! Why:
5//! A line on a journal stream socket is stamped by journald when it processes it, so under load the times are seconds
6//! late and out of order with the service manager's own lines. A datagram on the native socket carries the kernel's
7//! time from the moment it was sent (measured in `doc/handover/cli-git-logging.md`), and the entry can carry a
8//! priority and fields a reader filters on (`journalctl CLI_GIT_REQUEST_ID=<id>`). The protocol is at
9//! <https://systemd.io/JOURNAL_NATIVE_PROTOCOL/>, and `$JOURNAL_STREAM` is described in `systemd.exec(5)`.
10//! No lock is taken: the socket is unconnected and non-blocking, one `sendto` per entry, and an entry that cannot be
11//! sent (journald restarting, a full queue) goes to standard error instead, so a commit never waits for the journal.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! if (sameFile(process.env.JOURNAL_STREAM, stderr)) socket.send(`PRIORITY=6\nMESSAGE=${line}\n`); else console.error(line);
16//! ```
17
18use std::io::Write;
19use std::os::linux::net::SocketAddrExt;
20use std::os::unix::net::{SocketAddr, UnixDatagram};
21
22/// The socket journald reads native entries from.
23pub const JOURNAL_SOCKET: &str = "/run/systemd/journal/socket";
24
25/// Test-only: names another socket to send entries to, as a path or, after an `@`, as an abstract name (a fixture path
26/// can be longer than a socket path may be). Detection by `$JOURNAL_STREAM` still applies, so a test points the
27/// variable at the file it gave the process as standard error.
28pub const TEST_SOCKET_VARIABLE: &str = "CLI_GIT_TEST_ONLY_JOURNAL_SOCKET";
29
30/// The variable the service manager sets to the device and inode of the stream it connected to the journal.
31pub const STREAM_VARIABLE: &str = "JOURNAL_STREAM";
32
33/// The `SYSLOG_IDENTIFIER` of every entry, the name `journalctl` shows after the host.
34pub const IDENTIFIER: &str = "cli-git-service";
35
36/// The most characters of a message an entry carries; longer text is cut. The limits count characters, not encoded bytes,
37/// 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
38/// fails with `EMSGSIZE` and the entry goes to standard error, as every entry that cannot be sent does.
39pub const MESSAGE_LIMIT: usize = 16 * 1024;
40
41/// The most characters of a field value an entry carries.
42pub const FIELD_LIMIT: usize = 1024;
43
44/// What is appended to a cut message.
45const CUT_MARK: &str = " [cut]";
46
47/// How serious an entry is, as journald's `PRIORITY` (syslog levels).
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum Priority {
50 /// Something the service needed failed; `journalctl --priority=err` shows it.
51 Error,
52 /// Something did not go as intended and the service went on.
53 Warning,
54 /// An event of the service's ordinary life.
55 Info,
56}
57
58impl Priority {
59 /// The syslog number journald reads.
60 pub fn number(self) -> u8 {
61 return match self {
62 Priority::Error => 3,
63 Priority::Warning => 4,
64 Priority::Info => 6,
65 };
66 }
67}
68
69/// What:
70/// The text with every control character (a newline included) replaced by a space, and cut to `limit` characters.
71/// Why:
72/// One entry is one line whatever a client or an error message held: a newline in a message could otherwise start a
73/// field of its own in the datagram, or a second line in the standard error stream.
74///
75/// In TS you'd write (pseudocode):
76/// ```ts
77/// text.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, limit)
78/// ```
79pub fn one_line(text: &str, limit: usize) -> String {
80 let mut single: String = String::with_capacity(text.len().min(limit + CUT_MARK.len()));
81 for (count, character) in text.chars().enumerate() {
82 if count >= limit {
83 single.push_str(CUT_MARK);
84 break;
85 }
86 single.push(if character.is_control() { ' ' } else { character });
87 }
88 return single;
89}
90
91/// Whether a field name is one journald accepts from a client: upper-case letters, digits and underscores, not starting
92/// with an underscore (those are the journal's own) or a digit.
93pub fn valid_field_name(name: &str) -> bool {
94 let mut characters = name.chars();
95 return characters.next().is_some_and(|first| { return first.is_ascii_uppercase(); })
96 && characters.all(|character| { return character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_'; });
97}
98
99/// What:
100/// The bytes of one native entry: `PRIORITY`, `SYSLOG_IDENTIFIER`, `MESSAGE`, then each field as `NAME=value` and a newline.
101/// Why:
102/// Every value is a single line (`one_line`), so the simple form of the protocol is enough and no value can start a
103/// field. A field whose name journald would refuse is left out.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// `PRIORITY=${n}\nSYSLOG_IDENTIFIER=cli-git-service\nMESSAGE=${message}\n${fields.map(([k, v]) => `${k}=${v}\n`).join('')}`
108/// ```
109pub fn datagram(priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
110 let mut text: String = format!("PRIORITY={}\nSYSLOG_IDENTIFIER={IDENTIFIER}\nMESSAGE={}\n", priority.number(), one_line(message, MESSAGE_LIMIT));
111 for (name, value) in fields {
112 if valid_field_name(name) {
113 text.push_str(&format!("{name}={}\n", one_line(value, FIELD_LIMIT)));
114 }
115 }
116 return text.into_bytes();
117}
118
119/// Whether `$JOURNAL_STREAM` names the file with this device and inode, which is how `systemd.exec(5)` says to tell that
120/// standard error is still the journal: the variable alone is not enough, because a redirection leaves it set.
121pub fn stream_matches(variable: Option<&str>, device: u64, inode: u64) -> bool {
122 let Some((named_device, named_inode)) = variable.and_then(|text| { return text.split_once(':'); }) else {
123 return false;
124 };
125 return named_device.parse::<u64>().ok() == Some(device) && named_inode.parse::<u64>().ok() == Some(inode);
126}
127
128/// The device and inode of standard error, or nothing when it cannot be read.
129pub fn stderr_identity() -> Option<(u64, u64)> {
130 let mut status: libc::stat = unsafe { std::mem::zeroed() };
131 // SAFETY: `status` is valid for the call to write one `stat`.
132 let result: libc::c_int = unsafe { libc::fstat(libc::STDERR_FILENO, &raw mut status) };
133 if result != 0 {
134 return None;
135 }
136 return Some((u64::from(status.st_dev), u64::from(status.st_ino)));
137}
138
139/// What:
140/// The address as the C structure `sendto` takes, and its length, or nothing when the name does not fit.
141/// Why:
142/// A signal handler may not build it (the standard library allocates), so it is made once, ahead of the signal.
143///
144/// In TS you'd write (pseudocode):
145/// ```ts
146/// rawAddress(address) // { family: AF_UNIX, path: '\0name' }
147/// ```
148pub fn raw_address(address: &SocketAddr) -> Option<(libc::sockaddr_un, libc::socklen_t)> {
149 use std::os::unix::ffi::OsStrExt;
150 // SAFETY: `sockaddr_un` is a plain C structure for which all zero bytes is a valid value.
151 let mut raw: libc::sockaddr_un = unsafe { std::mem::zeroed() };
152 raw.sun_family = libc::sa_family_t::try_from(libc::AF_UNIX).ok()?;
153 let (offset, name): (usize, &[u8]) = match (address.as_pathname(), address.as_abstract_name()) {
154 (Some(path), _) => (0, path.as_os_str().as_bytes()),
155 (None, Some(name)) => (1, name),
156 (None, None) => return None,
157 };
158 // A path needs room for the zero byte that ends it; an abstract name starts with one, so it may fill the whole field.
159 if offset + name.len() + usize::from(offset == 0) > raw.sun_path.len() {
160 return None;
161 }
162 for (slot, byte) in raw.sun_path[offset..].iter_mut().zip(name) {
163 *slot = libc::c_char::from_ne_bytes([*byte]);
164 }
165 let length: usize = std::mem::size_of::<libc::sa_family_t>() + offset + name.len() + usize::from(offset == 0);
166 return Some((raw, libc::socklen_t::try_from(length).ok()?));
167}
168
169/// Where an entry went.
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
171pub enum Delivery {
172 /// As a datagram on the native socket.
173 Native,
174 /// As a line on standard error.
175 Stderr,
176}
177
178/// Where the journal listens.
179#[derive(Clone, Debug, Eq, PartialEq)]
180pub enum Target {
181 /// A socket file.
182 Path(String),
183 /// An abstract name, which a test uses.
184 Abstract(String),
185}
186
187impl Target {
188 /// The journal's own socket.
189 pub fn journal() -> Self {
190 return Self::Path(String::from(JOURNAL_SOCKET));
191 }
192
193 /// Reads a test-only setting: `@name` is abstract, anything else a path.
194 pub fn parse(text: &str) -> Self {
195 return text.strip_prefix('@').map_or_else(|| { return Self::Path(text.to_string()); }, |name| { return Self::Abstract(name.to_string()); });
196 }
197
198 /// The socket address, or nothing when the name does not fit one.
199 pub fn address(&self) -> Option<SocketAddr> {
200 return match self {
201 Self::Path(path) => SocketAddr::from_pathname(path).ok(),
202 Self::Abstract(name) => SocketAddr::from_abstract_name(name.as_bytes()).ok(),
203 };
204 }
205}
206
207/// Where the service's entries go, decided once.
208#[derive(Debug)]
209pub struct Sink {
210 /// The unconnected, non-blocking socket, when standard error is the journal and a socket could be made.
211 native: Option<UnixDatagram>,
212 /// The journal's socket.
213 address: Option<SocketAddr>,
214 /// Whether standard error is the journal's stream, so that a line there may carry a `<priority>` prefix.
215 stream: bool,
216}
217
218impl Sink {
219 /// What:
220 /// The sink for a process whose `$JOURNAL_STREAM` is `variable`, whose standard error is the file `stderr`, and
221 /// whose journal socket is at `socket`.
222 /// Why:
223 /// The native socket is used only when standard error is the journal; a terminal, a file or a pipe keeps its plain
224 /// lines, and a service run by hand reads as before.
225 ///
226 /// In TS you'd write (pseudocode):
227 /// ```ts
228 /// Sink.choose(process.env.JOURNAL_STREAM, stderrIdentity(), '/run/systemd/journal/socket')
229 /// ```
230 pub fn choose(variable: Option<&str>, stderr: Option<(u64, u64)>, socket: &Target) -> Self {
231 let stream: bool = stderr.is_some_and(|(device, inode)| { return stream_matches(variable, device, inode); });
232 let native: Option<UnixDatagram> = if stream { Self::open() } else { None };
233 return Self { native, address: socket.address(), stream };
234 }
235
236 /// An unconnected datagram socket that never blocks, or nothing.
237 fn open() -> Option<UnixDatagram> {
238 let socket: UnixDatagram = UnixDatagram::unbound().ok()?;
239 socket.set_nonblocking(true).ok()?;
240 return Some(socket);
241 }
242
243 /// The sink of this process: the environment's `$JOURNAL_STREAM`, the real standard error, and the real socket or the
244 /// test-only one.
245 pub fn from_environment() -> Self {
246 let variable: Option<String> = std::env::var(STREAM_VARIABLE).ok();
247 let socket: Target = std::env::var(TEST_SOCKET_VARIABLE).map_or_else(|_| { return Target::journal(); }, |text| { return Target::parse(text.as_str()); });
248 return Self::choose(variable.as_deref(), stderr_identity(), &socket);
249 }
250
251 /// Whether standard error is the journal's stream, so that a write there may carry a `<priority>` prefix and must not wait.
252 pub fn on_stream(&self) -> bool {
253 return self.stream;
254 }
255
256 /// Whether this sink sends datagrams.
257 pub fn is_native(&self) -> bool {
258 return self.native.is_some();
259 }
260
261 /// The raw descriptor and socket address a signal handler sends its entry with, when this sink is native.
262 pub fn signal_target(&self) -> Option<(std::os::fd::RawFd, &SocketAddr)> {
263 use std::os::fd::AsRawFd;
264 let socket: &UnixDatagram = self.native.as_ref()?;
265 return self.address.as_ref().map(|address| { return (socket.as_raw_fd(), address); });
266 }
267
268 /// What:
269 /// Sends one entry, and gives where it went: a datagram when the sink is native and the socket takes it, otherwise
270 /// `line` on `stderr`.
271 /// Why:
272 /// A journal that is restarting, absent or full is no reason for the service to wait or to lose the entry. Standard
273 /// error is the journal's stream when the sink was chosen for it, and a `<priority>` prefix there keeps the level.
274 ///
275 /// In TS you'd write (pseudocode):
276 /// ```ts
277 /// try { socket.send(datagram) } catch { stderr.write(prefix + line) }
278 /// ```
279 pub fn deliver(&self, priority: Priority, message: &str, fields: &[(&str, String)], line: &str, stderr: &mut dyn Write) -> Delivery {
280 if let Some(socket) = self.native.as_ref()
281 && let Some(address) = self.address.as_ref()
282 && socket.send_to_addr(datagram(priority, message, fields).as_slice(), address).is_ok()
283 {
284 return Delivery::Native;
285 }
286 let prefix: String = if self.stream { format!("<{}>", priority.number()) } else { String::new() };
287 // The result is dropped on purpose: a closed standard error is no reason to fail the work the entry describes.
288 let _written = stderr.write_all(format!("{prefix}{line}").as_bytes());
289 return Delivery::Stderr;
290 }
291}
292
293/// What:
294/// A writer to the journal's stream (standard error as the service manager connected it) that never waits for room.
295/// Why:
296/// `O_NONBLOCK` on the datagram socket does not make standard error non-blocking, and a journal that stops reading would
297/// fill the stream and block the service, and so the commits that wait for it. Each line is sent with `MSG_DONTWAIT`: a
298/// line that finds no room at all is dropped whole. A line the socket took only in part is finished within a short bound
299/// (three waits of at most 100 milliseconds), because the stream would otherwise carry half a line into the next one.
300///
301/// In TS you'd write (pseudocode):
302/// ```ts
303/// try { socket.sendNonBlocking(line) } catch (error) { if (error.code === 'EAGAIN') drop(line) }
304/// ```
305#[derive(Clone, Copy, Debug)]
306pub struct StreamWriter {
307 /// The descriptor of the stream.
308 descriptor: std::os::fd::RawFd,
309}
310
311impl StreamWriter {
312 /// A writer to standard error.
313 pub fn stderr() -> Self {
314 return Self { descriptor: libc::STDERR_FILENO };
315 }
316
317 /// A writer to a stream socket descriptor.
318 pub fn on(descriptor: std::os::fd::RawFd) -> Self {
319 return Self { descriptor };
320 }
321
322 /// Sends bytes without waiting; the count sent, or the error.
323 fn send_now(&self, bytes: &[u8]) -> std::io::Result<usize> {
324 // SAFETY: `bytes` is valid for its length for the call.
325 let sent: isize = unsafe { libc::send(self.descriptor, bytes.as_ptr().cast(), bytes.len(), libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL) };
326 let Err(error) = usize::try_from(sent).map_err(|_| { return std::io::Error::last_os_error(); }) else {
327 return Ok(usize::try_from(sent).unwrap_or(0));
328 };
329 if error.raw_os_error() != Some(libc::ENOTSOCK) {
330 return Err(error);
331 }
332 // Not a socket (a file or a pipe a test or a redirection put there): an ordinary write.
333 // SAFETY: `bytes` is valid for its length for the call.
334 let written: isize = unsafe { libc::write(self.descriptor, bytes.as_ptr().cast(), bytes.len()) };
335 return usize::try_from(written).map_err(|_| { return std::io::Error::last_os_error(); });
336 }
337
338 /// Waits at most 100 milliseconds for room.
339 fn wait_for_room(&self) {
340 let mut request: libc::pollfd = libc::pollfd { fd: self.descriptor, events: libc::POLLOUT, revents: 0 };
341 // SAFETY: `request` is valid for the call, which reads and writes one `pollfd`.
342 unsafe { libc::poll(&raw mut request, 1, 100) };
343 }
344}
345
346impl Write for StreamWriter {
347 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
348 return self.send_now(bytes);
349 }
350
351 fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
352 let mut rest: &[u8] = bytes;
353 let mut waits: u32 = 0;
354 while !rest.is_empty() {
355 match self.send_now(rest) {
356 Ok(count) => rest = &rest[count..],
357 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock && rest.len() == bytes.len() => return Err(error),
358 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock && waits < 3 => {
359 waits += 1;
360 self.wait_for_room();
361 }
362 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
363 Err(error) => return Err(error),
364 }
365 }
366 return Ok(());
367 }
368
369 fn flush(&mut self) -> std::io::Result<()> {
370 return Ok(());
371 }
372}
373
374/// The datagram, the detection, the fallback.
375#[cfg(test)]
376#[path = "journald_native_tests.rs"]
377mod tests;