use crate::errors::SdError;
use libc::{dev_t, ino_t};
use nix::fcntl::*;
use nix::sys::memfd::memfd_create;
use nix::sys::memfd::MemFdCreateFlag;
use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags, SockAddr};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::ffi::{CString, OsStr};
use std::fs::File;
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::FromRawFd;
use std::os::unix::net::UnixDatagram;
use std::str::FromStr;
pub static SD_JOURNAL_SOCK_PATH: &str = "/run/systemd/journal/socket";
static SD_SOCK: OnceCell<UnixDatagram> = OnceCell::new();
#[derive(Clone, Copy, Debug)]
#[repr(u8)]
pub enum Priority {
Emergency = 0,
Alert,
Critical,
Error,
Warning,
Notice,
Info,
Debug,
}
impl std::convert::From<Priority> for u8 {
fn from(p: Priority) -> Self {
match p {
Priority::Emergency => 0,
Priority::Alert => 1,
Priority::Critical => 2,
Priority::Error => 3,
Priority::Warning => 4,
Priority::Notice => 5,
Priority::Info => 6,
Priority::Debug => 7,
}
}
}
#[inline(always)]
fn is_valid_char(c: char) -> bool {
c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_'
}
fn is_valid_field(input: &str) -> bool {
if input.is_empty() || 64 < input.len() {
return false;
}
if input.starts_with('_') {
return false;
}
if input.starts_with(|c: char| c.is_ascii_digit()) {
return false;
}
input.chars().all(is_valid_char)
}
fn add_field_and_payload_explicit_length(data: &mut Vec<u8>, field: &str, payload: &str) {
data.extend(field.as_bytes());
data.push(b'\n');
data.extend(&(payload.len() as u64).to_le_bytes());
data.extend(payload.as_bytes());
data.push(b'\n');
}
fn add_field_and_payload(data: &mut Vec<u8>, field: &str, payload: &str) {
if is_valid_field(field) {
if payload.contains('\n') {
add_field_and_payload_explicit_length(data, field, payload);
} else {
data.extend(field.as_bytes());
data.push(b'=');
data.extend(payload.as_bytes());
data.push(b'\n');
}
}
}
pub fn journal_send<K, V>(
priority: Priority,
msg: &str,
vars: impl Iterator<Item = (K, V)>,
) -> Result<(), SdError>
where
K: AsRef<str>,
V: AsRef<str>,
{
let sock = SD_SOCK.get_or_try_init(|| {
UnixDatagram::unbound().map_err(|e| format!("failed to open datagram socket: {}", e))
})?;
let mut data = Vec::new();
add_field_and_payload(&mut data, "PRIORITY", &(u8::from(priority)).to_string());
add_field_and_payload(&mut data, "MESSAGE", msg);
for (ref k, ref v) in vars {
if k.as_ref() != "PRIORITY" && k.as_ref() != "MESSAGE" {
add_field_and_payload(&mut data, k.as_ref(), v.as_ref())
}
}
let fast_res = sock.send_to(&data, SD_JOURNAL_SOCK_PATH);
let res = match fast_res {
Err(ref err) if err.raw_os_error() == Some(90) => send_memfd_payload(sock, &data),
r => r.map_err(|err| err.to_string().into()),
};
res.map_err(|e| {
format!(
"failed to print to journal at '{}': {}",
SD_JOURNAL_SOCK_PATH, e
)
})?;
Ok(())
}
pub fn journal_print(priority: Priority, msg: &str) -> Result<(), SdError> {
let map: HashMap<&str, &str> = HashMap::new();
journal_send(priority, msg, map.iter())
}
fn send_memfd_payload(sock: &UnixDatagram, data: &[u8]) -> Result<usize, SdError> {
let memfd = {
let fdname = &CString::new("libsystemd-rs-logging").map_err(|e| e.to_string())?;
let tmpfd =
memfd_create(fdname, MemFdCreateFlag::MFD_ALLOW_SEALING).map_err(|e| e.to_string())?;
let mut file = unsafe { File::from_raw_fd(tmpfd) };
file.write_all(data).map_err(|e| e.to_string())?;
file
};
fcntl(memfd.as_raw_fd(), FcntlArg::F_ADD_SEALS(SealFlag::all())).map_err(|e| e.to_string())?;
let fds = &[memfd.as_raw_fd()];
let ancillary = [ControlMessage::ScmRights(fds)];
let path = SockAddr::new_unix(SD_JOURNAL_SOCK_PATH).map_err(|e| e.to_string())?;
sendmsg(
sock.as_raw_fd(),
&[],
&ancillary,
MsgFlags::empty(),
Some(&path),
)
.map_err(|e| e.to_string())?;
drop(memfd);
Ok(data.len())
}
#[derive(Debug, PartialEq)]
pub struct JournalStream {
device: dev_t,
inode: ino_t,
}
impl JournalStream {
pub fn parse<S: AsRef<OsStr>>(value: S) -> Result<Self, SdError> {
let s = value.as_ref().to_str().ok_or_else(|| {
format!(
"Failed to parse journal stream: Value {:?} not UTF-8 encoded",
value.as_ref()
)
})?;
let (device_s, inode_s) = s.find(':').map(|i| (&s[..i], &s[i + 1..])).ok_or_else(|| {
format!(
"Failed to parse journal stream: Missing separator ':' in value '{}'",
s
)
})?;
let device = dev_t::from_str(device_s).map_err(|err| {
format!(
"Failed to parse journal stream: Device part is not a number '{}': {}",
device_s, err
)
})?;
let inode = ino_t::from_str(inode_s).map_err(|err| {
format!(
"Failed to parse journal stream: Inode part is not a number '{}': {}",
inode_s, err
)
})?;
Ok(JournalStream { device, inode })
}
pub fn from_env<S: AsRef<OsStr>>(key: S) -> Result<Self, SdError> {
Self::parse(std::env::var_os(&key).ok_or_else(|| {
format!(
"Failed to parse journal stream: Environment variable {:?} unset",
key.as_ref()
)
})?)
}
pub fn from_env_default() -> Result<Self, SdError> {
Self::from_env("JOURNAL_STREAM")
}
pub fn from_fd<F: AsRawFd>(fd: F) -> std::io::Result<Self> {
nix::sys::stat::fstat(fd.as_raw_fd())
.map(|stat| JournalStream {
device: stat.st_dev,
inode: stat.st_ino,
})
.map_err(std::io::Error::from)
}
}
pub fn connected_to_journal() -> bool {
let stream = JournalStream::from_env_default().ok();
stream == JournalStream::from_fd(std::io::stderr()).ok()
|| stream == JournalStream::from_fd(std::io::stdout()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_journald_socket() -> bool {
match std::fs::metadata(SD_JOURNAL_SOCK_PATH) {
Ok(_) => true,
Err(_) => {
eprintln!(
"skipped, journald socket not found at '{}'",
SD_JOURNAL_SOCK_PATH
);
false
}
}
}
#[test]
fn test_journal_print_simple() {
if !ensure_journald_socket() {
return;
}
journal_print(Priority::Info, "TEST LOG!").unwrap();
}
#[test]
fn test_journal_print_large_buffer() {
if !ensure_journald_socket() {
return;
}
let data = "A".repeat(212995);
journal_print(Priority::Debug, &data).unwrap();
}
#[test]
fn test_journal_send_simple() {
if !ensure_journald_socket() {
return;
}
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("TEST_JOURNALD_LOG1", "foo");
map.insert("TEST_JOURNALD_LOG2", "bar");
journal_send(Priority::Info, "Test Journald Log", map.iter()).unwrap()
}
#[test]
fn test_journal_skip_fields() {
if !ensure_journald_socket() {
return;
}
let mut map: HashMap<&str, &str> = HashMap::new();
let priority = format!("{}", u8::from(Priority::Warning));
map.insert("TEST_JOURNALD_LOG3", "result");
map.insert("PRIORITY", &priority);
map.insert("MESSAGE", "Duplicate value");
journal_send(Priority::Info, "Test Skip Fields", map.iter()).unwrap()
}
#[test]
fn test_is_valid_field_lowercase_invalid() {
let field = "test";
assert_eq!(is_valid_field(&field), false);
}
#[test]
fn test_is_valid_field_uppercase_non_ascii_invalid() {
let field = "TRÖT";
assert_eq!(is_valid_field(&field), false);
}
#[test]
fn test_is_valid_field_uppercase_valid() {
let field = "TEST";
assert_eq!(is_valid_field(&field), true);
}
#[test]
fn test_is_valid_field_uppercase_non_alpha_invalid() {
let field = "TE!ST";
assert_eq!(is_valid_field(&field), false);
}
#[test]
fn test_is_valid_field_uppercase_leading_underscore_invalid() {
let field = "_TEST";
assert_eq!(is_valid_field(&field), false);
}
#[test]
fn test_is_valid_field_uppercase_leading_digit_invalid() {
assert_eq!(is_valid_field("1TEST"), false);
}
#[test]
fn add_field_and_payload_explicit_length_simple() {
let mut data = Vec::new();
add_field_and_payload_explicit_length(&mut data, "FOO", "BAR");
assert_eq!(
data,
vec![b'F', b'O', b'O', b'\n', 3, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n']
);
}
#[test]
fn add_field_and_payload_explicit_length_internal_newline() {
let mut data = Vec::new();
add_field_and_payload_explicit_length(&mut data, "FOO", "B\nAR");
assert_eq!(
data,
vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n']
);
}
#[test]
fn add_field_and_payload_explicit_length_trailing_newline() {
let mut data = Vec::new();
add_field_and_payload_explicit_length(&mut data, "FOO", "BAR\n");
assert_eq!(
data,
vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n']
);
}
#[test]
fn add_field_and_payload_simple() {
let mut data = Vec::new();
add_field_and_payload(&mut data, "FOO", "BAR");
assert_eq!(data, "FOO=BAR\n".as_bytes());
}
#[test]
fn add_field_and_payload_internal_newline() {
let mut data = Vec::new();
add_field_and_payload(&mut data, "FOO", "B\nAR");
assert_eq!(
data,
vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n']
);
}
#[test]
fn add_field_and_payload_trailing_newline() {
let mut data = Vec::new();
add_field_and_payload(&mut data, "FOO", "BAR\n");
assert_eq!(
data,
vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n']
);
}
}