| 1 | //! gardmd - gar display manager daemon |
| 2 | //! |
| 3 | //! Main entry point with X11 server management, greeter process handling, |
| 4 | //! and user session launching. |
| 5 | |
| 6 | use anyhow::Result; |
| 7 | use clap::Parser; |
| 8 | use gardm_ipc::{Request, Response}; |
| 9 | use gardmd::{ |
| 10 | auth::AuthSession, config::Config, greeter::GreeterProcess, ipc, session::UserSession, |
| 11 | vt, x11::XServer, |
| 12 | }; |
| 13 | use tokio::signal::unix::{signal, SignalKind}; |
| 14 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; |
| 15 | |
| 16 | /// gar display manager daemon |
| 17 | #[derive(Parser, Debug)] |
| 18 | #[command(name = "gardmd", about = "gar display manager daemon")] |
| 19 | struct Args { |
| 20 | /// Don't start X server (use existing DISPLAY for development) |
| 21 | #[arg(long)] |
| 22 | no_x: bool, |
| 23 | |
| 24 | /// X display to use (default: :0 or auto-detect) |
| 25 | #[arg(long, short = 'd')] |
| 26 | display: Option<String>, |
| 27 | |
| 28 | /// VT to use (default: auto-detect) |
| 29 | #[arg(long)] |
| 30 | vt: Option<u32>, |
| 31 | |
| 32 | /// Greeter command (default: from config) |
| 33 | #[arg(long)] |
| 34 | greeter: Option<String>, |
| 35 | |
| 36 | /// Run in test mode (IPC only, no greeter process management) |
| 37 | #[arg(long)] |
| 38 | test_mode: bool, |
| 39 | } |
| 40 | |
| 41 | #[tokio::main] |
| 42 | async fn main() -> Result<()> { |
| 43 | // Initialize logging |
| 44 | tracing_subscriber::registry() |
| 45 | .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) |
| 46 | .with(tracing_subscriber::fmt::layer()) |
| 47 | .init(); |
| 48 | |
| 49 | let args = Args::parse(); |
| 50 | tracing::info!("gardmd starting"); |
| 51 | tracing::debug!(?args, "Command line arguments"); |
| 52 | |
| 53 | // Load configuration |
| 54 | let config = Config::load()?; |
| 55 | tracing::debug!(?config, "Loaded configuration"); |
| 56 | |
| 57 | if args.test_mode { |
| 58 | run_test_mode(config).await |
| 59 | } else { |
| 60 | run_display_manager(args, config).await |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /// Run in test mode - IPC server only, for testing auth without X11 |
| 65 | async fn run_test_mode(config: Config) -> Result<()> { |
| 66 | tracing::info!("Running in test mode (IPC only)"); |
| 67 | |
| 68 | let server = ipc::Server::new().await?; |
| 69 | |
| 70 | // Notify systemd we're ready |
| 71 | if let Err(e) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { |
| 72 | tracing::warn!("Failed to notify systemd: {}", e); |
| 73 | } |
| 74 | |
| 75 | let mut sigterm = signal(SignalKind::terminate())?; |
| 76 | let mut sigint = signal(SignalKind::interrupt())?; |
| 77 | |
| 78 | tracing::info!("gardmd ready (test mode)"); |
| 79 | |
| 80 | loop { |
| 81 | tokio::select! { |
| 82 | result = server.accept() => { |
| 83 | match result { |
| 84 | Ok(conn) => { |
| 85 | tokio::spawn(handle_test_client(conn, config.clone())); |
| 86 | } |
| 87 | Err(e) => { |
| 88 | tracing::error!("Failed to accept connection: {}", e); |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | _ = sigterm.recv() => { |
| 93 | tracing::info!("Received SIGTERM, shutting down"); |
| 94 | break; |
| 95 | } |
| 96 | _ = sigint.recv() => { |
| 97 | tracing::info!("Received SIGINT, shutting down"); |
| 98 | break; |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]); |
| 104 | tracing::info!("gardmd stopped"); |
| 105 | Ok(()) |
| 106 | } |
| 107 | |
| 108 | /// Run as full display manager |
| 109 | async fn run_display_manager(args: Args, config: Config) -> Result<()> { |
| 110 | // Determine display and VT |
| 111 | let (x_display, mut x_server) = if args.no_x { |
| 112 | // Use existing X server |
| 113 | let x_display = args |
| 114 | .display |
| 115 | .or_else(|| std::env::var("DISPLAY").ok()) |
| 116 | .unwrap_or_else(|| ":0".to_string()); |
| 117 | tracing::info!(display = %x_display, "Using existing X server"); |
| 118 | (x_display, None) |
| 119 | } else { |
| 120 | // Start our own X server |
| 121 | let vt = args.vt.unwrap_or_else(|| { |
| 122 | config.general.vt.max(1) // Use config VT or find one |
| 123 | }); |
| 124 | let vt = if vt == 0 { |
| 125 | vt::find_unused_vt()? |
| 126 | } else { |
| 127 | vt |
| 128 | }; |
| 129 | |
| 130 | let x_display = args |
| 131 | .display |
| 132 | .unwrap_or_else(|| gardmd::x11::find_available_x_display().unwrap_or(":0".into())); |
| 133 | |
| 134 | tracing::info!(display = %x_display, vt, "Starting X server"); |
| 135 | let x_server = XServer::start(&x_display, vt)?; |
| 136 | |
| 137 | // Switch to our VT |
| 138 | vt::switch_to_vt(vt)?; |
| 139 | |
| 140 | (x_display, Some(x_server)) |
| 141 | }; |
| 142 | |
| 143 | // Start IPC server |
| 144 | let server = ipc::Server::new().await?; |
| 145 | |
| 146 | // Notify systemd we're ready |
| 147 | if let Err(e) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) { |
| 148 | tracing::warn!("Failed to notify systemd: {}", e); |
| 149 | } |
| 150 | |
| 151 | tracing::info!("gardmd ready"); |
| 152 | |
| 153 | // Set up signal handlers |
| 154 | let mut sigterm = signal(SignalKind::terminate())?; |
| 155 | let mut sigint = signal(SignalKind::interrupt())?; |
| 156 | |
| 157 | // Main greeter/session loop |
| 158 | let greeter_cmd = args |
| 159 | .greeter |
| 160 | .unwrap_or_else(|| config.general.greeter.to_string_lossy().to_string()); |
| 161 | |
| 162 | loop { |
| 163 | // Start greeter |
| 164 | tracing::info!("Starting greeter"); |
| 165 | let mut greeter = GreeterProcess::start(&greeter_cmd, &x_display)?; |
| 166 | |
| 167 | // Handle greeter authentication |
| 168 | let session_result = tokio::select! { |
| 169 | result = handle_greeter_session(&server, &x_display, &config.general.default_session) => result, |
| 170 | _ = sigterm.recv() => { |
| 171 | tracing::info!("Received SIGTERM during greeter"); |
| 172 | break; |
| 173 | } |
| 174 | _ = sigint.recv() => { |
| 175 | tracing::info!("Received SIGINT during greeter"); |
| 176 | break; |
| 177 | } |
| 178 | }; |
| 179 | |
| 180 | // Kill greeter |
| 181 | if let Err(e) = greeter.kill() { |
| 182 | tracing::warn!(error = %e, "Failed to kill greeter"); |
| 183 | } |
| 184 | |
| 185 | tracing::debug!(?session_result, "Greeter session ended, processing result"); |
| 186 | |
| 187 | match session_result { |
| 188 | Ok(Some(session_info)) => { |
| 189 | tracing::info!( |
| 190 | "Processing session start: {} {:?} ({})", |
| 191 | session_info.username, |
| 192 | session_info.cmd, |
| 193 | session_info.session_type |
| 194 | ); |
| 195 | let vt = x_server.as_ref().map(|x| x.vt()).unwrap_or(1); |
| 196 | let is_wayland = session_info.session_type == "wayland"; |
| 197 | |
| 198 | if is_wayland { |
| 199 | // WAYLAND SESSION: Stop X server first, then start compositor |
| 200 | tracing::info!( |
| 201 | username = %session_info.username, |
| 202 | cmd = ?session_info.cmd, |
| 203 | vt, |
| 204 | "Wayland session selected, stopping X server" |
| 205 | ); |
| 206 | |
| 207 | // Stop X server - compositor needs direct VT access |
| 208 | if let Some(x) = x_server.take() { |
| 209 | drop(x); // XServer::drop() handles graceful shutdown |
| 210 | tracing::info!("X server stopped"); |
| 211 | } |
| 212 | |
| 213 | // Start Wayland compositor directly on VT |
| 214 | tracing::info!("Starting Wayland compositor"); |
| 215 | let mut session = match UserSession::start( |
| 216 | &session_info.username, |
| 217 | &session_info.password, |
| 218 | &session_info.cmd, |
| 219 | "wayland", |
| 220 | None, // No DISPLAY for Wayland |
| 221 | vt, |
| 222 | ) { |
| 223 | Ok(s) => { |
| 224 | tracing::info!(pid = s.pid(), "Wayland session started successfully"); |
| 225 | s |
| 226 | } |
| 227 | Err(e) => { |
| 228 | tracing::error!(error = %e, "Failed to start Wayland session"); |
| 229 | // Restart X server and try again with greeter |
| 230 | tracing::info!("Restarting X server after Wayland session failure"); |
| 231 | let new_x = XServer::start(&x_display, vt)?; |
| 232 | vt::switch_to_vt(vt)?; |
| 233 | x_server = Some(new_x); |
| 234 | continue; |
| 235 | } |
| 236 | }; |
| 237 | |
| 238 | // Wait for session to end |
| 239 | let session_ended = tokio::select! { |
| 240 | _ = tokio::task::spawn_blocking(move || session.wait()) => { |
| 241 | tracing::info!("Wayland session ended"); |
| 242 | true |
| 243 | } |
| 244 | _ = sigterm.recv() => { |
| 245 | tracing::info!("Received SIGTERM during Wayland session"); |
| 246 | false |
| 247 | } |
| 248 | _ = sigint.recv() => { |
| 249 | tracing::info!("Received SIGINT during Wayland session"); |
| 250 | false |
| 251 | } |
| 252 | }; |
| 253 | |
| 254 | if !session_ended { |
| 255 | break; |
| 256 | } |
| 257 | |
| 258 | // Restart X server for greeter |
| 259 | tracing::info!("Restarting X server for greeter"); |
| 260 | let new_x = XServer::start(&x_display, vt)?; |
| 261 | vt::switch_to_vt(vt)?; |
| 262 | x_server = Some(new_x); |
| 263 | } else { |
| 264 | // X11 SESSION: Keep X server running (existing behavior) |
| 265 | let mut session = UserSession::start( |
| 266 | &session_info.username, |
| 267 | &session_info.password, |
| 268 | &session_info.cmd, |
| 269 | "x11", |
| 270 | Some(&x_display), |
| 271 | vt, |
| 272 | )?; |
| 273 | |
| 274 | // Wait for session to end |
| 275 | tokio::select! { |
| 276 | _ = tokio::task::spawn_blocking(move || session.wait()) => { |
| 277 | tracing::info!("X11 session ended, restarting greeter"); |
| 278 | } |
| 279 | _ = sigterm.recv() => { |
| 280 | tracing::info!("Received SIGTERM during session"); |
| 281 | break; |
| 282 | } |
| 283 | _ = sigint.recv() => { |
| 284 | tracing::info!("Received SIGINT during session"); |
| 285 | break; |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | } |
| 290 | Ok(None) => { |
| 291 | tracing::info!("Greeter disconnected without session, restarting"); |
| 292 | } |
| 293 | Err(e) => { |
| 294 | tracing::error!(error = %e, "Error during greeter session"); |
| 295 | // Brief delay before retry |
| 296 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; |
| 297 | } |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | // Cleanup |
| 302 | let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]); |
| 303 | drop(x_server); // Stop X server |
| 304 | tracing::info!("gardmd stopped"); |
| 305 | Ok(()) |
| 306 | } |
| 307 | |
| 308 | /// Information needed to start a user session |
| 309 | struct SessionStartInfo { |
| 310 | username: String, |
| 311 | password: String, // Needed for PAM open_session in child process |
| 312 | cmd: Vec<String>, |
| 313 | session_type: String, |
| 314 | } |
| 315 | |
| 316 | impl std::fmt::Debug for SessionStartInfo { |
| 317 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 318 | f.debug_struct("SessionStartInfo") |
| 319 | .field("username", &self.username) |
| 320 | .field("password", &"[REDACTED]") |
| 321 | .field("cmd", &self.cmd) |
| 322 | .field("session_type", &self.session_type) |
| 323 | .finish() |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | /// Handle greeter IPC until we get a successful auth and StartSession |
| 328 | async fn handle_greeter_session( |
| 329 | server: &ipc::Server, |
| 330 | _display: &str, |
| 331 | default_session: &str, |
| 332 | ) -> Result<Option<SessionStartInfo>> { |
| 333 | let mut conn = server.accept().await?; |
| 334 | let mut auth = AuthSession::new(); |
| 335 | |
| 336 | loop { |
| 337 | let request = match conn.recv().await? { |
| 338 | Some(req) => req, |
| 339 | None => return Ok(None), // Greeter disconnected |
| 340 | }; |
| 341 | |
| 342 | let (response, session_info) = handle_greeter_request(request, &mut auth, default_session).await; |
| 343 | |
| 344 | tracing::debug!(?response, has_session_info = session_info.is_some(), "Sending response to greeter"); |
| 345 | conn.send(&response).await?; |
| 346 | |
| 347 | if let Some(info) = session_info { |
| 348 | tracing::info!( |
| 349 | username = %info.username, |
| 350 | cmd = ?info.cmd, |
| 351 | session_type = %info.session_type, |
| 352 | "Returning session info to main loop" |
| 353 | ); |
| 354 | return Ok(Some(info)); |
| 355 | } |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | /// Handle a single greeter request, returning (response, optional session info) |
| 360 | async fn handle_greeter_request( |
| 361 | request: Request, |
| 362 | auth: &mut AuthSession, |
| 363 | default_session: &str, |
| 364 | ) -> (Response, Option<SessionStartInfo>) { |
| 365 | use gardmd::auth::AuthResponse; |
| 366 | |
| 367 | match request { |
| 368 | Request::CreateSession { username } => { |
| 369 | let response = match auth.create_session(&username) { |
| 370 | AuthResponse::Prompt { prompt, echo } => Response::AuthPrompt { prompt, echo }, |
| 371 | AuthResponse::Error { message } => Response::Error { message }, |
| 372 | _ => Response::Error { |
| 373 | message: "Unexpected auth response".to_string(), |
| 374 | }, |
| 375 | }; |
| 376 | (response, None) |
| 377 | } |
| 378 | |
| 379 | Request::Authenticate { response: password } => { |
| 380 | let response = match auth.authenticate(&password).await { |
| 381 | AuthResponse::Success => Response::Success, |
| 382 | AuthResponse::Prompt { prompt, echo } => Response::AuthPrompt { prompt, echo }, |
| 383 | AuthResponse::Error { message } => Response::AuthError { message }, |
| 384 | AuthResponse::Info { message } => Response::AuthInfo { message }, |
| 385 | }; |
| 386 | (response, None) |
| 387 | } |
| 388 | |
| 389 | Request::StartSession { cmd, session_type, env: _ } => { |
| 390 | tracing::debug!("Processing StartSession request"); |
| 391 | if let Some((username, password)) = auth.take_authenticated() { |
| 392 | tracing::info!(%username, ?cmd, %session_type, "Session start requested"); |
| 393 | let info = SessionStartInfo { username, password, cmd, session_type }; |
| 394 | tracing::debug!(?info, "Created SessionStartInfo"); |
| 395 | (Response::Success, Some(info)) |
| 396 | } else { |
| 397 | ( |
| 398 | Response::Error { |
| 399 | message: "Not authenticated".to_string(), |
| 400 | }, |
| 401 | None, |
| 402 | ) |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | Request::CancelSession => { |
| 407 | auth.cancel(); |
| 408 | (Response::Success, None) |
| 409 | } |
| 410 | |
| 411 | Request::Shutdown => { |
| 412 | tracing::info!("Shutdown requested"); |
| 413 | match gardmd::power::execute_async(gardmd::power::PowerAction::Shutdown).await { |
| 414 | Ok(()) => (Response::Success, None), |
| 415 | Err(e) => ( |
| 416 | Response::Error { |
| 417 | message: format!("Shutdown failed: {}", e), |
| 418 | }, |
| 419 | None, |
| 420 | ), |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | Request::Reboot => { |
| 425 | tracing::info!("Reboot requested"); |
| 426 | match gardmd::power::execute_async(gardmd::power::PowerAction::Reboot).await { |
| 427 | Ok(()) => (Response::Success, None), |
| 428 | Err(e) => ( |
| 429 | Response::Error { |
| 430 | message: format!("Reboot failed: {}", e), |
| 431 | }, |
| 432 | None, |
| 433 | ), |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | Request::Suspend => { |
| 438 | tracing::info!("Suspend requested"); |
| 439 | match gardmd::power::execute_async(gardmd::power::PowerAction::Suspend).await { |
| 440 | Ok(()) => (Response::Success, None), |
| 441 | Err(e) => ( |
| 442 | Response::Error { |
| 443 | message: format!("Suspend failed: {}", e), |
| 444 | }, |
| 445 | None, |
| 446 | ), |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | Request::ListSessions => { |
| 451 | let sessions = gardmd::list_sessions(); |
| 452 | (Response::Sessions { |
| 453 | sessions, |
| 454 | default_session: Some(default_session.to_string()), |
| 455 | }, None) |
| 456 | } |
| 457 | |
| 458 | Request::ListUsers => { |
| 459 | let users = gardmd::list_users(); |
| 460 | (Response::Users { users }, None) |
| 461 | } |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | /// Handle a test mode client (same as before, for backwards compatibility) |
| 466 | async fn handle_test_client(mut conn: ipc::ClientConnection, config: Config) { |
| 467 | let mut auth = AuthSession::new(); |
| 468 | |
| 469 | loop { |
| 470 | match conn.recv().await { |
| 471 | Ok(Some(request)) => { |
| 472 | let (response, _) = handle_greeter_request(request, &mut auth, &config.general.default_session).await; |
| 473 | if let Err(e) = conn.send(&response).await { |
| 474 | tracing::error!("Failed to send response: {}", e); |
| 475 | break; |
| 476 | } |
| 477 | } |
| 478 | Ok(None) => { |
| 479 | tracing::debug!("Client disconnected"); |
| 480 | break; |
| 481 | } |
| 482 | Err(e) => { |
| 483 | tracing::error!("Error receiving request: {}", e); |
| 484 | break; |
| 485 | } |
| 486 | } |
| 487 | } |
| 488 | } |
| 489 |