use std::path::{Path, PathBuf};
use tokio::sync::{mpsc, oneshot};
#[derive(Debug)]
pub enum CtlRequest {
Boundary { note: Option<String>, reply: oneshot::Sender<serde_json::Value> },
Sync { reply: oneshot::Sender<serde_json::Value> },
Status { reply: oneshot::Sender<serde_json::Value> },
}
pub fn bind(path: &Path) -> std::io::Result<tokio::net::UnixListener> {
if let Ok(m) = std::fs::symlink_metadata(path) {
use std::os::unix::fs::FileTypeExt;
if !m.file_type().is_socket() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"control socket path is occupied by a non-socket",
));
}
let _ = std::fs::remove_file(path);
}
tokio::net::UnixListener::bind(path)
}
pub async fn serve(listener: tokio::net::UnixListener, tx: mpsc::Sender<CtlRequest>) {
loop {
let (stream, _) = match listener.accept().await {
Ok(v) => v,
Err(e) => {
eprintln!("flint-sync: ctl.sock accept failed: {e}");
return;
}
};
let tx = tx.clone();
tokio::spawn(async move {
if let Err(e) = handle_conn(stream, tx).await {
eprintln!("flint-sync: ctl.sock connection: {e}");
}
});
}
}
const MAX_REQUEST_BYTES: usize = 16 * 1024;
async fn handle_conn(
mut stream: tokio::net::UnixStream,
tx: mpsc::Sender<CtlRequest>,
) -> std::io::Result<()> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
let n = stream.read(&mut chunk).await?;
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
if buf.windows(4).any(|w| w == b"\r\n\r\n") || buf.len() >= MAX_REQUEST_BYTES {
break;
}
}
let head = String::from_utf8_lossy(&buf);
let mut parts = head.lines().next().unwrap_or("").split_whitespace();
let method = parts.next().unwrap_or("");
let path = parts.next().unwrap_or("");
let (code, body) = match (method, path) {
("POST", "/v1/boundary") => {
let (reply, rx) = oneshot::channel();
match tx.send(CtlRequest::Boundary { note: None, reply }).await {
Ok(()) => match rx.await {
Ok(v) => (200, v),
Err(_) => (503, json_err("the syncer did not answer")),
},
Err(_) => (503, json_err("the syncer run loop is gone")),
}
}
("POST", "/v1/sync") => {
let (reply, rx) = oneshot::channel();
match tx.send(CtlRequest::Sync { reply }).await {
Ok(()) => match rx.await {
Ok(v) => (200, v),
Err(_) => (503, json_err("the syncer did not answer")),
},
Err(_) => (503, json_err("the syncer run loop is gone")),
}
}
("GET", "/v1/status") => {
let (reply, rx) = oneshot::channel();
match tx.send(CtlRequest::Status { reply }).await {
Ok(()) => match rx.await {
Ok(v) => (200, v),
Err(_) => (503, json_err("the syncer did not answer")),
},
Err(_) => (503, json_err("the syncer run loop is gone")),
}
}
_ => (
404,
json_err("unknown verb (POST /v1/boundary, POST /v1/sync, GET /v1/status)"),
),
};
let body = serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec());
let resp = format!(
"HTTP/1.1 {code} {}\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\
connection: close\r\n\r\n",
match code {
200 => "OK",
404 => "Not Found",
_ => "Service Unavailable",
},
body.len()
);
stream.write_all(resp.as_bytes()).await?;
stream.write_all(&body).await?;
stream.flush().await?;
Ok(())
}
fn json_err(msg: &str) -> serde_json::Value {
serde_json::json!({ "status": "error", "message": msg })
}
pub fn socket_path(state_dir: &Path) -> PathBuf {
state_dir.join("ctl.sock")
}