use anyhow::{Context, Result};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use super::api::{authorize, handle_command};
use super::recovery::startup_recovery;
use super::scheduler::scheduler_drain_loop;
use super::subscribe::handle_subscribe_stream;
pub(super) const DEFAULT_TCP_ADDR: &str = "127.0.0.1:39871";
#[cfg(unix)]
pub(super) async fn serve_unix() -> Result<()> {
let data_dir = mermaid_runtime::data_dir()?;
let socket_path = data_dir.join("mermaidd.sock");
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700))
.with_context(|| format!("failed to lock data dir {} to 0700", data_dir.display()))?;
}
let lock_path = data_dir.join("mermaidd.lock");
let _daemon_lock = match mermaid_runtime::try_exclusive_lock(&lock_path)
.with_context(|| format!("failed to open daemon lock {}", lock_path.display()))?
{
Some(file) => file,
None => anyhow::bail!(
"another mermaidd is starting or running (lock held on {}) — use `mermaid daemon restart`",
lock_path.display()
),
};
startup_recovery();
tokio::spawn(scheduler_drain_loop());
if socket_path.exists() {
if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
anyhow::bail!(
"a mermaidd daemon is already running on {} — use `mermaid daemon restart` to replace it",
socket_path.display()
);
}
std::fs::remove_file(&socket_path)
.with_context(|| format!("failed to remove stale socket {}", socket_path.display()))?;
}
let listener = tokio::net::UnixListener::bind(&socket_path)
.with_context(|| format!("failed to bind {}", socket_path.display()))?;
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600))
.with_context(|| {
format!(
"failed to lock control socket {} to 0600",
socket_path.display()
)
})?;
}
println!("mermaidd listening on {}", socket_path.display());
maybe_spawn_tcp_listener().await;
use std::os::unix::fs::MetadataExt;
let owner_uid = std::fs::metadata(&socket_path)
.with_context(|| format!("failed to stat control socket {}", socket_path.display()))?
.uid();
loop {
let stream = match listener.accept().await {
Ok((stream, _)) => stream,
Err(err) => {
tracing::warn!(error = %err, "mermaidd unix accept failed; continuing");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
continue;
},
};
match stream.peer_cred() {
Ok(cred) if uid_allowed(cred.uid(), owner_uid) => {},
Ok(cred) => {
tracing::warn!(
peer_uid = cred.uid(),
owner_uid,
"rejecting unix client: uid mismatch"
);
continue;
},
Err(err) => {
tracing::warn!(error = %err, "rejecting unix client: peer_cred failed");
continue;
},
}
tokio::spawn(async move {
if let Err(err) = handle_stream(stream).await {
tracing::warn!(error = %err, "mermaidd client failed");
}
});
}
}
#[cfg(unix)]
pub(super) fn uid_allowed(peer_uid: u32, owner_uid: u32) -> bool {
peer_uid == owner_uid || peer_uid == 0
}
#[cfg(windows)]
pub(super) async fn serve_windows() -> Result<()> {
use tokio::net::windows::named_pipe::ServerOptions;
let pipe_name = mermaid_runtime::daemon::daemon_pipe_name()?;
let mut security = mermaid_runtime::daemon::PipeSecurity::owner_only()?;
let mut server = match unsafe {
ServerOptions::new()
.first_pipe_instance(true)
.reject_remote_clients(true)
.create_with_security_attributes_raw(&pipe_name, security.attributes_ptr())
} {
Ok(server) => server,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => anyhow::bail!(
"a mermaidd daemon is already serving {pipe_name} — stop it before starting another"
),
Err(err) => {
return Err(err).with_context(|| format!("failed to create pipe {pipe_name}"));
},
};
startup_recovery();
tokio::spawn(scheduler_drain_loop());
println!("mermaidd listening on {pipe_name}");
maybe_spawn_tcp_listener().await;
loop {
if let Err(err) = server.connect().await {
tracing::warn!(error = %err, "mermaidd pipe connect failed; continuing");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
continue;
}
let next = loop {
match unsafe {
ServerOptions::new()
.reject_remote_clients(true)
.create_with_security_attributes_raw(&pipe_name, security.attributes_ptr())
} {
Ok(next) => break next,
Err(err) => {
tracing::warn!(error = %err, "mermaidd pipe re-create failed; retrying");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
},
}
};
let client = std::mem::replace(&mut server, next);
tokio::spawn(async move {
if let Err(err) = handle_stream(client).await {
tracing::warn!(error = %err, "mermaidd client failed");
}
});
}
}
pub(super) async fn maybe_spawn_tcp_listener() {
if !std::env::var("MERMAID_DAEMON_ENABLE_TCP")
.is_ok_and(|value| value == "1" || value == "true")
{
return;
}
let addr =
std::env::var("MERMAID_DAEMON_TCP_ADDR").unwrap_or_else(|_| DEFAULT_TCP_ADDR.to_string());
match tokio::net::TcpListener::bind(&addr).await {
Ok(listener) => {
if let Ok(local_addr) = listener.local_addr() {
if let Ok(dir) = mermaid_runtime::data_dir() {
let tcp_file = dir.join("mermaidd.tcp");
match std::fs::write(&tcp_file, local_addr.to_string()) {
Ok(()) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(err) = std::fs::set_permissions(
&tcp_file,
std::fs::Permissions::from_mode(0o600),
) {
tracing::warn!(file = %tcp_file.display(), error = %err, "failed to lock tcp hint file to 0600");
}
}
},
Err(err) => tracing::warn!(
file = %tcp_file.display(),
error = %err,
"failed to write tcp hint file; remote attach may not find the daemon"
),
}
}
println!("mermaidd tcp listening on {local_addr}");
}
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, _)) => {
tokio::spawn(async move {
if let Err(err) = handle_remote_stream(stream).await {
tracing::warn!(error = %err, "mermaidd tcp client failed");
}
});
},
Err(err) => {
tracing::warn!(error = %err, "mermaidd tcp accept failed");
break;
},
}
}
});
},
Err(err) => {
tracing::warn!(addr = %addr, error = %err, "mermaidd tcp listener disabled");
},
}
}
pub(super) async fn handle_stream<S>(stream: S) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
handle_stream_inner(stream, false).await
}
pub(super) async fn handle_remote_stream<S>(stream: S) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
handle_stream_inner(stream, true).await
}
pub(super) async fn handle_stream_inner<S>(stream: S, require_auth: bool) -> Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let timeout =
std::time::Duration::from_secs(mermaid_model::constants::DAEMON_CONNECTION_TIMEOUT_SECS);
let mut reader = BufReader::new(stream);
let line = match tokio::time::timeout(
timeout,
mermaid_model::utils::read_line_capped(
&mut reader,
mermaid_model::constants::MAX_DAEMON_COMMAND_BYTES,
),
)
.await
.map_err(|_| anyhow::anyhow!("client sent no complete command within the timeout"))??
{
mermaid_model::utils::CappedLine::Line(bytes) => {
String::from_utf8_lossy(&bytes).into_owned()
},
mermaid_model::utils::CappedLine::TooLong => {
anyhow::bail!("daemon command exceeded size cap")
},
mermaid_model::utils::CappedLine::Eof => String::new(),
};
let line = line.trim();
if let Some(request) = parse_subscribe(line) {
let authorized = !(require_auth || request_requires_auth_wire(line)) || {
let body: serde_json::Value = serde_json::from_str(line)?;
authorize(&body)?
};
let stream = reader.into_inner();
return handle_subscribe_stream(stream, request, authorized).await;
}
let response = tokio::time::timeout(timeout, handle_command(line, require_auth))
.await
.map_err(|_| anyhow::anyhow!("command handler exceeded the connection timeout"))??;
let mut stream = reader.into_inner();
stream.write_all(response.to_string().as_bytes()).await?;
stream.write_all(b"\n").await?;
stream.shutdown().await?;
Ok(())
}
pub(super) fn parse_subscribe(line: &str) -> Option<crate::runtime_client::DaemonRequest> {
if !line.starts_with('{') {
return None;
}
match serde_json::from_str::<crate::runtime_client::DaemonRequest>(line) {
Ok(req @ crate::runtime_client::DaemonRequest::SubscribeTask { .. }) => Some(req),
_ => None,
}
}
pub(super) fn request_requires_auth_wire(_line: &str) -> bool {
true
}