use anyhow::Context;
use oxibrain::{Brain, BrainConfig};
#[cfg(unix)]
use oxibrain_client::discovery::default_socket_path;
use oxibrain_ports::BrainError;
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::path::Path;
pub async fn run(
dir: &Path,
socket: Option<std::path::PathBuf>,
http: Option<String>,
require_token: bool,
daemon: bool,
ui_dir: Option<std::path::PathBuf>,
) -> anyhow::Result<()> {
let brain = match Brain::open(BrainConfig::at(dir)).await {
Ok(b) => b,
Err(BrainError::Locked { holder }) => {
anyhow::bail!(
"store is locked — another oxibrain process owns it ({holder}).\n\
If a daemon is already running, connect to it (e.g. via its socket) \
instead of starting a second one.\n\
To start a new daemon, ensure no other oxibrain process is running."
);
}
Err(e) => return Err(e.into()),
};
let _pid = if daemon {
let pid = oxibrain_mcp::PidFile::acquire(dir)
.map_err(|e| anyhow::anyhow!("write PID file: {e}"))?;
tracing::info!(
"daemon PID {} → {}",
std::process::id(),
pid.path().display()
);
Some(pid)
} else {
None
};
if let Some(addr_str) = http {
let addr: std::net::SocketAddr = addr_str
.parse()
.map_err(|e| anyhow::anyhow!("invalid --http address '{addr_str}': {e}"))?;
return oxibrain_mcp::serve_http(brain, addr, ui_dir).await;
}
let socket_path = match socket {
Some(p) => Some(p),
#[cfg(unix)]
None if daemon => Some(resolve_default_socket()?),
None => None,
};
match socket_path {
#[cfg(unix)]
Some(path) => {
prepare_socket_path(&path)?;
if require_token {
oxibrain_mcp::serve_socket_auth(brain, &path).await
} else {
tracing::warn!(
"serving on socket without --require-token: relying on filesystem \
permissions alone (DESIGN §11.2). Pass --require-token for token auth."
);
oxibrain_mcp::serve_socket(brain, &path).await
}
}
#[cfg(not(unix))]
Some(_) => anyhow::bail!("--socket is only supported on Unix"),
None => {
if require_token {
anyhow::bail!("--require-token requires --socket");
}
oxibrain_mcp::serve_stdio(brain).await
}
}
}
#[cfg(unix)]
fn resolve_default_socket() -> anyhow::Result<std::path::PathBuf> {
if let Some(p) = default_socket_path() {
return Ok(p);
}
anyhow::bail!(
"no default oxibrain socket: neither $OXIBRAIN_SOCKET nor $HOME is set. \
Specify --socket explicitly or export one of these environment variables."
);
}
#[cfg(unix)]
fn prepare_socket_path(path: &Path) -> anyhow::Result<()> {
use std::fs;
use std::io::ErrorKind;
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
fs::create_dir_all(parent)
.with_context(|| format!("create socket parent {}", parent.display()))?;
fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
.with_context(|| format!("tighten socket parent {} to 0o700", parent.display()))?;
} else if parent.exists() {
match std::fs::metadata(parent) {
Ok(meta) => {
let mode = meta.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
tracing::warn!(
"socket parent {} already exists with mode {mode:o}; not chmod-ing (operator-controlled). Other users may be able to reach the socket — use --require-token or restrict the directory manually.",
parent.display(),
);
}
}
Err(e) => tracing::warn!("could not stat socket parent {}: {e}", parent.display(),),
}
}
}
if let Ok(meta) = std::fs::symlink_metadata(path) {
let ft = meta.file_type();
if !(ft.is_socket() || ft.is_fifo()) {
anyhow::bail!("{} exists and is not a socket; cannot bind", path.display());
}
match futures_probe(path) {
Ok(()) => anyhow::bail!(
"{} is held by a live daemon; refusing to bind. If that daemon has crashed, remove the socket manually after verifying no process is listening.",
path.display()
),
Err(e)
if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::ConnectionRefused =>
{
fs::remove_file(path)
.with_context(|| format!("remove stale socket {}", path.display()))?;
}
Err(other) => {
anyhow::bail!(
"{} could not be probed ({}); refusing to bind to avoid clobbering an unreachable owner",
path.display(),
other
);
}
}
}
Ok(())
}
#[cfg(unix)]
fn futures_probe(path: &Path) -> std::io::Result<()> {
use std::io::{Error, ErrorKind};
let path = path.to_path_buf();
let handle = std::thread::Builder::new()
.name("oxibrain-socket-probe".into())
.spawn(move || -> std::io::Result<()> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| Error::new(ErrorKind::Other, format!("probe rt: {e}")))?;
rt.block_on(async move {
let stream = tokio::net::UnixStream::connect(&path).await?;
drop(stream);
Ok::<(), std::io::Error>(())
})
})
.map_err(|e| Error::new(ErrorKind::Other, format!("probe thread: {e}")))?;
handle
.join()
.map_err(|_| Error::new(ErrorKind::Other, "probe thread panicked"))?
}
#[cfg(not(unix))]
fn prepare_socket_path(_path: &Path) -> anyhow::Result<()> {
Ok(())
}