use super::{free_cstring, usec_from_duration, Result};
use crate::ffi::const_iovec;
use crate::ffi::journal as ffi;
use crate::ffi_result;
use crate::id128::Id128;
use cstr_argument::CStrArgument;
use foreign_types::{foreign_type, ForeignType, ForeignTypeRef};
use libc::{c_char, c_int, size_t};
use log::{self, Level, Log, Record, SetLoggerError};
use memchr::memchr;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::io::ErrorKind::InvalidData;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
use std::os::unix::io::AsRawFd;
use std::{fmt, io, ptr, result, slice, time};
fn collect_and_send<T, S>(args: T) -> c_int
where
T: Iterator<Item = S>,
S: AsRef<str>,
{
let iovecs: Vec<const_iovec> = args
.map(|x| unsafe { const_iovec::from_str(x) })
.collect();
unsafe { ffi::sd_journal_sendv(iovecs.as_ptr(), iovecs.len() as c_int) }
}
pub fn send(args: &[&str]) -> c_int {
collect_and_send(args.iter())
}
pub fn print(lvl: u32, s: &str) -> c_int {
send(&[&format!("PRIORITY={lvl}"), &format!("MESSAGE={s}")])
}
enum SyslogLevel {
Err = 3,
Warning = 4,
Notice = 5,
Info = 6,
Debug = 7,
}
impl From<log::Level> for SyslogLevel {
fn from(level: log::Level) -> Self {
match level {
Level::Error => SyslogLevel::Err,
Level::Warn => SyslogLevel::Warning,
Level::Info => SyslogLevel::Notice,
Level::Debug => SyslogLevel::Info,
Level::Trace => SyslogLevel::Debug,
}
}
}
pub fn log(level: usize, file: &str, line: u32, module_path: &str, args: &fmt::Arguments<'_>) {
send(&[
&format!("PRIORITY={level}"),
&format!("MESSAGE={args}"),
&format!("CODE_LINE={line}"),
&format!("CODE_FILE={file}"),
&format!("CODE_MODULE={module_path}"),
]);
}
pub fn log_record(record: &Record<'_>) {
let keys = [
format!("PRIORITY={}", SyslogLevel::from(record.level()) as usize),
format!("MESSAGE={}", record.args()),
format!("TARGET={}", record.target()),
];
let opt_keys = [
record.line().map(|line| format!("CODE_LINE={line}")),
record.file().map(|file| format!("CODE_FILE={file}")),
record.module_path().map(|path| format!("CODE_FUNC={path}")),
];
collect_and_send(keys.iter().chain(opt_keys.iter().flatten()));
}
pub struct JournalLog;
impl Log for JournalLog {
fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
true
}
fn log(&self, record: &Record<'_>) {
log_record(record);
}
fn flush(&self) {
}
}
static LOGGER: JournalLog = JournalLog;
impl JournalLog {
pub fn init() -> result::Result<(), SetLoggerError> {
log::set_logger(&LOGGER)
}
}
fn duration_from_usec(usec: u64) -> time::Duration {
let secs = usec / 1_000_000;
let sub_usec = (usec % 1_000_000) as u32;
let sub_nsec = sub_usec * 1000;
time::Duration::new(secs, sub_nsec)
}
fn system_time_from_realtime_usec(usec: u64) -> time::SystemTime {
let d = duration_from_usec(usec);
time::UNIX_EPOCH + d
}
foreign_type! {
pub unsafe type Journal {
type CType = ffi::sd_journal;
fn drop = ffi::sd_journal_close;
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct JournalEntryField<'a> {
data: &'a [u8],
eq_offs: usize,
}
impl<'a> JournalEntryField<'a> {
pub fn data(&self) -> &[u8] {
self.data
}
pub fn name(&self) -> &[u8] {
&self.data[..self.eq_offs]
}
pub fn value(&self) -> Option<&[u8]> {
if self.eq_offs != self.data.len() {
Some(&self.data[(self.eq_offs + 1)..])
} else {
None
}
}
}
impl<'a> From<&'a [u8]> for JournalEntryField<'a> {
fn from(data: &'a [u8]) -> Self {
let eq_offs = match memchr(b'=', data) {
Some(v) => v,
None => data.len(),
};
Self { data, eq_offs }
}
}
pub type JournalRecord = BTreeMap<String, String>;
#[deprecated(
since = "0.8.0",
note = "Use `OpenOptions` instead. `JournalFiles` doesn't completely represent the filtering/inclusion options"
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum JournalFiles {
System,
CurrentUser,
All,
}
#[allow(deprecated)]
impl JournalFiles {
fn as_flags(self) -> c_int {
match self {
JournalFiles::System => ffi::SD_JOURNAL_SYSTEM,
JournalFiles::CurrentUser => ffi::SD_JOURNAL_CURRENT_USER,
JournalFiles::All => 0,
}
}
}
pub struct DisplayEntryData<'a> {
journal: RefCell<&'a mut JournalRef>,
}
impl<'a> fmt::Display for DisplayEntryData<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(fmt, "{{")?;
let mut j = self.journal.borrow_mut();
j.restart_data();
loop {
match j.enumerate_data() {
Ok(Some(v)) => {
writeln!(fmt, " \"{}\",", std::str::from_utf8(v.data()).unwrap())?;
}
Ok(None) => break,
Err(e) => {
writeln!(fmt, "E: {e:?}")?;
break;
}
}
}
writeln!(fmt, "}}")
}
}
impl<'a> From<&'a mut JournalRef> for DisplayEntryData<'a> {
fn from(v: &'a mut JournalRef) -> Self {
Self {
journal: RefCell::new(v),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum JournalSeek {
Head,
Tail,
ClockMonotonic { boot_id: Id128, usec: u64 },
ClockRealtime { usec: u64 },
Cursor { cursor: String },
}
#[derive(Clone, Debug)]
pub enum JournalWaitResult {
Nop,
Append,
Invalidate,
}
#[derive(Clone, Debug, Default)]
pub struct OpenOptions {
current_user: bool,
system: bool,
local_only: bool,
runtime_only: bool,
all_namespaces: bool,
include_default_namespace: bool,
extra_raw_flags: libc::c_int,
}
impl OpenOptions {
pub fn current_user(&mut self, current_user: bool) -> &mut Self {
self.current_user = current_user;
self
}
pub fn system(&mut self, system: bool) -> &mut Self {
self.system = system;
self
}
pub fn local_only(&mut self, local_only: bool) -> &mut Self {
self.local_only = local_only;
self
}
pub fn runtime_only(&mut self, runtime_only: bool) -> &mut Self {
self.runtime_only = runtime_only;
self
}
pub fn all_namespaces(&mut self, all_namespaces: bool) -> &mut Self {
self.all_namespaces = all_namespaces;
self
}
pub fn include_default_namespace(&mut self, include_default_namespace: bool) -> &mut Self {
self.include_default_namespace = include_default_namespace;
self
}
pub fn extra_raw_flags(&mut self, extra_raw_flags: libc::c_int) -> &mut Self {
self.extra_raw_flags = extra_raw_flags;
self
}
pub fn open(&self) -> Result<Journal> {
Journal::open_with_opts(self)
}
#[cfg(feature = "systemd_v245")]
#[cfg_attr(feature = "unstable-doc-cfg", doc(cfg(feature = "systemd_v245")))]
pub fn open_namespace<A: CStrArgument>(&self, namespace: A) -> Result<Journal> {
Journal::open_with_opts_ns(Some(namespace), self)
}
}
#[derive(Clone, Debug, Default)]
pub struct OpenDirectoryOptions {
os_root: bool,
current_user: bool,
system: bool,
extra_raw_flags: libc::c_int,
}
impl OpenDirectoryOptions {
pub fn os_root(&mut self, os_root: bool) -> &mut Self {
self.os_root = os_root;
self
}
pub fn current_user(&mut self, current_user: bool) -> &mut Self {
self.current_user = current_user;
self
}
pub fn system(&mut self, system: bool) -> &mut Self {
self.system = system;
self
}
pub fn extra_raw_flags(&mut self, extra_raw_flags: libc::c_int) -> &mut Self {
self.extra_raw_flags = extra_raw_flags;
self
}
pub fn open_directory<A: CStrArgument>(&self, directory: A) -> Result<Journal> {
Journal::open_with_opts_dir(directory, self)
}
}
#[derive(Clone, Debug, Default)]
pub struct OpenFilesOptions {
extra_raw_flags: libc::c_int,
}
impl OpenFilesOptions {
pub fn extra_raw_flags(&mut self, extra_raw_flags: libc::c_int) -> &mut Self {
self.extra_raw_flags = extra_raw_flags;
self
}
pub fn open_files<A: CStrArgument, I: IntoIterator<Item = A>>(
&self,
files: I,
) -> Result<Journal> {
Journal::open_with_opts_files(files, self)
}
}
impl Journal {
fn open_with_opts(opts: &OpenOptions) -> Result<Journal> {
let mut flags = opts.extra_raw_flags;
if opts.current_user {
flags |= ffi::SD_JOURNAL_CURRENT_USER;
}
if opts.system {
flags |= ffi::SD_JOURNAL_SYSTEM;
}
if opts.local_only {
flags |= ffi::SD_JOURNAL_LOCAL_ONLY;
}
if opts.local_only {
flags |= ffi::SD_JOURNAL_LOCAL_ONLY;
}
if opts.runtime_only {
flags |= ffi::SD_JOURNAL_RUNTIME_ONLY;
}
if opts.all_namespaces {
flags |= ffi::SD_JOURNAL_ALL_NAMESPACES;
}
if opts.include_default_namespace {
flags |= ffi::SD_JOURNAL_INCLUDE_DEFAULT_NAMESPACE;
}
let mut jp = MaybeUninit::uninit();
crate::ffi_result(unsafe { ffi::sd_journal_open(jp.as_mut_ptr(), flags) })?;
Ok(unsafe { Journal::from_ptr(jp.assume_init()) })
}
#[cfg(feature = "systemd_v245")]
fn open_with_opts_ns<A: CStrArgument>(
namespace: Option<A>,
opts: &OpenOptions,
) -> Result<Journal> {
let mut flags = opts.extra_raw_flags;
if opts.current_user {
flags |= ffi::SD_JOURNAL_CURRENT_USER;
}
if opts.system {
flags |= ffi::SD_JOURNAL_SYSTEM;
}
if opts.local_only {
flags |= ffi::SD_JOURNAL_LOCAL_ONLY;
}
if opts.local_only {
flags |= ffi::SD_JOURNAL_LOCAL_ONLY;
}
if opts.runtime_only {
flags |= ffi::SD_JOURNAL_RUNTIME_ONLY;
}
if opts.all_namespaces {
flags |= ffi::SD_JOURNAL_ALL_NAMESPACES;
}
if opts.include_default_namespace {
flags |= ffi::SD_JOURNAL_INCLUDE_DEFAULT_NAMESPACE;
}
let ns = namespace.map(|a| a.into_cstr());
let ns_p = ns
.as_ref()
.map(|a| a.as_ref().as_ptr())
.unwrap_or(ptr::null());
let mut jp = MaybeUninit::uninit();
crate::ffi_result(unsafe { ffi::sd_journal_open_namespace(jp.as_mut_ptr(), ns_p, flags) })?;
Ok(unsafe { Journal::from_ptr(jp.assume_init()) })
}
fn open_with_opts_dir<A: CStrArgument>(
directory: A,
opts: &OpenDirectoryOptions,
) -> Result<Journal> {
let mut flags = opts.extra_raw_flags;
if opts.os_root {
flags |= ffi::SD_JOURNAL_OS_ROOT;
}
if opts.current_user {
flags |= ffi::SD_JOURNAL_CURRENT_USER;
}
if opts.system {
flags |= ffi::SD_JOURNAL_SYSTEM;
}
let d_path = directory.into_cstr();
let mut jp = MaybeUninit::uninit();
crate::ffi_result(unsafe {
ffi::sd_journal_open_directory(jp.as_mut_ptr(), d_path.as_ref().as_ptr(), flags)
})?;
Ok(unsafe { Journal::from_ptr(jp.assume_init()) })
}
fn open_with_opts_files<A: CStrArgument, I: IntoIterator<Item = A>>(
files: I,
opts: &OpenFilesOptions,
) -> Result<Journal> {
let mut file_cstrs = Vec::new();
for i in files {
file_cstrs.push(i.into_cstr());
}
let mut file_ptrs = Vec::new();
for i in &file_cstrs {
file_ptrs.push(i.as_ref().as_ptr());
}
file_ptrs.push(ptr::null());
let mut jp = MaybeUninit::uninit();
crate::ffi_result(unsafe {
ffi::sd_journal_open_files(jp.as_mut_ptr(), file_ptrs.as_ptr(), opts.extra_raw_flags)
})?;
Ok(unsafe { Journal::from_ptr(jp.assume_init()) })
}
#[deprecated(
since = "0.8.0",
note = "Use `OpenOptions` instead. It removes the blind boolean options, and allows complete specification of the selected/filtered files"
)]
#[allow(deprecated)]
pub fn open(files: JournalFiles, runtime_only: bool, local_only: bool) -> Result<Journal> {
OpenOptions::default()
.extra_raw_flags(files.as_flags())
.local_only(local_only)
.runtime_only(runtime_only)
.open()
}
}
impl JournalRef {
#[inline]
pub fn fd(&self) -> Result<c_int> {
ffi_result(unsafe { ffi::sd_journal_get_fd(self.as_ptr()) })
}
pub fn data_threshold(&mut self) -> Result<usize> {
let mut curr_thresh = MaybeUninit::uninit();
crate::ffi_result(unsafe {
ffi::sd_journal_get_data_threshold(self.as_ptr(), curr_thresh.as_mut_ptr())
})?;
Ok(unsafe { curr_thresh.assume_init() })
}
pub fn set_data_threshold(&mut self, new_theshold: usize) -> Result<()> {
crate::ffi_result(unsafe {
ffi::sd_journal_set_data_threshold(self.as_ptr(), new_theshold)
})?;
Ok(())
}
pub fn get_data<A: CStrArgument>(&mut self, field: A) -> Result<Option<JournalEntryField<'_>>> {
let mut data = MaybeUninit::uninit();
let mut data_len = MaybeUninit::uninit();
let f = field.into_cstr();
match crate::ffi_result(unsafe {
ffi::sd_journal_get_data(
self.as_ptr(),
f.as_ref().as_ptr(),
data.as_mut_ptr(),
data_len.as_mut_ptr(),
)
}) {
Ok(_) => Ok(Some(
unsafe { slice::from_raw_parts(data.assume_init(), data_len.assume_init()) }.into(),
)),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn restart_data(&mut self) {
unsafe { ffi::sd_journal_restart_data(self.as_ptr()) }
}
pub fn enumerate_data(&mut self) -> Result<Option<JournalEntryField<'_>>> {
let mut data = MaybeUninit::uninit();
let mut data_len = MaybeUninit::uninit();
let r = crate::ffi_result(unsafe {
ffi::sd_journal_enumerate_data(self.as_ptr(), data.as_mut_ptr(), data_len.as_mut_ptr())
});
let v = r?;
if v == 0 {
return Ok(None);
}
let b = unsafe { std::slice::from_raw_parts(data.assume_init(), data_len.assume_init()) };
Ok(Some(b.into()))
}
pub fn display_entry_data(&mut self) -> DisplayEntryData<'_> {
self.into()
}
fn collect_entry(&mut self) -> Result<JournalRecord> {
let mut ret: JournalRecord = BTreeMap::new();
self.restart_data();
while let Some(d) = self.enumerate_data()? {
ret.insert(
String::from_utf8_lossy(d.name()).into(),
String::from_utf8_lossy(d.value().unwrap()).into(),
);
}
Ok(ret)
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<u64> {
crate::ffi_result(unsafe { ffi::sd_journal_next(self.as_ptr()) })
.map(|v| v.try_into().unwrap())
}
pub fn next_skip(&mut self, skip_count: u64) -> Result<u64> {
crate::ffi_result(unsafe { ffi::sd_journal_next_skip(self.as_ptr(), skip_count) })
.map(|v| v.try_into().unwrap())
}
pub fn previous(&mut self) -> Result<u64> {
crate::ffi_result(unsafe { ffi::sd_journal_previous(self.as_ptr()) })
.map(|v| v.try_into().unwrap())
}
pub fn previous_skip(&mut self, skip_count: u64) -> Result<usize> {
crate::ffi_result(unsafe { ffi::sd_journal_previous_skip(self.as_ptr(), skip_count) })
.map(|v| v.try_into().unwrap())
}
pub fn next_entry(&mut self) -> Result<Option<JournalRecord>> {
if self.next()? == 0 {
return Ok(None);
}
self.collect_entry().map(Some)
}
pub fn previous_entry(&mut self) -> Result<Option<JournalRecord>> {
if self.previous()? == 0 {
return Ok(None);
}
self.collect_entry().map(Some)
}
pub fn wait(&mut self, wait_time: Option<time::Duration>) -> Result<JournalWaitResult> {
let time = wait_time.map(usec_from_duration).unwrap_or(u64::MAX);
match ffi_result(unsafe { ffi::sd_journal_wait(self.as_ptr(), time) })? {
ffi::SD_JOURNAL_NOP => Ok(JournalWaitResult::Nop),
ffi::SD_JOURNAL_APPEND => Ok(JournalWaitResult::Append),
ffi::SD_JOURNAL_INVALIDATE => Ok(JournalWaitResult::Invalidate),
_ => Err(io::Error::new(InvalidData, "Failed to wait for changes")),
}
}
pub fn process(&mut self) -> Result<JournalWaitResult> {
match ffi_result(unsafe { ffi::sd_journal_process(self.as_ptr()) })? {
ffi::SD_JOURNAL_NOP => Ok(JournalWaitResult::Nop),
ffi::SD_JOURNAL_APPEND => Ok(JournalWaitResult::Append),
ffi::SD_JOURNAL_INVALIDATE => Ok(JournalWaitResult::Invalidate),
_ => Err(io::Error::new(InvalidData, "Failed to process journal")),
}
}
pub fn await_next_entry(
&mut self,
wait_time: Option<time::Duration>,
) -> Result<Option<JournalRecord>> {
match self.wait(wait_time)? {
JournalWaitResult::Nop => Ok(None),
JournalWaitResult::Append => self.next_entry(),
JournalWaitResult::Invalidate => self.next_entry(),
}
}
pub fn watch_all_elements<F>(&mut self, mut f: F) -> Result<()>
where
F: FnMut(JournalRecord) -> Result<()>,
{
loop {
let candidate = self.next_entry()?;
let rec = match candidate {
Some(rec) => rec,
None => loop {
if let Some(r) = self.await_next_entry(None)? {
break r;
}
},
};
f(rec)?
}
}
pub fn seek_head(&mut self) -> Result<()> {
crate::ffi_result(unsafe { ffi::sd_journal_seek_head(self.as_ptr()) })?;
Ok(())
}
pub fn seek_tail(&mut self) -> Result<()> {
crate::ffi_result(unsafe { ffi::sd_journal_seek_tail(self.as_ptr()) })?;
Ok(())
}
pub fn seek_monotonic_usec(&mut self, boot_id: Id128, usec: u64) -> Result<()> {
crate::ffi_result(unsafe {
ffi::sd_journal_seek_monotonic_usec(self.as_ptr(), *boot_id.as_raw(), usec)
})?;
Ok(())
}
pub fn seek_realtime_usec(&mut self, usec: u64) -> Result<()> {
crate::ffi_result(unsafe { ffi::sd_journal_seek_realtime_usec(self.as_ptr(), usec) })?;
Ok(())
}
pub fn seek_cursor<A: CStrArgument>(&mut self, cursor: A) -> Result<()> {
let c = cursor.into_cstr();
crate::ffi_result(unsafe {
ffi::sd_journal_seek_cursor(self.as_ptr(), c.as_ref().as_ptr())
})?;
Ok(())
}
pub fn seek(&mut self, seek: JournalSeek) -> Result<()> {
match seek {
JournalSeek::Head => self.seek_head()?,
JournalSeek::Tail => {
self.seek_tail()?;
}
JournalSeek::ClockMonotonic { boot_id, usec } => {
self.seek_monotonic_usec(boot_id, usec)?;
}
JournalSeek::ClockRealtime { usec } => {
self.seek_realtime_usec(usec)?;
}
JournalSeek::Cursor { cursor } => {
self.seek_cursor(cursor)?;
}
};
Ok(())
}
pub fn cursor(&self) -> Result<String> {
let mut c_cursor: *const c_char = ptr::null_mut();
ffi_result(unsafe { ffi::sd_journal_get_cursor(self.as_ptr(), &mut c_cursor) })?;
let cursor = unsafe { free_cstring(c_cursor as *mut _).unwrap() };
Ok(cursor)
}
pub fn test_cursor<A: CStrArgument>(&self, cursor: A) -> Result<bool> {
let c = cursor.into_cstr();
ffi_result(unsafe { ffi::sd_journal_test_cursor(self.as_ptr(), c.as_ref().as_ptr()) })
.map(|v| v != 0)
}
pub fn timestamp(&self) -> Result<time::SystemTime> {
Ok(system_time_from_realtime_usec(self.timestamp_usec()?))
}
pub fn timestamp_usec(&self) -> Result<u64> {
let mut timestamp_us: u64 = 0;
ffi_result(unsafe { ffi::sd_journal_get_realtime_usec(self.as_ptr(), &mut timestamp_us) })?;
Ok(timestamp_us)
}
pub fn monotonic_timestamp(&self) -> Result<(u64, Id128)> {
let mut monotonic_timestamp_us: u64 = 0;
let mut id = Id128::default();
ffi_result(unsafe {
ffi::sd_journal_get_monotonic_usec(
self.as_ptr(),
&mut monotonic_timestamp_us,
&mut id.inner,
)
})?;
Ok((monotonic_timestamp_us, id))
}
pub fn monotonic_timestamp_current_boot(&self) -> Result<u64> {
let mut monotonic_timestamp_us: u64 = 0;
ffi_result(unsafe {
ffi::sd_journal_get_monotonic_usec(
self.as_ptr(),
&mut monotonic_timestamp_us,
ptr::null_mut(),
)
})?;
Ok(monotonic_timestamp_us)
}
pub fn match_add<T: Into<Vec<u8>>>(&mut self, key: &str, val: T) -> Result<&mut JournalRef> {
let mut filter = Vec::<u8>::from(key);
filter.push(b'=');
filter.extend(val.into());
let data = filter.as_ptr() as *const c_void;
let datalen = filter.len() as size_t;
ffi_result(unsafe { ffi::sd_journal_add_match(self.as_ptr(), data, datalen) })?;
Ok(self)
}
pub fn match_or(&mut self) -> Result<&mut JournalRef> {
ffi_result(unsafe { ffi::sd_journal_add_disjunction(self.as_ptr()) })?;
Ok(self)
}
pub fn match_and(&mut self) -> Result<&mut JournalRef> {
ffi_result(unsafe { ffi::sd_journal_add_conjunction(self.as_ptr()) })?;
Ok(self)
}
pub fn match_flush(&mut self) -> Result<&mut JournalRef> {
unsafe { ffi::sd_journal_flush_matches(self.as_ptr()) };
Ok(self)
}
}
impl AsRawFd for JournalRef {
#[inline]
fn as_raw_fd(&self) -> c_int {
self.fd().unwrap()
}
}