use std::path::{Path, PathBuf};
use std::sync::Arc;
use mcpmesh_local_api::client::ClientError;
use mcpmesh_local_api::{ControlClient, connect_control_io};
use crate::config::Config;
use crate::control::serve_control_io;
use crate::daemon::boot::{BootedNode, start_node};
use crate::paths::NodePaths;
#[derive(Debug, thiserror::Error)]
pub enum StartError {
#[error("config error: {0:#}")]
Config(#[source] anyhow::Error),
#[error("data dir already in use by another node: {path}")]
DataDirInUse { path: PathBuf },
#[error(transparent)]
Other(anyhow::Error),
}
impl StartError {
pub(crate) fn classify(e: anyhow::Error, _config_path: &Path, db_path: &Path) -> StartError {
if e.chain()
.any(|c| c.downcast_ref::<redb::DatabaseError>().is_some())
{
return StartError::DataDirInUse {
path: db_path.to_path_buf(),
};
}
if e.chain()
.any(|c| c.downcast_ref::<figment::Error>().is_some())
{
return StartError::Config(e);
}
StartError::Other(e)
}
}
pub struct NodeBuilder {
root: PathBuf,
config: Option<Config>,
}
impl NodeBuilder {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
config: None,
}
}
pub fn config(mut self, config: Config) -> Self {
self.config = Some(config);
self
}
pub async fn start(self) -> Result<Node, StartError> {
let paths = NodePaths::under_root(&self.root);
let booted = start_node(paths, self.config).await?;
Ok(Node { booted })
}
}
pub struct Node {
booted: BootedNode,
}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Node")
.field("endpoint_id", &self.endpoint_id())
.finish_non_exhaustive()
}
}
impl Node {
pub async fn control(&self) -> Result<ControlClient, ClientError> {
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
let (server_read, server_write) = tokio::io::split(server_io);
let state = self.booted.state.clone();
let track_state = state.clone();
let handle = tokio::spawn(async move {
if let Err(e) = serve_control_io(server_read, server_write, state).await {
tracing::debug!(%e, "in-process control connection ended");
}
});
track_state.track_control_task(handle);
let (client_read, client_write) = tokio::io::split(client_io);
connect_control_io(client_read, client_write).await
}
pub fn endpoint_id(&self) -> iroh::EndpointId {
self.mesh().endpoint.id()
}
pub async fn wait(&self) {
self.booted.state.shutdown_requested().await;
}
pub async fn shutdown(self) {
let state = &self.booted.state;
state.request_shutdown();
state.abort_control_tasks();
let mesh = self
.booted
.state
.mesh()
.expect("a started Node always owns a mesh")
.clone();
if let Some(task) = mesh.accept_task.lock().await.take() {
task.abort();
}
if let Some(task) = mesh.poll_loop.lock().await.take() {
task.abort();
}
for task in self.booted.background {
task.abort();
}
mesh.endpoint.close().await;
}
fn mesh(&self) -> &Arc<crate::daemon::MeshState> {
self.booted
.state
.mesh()
.expect("a started Node always owns a mesh")
}
}