use std::path::{Path, PathBuf};
use std::sync::Arc;
use clap::Args;
use serde::Serialize;
use crate::output::OutputFormat;
use super::daemon::{start_daemon_background, wait_for_daemon, TLDRDaemon};
use super::error::DaemonError;
use super::ipc::{check_socket_alive, cleanup_socket, compute_socket_path, IpcListener};
use super::pid::{check_stale_pid, cleanup_stale_pid, compute_pid_path, try_acquire_lock};
use super::types::DaemonConfig;
#[derive(Debug, Clone, Args)]
pub struct DaemonStartArgs {
#[arg(long, short = 'p', default_value = ".")]
pub project: PathBuf,
#[arg(long)]
pub foreground: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct DaemonStartOutput {
pub status: String,
pub pid: u32,
pub socket: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl DaemonStartArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(self.run_async(format, quiet))
}
async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
let project = self.project.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(&self.project)
});
let pid_path = compute_pid_path(&project);
if check_stale_pid(&pid_path)? {
cleanup_stale_pid(&pid_path)?;
}
let socket_path = compute_socket_path(&project);
if socket_path.exists() && !check_socket_alive(&project).await {
cleanup_socket(&project)?;
}
if self.foreground {
self.run_foreground(&project, format, quiet).await
} else {
self.run_background(&project, format, quiet).await
}
}
async fn run_foreground(
&self,
project: &Path,
format: OutputFormat,
quiet: bool,
) -> anyhow::Result<()> {
let pid_path = compute_pid_path(project);
let _pid_guard = try_acquire_lock(&pid_path).map_err(|e| match e {
DaemonError::AlreadyRunning { pid } => {
anyhow::anyhow!("Daemon already running (PID: {})", pid)
}
DaemonError::StalePidFile { pid } => {
anyhow::anyhow!("Stale PID file (process {} not running)", pid)
}
other => anyhow::anyhow!("Failed to acquire lock: {}", other),
})?;
let listener = IpcListener::bind(project).await.map_err(|e| match e {
DaemonError::AddressInUse { addr } => {
anyhow::anyhow!("Address already in use: {}", addr)
}
DaemonError::SocketBindFailed(io_err) => {
anyhow::anyhow!("Failed to bind socket: {}", io_err)
}
other => anyhow::anyhow!("Socket error: {}", other),
})?;
let socket_path = compute_socket_path(project);
let our_pid = std::process::id();
let output = DaemonStartOutput {
status: "ok".to_string(),
pid: our_pid,
socket: socket_path.clone(),
message: Some("Daemon started in foreground".to_string()),
};
if !quiet {
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
println!("Daemon started with PID {}", our_pid);
println!("Socket: {}", socket_path.display());
}
}
}
let config = DaemonConfig::default();
let daemon = Arc::new(TLDRDaemon::new(project.to_path_buf(), config));
daemon.run(listener).await?;
let _ = cleanup_socket(project);
Ok(())
}
async fn run_background(
&self,
project: &Path,
format: OutputFormat,
quiet: bool,
) -> anyhow::Result<()> {
if check_socket_alive(project).await {
let pid_path = compute_pid_path(project);
let pid = std::fs::read_to_string(&pid_path)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
return Err(anyhow::anyhow!("Daemon already running (PID: {})", pid));
}
let pid = start_daemon_background(project).await?;
wait_for_daemon(project, 10)
.await
.map_err(|_| anyhow::anyhow!("Daemon failed to start within timeout"))?;
let socket_path = compute_socket_path(project);
let output = DaemonStartOutput {
status: "ok".to_string(),
pid,
socket: socket_path.clone(),
message: Some("Daemon started".to_string()),
};
if !quiet {
match format {
OutputFormat::Json | OutputFormat::Compact => {
println!("{}", serde_json::to_string_pretty(&output)?);
}
OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
println!("Daemon started with PID {}", pid);
println!("Socket: {}", socket_path.display());
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_start_args_default() {
let args = DaemonStartArgs {
project: PathBuf::from("."),
foreground: false,
};
assert_eq!(args.project, PathBuf::from("."));
assert!(!args.foreground);
}
#[test]
fn test_daemon_start_args_foreground() {
let args = DaemonStartArgs {
project: PathBuf::from("/test/project"),
foreground: true,
};
assert!(args.foreground);
}
#[test]
fn test_daemon_start_output_serialization() {
let output = DaemonStartOutput {
status: "ok".to_string(),
pid: 12345,
socket: PathBuf::from("/tmp/tldr-abc123.sock"),
message: Some("Daemon started".to_string()),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("ok"));
assert!(json.contains("12345"));
assert!(json.contains("tldr-abc123.sock"));
}
}