#![allow(dead_code)]
use crate::error::{GdeltError, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;
use super::state::DaemonState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonInfo {
pub running: bool,
pub pid: Option<u32>,
pub uptime_secs: Option<u64>,
pub last_sync: Option<String>,
pub next_sync: Option<String>,
pub sync_status: super::state::SyncStatus,
pub mcp_enabled: bool,
pub mcp_port: u16,
}
#[cfg(unix)]
pub fn start_daemon(mcp_enabled: bool, sync_enabled: bool, port: u16) -> Result<DaemonInfo> {
let state = DaemonState::load()?;
if state.is_actually_running() {
return Err(GdeltError::Daemon(format!(
"Daemon already running with PID {}",
state.pid.unwrap_or(0)
)));
}
let exe = std::env::current_exe()
.map_err(|e| GdeltError::Daemon(format!("Could not get executable path: {}", e)))?;
let mut args = vec!["daemon".to_string(), "run".to_string()];
if mcp_enabled {
args.push("--mcp".to_string());
}
if sync_enabled {
args.push("--sync".to_string());
}
args.push("--port".to_string());
args.push(port.to_string());
let log_path = DaemonState::log_file_path()?;
let log_file = std::fs::File::create(&log_path)
.map_err(|e| GdeltError::Daemon(format!("Could not create log file: {}", e)))?;
let child = Command::new(&exe)
.args(&args)
.stdout(log_file.try_clone()?)
.stderr(log_file)
.spawn()
.map_err(|e| GdeltError::Daemon(format!("Could not spawn daemon: {}", e)))?;
let pid = child.id();
let mut state = DaemonState::default();
state.mark_started(pid);
state.mcp_enabled = mcp_enabled;
state.mcp_port = port;
state.save()?;
let pid_path = DaemonState::pid_file_path()?;
std::fs::write(&pid_path, pid.to_string())?;
Ok(get_daemon_info(&state))
}
#[cfg(not(unix))]
pub fn start_daemon(_mcp_enabled: bool, _sync_enabled: bool, _port: u16) -> Result<DaemonInfo> {
Err(GdeltError::Daemon(
"Daemon mode is only supported on Unix systems".into(),
))
}
pub fn stop_daemon() -> Result<DaemonInfo> {
let mut state = DaemonState::load()?;
if !state.is_actually_running() {
state.mark_stopped();
state.save()?;
return Err(GdeltError::Daemon("Daemon is not running".into()));
}
let pid = state.pid.ok_or_else(|| GdeltError::Daemon("No PID found".into()))?;
#[cfg(unix)]
{
use std::process::Command;
Command::new("kill")
.args([&pid.to_string()])
.output()
.map_err(|e| GdeltError::Daemon(format!("Could not kill daemon: {}", e)))?;
}
state.mark_stopped();
state.save()?;
let pid_path = DaemonState::pid_file_path()?;
let _ = std::fs::remove_file(pid_path);
Ok(get_daemon_info(&state))
}
pub fn get_daemon_status() -> Result<DaemonInfo> {
let mut state = DaemonState::load()?;
if state.running && !state.is_actually_running() {
state.mark_stopped();
state.save()?;
}
Ok(get_daemon_info(&state))
}
pub fn restart_daemon() -> Result<DaemonInfo> {
let state = DaemonState::load()?;
let mcp_enabled = state.mcp_enabled;
let port = state.mcp_port;
if state.is_actually_running() {
stop_daemon()?;
std::thread::sleep(std::time::Duration::from_millis(500));
}
start_daemon(mcp_enabled, true, port)
}
fn get_daemon_info(state: &DaemonState) -> DaemonInfo {
let uptime_secs = state.started_at.map(|started| {
let now = chrono::Utc::now();
(now - started).num_seconds() as u64
});
DaemonInfo {
running: state.running && state.is_actually_running(),
pid: state.pid,
uptime_secs,
last_sync: state.last_sync.map(|t| t.format("%Y-%m-%d %H:%M:%S UTC").to_string()),
next_sync: state.next_sync.map(|t| t.format("%Y-%m-%d %H:%M:%S UTC").to_string()),
sync_status: state.sync_status.clone(),
mcp_enabled: state.mcp_enabled,
mcp_port: state.mcp_port,
}
}
pub fn read_daemon_logs(tail_lines: u32, follow: bool) -> Result<String> {
let log_path = DaemonState::log_file_path()?;
if !log_path.exists() {
return Ok("No log file found".to_string());
}
if follow {
#[cfg(unix)]
{
let output = Command::new("tail")
.args(["-n", &tail_lines.to_string(), &log_path.to_string_lossy()])
.output()
.map_err(|e| GdeltError::Daemon(format!("Could not read logs: {}", e)))?;
return Ok(String::from_utf8_lossy(&output.stdout).to_string());
}
}
let content = std::fs::read_to_string(&log_path)?;
let lines: Vec<&str> = content.lines().collect();
let start = lines.len().saturating_sub(tail_lines as usize);
Ok(lines[start..].join("\n"))
}
pub fn generate_systemd_service() -> String {
let exe_path = std::env::current_exe()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "gdelt".to_string());
format!(
r#"[Unit]
Description=GDELT CLI Daemon
After=network.target
[Service]
Type=simple
ExecStart={} daemon run --mcp --sync
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
"#,
exe_path
)
}
pub fn generate_launchd_plist() -> String {
let exe_path = std::env::current_exe()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "gdelt".to_string());
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.gdelt.daemon</string>
<key>ProgramArguments</key>
<array>
<string>{}</string>
<string>daemon</string>
<string>run</string>
<string>--mcp</string>
<string>--sync</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
"#,
exe_path
)
}