use serde::Serialize;
use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::unix::OwnedWriteHalf;
use tokio::net::{UnixListener, UnixStream};
use tokio::runtime::Builder;
use tokio::signal::unix::{self, SignalKind};
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::{RecvError, TryRecvError};
use tokio::sync::watch;
use tokio::task::JoinSet;
use crate::daemon::api::{Reply, Request, Status};
use crate::daemon::machines::Machines;
use crate::daemon::{decider, egress, instance, state};
use crate::machine::Engine;
use crate::{Error, Result};
const MAX_REQUEST_BYTES: u64 = 64 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
pub(super) const TEARDOWN_TIMEOUT: Duration = Duration::from_mins(5);
pub(super) const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(100);
const LOG_LINES_MAX: usize = 1024;
#[derive(Debug, Clone, Copy)]
pub enum Mode {
Foreground,
Detached,
}
pub fn serve_in<E: Engine>(
dir: &Path,
mode: Mode,
out: &mut impl Write,
teardown_timeout: Duration,
) -> Result<()> {
if matches!(mode, Mode::Detached) {
rustix::process::setsid().map_err(|e| Error::DaemonServe(e.into()))?;
}
let dir = &state::prepare_in(dir, rustix::process::geteuid().as_raw())?;
let lock = instance::claim(dir)?;
let egress = Arc::new(egress::State::bind(dir)?);
let machines = Arc::new(Machines::new(dir, lock, Arc::clone(&egress)));
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.map_err(Error::DaemonServe)?;
let (tx, mut rx) = watch::channel(false);
let result = runtime.block_on(async {
let socket = instance::socket_path(dir);
let listener = UnixListener::bind(&socket).map_err(|e| Error::io(&socket, e))?;
let mut sigterm = unix::signal(SignalKind::terminate()).map_err(Error::DaemonServe)?;
let log = match mode {
Mode::Detached => Some(instance::open_log(dir)?),
Mode::Foreground => None,
};
let (log_tx, mut log_rx) = broadcast::channel::<String>(LOG_LINES_MAX);
let decider = tokio::spawn(decider::serve::<E::Gateway>(Arc::clone(&egress), log_tx));
announce_ready(out, &socket, log.as_ref())?;
let mut handlers = JoinSet::new();
loop {
tokio::select! {
biased;
_ = rx.wait_for(|&stop| stop) => break,
_ = sigterm.recv() => break,
Some(_) = handlers.join_next() => {}
res = listener.accept() => match res {
Ok((stream, _)) => {
handlers.spawn(handle::<E>(stream, tx.clone(), Arc::clone(&machines)));
}
Err(e) => {
drop(writeln!(out, "accept failed: {e}"));
tokio::time::sleep(ACCEPT_RETRY_DELAY).await;
}
},
Some(line) = next_line(&mut log_rx) => {
drop(writeln!(out, "{line}"));
}
}
}
while let Some(line) = queued_line(&mut log_rx) {
drop(writeln!(out, "{line}"));
}
teardown::<E>(&machines, &mut handlers, out, teardown_timeout).await;
decider.abort();
Ok(())
});
runtime.shutdown_background();
result
}
fn announce_ready(out: &mut impl Write, socket: &Path, log: Option<&File>) -> Result<()> {
if let Some(log) = log {
rustix::stdio::dup2_stderr(log).map_err(|e| Error::DaemonServe(e.into()))?;
}
writeln!(out, "listening on {}", socket.display()).map_err(Error::DaemonServe)?;
out.flush().map_err(Error::DaemonServe)?;
if let Some(log) = log {
rustix::stdio::dup2_stdout(log).map_err(|e| Error::DaemonServe(e.into()))?;
}
Ok(())
}
async fn next_line(log: &mut broadcast::Receiver<String>) -> Option<String> {
match log.recv().await {
Ok(line) => Some(line),
Err(RecvError::Lagged(missed)) => Some(dropped_lines(missed)),
Err(RecvError::Closed) => None,
}
}
fn queued_line(log: &mut broadcast::Receiver<String>) -> Option<String> {
match log.try_recv() {
Ok(line) => Some(line),
Err(TryRecvError::Lagged(missed)) => Some(dropped_lines(missed)),
Err(TryRecvError::Empty | TryRecvError::Closed) => None,
}
}
fn dropped_lines(missed: u64) -> String {
format!("egress: {missed} lines dropped: the log fell behind")
}
async fn teardown<E: Engine>(
machines: &Machines,
handlers: &mut JoinSet<io::Result<()>>,
out: &mut impl Write,
timeout: Duration,
) {
drop(machines.close(true));
drain(handlers, timeout, out).await;
stop_machines::<E>(machines, timeout, out).await;
drop(writeln!(out, "stopped"));
}
async fn drain(handlers: &mut JoinSet<io::Result<()>>, timeout: Duration, out: &mut impl Write) {
let drained = tokio::time::timeout(timeout, async {
while handlers.join_next().await.is_some() {}
})
.await;
if drained.is_err() {
drop(writeln!(
out,
"gave up waiting for {} request(s) after {timeout:?}",
handlers.len()
));
}
}
async fn stop_machines<E: Engine>(machines: &Machines, timeout: Duration, out: &mut impl Write) {
match machines.stop_all::<E>(timeout).await {
Ok(outcomes) => {
for (name, outcome) in outcomes {
drop(match outcome {
Ok(()) => writeln!(out, "stopped machine {name}"),
Err(e) => writeln!(out, "failed to stop machine {name}: {e}"),
});
}
}
Err(e) => {
drop(writeln!(out, "failed to list machines: {e}"));
}
}
}
async fn handle<E: Engine>(
stream: UnixStream,
tx: watch::Sender<bool>,
machines: Arc<Machines>,
) -> io::Result<()> {
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader.take(MAX_REQUEST_BYTES));
let mut line = String::new();
let Ok(read) = tokio::time::timeout(REQUEST_TIMEOUT, reader.read_line(&mut line)).await else {
return Ok(());
};
read?;
let request = serde_json::from_str::<Request>(&line);
match request {
Ok(Request::Status {}) => send(&mut writer, Ok(status())).await,
Ok(Request::Shutdown { force }) => shutdown(&mut writer, &machines, &tx, force).await,
Ok(Request::MachineStart { name, spec }) => {
send(&mut writer, machines.start::<E>(name, spec).await).await
}
Ok(Request::MachineStatus { name }) => {
send(&mut writer, machines.status::<E>(name).await).await
}
Ok(Request::MachineStop { name }) => {
send(&mut writer, machines.stop::<E>(name).await).await
}
Ok(Request::MachineList {}) => send(&mut writer, machines.list::<E>().await).await,
Ok(Request::MachineResolve { name }) => send(&mut writer, machines.resolve(name)).await,
Err(e) => invalid_request(&mut writer, &e).await,
}
}
fn status() -> Status {
Status {
pid: std::process::id(),
version: crate::VERSION.to_string(),
}
}
async fn shutdown(
writer: &mut OwnedWriteHalf,
machines: &Machines,
tx: &watch::Sender<bool>,
force: bool,
) -> io::Result<()> {
match machines.close(force) {
Ok(count) if count > 0 && !force => {
send(writer, Err::<(), _>(Error::MachinesRunning { count })).await
}
Ok(_) => {
let sent = send_reply(writer, &Reply::Done(())).await;
tx.send_replace(true);
sent
}
Err(e) => send(writer, Err::<(), _>(e)).await,
}
}
async fn invalid_request(writer: &mut OwnedWriteHalf, error: &serde_json::Error) -> io::Result<()> {
let reply: Reply<()> = Reply::Error {
message: format!("invalid request: {error}"),
};
send_reply(writer, &reply).await
}
async fn send_reply<T: serde::Serialize>(
stream: &mut OwnedWriteHalf,
reply: &Reply<T>,
) -> io::Result<()> {
let reply_json = serde_json::to_string(reply)?;
tokio::time::timeout(
REPLY_TIMEOUT,
stream.write_all(format!("{reply_json}\n").as_bytes()),
)
.await?
}
async fn send<T: Serialize>(stream: &mut OwnedWriteHalf, result: Result<T>) -> io::Result<()> {
let reply = match result {
Ok(value) => Reply::Done(value),
Err(error) => Reply::Error {
message: error.to_string(),
},
};
send_reply(stream, &reply).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn lines_lost_to_a_full_queue_are_counted() {
let (log, mut waited) = broadcast::channel(2);
let mut drained = log.subscribe();
for line in ["1", "2", "3", "4", "5"] {
log.send(line.to_string()).unwrap();
}
let lines = ["egress: 3 lines dropped: the log fell behind", "4", "5"];
for line in lines {
assert_eq!(next_line(&mut waited).await.as_deref(), Some(line));
}
assert_eq!(
std::iter::from_fn(|| queued_line(&mut drained)).collect::<Vec<_>>(),
lines
);
drop(log);
assert_eq!(next_line(&mut waited).await, None);
}
}