use mpdpopm::config;
use mpdpopm::config::Config;
use mpdpopm::mpdpopm;
use mpdpopm::vars::LOCALSTATEDIR;
use backtrace::Backtrace;
use clap::{value_parser, Arg, ArgAction, Command};
use errno::errno;
use lazy_static::lazy_static;
use libc::{
close, dup, exit, fork, getdtablesize, getpid, lockf, open, setsid, umask, write, F_TLOCK,
};
use log::{info, LevelFilter};
use log4rs::{
append::{
console::{ConsoleAppender, Target},
file::FileAppender,
},
config::{Appender, Root},
encode::pattern::PatternEncoder,
};
use std::{ffi::CString, fmt, path::PathBuf};
#[non_exhaustive]
pub enum Error {
NoConfigArg,
NoConfig {
config: std::path::PathBuf,
cause: std::io::Error,
},
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,
},
Logging {
source: log::SetLoggerError,
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::Logging { source, back: _ } => write!(f, "Logging 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>;
lazy_static! {
static ref DEF_CONF: String = format!("{}/mppopmd.conf", mpdpopm::vars::SYSCONFDIR);
}
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,
};
if matches.get_flag("no-daemon") {
let app = ConsoleAppender::builder()
.target(Target::Stdout)
.encoder(Box::new(PatternEncoder::new("[{d}][{M}] {m}{n}")))
.build();
let lcfg = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(app)))
.build(Root::builder().appender("stdout").build(lf))
.unwrap();
log4rs::init_config(lcfg).map_err(|err| Error::Logging {
source: err,
back: Backtrace::new(),
})?;
} else {
daemonize()?;
let app = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d}|{M}|{f}|{l}| {m}{n}")))
.build(&cfg.log)
.unwrap();
let lcfg = log4rs::config::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(app)))
.build(Root::builder().appender("logfile").build(lf))
.unwrap();
log4rs::init_config(lcfg).map_err(|err| Error::Logging {
source: err,
back: Backtrace::new(),
})?;
}
info!("mppopmd {} logging at level {:#?}.", VERSION, lf);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(mpdpopm(cfg)).map_err(|err| Error::MpdPopm {
source: err,
back: Backtrace::new(),
})
}