use std::fs::remove_file;
use std::io;
use std::io::ErrorKind;
use std::io::Read as _;
use std::io::Write as _;
use std::io::stdout;
use std::mem::MaybeUninit;
use std::os::unix::net::UnixListener;
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::process::exit;
use std::ptr::null_mut;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::mpsc::SyncSender;
use std::sync::mpsc::sync_channel;
use std::thread;
use std::thread::ScopedJoinHandle;
use std::thread::Thread;
use std::time::Duration;
use anyhow::Context as _;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::ensure;
use dbus::arg::PropMap;
use dbus::blocking::Connection;
use libc::O_RDWR;
use libc::SIGINT;
use libc::SIGTERM;
use libc::STDERR_FILENO;
use libc::STDIN_FILENO;
use libc::STDOUT_FILENO;
use libc::c_int;
use libc::chdir;
use libc::close;
use libc::dup2;
use libc::fork;
use libc::open;
use libc::setsid;
use libc::sigaction;
use libc::sigemptyset;
use libc::umask;
use postcard::from_bytes as postcard_from_bytes;
use postcard::to_io as postcard_to_io;
use crate::args::Command;
use crate::args::RemindAt;
use crate::channel::Channel;
use crate::reminder::Reminder;
use crate::reminder::format_reminders;
use crate::reminders::Reminders;
use crate::request::Request;
static THREAD: OnceLock<Thread> = OnceLock::new();
static EXITING: AtomicBool = AtomicBool::new(false);
extern "C" fn handle_exit_signal(_signum: c_int) {
let () = EXITING.store(true, Ordering::SeqCst);
if let Some(thread) = THREAD.get() {
let () = thread.unpark();
}
}
fn daemonize() -> Result<()> {
match unsafe { fork() } {
0 => (),
..=-1 => return Err(io::Error::last_os_error()).context("failed to fork into background"),
_pid => exit(0),
}
if unsafe { setsid() } < 0 {
return Err(io::Error::last_os_error()).context("setsid failed")
}
match unsafe { fork() } {
0 => (),
..=-1 => return Err(io::Error::last_os_error()).context("failed to fork second time"),
_pid => exit(0),
}
if unsafe { chdir(c"/".as_ptr()) } < 0 {
return Err(io::Error::last_os_error()).context("chdir failed")
}
let _mode = unsafe { umask(0) };
let fd = unsafe { open(c"/dev/null".as_ptr(), O_RDWR) };
if fd >= 0 {
let _rc = unsafe { dup2(fd, STDIN_FILENO) };
let _rc = unsafe { dup2(fd, STDOUT_FILENO) };
let _rc = unsafe { dup2(fd, STDERR_FILENO) };
if fd > STDERR_FILENO {
let _rc = unsafe { close(fd) };
}
}
Ok(())
}
fn install_signal_handlers(signal_thread: &Thread) -> Result<()> {
let () = THREAD.set(Thread::clone(signal_thread)).unwrap();
let mut action = unsafe { MaybeUninit::<sigaction>::zeroed().assume_init() };
action.sa_sigaction = handle_exit_signal as *const () as usize;
let rc = unsafe { sigemptyset(&mut action.sa_mask) };
ensure!(rc == 0, "failed to clear sigaction mask");
let rc = unsafe { sigaction(SIGINT, &action, null_mut()) };
ensure!(rc == 0, "failed to install SIGINT handler");
let rc = unsafe { sigaction(SIGTERM, &action, null_mut()) };
ensure!(rc == 0, "failed to install SIGTERM handler");
Ok(())
}
fn listen_incoming(
listener: UnixListener,
request_send: SyncSender<Request>,
server_thread: Thread,
exiting: &AtomicBool,
) -> Result<()> {
for result in listener.incoming() {
if exiting.load(Ordering::SeqCst) {
break
}
match result {
Ok(mut stream) => {
let mut buf = Vec::new();
let _cnt = stream
.read_to_end(&mut buf)
.context("failed to read Request object data")?;
let request =
postcard_from_bytes::<Request>(&buf).context("failed to deserialize Request object")?;
let () = request_send.send(request).unwrap();
let () = server_thread.unpark();
},
Err(e) => return Err(e).context("failed to accept new incoming connection"),
}
}
Ok(())
}
fn join_thread(thread: ScopedJoinHandle<'_, Result<()>>) -> Result<()> {
thread.join().unwrap_or_else(|payload| {
if let Some(s) = payload.downcast_ref::<&'static str>() {
Err(anyhow!("encountered unexpected panic: {s}"))
} else if let Some(s) = payload.downcast_ref::<String>() {
Err(anyhow!("encountered unexpected panic: {s}"))
} else {
Err(anyhow!("encountered unexpected panic"))
}
})
}
fn send_notification(message: &str) -> Result<()> {
let appname = env!("CARGO_BIN_NAME");
let replaces_id = 0u32;
let icon = "alarm";
let body = "";
let timeout = 0i32;
let connection = Connection::new_session().context("failed to connect to session bus")?;
let proxy = connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_secs(5),
);
let (_msg_id,) = proxy
.method_call::<(u32,), _, _, _>(
"org.freedesktop.Notifications",
"Notify",
(
appname,
replaces_id,
icon,
message,
body,
Vec::<String>::new(),
PropMap::new(),
timeout,
),
)
.context("failed to call D-Bus Notify")?;
Ok(())
}
fn run_now(listener: UnixListener, reminder: Reminder) -> Result<()> {
let server_thread = thread::current();
let () = install_signal_handlers(&server_thread)?;
let (request_send, request_recv) = sync_channel::<Request>(1);
let mut reminders = Reminders::new(request_recv, reminder);
let addr = listener
.local_addr()
.context("failed to retrieve UNIX socket address")?;
let () = thread::scope(|scope| {
let listener_thread =
scope.spawn(|| listen_incoming(listener, request_send, server_thread, &EXITING));
let run = || {
loop {
if listener_thread.is_finished() {
return join_thread(listener_thread)
}
if EXITING.load(Ordering::SeqCst) {
return Ok(())
}
if let Some(next) = reminders.next_reminder() {
if let Some(duration) = next.duration() {
let () = thread::park_timeout(duration);
}
if next.duration().is_none() {
if let Err(err) = send_notification(&next.message) {
eprintln!("failed to send D-Bus notification: {err:#}");
}
let () = reminders.remove_next();
}
} else {
return Ok(())
}
if let Err(()) = reminders.try_recv() {
return Ok(())
}
}
};
let result = run();
let () = EXITING.store(true, Ordering::SeqCst);
let _ignored = UnixStream::connect_addr(&addr);
let () = drop(reminders);
result
})?;
Ok(())
}
fn run_server(socket_path: &Path, reminder: Reminder, foreground: bool) -> Result<()> {
let _result = remove_file(socket_path);
let listener = match UnixListener::bind(socket_path) {
Ok(listener) => listener,
Err(err) if err.kind() == ErrorKind::AddrInUse => {
return run(
socket_path,
Command::At(RemindAt::from(reminder)),
foreground,
)
},
Err(err) => {
return Err(err)
.with_context(|| format!("failed to bind to Unix socket `{}`", socket_path.display()))
},
};
if !foreground {
let () = daemonize()?;
}
run_now(listener, reminder)
}
fn signal_server(stream: UnixStream, command: Command) -> Result<()> {
let send = |request| -> Result<()> {
let _stream =
postcard_to_io(request, stream).context("failed to send Request object to server")?;
Ok(())
};
match command {
Command::At(remind_at) => {
let request = Request::from(remind_at);
let () = send(&request)?;
Ok(())
},
Command::In(remind_in) => {
let request = Request::from(remind_in);
let () = send(&request)?;
Ok(())
},
Command::List => {
let (read, write) = Channel::oneshot().unwrap();
let request = Request::List(write);
let () = send(&request)?;
let mut list = read
.read()
.context("failed to read Request::List response")
.unwrap();
let _result = stdout().write(format_reminders(&mut list).as_bytes());
Ok(())
},
}
}
pub(crate) fn run(socket_path: &Path, command: Command, foreground: bool) -> Result<()> {
match UnixStream::connect(socket_path) {
Ok(stream) => signal_server(stream, command),
Err(_) => match command {
Command::At(remind_at) => {
let reminder = Reminder::from(remind_at);
let () = run_server(socket_path, reminder, foreground)?;
Ok(())
},
Command::In(remind_in) => {
let reminder = Reminder::from(remind_in);
let () = run_server(socket_path, reminder, foreground)?;
Ok(())
},
Command::List => {
Ok(())
},
},
}
}