use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
pub enum IpcMessage {
Stop,
Status,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum IpcResponse {
Ok,
Recording,
Idle,
Processing,
Error(String),
}
pub fn get_socket_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("whis.sock")
}
pub fn get_pid_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("whis.pid")
}
pub struct IpcServer {
listener: UnixListener,
}
impl IpcServer {
pub fn new() -> Result<Self> {
let socket_path = get_socket_path();
if socket_path.exists() {
std::fs::remove_file(&socket_path).context("Failed to remove old socket file")?;
}
let listener = UnixListener::bind(&socket_path).context("Failed to bind Unix socket")?;
listener
.set_nonblocking(true)
.context("Failed to set non-blocking mode")?;
Ok(Self { listener })
}
pub fn try_accept(&self) -> Result<Option<IpcConnection>> {
match self.listener.accept() {
Ok((stream, _)) => Ok(Some(IpcConnection { stream })),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(e.into()),
}
}
}
impl Drop for IpcServer {
fn drop(&mut self) {
let socket_path = get_socket_path();
let _ = std::fs::remove_file(socket_path);
}
}
pub struct IpcConnection {
stream: UnixStream,
}
impl IpcConnection {
pub fn receive(&mut self) -> Result<IpcMessage> {
let mut reader = BufReader::new(&self.stream);
let mut line = String::new();
reader
.read_line(&mut line)
.context("Failed to read from socket")?;
serde_json::from_str(line.trim()).context("Failed to deserialize message")
}
pub fn send(&mut self, response: IpcResponse) -> Result<()> {
let json = serde_json::to_string(&response)?;
writeln!(self.stream, "{json}").context("Failed to write to socket")?;
self.stream.flush().context("Failed to flush socket")?;
Ok(())
}
}
pub struct IpcClient {
stream: UnixStream,
}
impl IpcClient {
pub fn connect() -> Result<Self> {
let socket_path = get_socket_path();
if !socket_path.exists() {
anyhow::bail!(
"whis service is not running.\n\
Start it with: whis listen"
);
}
let stream = UnixStream::connect(&socket_path).with_context(|| {
"Failed to connect to whis service.\n\
The service may have crashed. Try removing stale files:\n\
rm -f $XDG_RUNTIME_DIR/whis.*\n\
Then start the service again with: whis listen"
})?;
Ok(Self { stream })
}
pub fn send_message(&mut self, message: IpcMessage) -> Result<IpcResponse> {
let json = serde_json::to_string(&message)?;
writeln!(self.stream, "{json}").context("Failed to send message")?;
self.stream.flush().context("Failed to flush stream")?;
let mut reader = BufReader::new(&self.stream);
let mut line = String::new();
reader
.read_line(&mut line)
.context("Failed to read response")?;
serde_json::from_str(line.trim()).context("Failed to deserialize response")
}
}
pub fn is_service_running() -> bool {
let socket_path = get_socket_path();
if !socket_path.exists() {
return false;
}
match UnixStream::connect(&socket_path) {
Ok(_) => {
true
}
Err(_) => {
let _ = std::fs::remove_file(&socket_path);
remove_pid_file();
false
}
}
}
pub fn write_pid_file() -> Result<()> {
let pid_path = get_pid_path();
let pid = std::process::id();
std::fs::write(&pid_path, pid.to_string()).context("Failed to write PID file")?;
Ok(())
}
pub fn remove_pid_file() {
let pid_path = get_pid_path();
let _ = std::fs::remove_file(pid_path);
}