use crate::has_systemd_run;
use crate::protocol::{Request, Response};
use crate::runtime::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use crate::runtime::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use thiserror::Error;
use tracing::{debug, info, warn};
#[derive(Debug, Error)]
pub enum ClientError {
#[error("failed to connect to server: {0}")]
Connect(#[source] std::io::Error),
#[error("failed to send request: {0}")]
Send(#[source] std::io::Error),
#[error("failed to receive response: {0}")]
Receive(#[source] std::io::Error),
#[error("failed to serialize request: {0}")]
Serialize(#[source] serde_json::Error),
#[error("failed to deserialize response: {0}")]
Deserialize(#[source] serde_json::Error),
#[error("failed to start server: {0}")]
ServerStart(#[source] std::io::Error),
#[error("server did not start in time")]
ServerTimeout,
#[error("server returned error: {0}")]
ServerError(String),
#[error("connection lost")]
ConnectionLost,
}
#[must_use]
pub fn default_socket_path() -> PathBuf {
let uid = crate::sys::getuid();
let runtime_dir = PathBuf::from(format!("/run/user/{uid}"));
if runtime_dir.is_dir() {
runtime_dir.join("vessel.sock")
} else {
PathBuf::from(format!("/tmp/vessel-{uid}.sock"))
}
}
pub struct Client {
socket_path: PathBuf,
stream: Option<BufReader<UnixStream>>,
}
impl Client {
#[must_use]
pub const fn new(socket_path: PathBuf) -> Self {
Self {
socket_path,
stream: None,
}
}
#[must_use]
pub fn with_default_path() -> Self {
Self::new(default_socket_path())
}
pub async fn connect(&mut self) -> Result<(), ClientError> {
if self.stream.is_some() {
return Ok(());
}
match UnixStream::connect(&self.socket_path).await {
Ok(stream) => {
debug!("Connected to existing server");
self.stream = Some(BufReader::new(stream));
return Ok(());
}
Err(e) => {
debug!("Could not connect to server: {}", e);
}
}
Self::start_server()?;
for i in 0..50 {
crate::runtime::time::sleep(Duration::from_millis(100)).await;
match UnixStream::connect(&self.socket_path).await {
Ok(stream) => {
info!("Connected to server after {} attempts", i + 1);
self.stream = Some(BufReader::new(stream));
return Ok(());
}
Err(e) => {
if i % 10 == 9 {
debug!("Still waiting for server (attempt {}): {}", i + 1, e);
}
}
}
}
Err(ClientError::ServerTimeout)
}
fn start_server() -> Result<(), ClientError> {
info!("Starting server...");
let exe = std::env::current_exe().map_err(ClientError::ServerStart)?;
if has_systemd_run() {
info!("Launching server via systemd-run --scope");
let result = Command::new("systemd-run")
.args([
"--user",
"--scope",
"--collect",
"--unit=vessel-server",
"--",
])
.arg(&exe)
.args(["server", "--daemon"])
.spawn();
match result {
Ok(_) => return Ok(()),
Err(e) => {
warn!("systemd-run failed, falling back to bare spawn: {e}");
}
}
}
Command::new(&exe)
.arg("server")
.arg("--daemon")
.spawn()
.map_err(ClientError::ServerStart)?;
Ok(())
}
pub async fn request(&mut self, request: Request) -> Result<Response, ClientError> {
self.connect().await?;
let stream = self.stream.as_mut().ok_or(ClientError::ConnectionLost)?;
let mut json = serde_json::to_string(&request).map_err(ClientError::Serialize)?;
json.push('\n');
stream
.get_mut()
.write_all(json.as_bytes())
.await
.map_err(ClientError::Send)?;
let mut line = String::new();
let n = stream
.read_line(&mut line)
.await
.map_err(ClientError::Receive)?;
if n == 0 {
return Err(ClientError::ConnectionLost);
}
let response: Response = serde_json::from_str(&line).map_err(ClientError::Deserialize)?;
if let Response::Error { message } = &response {
warn!("Server returned error: {}", message);
}
Ok(response)
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
}