git_policy_cli/service/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 on the native path: the socket is unconnected and non-blocking, one `sendto` per entry, and an entry
11//! that cannot be sent (journald restarting, a full queue) goes to standard error instead, so a commit never waits for the
12//! journal. A value is carried as written, one with a newline in the protocol's length-prefixed form. On the fallback a line
13//! that finds no room is dropped and counted, never waited for, and a line the stream took only in part is finished before
14//! the next one starts.
15//!
16//! In TS you'd write (pseudocode):
17//! ```ts
18//! if (sameFile(process.env.JOURNAL_STREAM, stderr)) socket.send(`PRIORITY=6\nMESSAGE=${line}\n`); else console.error(line);
19//! ```
20
21use std::io::Write;
22use std::sync::Mutex;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::os::linux::net::SocketAddrExt;
25use std::os::unix::net::{SocketAddr, UnixDatagram};
26
27/// The socket journald reads native entries from.
28pub const JOURNAL_SOCKET: &str = "/run/systemd/journal/socket";
29
30/// Test-only: names another socket to send entries to, as a path or, after an `@`, as an abstract name (a fixture path
31/// can be longer than a socket path may be). Detection by `$JOURNAL_STREAM` still applies, so a test points the
32/// variable at the file it gave the process as standard error.
33pub const TEST_SOCKET_VARIABLE: &str = "CLI_GIT_TEST_ONLY_JOURNAL_SOCKET";
34
35/// The variable the service manager sets to the device and inode of the stream it connected to the journal.
36pub const STREAM_VARIABLE: &str = "JOURNAL_STREAM";
37
38/// The `SYSLOG_IDENTIFIER` of every entry, the name `journalctl` shows after the host.
39pub const IDENTIFIER: &str = "cli-git-service";
40
41/// The `SYSLOG_IDENTIFIER` of an entry the wrapper itself sends, as the command a person ran, beside the service's own.
42pub const CLIENT_IDENTIFIER: &str = "cli-git";
43
44/// The most characters of a message an entry carries; longer text is cut. The limits count characters, not encoded bytes,
45/// 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
46/// fails with `EMSGSIZE` and the entry goes to standard error, as every entry that cannot be sent does.
47/// Exempt from a citation under `LQM` (class: display cap): it limits how much text a message, log field or record
48/// names, and no command result or duration depends on it. Classed once in `doc/planning/cli-git-shipped-limits-
49/// audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
50pub const MESSAGE_LIMIT: usize = 16 * 1024;
51
52/// The most characters of a field value an entry carries.
53/// Exempt from a citation under `LQM` (class: display cap): it limits how much text a message, log field or record
54/// names, and no command result or duration depends on it. Classed once in `doc/planning/cli-git-shipped-limits-
55/// audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
56pub const FIELD_LIMIT: usize = 1024;
57
58/// What is appended to a cut message.
59const CUT_MARK: &str = " [cut]";
60
61/// How serious an entry is, as journald's `PRIORITY` (syslog levels).
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum Priority {
64 /// Something the service needed failed; `journalctl --priority=err` shows it.
65 Error,
66 /// Something did not go as intended and the service went on.
67 Warning,
68 /// An event of the service's ordinary life.
69 Info,
70}
71
72impl Priority {
73 /// The syslog number journald reads.
74 pub fn number(self) -> u8 {
75 return match self {
76 Priority::Error => 3,
77 Priority::Warning => 4,
78 Priority::Info => 6,
79 };
80 }
81}
82
83/// What:
84/// The text cut to `limit` characters, with the mark appended when it was cut; control characters and newlines are kept.
85/// Why:
86/// The native protocol carries any bytes (a value with a newline in the length-prefixed form), so a value is cut and not
87/// changed; only the line on standard error needs `one_line`.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// text.length > limit ? text.slice(0, limit) + ' [cut]' : text
92/// ```
93pub fn cut(text: &str, limit: usize) -> String {
94 return match text.char_indices().nth(limit) {
95 Some((end, _)) => format!("{}{CUT_MARK}", &text[..end]),
96 None => text.to_string(),
97 };
98}
99
100/// What:
101/// The text with every control character (a newline included) replaced by a space, and cut to `limit` characters.
102/// Why:
103/// A line on standard error is one line whatever a client or an error message held: a newline in a message could
104/// otherwise start a second line in the stream.
105///
106/// In TS you'd write (pseudocode):
107/// ```ts
108/// text.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, limit)
109/// ```
110pub fn one_line(text: &str, limit: usize) -> String {
111 let mut single: String = String::with_capacity(text.len().min(limit + CUT_MARK.len()));
112 for (count, character) in text.chars().enumerate() {
113 if count >= limit {
114 single.push_str(CUT_MARK);
115 break;
116 }
117 single.push(if character.is_control() { ' ' } else { character });
118 }
119 return single;
120}
121
122/// The longest field name journald takes, in characters.
123pub const FIELD_NAME_LIMIT: usize = 64;
124
125/// What:
126/// Whether a field name is one journald accepts from a client: 1 to 64 upper-case letters, digits and underscores, not
127/// starting with an underscore (the journal's own fields) or a digit.
128/// Why:
129/// This is systemd's `journal_field_valid` called with `allow_protected` false (`src/libsystemd/sd-journal/journal-file.c`,
130/// read from the `main` branch; its comments say names longer than 64 are refused and a digit may not come first), which
131/// `manager_process_entry` in `src/journal/journald-native.c` applies to both forms of a field. A refused name is dropped by
132/// journald without a word, so the entry is built without it.
133///
134/// In TS you'd write (pseudocode):
135/// ```ts
136/// /^[A-Z][A-Z0-9_]{0,63}$/.test(name)
137/// ```
138pub fn valid_field_name(name: &str) -> bool {
139 let bytes: &[u8] = name.as_bytes();
140 return bytes.len() <= FIELD_NAME_LIMIT
141 && bytes.first().is_some_and(|first| { return first.is_ascii_uppercase(); })
142 && bytes.iter().all(|byte| { return byte.is_ascii_uppercase() || byte.is_ascii_digit() || *byte == b'_'; });
143}
144
145/// Appends one field: `NAME=value\n` when the value has no newline, otherwise the length-prefixed form, `NAME\n`, the byte
146/// length as eight little-endian bytes, the value and `\n`.
147fn push_field(entry: &mut Vec<u8>, name: &str, value: &str) {
148 if value.contains('\n') {
149 entry.extend_from_slice(name.as_bytes());
150 entry.push(b'\n');
151 entry.extend_from_slice(&u64::try_from(value.len()).unwrap_or(u64::MAX).to_le_bytes());
152 entry.extend_from_slice(value.as_bytes());
153 } else {
154 entry.extend_from_slice(name.as_bytes());
155 entry.push(b'=');
156 entry.extend_from_slice(value.as_bytes());
157 }
158 entry.push(b'\n');
159}
160
161/// What:
162/// The bytes of one native entry of the service: see `datagram_as`.
163/// Why:
164/// Every entry of the service carries the service's identifier.
165///
166/// In TS you'd write (pseudocode):
167/// ```ts
168/// datagramAs('cli-git-service', priority, message, fields)
169/// ```
170pub fn datagram(priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
171 return datagram_as(IDENTIFIER, priority, message, fields);
172}
173
174/// What:
175/// The bytes of one native entry: `PRIORITY`, `SYSLOG_IDENTIFIER`, `MESSAGE`, then each field, each value as written (cut
176/// to its limit), in the length-prefixed form when it holds a newline.
177/// Why:
178/// A newline cannot be written in the simple `NAME=value` form without starting a field, and the length-prefixed form
179/// carries any bytes, so a value reads back in the journal as it was sent. A field whose name journald would refuse is left
180/// out. The identifier names the sender: the service, or the wrapper a person ran.
181///
182/// In TS you'd write (pseudocode):
183/// ```ts
184/// `PRIORITY=${n}\nSYSLOG_IDENTIFIER=${identifier}\nMESSAGE=${message}\n${fields.map(([k, v]) => `${k}=${v}\n`).join('')}`
185/// ```
186pub fn datagram_as(identifier: &str, priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
187 let mut entry: Vec<u8> = Vec::new();
188 push_field(&mut entry, "PRIORITY", priority.number().to_string().as_str());
189 push_field(&mut entry, "SYSLOG_IDENTIFIER", identifier);
190 push_field(&mut entry, "MESSAGE", cut(message, MESSAGE_LIMIT).as_str());
191 for (name, value) in fields {
192 if valid_field_name(name) {
193 push_field(&mut entry, name, cut(value, FIELD_LIMIT).as_str());
194 }
195 }
196 return entry;
197}
198
199/// Whether `$JOURNAL_STREAM` names the file with this device and inode, which is how `systemd.exec(5)` says to tell that
200/// standard error is still the journal: the variable alone is not enough, because a redirection leaves it set.
201pub fn stream_matches(variable: Option<&str>, device: u64, inode: u64) -> bool {
202 let Some((named_device, named_inode)) = variable.and_then(|text| { return text.split_once(':'); }) else {
203 return false;
204 };
205 return named_device.parse::<u64>().ok() == Some(device) && named_inode.parse::<u64>().ok() == Some(inode);
206}
207
208/// The device and inode of standard error, or nothing when it cannot be read.
209pub fn stderr_identity() -> Option<(u64, u64)> {
210 let mut status: libc::stat = unsafe { std::mem::zeroed() };
211 // SAFETY: `status` is valid for the call to write one `stat`.
212 let result: libc::c_int = unsafe { libc::fstat(libc::STDERR_FILENO, &raw mut status) };
213 if result != 0 {
214 return None;
215 }
216 return Some((u64::from(status.st_dev), u64::from(status.st_ino)));
217}
218
219/// What:
220/// The address as the C structure `sendto` takes, and its length, or nothing when the name does not fit.
221/// Why:
222/// A signal handler may not build it (the standard library allocates), so it is made once, ahead of the signal.
223///
224/// In TS you'd write (pseudocode):
225/// ```ts
226/// rawAddress(address) // { family: AF_UNIX, path: '\0name' }
227/// ```
228pub fn raw_address(address: &SocketAddr) -> Option<(libc::sockaddr_un, libc::socklen_t)> {
229 use std::os::unix::ffi::OsStrExt;
230 // SAFETY: `sockaddr_un` is a plain C structure for which all zero bytes is a valid value.
231 let mut raw: libc::sockaddr_un = unsafe { std::mem::zeroed() };
232 raw.sun_family = libc::sa_family_t::try_from(libc::AF_UNIX).ok()?;
233 let (offset, name): (usize, &[u8]) = match (address.as_pathname(), address.as_abstract_name()) {
234 (Some(path), _) => (0, path.as_os_str().as_bytes()),
235 (None, Some(name)) => (1, name),
236 (None, None) => return None,
237 };
238 // A path needs room for the zero byte that ends it; an abstract name starts with one, so it may fill the whole field.
239 if offset + name.len() + usize::from(offset == 0) > raw.sun_path.len() {
240 return None;
241 }
242 for (slot, byte) in raw.sun_path[offset..].iter_mut().zip(name) {
243 *slot = libc::c_char::from_ne_bytes([*byte]);
244 }
245 let length: usize = std::mem::size_of::<libc::sa_family_t>() + offset + name.len() + usize::from(offset == 0);
246 return Some((raw, libc::socklen_t::try_from(length).ok()?));
247}
248
249/// Where an entry went.
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub enum Delivery {
252 /// As a datagram on the native socket.
253 Native,
254 /// As a line on standard error (all of it, or the start of it with the rest kept to finish before the next line).
255 Stderr,
256 /// Nowhere: the journal's socket refused it and standard error had no room, so the entry was dropped and counted.
257 Dropped,
258}
259
260/// Where the journal listens.
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub enum Target {
263 /// A socket file.
264 Path(String),
265 /// An abstract name, which a test uses.
266 Abstract(String),
267}
268
269impl Target {
270 /// The journal's own socket.
271 pub fn journal() -> Self {
272 return Self::Path(String::from(JOURNAL_SOCKET));
273 }
274
275 /// Reads a test-only setting: `@name` is abstract, anything else a path.
276 pub fn parse(text: &str) -> Self {
277 return text.strip_prefix('@').map_or_else(|| { return Self::Path(text.to_string()); }, |name| { return Self::Abstract(name.to_string()); });
278 }
279
280 /// The socket address, or nothing when the name does not fit one.
281 pub fn address(&self) -> Option<SocketAddr> {
282 return match self {
283 Self::Path(path) => SocketAddr::from_pathname(path).ok(),
284 Self::Abstract(name) => SocketAddr::from_abstract_name(name.as_bytes()).ok(),
285 };
286 }
287}
288
289/// Where the service's entries go, decided once.
290#[derive(Debug)]
291pub struct Sink {
292 /// The unconnected, non-blocking socket, when standard error is the journal and a socket could be made.
293 native: Option<UnixDatagram>,
294 /// The journal's socket.
295 address: Option<SocketAddr>,
296 /// Whether standard error is the journal's stream, so that a line there may carry a `<priority>` prefix.
297 stream: bool,
298 /// The rest of a line the stream took only in part, to be written before any other line.
299 ///
300 /// Lock kept: every thread of the service logs, the journal's stream is a byte stream that may take a line in parts, and
301 /// two lines must never run together, so the unfinished rest of a line and the write that finishes it are one critical
302 /// section. The section is one non-blocking write of what fits (the stream is never waited for), so a thread that finds
303 /// the lock held waits for one system call and never for the journal. A design without the lock needs a thread and a
304 /// queue for every entry, or a rule that drops an entry whenever another thread is mid-line; both give up the guarantee
305 /// that an entry is lost only when the stream is full. The lock covers one sink's pending bytes, the smallest unit there is.
306 pending: Mutex<Vec<u8>>,
307 /// How many entries were dropped since one last said so.
308 lost: AtomicU64,
309}
310
311impl Sink {
312 /// What:
313 /// The sink for a process whose `$JOURNAL_STREAM` is `variable`, whose standard error is the file `stderr`, and
314 /// whose journal socket is at `socket`.
315 /// Why:
316 /// The native socket is used only when standard error is the journal; a terminal, a file or a pipe keeps its plain
317 /// lines, and a service run by hand reads as before.
318 ///
319 /// In TS you'd write (pseudocode):
320 /// ```ts
321 /// Sink.choose(process.env.JOURNAL_STREAM, stderrIdentity(), '/run/systemd/journal/socket')
322 /// ```
323 pub fn choose(variable: Option<&str>, stderr: Option<(u64, u64)>, socket: &Target) -> Self {
324 let stream: bool = stderr.is_some_and(|(device, inode)| { return stream_matches(variable, device, inode); });
325 let native: Option<UnixDatagram> = if stream { Self::open() } else { None };
326 return Self { native, address: socket.address(), stream, pending: Mutex::new(Vec::new()), lost: AtomicU64::new(0) };
327 }
328
329 /// An unconnected datagram socket that never blocks, or nothing.
330 fn open() -> Option<UnixDatagram> {
331 let socket: UnixDatagram = UnixDatagram::unbound().ok()?;
332 socket.set_nonblocking(true).ok()?;
333 return Some(socket);
334 }
335
336 /// The sink of this process: the environment's `$JOURNAL_STREAM`, the real standard error, and the real socket or the
337 /// test-only one.
338 pub fn from_environment() -> Self {
339 let variable: Option<String> = std::env::var(STREAM_VARIABLE).ok();
340 let socket: Target = std::env::var(TEST_SOCKET_VARIABLE).map_or_else(|_| { return Target::journal(); }, |text| { return Target::parse(text.as_str()); });
341 return Self::choose(variable.as_deref(), stderr_identity(), &socket);
342 }
343
344 /// Whether standard error is the journal's stream, so that a write there may carry a `<priority>` prefix and must not wait.
345 pub fn on_stream(&self) -> bool {
346 return self.stream;
347 }
348
349 /// Whether this sink sends datagrams.
350 pub fn is_native(&self) -> bool {
351 return self.native.is_some();
352 }
353
354 /// The raw descriptor and socket address a signal handler sends its entry with, when this sink is native.
355 pub fn signal_target(&self) -> Option<(std::os::fd::RawFd, &SocketAddr)> {
356 use std::os::fd::AsRawFd;
357 let socket: &UnixDatagram = self.native.as_ref()?;
358 return self.address.as_ref().map(|address| { return (socket.as_raw_fd(), address); });
359 }
360
361 /// What:
362 /// Sends one entry, and gives where it went: a datagram when the sink is native and the socket takes it, otherwise
363 /// `line` on `stderr`, otherwise nowhere.
364 /// Why:
365 /// A journal that is restarting, absent or full is no reason for the service to wait or to lose the entry, and standard
366 /// error that has no room is no reason to wait either: that entry is dropped and counted, and the next one that gets
367 /// through says how many were. On the journal's stream a `<priority>` prefix keeps the level, and a line taken in part is
368 /// finished before another starts, so two lines never run together.
369 ///
370 /// In TS you'd write (pseudocode):
371 /// ```ts
372 /// try { socket.send(datagram) } catch { stderr.write(prefix + line) ?? count(dropped) }
373 /// ```
374 pub fn deliver(&self, priority: Priority, message: &str, fields: &[(&str, String)], line: &str, stderr: &mut dyn Write) -> Delivery {
375 let lost: u64 = self.lost.load(Ordering::Relaxed);
376 if let Some(socket) = self.native.as_ref()
377 && let Some(address) = self.address.as_ref()
378 {
379 let mut all: Vec<(&str, String)> = fields.to_vec();
380 if lost > 0 {
381 all.push((LOST_FIELD, lost.to_string()));
382 }
383 if socket.send_to_addr(datagram(priority, message, all.as_slice()).as_slice(), address).is_ok() {
384 self.forgive(lost);
385 return Delivery::Native;
386 }
387 }
388 return self.fall_back(priority, line, stderr);
389 }
390
391 /// Takes `count` off the dropped-entry count without going below zero.
392 ///
393 /// Two threads can each have said the same count (one on the native path, one on the stream), so the second
394 /// subtraction must find less than it expects and stop at zero instead of wrapping.
395 fn forgive(&self, count: u64) {
396 let mut current: u64 = self.lost.load(Ordering::Relaxed);
397 while let Err(seen) = self.lost.compare_exchange_weak(current, current.saturating_sub(count), Ordering::Relaxed, Ordering::Relaxed) {
398 current = seen;
399 }
400 }
401
402 /// What:
403 /// Writes the line to `stderr` without waiting: finishes what an earlier line left unwritten first, says how many
404 /// entries were dropped before this one, and keeps the rest of a line the stream takes only in part.
405 /// Why:
406 /// Only the journal's stream needs this care; any other standard error gets its plain line as it always did. Entries
407 /// that find no room are counted (`lost`) and reported with the next that gets through, because a dropped entry nobody
408 /// hears of is a gap nobody can explain.
409 ///
410 /// In TS you'd write (pseudocode):
411 /// ```ts
412 /// if (pending.length) flush(pending); if (!pending.length) write(note + line) else lost++
413 /// ```
414 fn fall_back(&self, priority: Priority, line: &str, stderr: &mut dyn Write) -> Delivery {
415 if !self.stream {
416 // The result is dropped on purpose: a closed standard error is no reason to fail the work the entry describes.
417 let _written = stderr.write_all(line.as_bytes());
418 return Delivery::Stderr;
419 }
420 let mut pending = self.pending.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
421 if !pending.is_empty() {
422 let finished: usize = write_what_fits(stderr, pending.as_slice());
423 pending.drain(..finished);
424 if !pending.is_empty() {
425 self.lost.fetch_add(1, Ordering::Relaxed);
426 return Delivery::Dropped;
427 }
428 }
429 let lost: u64 = self.lost.load(Ordering::Relaxed);
430 let mut bytes: Vec<u8> = Vec::new();
431 if lost > 0 {
432 bytes.extend_from_slice(format!("<{}>{LOST_NOTICE_HEAD}{lost}{LOST_NOTICE_TAIL}\n", Priority::Warning.number()).as_bytes());
433 }
434 bytes.extend_from_slice(format!("<{}>{line}", priority.number()).as_bytes());
435 let written: usize = write_what_fits(stderr, bytes.as_slice());
436 if written == 0 {
437 self.lost.fetch_add(1, Ordering::Relaxed);
438 return Delivery::Dropped;
439 }
440 // The note, if any, is now in the stream or in `pending` ahead of the rest of the line, so it is not owed again.
441 self.forgive(lost);
442 pending.extend_from_slice(&bytes[written..]);
443 return Delivery::Stderr;
444 }
445}
446
447/// The field a native entry carries the count of earlier dropped entries in.
448pub const LOST_FIELD: &str = "CLI_GIT_LOG_DROPPED";
449
450/// What a line on standard error says before the count of entries dropped before it.
451const LOST_NOTICE_HEAD: &str = "cli-git-service: ";
452
453/// What it says after the count.
454const LOST_NOTICE_TAIL: &str = " log entries were dropped before this one because the log stream had no room";
455
456/// Writes as much of `bytes` as `stream` takes without waiting, and gives how much it took.
457fn write_what_fits(stream: &mut dyn Write, bytes: &[u8]) -> usize {
458 let mut done: usize = 0;
459 while done < bytes.len() {
460 match stream.write(&bytes[done..]) {
461 Ok(0) => break,
462 Ok(count) => done += count,
463 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
464 Err(_) => break,
465 }
466 }
467 return done;
468}
469
470/// What:
471/// A writer to the journal's stream (standard error as the service manager connected it) that never waits for room.
472/// Why:
473/// `O_NONBLOCK` on the datagram socket does not make standard error non-blocking, and a journal that stops reading would
474/// fill the stream and block the service, and so the commits that wait for it. Each write is `MSG_DONTWAIT` on a socket, or,
475/// on a pipe or file a test or a redirection put there, a write made only after a poll shows room and of no more than a
476/// pipe takes whole; a write that finds no room is refused with `WouldBlock`, and one that takes part of the bytes says how
477/// many, so the caller keeps the rest (`Sink::deliver` does) instead of the writer waiting for room.
478///
479/// In TS you'd write (pseudocode):
480/// ```ts
481/// try { socket.sendNonBlocking(line) } catch (error) { if (error.code === 'EAGAIN') report(error) }
482/// ```
483#[derive(Clone, Copy, Debug)]
484pub struct StreamWriter {
485 /// The descriptor of the stream.
486 descriptor: std::os::fd::RawFd,
487}
488
489/// The most a pipe takes in one write once a poll shows room: a page, which a pipe holding a free slot takes whole.
490pub const PIPE_CHUNK: usize = 4096;
491
492/// What:
493/// Sends bytes to a descriptor without ever waiting for room, and gives the count taken or the error number.
494/// Why:
495/// One rule for every write the service makes to standard error, including the signal handler's, which may call only
496/// functions that `signal-safety(7)` lists (read from <https://man7.org/linux/man-pages/man7/signal-safety.7.html>): `send`,
497/// `poll` and `write` are, and this uses nothing else, allocates nothing and takes no lock. A socket is written with
498/// `MSG_DONTWAIT`. A pipe or file a test or a redirection put there is written only after a poll shows room, and in a piece
499/// of at most a page, which a pipe holding a free slot takes whole; so the write cannot block. No room is `EAGAIN`.
500///
501/// In TS you'd write (pseudocode):
502/// ```ts
503/// if (!(await pollWritable(fd, 0))) throw errno('EAGAIN'); return write(fd, bytes.slice(0, 4096));
504/// ```
505///
506/// # Errors
507/// The error number of the failed call, `EAGAIN` when there is no room.
508pub fn send_without_waiting(descriptor: std::os::fd::RawFd, bytes: &[u8]) -> Result<usize, libc::c_int> {
509 // SAFETY: `bytes` is valid for its length for the call, and `send` is safe in a signal handler.
510 let sent: isize = unsafe { libc::send(descriptor, bytes.as_ptr().cast(), bytes.len(), libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL) };
511 if let Ok(count) = usize::try_from(sent) {
512 return Ok(count);
513 }
514 // SAFETY: the location is the calling thread's own error number, valid for a read.
515 let failure: libc::c_int = unsafe { *libc::__errno_location() };
516 if failure != libc::ENOTSOCK {
517 return Err(failure);
518 }
519 // Not a socket (a file or a pipe a test or a redirection put there): an ordinary write, made only when the descriptor
520 // shows room and small enough for a pipe to take whole, so it cannot block.
521 let mut request: libc::pollfd = libc::pollfd { fd: descriptor, events: libc::POLLOUT, revents: 0 };
522 // SAFETY: `request` is valid for the call, which reads and writes one `pollfd`, and `poll` is safe in a signal handler.
523 let ready: libc::c_int = unsafe { libc::poll(&raw mut request, 1, 0) };
524 if ready <= 0 || request.revents & libc::POLLOUT == 0 {
525 return Err(libc::EAGAIN);
526 }
527 let chunk: &[u8] = &bytes[..bytes.len().min(PIPE_CHUNK)];
528 // SAFETY: `chunk` is valid for its length for the call, and `write` is safe in a signal handler.
529 let written: isize = unsafe { libc::write(descriptor, chunk.as_ptr().cast(), chunk.len()) };
530 return usize::try_from(written).map_err(|_| {
531 // SAFETY: the location is the calling thread's own error number, valid for a read.
532 return unsafe { *libc::__errno_location() };
533 });
534}
535
536impl StreamWriter {
537 /// A writer to standard error.
538 pub fn stderr() -> Self {
539 return Self { descriptor: libc::STDERR_FILENO };
540 }
541
542 /// A writer to a stream socket descriptor.
543 pub fn on(descriptor: std::os::fd::RawFd) -> Self {
544 return Self { descriptor };
545 }
546}
547
548impl Write for StreamWriter {
549 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
550 return send_without_waiting(self.descriptor, bytes).map_err(std::io::Error::from_raw_os_error);
551 }
552
553 fn flush(&mut self) -> std::io::Result<()> {
554 return Ok(());
555 }
556}
557
558/// The datagram, the detection, the fallback.
559#[cfg(test)]
560#[path = "journald_native_tests.rs"]
561mod tests;