aimux_server/
server_unix.rs1use 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 let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
65 if ret != 0 {
66 if let Ok(contents) = std::fs::read_to_string(&path) {
68 if let Ok(pid) = contents.trim().parse::<i32>() {
69 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 for mut backend in backends {
159 let _ = backend.close();
160 }
161 cleanup_pid_file();
162 info!("Server shutdown complete");
163 result
164}