use std::io::Write;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixDatagram};
pub const JOURNAL_SOCKET: &str = "/run/systemd/journal/socket";
pub const TEST_SOCKET_VARIABLE: &str = "CLI_GIT_TEST_ONLY_JOURNAL_SOCKET";
pub const STREAM_VARIABLE: &str = "JOURNAL_STREAM";
pub const IDENTIFIER: &str = "cli-git-service";
pub const CLIENT_IDENTIFIER: &str = "cli-git";
pub const MESSAGE_LIMIT: usize = 16 * 1024;
pub const FIELD_LIMIT: usize = 1024;
const CUT_MARK: &str = " [cut]";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Priority {
Error,
Warning,
Info,
}
impl Priority {
pub fn number(self) -> u8 {
return match self {
Priority::Error => 3,
Priority::Warning => 4,
Priority::Info => 6,
};
}
}
pub fn cut(text: &str, limit: usize) -> String {
return match text.char_indices().nth(limit) {
Some((end, _)) => format!("{}{CUT_MARK}", &text[..end]),
None => text.to_string(),
};
}
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;
}
pub const FIELD_NAME_LIMIT: usize = 64;
pub fn valid_field_name(name: &str) -> bool {
let bytes: &[u8] = name.as_bytes();
return bytes.len() <= FIELD_NAME_LIMIT
&& bytes.first().is_some_and(|first| { return first.is_ascii_uppercase(); })
&& bytes.iter().all(|byte| { return byte.is_ascii_uppercase() || byte.is_ascii_digit() || *byte == b'_'; });
}
fn push_field(entry: &mut Vec<u8>, name: &str, value: &str) {
if value.contains('\n') {
entry.extend_from_slice(name.as_bytes());
entry.push(b'\n');
entry.extend_from_slice(&u64::try_from(value.len()).unwrap_or(u64::MAX).to_le_bytes());
entry.extend_from_slice(value.as_bytes());
} else {
entry.extend_from_slice(name.as_bytes());
entry.push(b'=');
entry.extend_from_slice(value.as_bytes());
}
entry.push(b'\n');
}
pub fn datagram(priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
return datagram_as(IDENTIFIER, priority, message, fields);
}
pub fn datagram_as(identifier: &str, priority: Priority, message: &str, fields: &[(&str, String)]) -> Vec<u8> {
let mut entry: Vec<u8> = Vec::new();
push_field(&mut entry, "PRIORITY", priority.number().to_string().as_str());
push_field(&mut entry, "SYSLOG_IDENTIFIER", identifier);
push_field(&mut entry, "MESSAGE", cut(message, MESSAGE_LIMIT).as_str());
for (name, value) in fields {
if valid_field_name(name) {
push_field(&mut entry, name, cut(value, FIELD_LIMIT).as_str());
}
}
return entry;
}
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);
}
pub fn stderr_identity() -> Option<(u64, u64)> {
let mut status: libc::stat = unsafe { std::mem::zeroed() };
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)));
}
pub fn raw_address(address: &SocketAddr) -> Option<(libc::sockaddr_un, libc::socklen_t)> {
use std::os::unix::ffi::OsStrExt;
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,
};
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()?));
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Delivery {
Native,
Stderr,
Dropped,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Target {
Path(String),
Abstract(String),
}
impl Target {
pub fn journal() -> Self {
return Self::Path(String::from(JOURNAL_SOCKET));
}
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()); });
}
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(),
};
}
}
#[derive(Debug)]
pub struct Sink {
native: Option<UnixDatagram>,
address: Option<SocketAddr>,
stream: bool,
pending: Mutex<Vec<u8>>,
lost: AtomicU64,
}
impl Sink {
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, pending: Mutex::new(Vec::new()), lost: AtomicU64::new(0) };
}
fn open() -> Option<UnixDatagram> {
let socket: UnixDatagram = UnixDatagram::unbound().ok()?;
socket.set_nonblocking(true).ok()?;
return Some(socket);
}
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);
}
pub fn on_stream(&self) -> bool {
return self.stream;
}
pub fn is_native(&self) -> bool {
return self.native.is_some();
}
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); });
}
pub fn deliver(&self, priority: Priority, message: &str, fields: &[(&str, String)], line: &str, stderr: &mut dyn Write) -> Delivery {
let lost: u64 = self.lost.load(Ordering::Relaxed);
if let Some(socket) = self.native.as_ref()
&& let Some(address) = self.address.as_ref()
{
let mut all: Vec<(&str, String)> = fields.to_vec();
if lost > 0 {
all.push((LOST_FIELD, lost.to_string()));
}
if socket.send_to_addr(datagram(priority, message, all.as_slice()).as_slice(), address).is_ok() {
self.forgive(lost);
return Delivery::Native;
}
}
return self.fall_back(priority, line, stderr);
}
fn forgive(&self, count: u64) {
let mut current: u64 = self.lost.load(Ordering::Relaxed);
while let Err(seen) = self.lost.compare_exchange_weak(current, current.saturating_sub(count), Ordering::Relaxed, Ordering::Relaxed) {
current = seen;
}
}
fn fall_back(&self, priority: Priority, line: &str, stderr: &mut dyn Write) -> Delivery {
if !self.stream {
let _written = stderr.write_all(line.as_bytes());
return Delivery::Stderr;
}
let mut pending = self.pending.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if !pending.is_empty() {
let finished: usize = write_what_fits(stderr, pending.as_slice());
pending.drain(..finished);
if !pending.is_empty() {
self.lost.fetch_add(1, Ordering::Relaxed);
return Delivery::Dropped;
}
}
let lost: u64 = self.lost.load(Ordering::Relaxed);
let mut bytes: Vec<u8> = Vec::new();
if lost > 0 {
bytes.extend_from_slice(format!("<{}>{LOST_NOTICE_HEAD}{lost}{LOST_NOTICE_TAIL}\n", Priority::Warning.number()).as_bytes());
}
bytes.extend_from_slice(format!("<{}>{line}", priority.number()).as_bytes());
let written: usize = write_what_fits(stderr, bytes.as_slice());
if written == 0 {
self.lost.fetch_add(1, Ordering::Relaxed);
return Delivery::Dropped;
}
self.forgive(lost);
pending.extend_from_slice(&bytes[written..]);
return Delivery::Stderr;
}
}
pub const LOST_FIELD: &str = "CLI_GIT_LOG_DROPPED";
const LOST_NOTICE_HEAD: &str = "cli-git-service: ";
const LOST_NOTICE_TAIL: &str = " log entries were dropped before this one because the log stream had no room";
fn write_what_fits(stream: &mut dyn Write, bytes: &[u8]) -> usize {
let mut done: usize = 0;
while done < bytes.len() {
match stream.write(&bytes[done..]) {
Ok(0) => break,
Ok(count) => done += count,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => break,
}
}
return done;
}
#[derive(Clone, Copy, Debug)]
pub struct StreamWriter {
descriptor: std::os::fd::RawFd,
}
pub const PIPE_CHUNK: usize = 4096;
pub fn send_without_waiting(descriptor: std::os::fd::RawFd, bytes: &[u8]) -> Result<usize, libc::c_int> {
let sent: isize = unsafe { libc::send(descriptor, bytes.as_ptr().cast(), bytes.len(), libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL) };
if let Ok(count) = usize::try_from(sent) {
return Ok(count);
}
let failure: libc::c_int = unsafe { *libc::__errno_location() };
if failure != libc::ENOTSOCK {
return Err(failure);
}
let mut request: libc::pollfd = libc::pollfd { fd: descriptor, events: libc::POLLOUT, revents: 0 };
let ready: libc::c_int = unsafe { libc::poll(&raw mut request, 1, 0) };
if ready <= 0 || request.revents & libc::POLLOUT == 0 {
return Err(libc::EAGAIN);
}
let chunk: &[u8] = &bytes[..bytes.len().min(PIPE_CHUNK)];
let written: isize = unsafe { libc::write(descriptor, chunk.as_ptr().cast(), chunk.len()) };
return usize::try_from(written).map_err(|_| {
return unsafe { *libc::__errno_location() };
});
}
impl StreamWriter {
pub fn stderr() -> Self {
return Self { descriptor: libc::STDERR_FILENO };
}
pub fn on(descriptor: std::os::fd::RawFd) -> Self {
return Self { descriptor };
}
}
impl Write for StreamWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
return send_without_waiting(self.descriptor, bytes).map_err(std::io::Error::from_raw_os_error);
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
#[cfg(test)]
#[path = "journald_native_tests.rs"]
mod tests;