Skip to main content

aimux_server/
server_unix.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result};
4use tokio::sync::{watch, Mutex};
5use tracing::{info, warn};
6use tracing_subscriber::EnvFilter;
7
8use crate::session::SessionManager;
9use aimux_common::config::AimuxConfig;
10
11fn setup_logging(verbose: bool) -> Result<()> {
12    let log_dir = aimux_common::paths::log_dir_path();
13    std::fs::create_dir_all(&log_dir)
14        .with_context(|| format!("failed to create log directory: {}", log_dir.display()))?;
15    let log_path = log_dir.join("aimux.log");
16    let file = std::fs::OpenOptions::new()
17        .create(true)
18        .append(true)
19        .open(&log_path)
20        .with_context(|| format!("failed to open log file: {}", log_path.display()))?;
21    let filter = if verbose {
22        EnvFilter::new("debug")
23    } else {
24        EnvFilter::try_from_env("AIMUX_LOG").unwrap_or_else(|_| EnvFilter::new("info"))
25    };
26    tracing_subscriber::fmt()
27        .with_writer(file)
28        .with_env_filter(filter)
29        .with_ansi(false)
30        .with_target(false)
31        .with_level(true)
32        .with_timer(tracing_subscriber::fmt::time::SystemTime)
33        .init();
34    Ok(())
35}
36
37fn write_pid_file() -> Result<()> {
38    let path = aimux_common::paths::pid_file_path();
39    if let Some(parent) = path.parent() {
40        std::fs::create_dir_all(parent)?;
41    }
42    std::fs::write(&path, std::process::id().to_string())?;
43    crate::security_unix::set_pid_file_permissions(&path)?;
44    info!("PID file written to {}", path.display());
45    Ok(())
46}
47
48fn enforce_single_instance() -> Result<std::fs::File> {
49    use std::os::unix::io::AsRawFd;
50
51    let path = aimux_common::paths::pid_file_path();
52    if let Some(parent) = path.parent() {
53        std::fs::create_dir_all(parent)?;
54    }
55    let file = std::fs::OpenOptions::new()
56        .create(true)
57        .truncate(false)
58        .write(true)
59        .read(true)
60        .open(&path)
61        .context("open PID file for locking")?;
62
63    // SAFETY: fd is a valid open file descriptor
64    let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
65    if ret != 0 {
66        // Check if existing PID is still alive
67        if let Ok(contents) = std::fs::read_to_string(&path) {
68            if let Ok(pid) = contents.trim().parse::<i32>() {
69                // SAFETY: kill with signal 0 just checks if pid exists
70                let alive = unsafe { libc::kill(pid, 0) } == 0;
71                if alive {
72                    anyhow::bail!("aimux server is already running (pid {})", pid);
73                }
74            }
75        }
76        anyhow::bail!("aimux server is already running (PID file locked)");
77    }
78    Ok(file)
79}
80
81fn cleanup_pid_file() {
82    let path = aimux_common::paths::pid_file_path();
83    let _ = std::fs::remove_file(path);
84}
85
86fn unix_pty_factory() -> crate::session::PtyFactory {
87    Box::new(|command: &str, cols: u16, rows: u16| {
88        crate::pty_unix::UnixPty::spawn(command, cols, rows)
89    })
90}
91
92pub async fn run_server(verbose: bool) -> Result<()> {
93    if let Err(e) = setup_logging(verbose) {
94        tracing_subscriber::fmt::init();
95        warn!("file logging unavailable, using stderr: {}", e);
96    }
97
98    info!(
99        "aimux-server starting (pid {}, version {})",
100        std::process::id(),
101        env!("CARGO_PKG_VERSION")
102    );
103
104    let _lock_file = enforce_single_instance()
105        .context("single instance check failed - is another aimux server running?")?;
106
107    if let Err(e) = write_pid_file() {
108        warn!("failed to write PID file: {}", e);
109    }
110
111    let config = match aimux_common::config::load_config() {
112        Ok(cfg) => {
113            info!(
114                "loaded config from {}",
115                aimux_common::paths::config_file_path().display()
116            );
117            cfg
118        }
119        Err(e) => {
120            warn!("config load failed, using defaults: {}", e);
121            AimuxConfig::default()
122        }
123    };
124
125    let manager = Arc::new(Mutex::new(SessionManager::new(unix_pty_factory(), config)));
126    {
127        let mut mgr = manager.lock().await;
128        mgr.set_self_arc(&manager);
129    }
130
131    let (shutdown_tx, shutdown_rx) = watch::channel(false);
132
133    let signal_tx = shutdown_tx.clone();
134    tokio::spawn(async move {
135        use tokio::signal::unix::SignalKind;
136        let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())
137            .expect("failed to register SIGTERM handler");
138        tokio::select! {
139            _ = tokio::signal::ctrl_c() => {},
140            _ = sigterm.recv() => {},
141        }
142        info!("Received shutdown signal, cleaning up...");
143        let _ = signal_tx.send(true);
144    });
145
146    let socket_path = aimux_common::paths::socket_path();
147    aimux_common::paths::ensure_runtime_dir()?;
148    let runtime_dir = aimux_common::paths::runtime_dir();
149    crate::security_unix::verify_runtime_dir_permissions(&runtime_dir)?;
150    let result =
151        crate::ipc_unix::run_ipc_server(&socket_path, manager.clone(), shutdown_tx, shutdown_rx)
152            .await;
153
154    let mut mgr = manager.lock().await;
155    let backends = mgr.shutdown_all();
156    drop(mgr);
157    // Close backends sequentially — server is exiting, no need for spawn_blocking
158    for mut backend in backends {
159        let _ = backend.close();
160    }
161    cleanup_pid_file();
162    info!("Server shutdown complete");
163    result
164}