pub(crate) mod daemon;
pub(crate) mod graph;
pub(crate) mod microserver;
pub(crate) mod registry;
pub(crate) mod secrets;
use anyhow::{bail, Context, Result};
use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::path::Path;
use patina::paths;
pub use daemon::DaemonOptions;
#[derive(Debug, Clone, clap::Subcommand)]
pub enum MotherCommands {
Start {
#[arg(long)]
host: Option<String>,
#[arg(long, default_value = "50051")]
port: u16,
#[arg(long)]
mcp: bool,
},
Stop,
Status,
#[command(subcommand)]
Graph(GraphCommands),
}
#[derive(Debug, Clone, clap::Subcommand)]
pub enum GraphCommands {
Sync,
Show {
#[arg(long)]
nodes: bool,
#[arg(long)]
edges: bool,
},
Link {
from: String,
to: String,
edge_type: String,
#[arg(long)]
evidence: Option<String>,
},
Unlink {
from: String,
to: String,
edge_type: String,
},
Stats,
Learn {
#[arg(long, default_value = "0.1")]
alpha: f32,
},
}
pub fn execute_cli(
command: Option<MotherCommands>,
run_mcp: impl FnOnce() -> Result<()>,
) -> Result<()> {
match command {
None => {
println!("Mother daemon commands:\n");
println!(" patina mother start Start the daemon");
println!(" patina mother stop Stop the daemon (not yet implemented)");
println!(" patina mother status Show daemon status (not yet implemented)");
println!(" patina mother graph Graph operations\n");
println!("Run 'patina mother --help' for details.");
Ok(())
}
Some(MotherCommands::Start { host, port, mcp }) => {
if mcp {
run_mcp()
} else {
let options = DaemonOptions { host, port };
daemon::run_server(options)
}
}
Some(MotherCommands::Stop) => stop_daemon(),
Some(MotherCommands::Status) => show_status(),
Some(MotherCommands::Graph(graph_cmd)) => execute_graph(graph_cmd),
}
}
fn execute_graph(command: GraphCommands) -> Result<()> {
match command {
GraphCommands::Sync => graph::sync_from_registry(),
GraphCommands::Show { nodes, edges } => graph::show_graph(nodes, edges),
GraphCommands::Link {
from,
to,
edge_type,
evidence,
} => graph::add_link(&from, &to, &edge_type, evidence.as_deref()),
GraphCommands::Unlink {
from,
to,
edge_type,
} => graph::remove_link(&from, &to, &edge_type),
GraphCommands::Stats => graph::show_stats(),
GraphCommands::Learn { alpha } => graph::learn_weights(alpha),
}
}
fn stop_daemon() -> Result<()> {
let pid_path = paths::serve::pid_path();
if !pid_path.exists() {
println!("Mother daemon is not running (no PID file).");
return Ok(());
}
let pid_str = std::fs::read_to_string(&pid_path)
.with_context(|| format!("reading PID file {}", pid_path.display()))?;
let pid: i32 = pid_str
.trim()
.parse()
.with_context(|| format!("parsing PID from '{}'", pid_str.trim()))?;
let is_running = unsafe { libc::kill(pid, 0) == 0 };
if !is_running {
println!("Mother daemon is not running (stale PID file).");
let _ = std::fs::remove_file(&pid_path);
return Ok(());
}
println!("Stopping mother daemon (PID {})...", pid);
let result = unsafe { libc::kill(pid, libc::SIGTERM) };
if result != 0 {
let err = std::io::Error::last_os_error();
bail!("Failed to send SIGTERM to PID {}: {}", pid, err);
}
for i in 0..50 {
std::thread::sleep(std::time::Duration::from_millis(100));
let still_running = unsafe { libc::kill(pid, 0) == 0 };
if !still_running {
println!("Mother daemon stopped.");
let _ = std::fs::remove_file(&pid_path);
cleanup_socket();
return Ok(());
}
if i == 25 {
println!(" Still waiting...");
}
}
println!("Warning: daemon did not stop within 5 seconds.");
println!(" You may need to: kill -9 {}", pid);
Ok(())
}
fn show_status() -> Result<()> {
let pid_path = paths::serve::pid_path();
let socket_path = paths::serve::socket_path();
let pid = if pid_path.exists() {
match std::fs::read_to_string(&pid_path) {
Ok(s) => s.trim().parse::<i32>().ok(),
Err(_) => None,
}
} else {
None
};
let is_running = pid
.map(|p| unsafe { libc::kill(p, 0) == 0 })
.unwrap_or(false);
if !is_running {
println!("Mother daemon: stopped");
if pid.is_some() {
println!(" (stale PID file exists — run `patina mother stop` to clean up)");
}
return Ok(());
}
let pid = pid.unwrap();
println!("Mother daemon: running");
println!(" PID: {}", pid);
println!(" Socket: {}", socket_path.display());
match query_health() {
Ok(health) => {
println!(" Version: {}", health.version);
println!(" Uptime: {}s", health.uptime_secs);
if !health.children.is_empty() {
println!(" Children:");
for child in &health.children {
println!(" {}: {}", child.name, child.status);
}
}
}
Err(e) => {
println!(" Health check failed: {}", e);
}
}
Ok(())
}
#[derive(serde::Deserialize)]
struct HealthInfo {
version: String,
uptime_secs: u64,
#[serde(default)]
children: Vec<ChildHealthInfo>,
}
#[derive(serde::Deserialize)]
struct ChildHealthInfo {
name: String,
status: String,
}
fn query_health() -> Result<HealthInfo> {
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
let socket_path = paths::serve::socket_path();
let mut stream = UnixStream::connect(&socket_path)
.with_context(|| format!("connecting to {}", socket_path.display()))?;
stream.set_read_timeout(Some(std::time::Duration::from_secs(2)))?;
let request = "GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
stream.write_all(request.as_bytes())?;
let mut response = Vec::new();
stream.read_to_end(&mut response)?;
let response_str = String::from_utf8_lossy(&response);
let body_start = response_str.find("\r\n\r\n").map(|i| i + 4).unwrap_or(0);
let body = &response_str[body_start..];
serde_json::from_str(body).with_context(|| "parsing health response")
}
fn ensure_run_dir() -> Result<()> {
let run_dir = paths::serve::run_dir();
if !run_dir.exists() {
std::fs::create_dir_all(&run_dir)
.with_context(|| format!("creating runtime directory {}", run_dir.display()))?;
std::fs::set_permissions(&run_dir, std::fs::Permissions::from_mode(0o700))
.with_context(|| format!("setting permissions on {}", run_dir.display()))?;
} else {
let meta = std::fs::metadata(&run_dir)
.with_context(|| format!("reading metadata for {}", run_dir.display()))?;
let mode = meta.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
bail!(
"Refusing to start: {} has permissions {:o} (group/world accessible).\n \
Fix with: chmod 700 {}",
run_dir.display(),
mode,
run_dir.display()
);
}
}
Ok(())
}
fn cleanup_stale_socket(socket_path: &Path) -> Result<()> {
if !socket_path.exists() {
return Ok(());
}
let meta = std::fs::symlink_metadata(socket_path)
.with_context(|| format!("reading metadata for {}", socket_path.display()))?;
if !meta.file_type().is_socket() {
bail!(
"Refusing to start: {} exists but is not a socket.\n \
Remove manually if safe: rm {}",
socket_path.display(),
socket_path.display()
);
}
use std::os::unix::fs::MetadataExt;
let file_uid = meta.uid();
let my_uid = unsafe { libc::getuid() };
if file_uid != my_uid {
bail!(
"Refusing to start: {} is owned by uid {} (you are {}).\n \
This may indicate a security issue.",
socket_path.display(),
file_uid,
my_uid
);
}
std::fs::remove_file(socket_path)
.with_context(|| format!("removing stale socket {}", socket_path.display()))?;
Ok(())
}
pub fn setup_unix_listener() -> Result<std::os::unix::net::UnixListener> {
use std::os::unix::net::UnixListener;
ensure_run_dir()?;
let socket_path = paths::serve::socket_path();
cleanup_stale_socket(&socket_path)?;
let listener = UnixListener::bind(&socket_path)
.with_context(|| format!("binding socket {}", socket_path.display()))?;
std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600))
.with_context(|| format!("setting permissions on {}", socket_path.display()))?;
Ok(listener)
}
pub fn cleanup_socket() {
let socket_path = paths::serve::socket_path();
if socket_path.exists() {
let _ = std::fs::remove_file(&socket_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mother_command_variants() {
let start = MotherCommands::Start {
host: None,
port: 50051,
mcp: false,
};
assert!(matches!(start, MotherCommands::Start { .. }));
let graph = MotherCommands::Graph(GraphCommands::Sync);
assert!(matches!(graph, MotherCommands::Graph(_)));
}
}