use crate::paths;
use crate::proto::{Request, Response};
use anyhow::{bail, Context, Result};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
const SPAWN_TIMEOUT: Duration = Duration::from_secs(10);
pub struct Client {
stream: UnixStream,
reader: BufReader<UnixStream>,
}
impl Client {
pub fn connect() -> Result<Self> {
let socket = paths::socket_path()?;
if let Some(stream) = try_connect(&socket) {
return Client::with_stream(stream);
}
let _lock = SpawnLock::acquire()?;
if let Some(stream) = try_connect(&socket) {
return Client::with_stream(stream);
}
spawn_daemon()?;
let stream = wait_for_socket(&socket, SPAWN_TIMEOUT)?;
Client::with_stream(stream)
}
pub fn connect_existing() -> Option<Self> {
let socket = paths::socket_path().ok()?;
let stream = try_connect(&socket)?;
Client::with_stream(stream).ok()
}
fn with_stream(stream: UnixStream) -> Result<Self> {
let reader = BufReader::new(stream.try_clone().context("copying the socket handle")?);
Ok(Self { stream, reader })
}
pub fn call(&mut self, request: &Request) -> Result<Response> {
self.send(request)?;
self.recv()
}
pub fn send(&mut self, request: &Request) -> Result<()> {
let mut line = serde_json::to_string(request).context("writing the request")?;
line.push('\n');
self.stream
.write_all(line.as_bytes())
.context("sending the request to the coordinator")?;
self.stream.flush().ok();
Ok(())
}
pub fn recv(&mut self) -> Result<Response> {
let mut line = String::new();
let n = self
.reader
.read_line(&mut line)
.context("reading the answer of the coordinator")?;
if n == 0 {
bail!("the coordinator closed the connection without an answer");
}
serde_json::from_str(&line)
.with_context(|| format!("reading this answer of the coordinator: {}", line.trim()))
}
pub fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<()> {
self.stream
.set_read_timeout(timeout)
.context("setting the timeout of the socket")
}
}
fn try_connect(socket: &Path) -> Option<UnixStream> {
UnixStream::connect(socket).ok()
}
fn wait_for_socket(socket: &Path, timeout: Duration) -> Result<UnixStream> {
let deadline = Instant::now() + timeout;
let mut delay = Duration::from_millis(2);
while Instant::now() < deadline {
if let Some(stream) = try_connect(socket) {
return Ok(stream);
}
std::thread::sleep(delay);
delay = (delay * 2).min(Duration::from_millis(50));
}
let log = paths::daemon_log_path()?;
bail!(
"the coordinator did not start in {} seconds.\n\
Read its log file: {}",
timeout.as_secs(),
log.display()
)
}
fn spawn_daemon() -> Result<()> {
use std::os::unix::process::CommandExt;
let exe = paths::program_path()?;
let log_path = paths::daemon_log_path()?;
paths::ensure_dir(&paths::runtime_dir()?, 0o700)?;
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.with_context(|| format!("opening the log file {}", log_path.display()))?;
let log_err = log.try_clone().context("copying the log file handle")?;
let mut cmd = std::process::Command::new(exe);
cmd.arg("daemon")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log))
.stderr(std::process::Stdio::from(log_err))
.current_dir("/");
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
cmd.spawn().context("starting the coordinator")?;
Ok(())
}
pub struct SpawnLock {
file: std::fs::File,
}
impl SpawnLock {
pub fn acquire() -> Result<Self> {
let dir = paths::runtime_dir()?;
paths::ensure_dir(&dir, 0o700)?;
let path = paths::spawn_lock_path()?;
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path)
.with_context(|| format!("opening the lock file {}", path.display()))?;
use std::os::unix::io::AsRawFd;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if rc != 0 {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("locking {}", path.display()));
}
Ok(Self { file })
}
}
impl Drop for SpawnLock {
fn drop(&mut self) {
use std::os::unix::io::AsRawFd;
unsafe {
libc::flock(self.file.as_raw_fd(), libc::LOCK_UN);
}
}
}