crabtalk_command/service/
mod.rs1use std::path::Path;
4use wcore::paths::{CONFIG_DIR, LOGS_DIR, RUN_DIR};
5
6#[cfg(target_os = "linux")]
7mod linux;
8#[cfg(target_os = "macos")]
9mod macos;
10#[cfg(not(any(target_os = "macos", target_os = "linux")))]
11mod unknown;
12
13#[cfg(target_os = "macos")]
16pub use macos::{TEMPLATE, install, is_installed, uninstall};
17
18#[cfg(target_os = "linux")]
19pub use linux::{TEMPLATE, install, is_installed, uninstall};
20
21#[cfg(not(any(target_os = "macos", target_os = "linux")))]
22pub use unknown::{install, is_installed, uninstall};
23
24pub trait Service {
30 fn name(&self) -> &str;
32 fn description(&self) -> &str;
34 fn label(&self) -> &str;
36
37 fn start(&self, force: bool) -> anyhow::Result<()> {
42 if !force && is_installed(self.label()) {
43 println!("{} is already running", self.name());
44 return Ok(());
45 }
46 let binary = std::env::current_exe()?;
47 let rendered = render_service_template(self, &binary);
48 install(&rendered, self.label())
49 }
50
51 fn stop(&self) -> anyhow::Result<()> {
53 uninstall(self.label())?;
54 let port_file = RUN_DIR.join(format!("{}.port", self.name()));
55 let _ = std::fs::remove_file(&port_file);
56 Ok(())
57 }
58
59 fn logs(&self, tail_args: &[String]) -> anyhow::Result<()> {
61 view_logs(self.name(), tail_args)
62 }
63}
64
65pub fn verbose_flag(count: u8) -> String {
67 if count == 0 {
68 String::new()
69 } else {
70 format!("-{}", "v".repeat(count as usize))
71 }
72}
73
74#[cfg(any(target_os = "macos", target_os = "linux"))]
76pub fn render_service_template(svc: &(impl Service + ?Sized), binary: &Path) -> String {
77 let path_env = std::env::var("PATH").unwrap_or_default();
78 TEMPLATE
79 .replace("{label}", svc.label())
80 .replace("{description}", svc.description())
81 .replace("{log_name}", svc.name())
82 .replace("{binary}", &binary.display().to_string())
83 .replace("{logs_dir}", &LOGS_DIR.display().to_string())
84 .replace("{config_dir}", &CONFIG_DIR.display().to_string())
85 .replace("{path}", &path_env)
86}
87
88#[cfg(not(any(target_os = "macos", target_os = "linux")))]
89pub fn render_service_template(_svc: &(impl Service + ?Sized), _binary: &Path) -> String {
90 String::new()
91}
92
93pub fn view_logs(log_name: &str, tail_args: &[String]) -> anyhow::Result<()> {
99 let path = LOGS_DIR.join(format!("{log_name}.log"));
100 if !path.exists() {
101 anyhow::bail!("log file not found: {}", path.display());
102 }
103
104 let args = if tail_args.is_empty() {
105 vec!["-n".to_owned(), "50".to_owned()]
106 } else {
107 tail_args.to_vec()
108 };
109
110 let status = std::process::Command::new("tail")
111 .args(&args)
112 .arg(&path)
113 .status()
114 .map_err(|e| anyhow::anyhow!("failed to run tail: {e}"))?;
115 if !status.success() {
116 anyhow::bail!("tail exited with {status}");
117 }
118 Ok(())
119}
120
121#[cfg(feature = "mcp")]
126pub async fn run_mcp(svc: &(impl McpService + Sync)) -> anyhow::Result<()> {
127 let router = svc.router();
128 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
129 let addr = listener.local_addr()?;
130 std::fs::create_dir_all(&*RUN_DIR)?;
131 std::fs::write(
132 RUN_DIR.join(format!("{}.port", svc.name())),
133 addr.port().to_string(),
134 )?;
135 eprintln!("MCP server listening on {addr}");
136 axum::serve(listener, router).await?;
137 Ok(())
138}
139
140#[cfg(feature = "mcp")]
144pub trait McpService: Service {
145 fn router(&self) -> axum::Router;
147}