Rust · 17228 bytes Raw Blame History
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