1use crate::config::Config;
4use anyhow::{Context, Result};
5use std::fs;
6use std::path::PathBuf;
7use tracing::info;
8
9pub fn gateway_pid_path() -> PathBuf {
15 Config::hermes_home().join("gateway.pid")
16}
17
18pub fn gateway_state_path() -> PathBuf {
20 Config::hermes_home().join("gateway_state.json")
21}
22
23pub fn is_gateway_running() -> bool {
25 get_running_pid().is_some()
26}
27
28pub fn get_running_pid() -> Option<u32> {
30 let pid_path = gateway_pid_path();
31 if !pid_path.exists() {
32 return None;
33 }
34
35 let content = fs::read_to_string(&pid_path).ok()?;
36 let content = content.trim();
37
38 if content.starts_with('{') {
40 if let Ok(data) = serde_json::from_str::<serde_json::Value>(content) {
41 if let Some(pid) = data.get("pid").and_then(|p| p.as_u64()) {
42 let pid = pid as u32;
43 if is_process_alive(pid) {
44 return Some(pid);
45 } else {
46 let _ = fs::remove_file(&pid_path);
48 return None;
49 }
50 }
51 }
52 return None;
53 }
54
55 let pid: u32 = content.parse().ok()?;
57 if is_process_alive(pid) {
58 Some(pid)
59 } else {
60 let _ = fs::remove_file(&pid_path);
62 None
63 }
64}
65
66fn is_process_alive(pid: u32) -> bool {
68 #[cfg(target_os = "windows")]
70 {
71 std::process::Command::new("tasklist")
72 .args(["/FI", &format!("PID eq {}", pid), "/NH"])
73 .output()
74 .map(|output| {
75 let stdout = String::from_utf8_lossy(&output.stdout);
76 stdout.contains(&pid.to_string())
77 })
78 .unwrap_or(false)
79 }
80
81 #[cfg(not(target_os = "windows"))]
82 {
83 unsafe { libc::kill(pid as i32, 0) == 0 }
85 }
86}
87
88pub fn write_pid_file() -> Result<()> {
90 let pid_path = gateway_pid_path();
91 if let Some(parent) = pid_path.parent() {
92 fs::create_dir_all(parent).context("failed to create hermes home directory")?;
93 }
94 let pid = std::process::id();
95 let data = serde_json::json!({
96 "pid": pid,
97 "kind": "hermes-gateway",
98 "argv": std::env::args().collect::<Vec<_>>(),
99 "start_time": chrono::Utc::now().to_rfc3339(),
100 });
101 let content = serde_json::to_string_pretty(&data).context("failed to serialize PID data")?;
102 fs::write(&pid_path, content).context("failed to write gateway PID file")?;
103 info!("wrote gateway PID file with pid={}", pid);
104 Ok(())
105}
106
107pub fn remove_pid_file() -> Result<()> {
109 let pid_path = gateway_pid_path();
110 if pid_path.exists() {
111 fs::remove_file(&pid_path).context("failed to remove gateway PID file")?;
112 }
113 Ok(())
114}
115
116#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct GatewayState {
123 pub gateway_state: String,
124 pub pid: u32,
125 pub platform: Option<String>,
126 pub platform_state: Option<String>,
127 pub restart_requested: bool,
128 pub active_agents: u32,
129 pub updated_at: String,
130}
131
132impl Default for GatewayState {
133 fn default() -> Self {
134 Self {
135 gateway_state: "stopped".to_string(),
136 pid: std::process::id(),
137 platform: None,
138 platform_state: None,
139 restart_requested: false,
140 active_agents: 0,
141 updated_at: chrono::Utc::now().to_rfc3339(),
142 }
143 }
144}
145
146pub fn read_gateway_state() -> Option<GatewayState> {
148 let state_path = gateway_state_path();
149 if !state_path.exists() {
150 return None;
151 }
152 let content = fs::read_to_string(&state_path).ok()?;
153 serde_json::from_str(&content).ok()
154}
155
156pub fn write_gateway_state(state: &GatewayState) -> Result<()> {
158 let state_path = gateway_state_path();
159 if let Some(parent) = state_path.parent() {
160 fs::create_dir_all(parent).context("failed to create hermes home directory")?;
161 }
162 let content =
163 serde_json::to_string_pretty(state).context("failed to serialize gateway state")?;
164 fs::write(&state_path, content).context("failed to write gateway state file")?;
165 Ok(())
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum Platform {
175 Cli,
176 Telegram,
177 Discord,
178 Slack,
179 WhatsApp,
180 Webhook,
181}
182
183impl Platform {
184 pub fn parse(s: &str) -> Option<Self> {
185 match s.to_lowercase().as_str() {
186 "cli" | "local" => Some(Platform::Cli),
187 "telegram" | "tg" => Some(Platform::Telegram),
188 "discord" | "dc" => Some(Platform::Discord),
189 "slack" | "sl" => Some(Platform::Slack),
190 "whatsapp" | "wa" => Some(Platform::WhatsApp),
191 "webhook" | "http" => Some(Platform::Webhook),
192 _ => None,
193 }
194 }
195}
196
197impl std::str::FromStr for Platform {
198 type Err = String;
199 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
200 Self::parse(s).ok_or_else(|| format!("Invalid platform: {}", s))
201 }
202}
203
204impl Platform {
205 pub fn as_str(&self) -> &'static str {
206 match self {
207 Platform::Cli => "cli",
208 Platform::Telegram => "telegram",
209 Platform::Discord => "discord",
210 Platform::Slack => "slack",
211 Platform::WhatsApp => "whatsapp",
212 Platform::Webhook => "webhook",
213 }
214 }
215
216 pub fn all() -> &'static [Platform] {
218 &[
219 Platform::Cli,
220 Platform::Telegram,
221 Platform::Discord,
222 Platform::Slack,
223 Platform::WhatsApp,
224 Platform::Webhook,
225 ]
226 }
227}
228
229impl std::fmt::Display for Platform {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 write!(f, "{}", self.as_str())
232 }
233}
234
235const SERVICE_NAME: &str = "HermesGateway";
241const SERVICE_DISPLAY_NAME: &str = "Hermes Agent Gateway";
242
243pub fn is_service_installed() -> bool {
245 #[cfg(target_os = "windows")]
246 {
247 let output = std::process::Command::new("sc").args(["query", SERVICE_NAME]).output();
248 match output {
249 Ok(out) => {
250 let stdout = String::from_utf8_lossy(&out.stdout);
251 stdout.contains("RUNNING")
252 || stdout.contains("STOPPED")
253 || stdout.contains("PAUSED")
254 }
255 Err(_) => false,
256 }
257 }
258 #[cfg(not(target_os = "windows"))]
259 {
260 false
261 }
262}
263
264pub fn get_service_status() -> ServiceStatus {
266 #[cfg(target_os = "windows")]
267 {
268 let output = std::process::Command::new("sc").args(["query", SERVICE_NAME]).output();
269
270 match output {
271 Ok(out) => {
272 let stdout = String::from_utf8_lossy(&out.stdout);
273 if stdout.contains("RUNNING") {
274 ServiceStatus::Running
275 } else if stdout.contains("STOPPED") {
276 ServiceStatus::Stopped
277 } else if stdout.contains("PAUSED") {
278 ServiceStatus::Paused
279 } else if stdout.contains("START_PENDING") {
280 ServiceStatus::StartPending
281 } else if stdout.contains("STOP_PENDING") {
282 ServiceStatus::StopPending
283 } else {
284 ServiceStatus::NotFound
285 }
286 }
287 Err(_) => ServiceStatus::NotFound,
288 }
289 }
290 #[cfg(not(target_os = "windows"))]
291 {
292 ServiceStatus::NotApplicable
293 }
294}
295
296pub fn install_service() -> Result<()> {
298 let exe_path = std::env::current_exe().context("failed to get current executable path")?;
299 let exe_str = exe_path.to_string_lossy();
300
301 let bin_path = format!("{} gateway run", exe_str);
303
304 #[cfg(target_os = "windows")]
305 {
306 let output = std::process::Command::new("sc")
307 .args([
308 "create",
309 SERVICE_NAME,
310 "binPath=",
311 &bin_path,
312 "start=",
313 "auto",
314 "DisplayName=",
315 SERVICE_DISPLAY_NAME,
316 ])
317 .output()
318 .context("failed to run sc.exe to create service")?;
319
320 if !output.status.success() {
321 let stderr = String::from_utf8_lossy(&output.stderr);
322 anyhow::bail!("Failed to create service: {}", stderr);
323 }
324
325 let _ = std::process::Command::new("sc")
327 .args([
328 "failure",
329 SERVICE_NAME,
330 "reset=",
331 "86400", "actions=",
333 "restart/30000/restart/60000/restart/120000", ])
335 .output();
336
337 let desc = "Hermes Agent Gateway - Messaging platform integration service";
339 let _ = std::process::Command::new("sc").args(["description", SERVICE_NAME, desc]).output();
340
341 info!("installed Hermes gateway service");
342 Ok(())
343 }
344
345 #[cfg(not(target_os = "windows"))]
346 {
347 let _ = bin_path; anyhow::bail!("Windows service installation is only available on Windows");
349 }
350}
351
352pub fn uninstall_service() -> Result<()> {
354 #[cfg(target_os = "windows")]
355 {
356 let _ = stop_service();
358
359 let output = std::process::Command::new("sc")
361 .args(["delete", SERVICE_NAME])
362 .output()
363 .context("failed to run sc.exe to delete service")?;
364
365 if !output.status.success() {
366 let stderr = String::from_utf8_lossy(&output.stderr);
367 if !stderr.contains("does not exist") && !stderr.contains("1060") {
369 anyhow::bail!("Failed to delete service: {}", stderr);
370 }
371 }
372
373 info!("uninstalled Hermes gateway service");
374 Ok(())
375 }
376
377 #[cfg(not(target_os = "windows"))]
378 {
379 anyhow::bail!("Windows service uninstallation is only available on Windows");
380 }
381}
382
383pub fn start_service() -> Result<()> {
385 #[cfg(target_os = "windows")]
386 {
387 let output = std::process::Command::new("sc")
388 .args(["start", SERVICE_NAME])
389 .output()
390 .context("failed to run sc.exe to start service")?;
391
392 if !output.status.success() {
393 let stderr = String::from_utf8_lossy(&output.stderr);
394 anyhow::bail!("Failed to start service: {}", stderr);
395 }
396 info!("started Hermes gateway service");
397 Ok(())
398 }
399
400 #[cfg(not(target_os = "windows"))]
401 {
402 anyhow::bail!("Windows service start is only available on Windows");
403 }
404}
405
406pub fn stop_service() -> Result<()> {
408 #[cfg(target_os = "windows")]
409 {
410 let output = std::process::Command::new("sc")
411 .args(["stop", SERVICE_NAME])
412 .output()
413 .context("failed to run sc.exe to stop service")?;
414
415 let stderr = String::from_utf8_lossy(&output.stderr);
417 if !output.status.success() && !stderr.contains("1062") && !stderr.contains("not started") {
418 let stdout = String::from_utf8_lossy(&output.stdout);
419 if !stdout.contains("not started") {
420 }
422 }
423 info!("stopped Hermes gateway service");
424 Ok(())
425 }
426
427 #[cfg(not(target_os = "windows"))]
428 {
429 anyhow::bail!("Windows service stop is only available on Windows");
430 }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435pub enum ServiceStatus {
436 Running,
437 Stopped,
438 Paused,
439 StartPending,
440 StopPending,
441 NotFound,
442 NotApplicable,
443}
444
445impl std::fmt::Display for ServiceStatus {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 match self {
448 ServiceStatus::Running => write!(f, "running"),
449 ServiceStatus::Stopped => write!(f, "stopped"),
450 ServiceStatus::Paused => write!(f, "paused"),
451 ServiceStatus::StartPending => write!(f, "start_pending"),
452 ServiceStatus::StopPending => write!(f, "stop_pending"),
453 ServiceStatus::NotFound => write!(f, "not_found"),
454 ServiceStatus::NotApplicable => write!(f, "not_applicable"),
455 }
456 }
457}
458
459#[cfg(test)]
464mod tests {
465 use super::*;
466 use std::str::FromStr;
467
468 #[test]
469 fn test_platform_parse() {
470 assert_eq!(Platform::parse("cli"), Some(Platform::Cli));
471 assert_eq!(Platform::parse("telegram"), Some(Platform::Telegram));
472 assert_eq!(Platform::parse("TG"), Some(Platform::Telegram));
473 assert_eq!(Platform::parse("discord"), Some(Platform::Discord));
474 assert_eq!(Platform::parse("slack"), Some(Platform::Slack));
475 assert_eq!(Platform::parse("whatsapp"), Some(Platform::WhatsApp));
476 assert_eq!(Platform::parse("webhook"), Some(Platform::Webhook));
477 assert_eq!(Platform::parse("unknown"), None);
478 }
479
480 #[test]
481 fn test_platform_from_str() {
482 assert!(Platform::from_str("cli").is_ok());
483 assert!(Platform::from_str("unknown").is_err());
484 }
485
486 #[test]
487 fn test_platform_as_str() {
488 assert_eq!(Platform::Telegram.as_str(), "telegram");
489 assert_eq!(Platform::Discord.as_str(), "discord");
490 assert_eq!(Platform::WhatsApp.as_str(), "whatsapp");
491 assert_eq!(Platform::Webhook.as_str(), "webhook");
492 }
493
494 #[test]
495 fn test_platform_display() {
496 assert_eq!(format!("{}", Platform::Cli), "cli");
497 assert_eq!(format!("{}", Platform::Telegram), "telegram");
498 }
499
500 #[test]
501 fn test_platform_all() {
502 let platforms = Platform::all();
503 assert!(platforms.len() >= 5);
504 assert!(platforms.contains(&Platform::Cli));
505 assert!(platforms.contains(&Platform::Telegram));
506 }
507
508 #[test]
509 fn test_gateway_state_default() {
510 let state = GatewayState::default();
511 assert_eq!(state.gateway_state, "stopped");
512 assert_eq!(state.active_agents, 0);
513 assert!(!state.restart_requested);
514 }
515
516 #[test]
517 fn test_gateway_state_serialization() {
518 let state = GatewayState::default();
519 let json = serde_json::to_string_pretty(&state).unwrap();
520 let parsed: GatewayState = serde_json::from_str(&json).unwrap();
521 assert_eq!(parsed.gateway_state, "stopped");
522 assert_eq!(parsed.active_agents, 0);
523 }
524
525 #[test]
526 fn test_service_status_display() {
527 assert_eq!(format!("{}", ServiceStatus::Running), "running");
528 assert_eq!(format!("{}", ServiceStatus::Stopped), "stopped");
529 assert_eq!(format!("{}", ServiceStatus::NotFound), "not_found");
530 assert_eq!(format!("{}", ServiceStatus::NotApplicable), "not_applicable");
531 }
532
533 #[test]
534 fn test_pid_path() {
535 let path = gateway_pid_path();
536 assert!(path.to_string_lossy().contains("gateway.pid"));
537 }
538
539 #[test]
540 fn test_state_path() {
541 let path = gateway_state_path();
542 assert!(path.to_string_lossy().contains("gateway_state.json"));
543 }
544}