use mpdpopm::config;
use mpdpopm::config::Config;
use mpdpopm::mpdpopm;
use mpdpopm::vars::LOCALSTATEDIR;
use backtrace::Backtrace;
use clap::{Arg, ArgAction, Command, value_parser};
use errno::errno;
use lazy_static::lazy_static;
use libc::{
F_TLOCK, close, dup, exit, fork, getdtablesize, getpid, lockf, open, setsid, umask, write,
};
use tokio::sync::mpsc;
use tracing::{error, info, level_filters::LevelFilter};
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt::MakeWriter, layer::SubscriberExt};
use std::{
ffi::CString,
fmt,
fs::OpenOptions,
io,
path::{Path, PathBuf},
sync::{Arc, Mutex, MutexGuard},
};
#[non_exhaustive]
pub enum Error {
NoConfigArg,
NoConfig {
config: std::path::PathBuf,
cause: std::io::Error,
},
Filter {
source: tracing_subscriber::filter::FromEnvError,
back: Backtrace,
},
Fork {
errno: errno::Errno,
back: Backtrace,
},
PathContainsNull {
back: Backtrace,
},
OpenLockFile {
errno: errno::Errno,
back: Backtrace,
},
LockFile {
errno: errno::Errno,
back: Backtrace,
},
WritePid {
errno: errno::Errno,
back: Backtrace,
},
Config {
source: crate::config::Error,
back: Backtrace,
},
MpdPopm {
source: mpdpopm::Error,
back: Backtrace,
},
}
impl std::fmt::Display for Error {
#[allow(unreachable_patterns)] fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::NoConfigArg => write!(f, "No configuration file given"),
Error::NoConfig { config, cause } => {
write!(f, "Configuration error ({:?}): {}", config, cause)
}
Error::Fork { errno, back: _ } => write!(f, "When forking, got errno {}", errno),
Error::PathContainsNull { back: _ } => write!(f, "Path contains a null character"),
Error::OpenLockFile { errno, back: _ } => {
write!(f, "While opening lock file, got errno {}", errno)
}
Error::LockFile { errno, back: _ } => {
write!(f, "While locking the lock file, got errno {}", errno)
}
Error::WritePid { errno, back: _ } => {
write!(f, "While writing pid file, got errno {}", errno)
}
Error::Config { source, back: _ } => write!(f, "Configuration error: {}", source),
Error::MpdPopm { source, back: _ } => write!(f, "mpdpopm error: {}", source),
_ => write!(f, "Unknown mppopmd error"),
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self)
}
}
type Result = std::result::Result<(), Error>;
type StdResult<T, E> = std::result::Result<T, E>;
lazy_static! {
static ref DEF_CONF: String = format!("{}/mppopmd.conf", mpdpopm::vars::SYSCONFDIR);
}
struct LogFile {
fd: Arc<Mutex<std::fs::File>>,
}
impl LogFile {
pub fn open<P: AsRef<Path>>(
pth: P,
) -> StdResult<(LogFile, mpsc::Sender<PathBuf>), std::io::Error> {
let (tx, rx) = mpsc::channel::<PathBuf>(1);
let fd = OpenOptions::new()
.create(true)
.append(true)
.open(pth)
.map(|fd| Arc::new(Mutex::new(fd)))?;
tokio::spawn(LogFile::rehup(fd.clone(), rx));
Ok((LogFile { fd }, tx))
}
async fn rehup(fd: Arc<Mutex<std::fs::File>>, mut rx: mpsc::Receiver<PathBuf>) {
while let Some(ref pbuf) = rx.recv().await {
match OpenOptions::new().create(true).append(true).open(pbuf) {
Ok(f) => *fd.lock().unwrap() = f,
Err(err) => error!("Failed to open {:?} ({}).", pbuf, err),
}
}
}
}
pub struct MyMutexGuardWriter<'a>(MutexGuard<'a, std::fs::File>);
impl<'a> MakeWriter<'a> for LogFile {
type Writer = MyMutexGuardWriter<'a>;
fn make_writer(&'a self) -> Self::Writer {
MyMutexGuardWriter(self.fd.lock().expect("lock poisoned"))
}
}
impl io::Write for MyMutexGuardWriter<'_> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
#[inline]
fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
self.0.write_vectored(bufs)
}
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.0.write_all(buf)
}
#[inline]
fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> {
self.0.write_fmt(fmt)
}
}
fn daemonize() -> Result {
use std::os::unix::ffi::OsStringExt;
unsafe {
let pid = fork();
if pid < 0 {
return Err(Error::Fork {
errno: errno(),
back: Backtrace::new(),
});
} else if pid != 0 {
exit(0);
}
setsid();
let pid = fork();
if pid < 0 {
return Err(Error::Fork {
errno: errno(),
back: Backtrace::new(),
});
} else if pid != 0 {
exit(0);
}
std::env::set_current_dir("/tmp").unwrap();
umask(0);
let mut i = getdtablesize() - 1;
while i > -1 {
close(i);
i -= 1;
}
i = open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR);
dup(i);
dup(i);
let pth: PathBuf = [LOCALSTATEDIR, "run", "mppopmd.pid"].iter().collect();
let pth_c = CString::new(pth.into_os_string().into_vec()).unwrap();
let fd = open(
pth_c.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC,
0o640,
);
if -1 == fd {
return Err(Error::OpenLockFile {
errno: errno(),
back: Backtrace::new(),
});
}
if lockf(fd, F_TLOCK, 0) < 0 {
return Err(Error::LockFile {
errno: errno(),
back: Backtrace::new(),
});
}
let pid = getpid();
let pid_buf = format!("{}", pid).into_bytes();
let pid_length = pid_buf.len();
let pid_c = CString::new(pid_buf).unwrap();
if write(fd, pid_c.as_ptr() as *const libc::c_void, pid_length) < pid_length as isize {
return Err(Error::WritePid {
errno: errno(),
back: Backtrace::new(),
});
}
}
Ok(())
}
fn main() -> Result {
use mpdpopm::vars::{AUTHOR, VERSION};
let matches = Command::new("mppopmd")
.version(VERSION)
.author(AUTHOR)
.about("mpd + POPM")
.long_about(
"
`mppopmd' is a companion daemon for `mpd' that maintains playcounts & ratings,
as well as implementing some handy functions. It maintains ratings & playcounts in the sticker
database, but it allows you to keep that information in your tags, as well, by invoking external
commands to keep your tags up-to-date.",
)
.arg(
Arg::new("no-daemon")
.short('F')
.long("no-daemon")
.num_args(0)
.action(ArgAction::SetTrue)
.help("do not daemonize; remain in foreground"),
)
.arg(
Arg::new("config")
.short('c')
.long("config")
.num_args(1)
.value_parser(value_parser!(PathBuf))
.default_value(DEF_CONF.as_str())
.help("path to configuration file"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.num_args(0)
.action(ArgAction::SetTrue)
.help("enable verbose logging"),
)
.arg(
Arg::new("debug")
.short('d')
.long("debug")
.num_args(0)
.action(ArgAction::SetTrue)
.help("enable debug logging (implies --verbose)"),
)
.get_matches();
let cfgpth = matches
.get_one::<PathBuf>("config")
.ok_or_else(|| Error::NoConfigArg {})?;
let cfg = match std::fs::read_to_string(cfgpth) {
Ok(text) => config::from_str(&text).map_err(|err| Error::Config {
source: err,
back: Backtrace::new(),
})?,
Err(err) => match (err.kind(), matches.value_source("config").unwrap()) {
(std::io::ErrorKind::NotFound, clap::parser::ValueSource::DefaultValue) => {
Config::default()
}
(_, _) => {
return Err(Error::NoConfig {
config: PathBuf::from(cfgpth),
cause: err,
});
}
},
};
let lf = match (matches.get_flag("verbose"), matches.get_flag("debug")) {
(_, true) => LevelFilter::TRACE,
(true, false) => LevelFilter::DEBUG,
_ => LevelFilter::INFO,
};
let filter = EnvFilter::builder()
.with_default_directive(lf.into())
.from_env()
.map_err(|err| Error::Filter {
source: err,
back: Backtrace::new(),
})?;
let (formatter, reopen): (
Box<dyn Layer<Registry> + Send + Sync>,
Option<tokio::sync::mpsc::Sender<PathBuf>>,
) = if matches.get_flag("no-daemon") {
(
Box::new(
tracing_subscriber::fmt::Layer::default()
.compact()
.with_writer(io::stdout),
),
None,
)
} else {
daemonize()?;
let (log_file, tx) = LogFile::open(&cfg.log).unwrap();
(
Box::new(
tracing_subscriber::fmt::Layer::default()
.compact()
.with_ansi(false)
.with_writer(log_file),
),
Some(tx),
)
};
tracing::subscriber::set_global_default(Registry::default().with(formatter).with(filter))
.unwrap();
info!("mppopmd {VERSION} logging at level {lf:#?}.");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(mpdpopm(cfg, reopen))
.map_err(|err| Error::MpdPopm {
source: err,
back: Backtrace::new(),
})
}