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", target_os = "windows")))]
11mod unknown;
12#[cfg(target_os = "windows")]
13mod windows;
14
15#[cfg(target_os = "macos")]
18pub use macos::{TEMPLATE, install, is_installed, uninstall};
19
20#[cfg(target_os = "linux")]
21pub use linux::{TEMPLATE, install, is_installed, uninstall};
22
23#[cfg(target_os = "windows")]
24pub use windows::{TEMPLATE, install, is_installed, uninstall};
25
26#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
27pub use unknown::{install, is_installed, uninstall};
28
29pub trait Service {
35 fn name(&self) -> &str;
37 fn description(&self) -> &str;
39 fn label(&self) -> &str;
41
42 fn start(&self, force: bool) -> anyhow::Result<()> {
47 if !force && is_installed(self.label()) {
48 println!("{} is already running", self.name());
49 return Ok(());
50 }
51 let binary = std::env::current_exe()?;
52 let rendered = render_service_template(self, &binary);
53 install(&rendered, self.label())
54 }
55
56 fn stop(&self) -> anyhow::Result<()> {
58 uninstall(self.label())?;
59 let port_file = RUN_DIR.join(format!("{}.port", self.name()));
60 let _ = std::fs::remove_file(&port_file);
61 Ok(())
62 }
63
64 fn logs(&self, tail_args: &[String]) -> anyhow::Result<()> {
66 view_logs(self.name(), tail_args)
67 }
68}
69
70pub fn verbose_flag(count: u8) -> String {
72 if count == 0 {
73 String::new()
74 } else {
75 format!("-{}", "v".repeat(count as usize))
76 }
77}
78
79#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
81pub fn render_service_template(svc: &(impl Service + ?Sized), binary: &Path) -> String {
82 let path_env = std::env::var("PATH").unwrap_or_default();
83 TEMPLATE
84 .replace("{label}", svc.label())
85 .replace("{description}", svc.description())
86 .replace("{log_name}", svc.name())
87 .replace("{binary}", &binary.display().to_string())
88 .replace("{logs_dir}", &LOGS_DIR.display().to_string())
89 .replace("{config_dir}", &CONFIG_DIR.display().to_string())
90 .replace("{path}", &path_env)
91}
92
93#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
94fn render_service_template(_svc: &(impl Service + ?Sized), _binary: &Path) -> String {
95 String::new()
96}
97
98#[cfg(unix)]
105pub fn view_logs(log_name: &str, tail_args: &[String]) -> anyhow::Result<()> {
106 let path = LOGS_DIR.join(format!("{log_name}.log"));
107 if !path.exists() {
108 println!("no logs yet: {}", path.display());
109 return Ok(());
110 }
111
112 let args = if tail_args.is_empty() {
113 vec!["-n".to_owned(), "50".to_owned()]
114 } else {
115 tail_args.to_vec()
116 };
117
118 let status = std::process::Command::new("tail")
119 .args(&args)
120 .arg(&path)
121 .status()
122 .map_err(|e| anyhow::anyhow!("failed to run tail: {e}"))?;
123 if !status.success() {
124 anyhow::bail!("tail exited with {status}");
125 }
126 Ok(())
127}
128
129#[cfg(not(unix))]
130pub fn view_logs(log_name: &str, tail_args: &[String]) -> anyhow::Result<()> {
131 use std::io::{BufRead, BufReader};
132
133 let path = LOGS_DIR.join(format!("{log_name}.log"));
134 if !path.exists() {
135 println!("no logs yet: {}", path.display());
136 return Ok(());
137 }
138
139 let n: usize = parse_tail_n(tail_args).unwrap_or(50);
140 let file = std::fs::File::open(&path)
141 .map_err(|e| anyhow::anyhow!("failed to open {}: {e}", path.display()))?;
142 let lines: Vec<String> = BufReader::new(file).lines().collect::<Result<_, _>>()?;
143 let start = lines.len().saturating_sub(n);
144 for line in &lines[start..] {
145 println!("{line}");
146 }
147 Ok(())
148}
149
150#[cfg(not(unix))]
152fn parse_tail_n(args: &[String]) -> Option<usize> {
153 let mut iter = args.iter();
154 while let Some(arg) = iter.next() {
155 if arg == "-n" {
156 return iter.next().and_then(|v| v.parse().ok());
157 }
158 if let Some(n) = arg.strip_prefix("-n") {
159 return n.parse().ok();
160 }
161 }
162 None
163}
164
165#[cfg(feature = "mcp")]
170pub async fn run_mcp(svc: &(impl McpService + Sync)) -> anyhow::Result<()> {
171 let router = svc.router();
172 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
173 let addr = listener.local_addr()?;
174 std::fs::create_dir_all(&*RUN_DIR)?;
175 std::fs::write(
176 RUN_DIR.join(format!("{}.port", svc.name())),
177 addr.port().to_string(),
178 )?;
179 eprintln!("MCP server listening on {addr}");
180 axum::serve(listener, router).await?;
181 Ok(())
182}
183
184#[cfg(feature = "mcp")]
188pub trait McpService: Service {
189 fn router(&self) -> axum::Router;
191}