use std::path::Path;
use wcore::paths::{CONFIG_DIR, LOGS_DIR, service_log_path, service_port_file};
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
mod unknown;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "macos")]
pub use macos::{TEMPLATE, install, is_installed, uninstall};
#[cfg(target_os = "linux")]
pub use linux::{TEMPLATE, install, is_installed, uninstall};
#[cfg(target_os = "windows")]
pub use windows::{TEMPLATE, install, is_installed, uninstall};
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
pub use unknown::{install, is_installed, uninstall};
pub trait Service {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn label(&self) -> &str;
fn start(&self, force: bool) -> anyhow::Result<()> {
if !force && is_installed(self.label()) {
println!("{} is already running", self.name());
return Ok(());
}
let binary = std::env::current_exe()?;
let rendered = render_service_template(self, &binary);
install(&rendered, self.label())
}
fn stop(&self) -> anyhow::Result<()> {
uninstall(self.label())?;
let _ = std::fs::remove_file(service_port_file(self.name()));
Ok(())
}
fn logs(&self, tail_args: &[String]) -> anyhow::Result<()> {
view_logs(self.name(), tail_args)
}
}
pub fn verbose_flag(count: u8) -> String {
if count == 0 {
String::new()
} else {
format!("-{}", "v".repeat(count as usize))
}
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub fn render_service_template(svc: &(impl Service + ?Sized), binary: &Path) -> String {
let path_env = std::env::var("PATH").unwrap_or_default();
TEMPLATE
.replace("{label}", svc.label())
.replace("{description}", svc.description())
.replace("{log_name}", svc.name())
.replace("{binary}", &binary.display().to_string())
.replace("{logs_dir}", &LOGS_DIR.display().to_string())
.replace("{config_dir}", &CONFIG_DIR.display().to_string())
.replace("{path}", &path_env)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn render_service_template(_svc: &(impl Service + ?Sized), _binary: &Path) -> String {
String::new()
}
#[cfg(unix)]
pub fn view_logs(log_name: &str, tail_args: &[String]) -> anyhow::Result<()> {
let path = service_log_path(log_name);
if !path.exists() {
println!("no logs yet: {}", path.display());
return Ok(());
}
let args = if tail_args.is_empty() {
vec!["-n".to_owned(), "50".to_owned()]
} else {
tail_args.to_vec()
};
let status = std::process::Command::new("tail")
.args(&args)
.arg(&path)
.status()
.map_err(|e| anyhow::anyhow!("failed to run tail: {e}"))?;
if !status.success() {
anyhow::bail!("tail exited with {status}");
}
Ok(())
}
#[cfg(not(unix))]
pub fn view_logs(log_name: &str, tail_args: &[String]) -> anyhow::Result<()> {
use std::io::{BufRead, BufReader};
let path = service_log_path(log_name);
if !path.exists() {
println!("no logs yet: {}", path.display());
return Ok(());
}
let n: usize = parse_tail_n(tail_args).unwrap_or(50);
let file = std::fs::File::open(&path)
.map_err(|e| anyhow::anyhow!("failed to open {}: {e}", path.display()))?;
let lines: Vec<String> = BufReader::new(file).lines().collect::<Result<_, _>>()?;
let start = lines.len().saturating_sub(n);
for line in &lines[start..] {
println!("{line}");
}
Ok(())
}
#[cfg(not(unix))]
fn parse_tail_n(args: &[String]) -> Option<usize> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "-n" {
return iter.next().and_then(|v| v.parse().ok());
}
if let Some(n) = arg.strip_prefix("-n") {
return n.parse().ok();
}
}
None
}
#[cfg(feature = "mcp")]
pub async fn run_mcp(svc: &(impl McpService + Sync)) -> anyhow::Result<()> {
use wcore::paths::RUN_DIR;
let router = svc.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
std::fs::create_dir_all(&*RUN_DIR)?;
std::fs::write(service_port_file(svc.name()), addr.port().to_string())?;
eprintln!("MCP server listening on {addr}");
axum::serve(listener, router).await?;
Ok(())
}
#[cfg(feature = "mcp")]
pub trait McpService: Service {
fn router(&self) -> axum::Router;
}