| 1 | //! X11 server management |
| 2 | //! |
| 3 | //! Starts and manages Xorg server lifecycle. |
| 4 | |
| 5 | use anyhow::{Context, Result}; |
| 6 | use std::process::{Child, Command, Stdio}; |
| 7 | use std::time::{Duration, Instant}; |
| 8 | |
| 9 | /// Xorg server wrapper |
| 10 | pub struct XServer { |
| 11 | process: Child, |
| 12 | x_display: String, |
| 13 | vt: u32, |
| 14 | } |
| 15 | |
| 16 | impl XServer { |
| 17 | /// Start Xorg server on specified x_display and VT |
| 18 | pub fn start(x_display: &str, vt: u32) -> Result<Self> { |
| 19 | let vt_arg = format!("vt{}", vt); |
| 20 | |
| 21 | let process = Command::new("/usr/bin/Xorg") |
| 22 | .arg(x_display) |
| 23 | .arg(&vt_arg) |
| 24 | .arg("-keeptty") |
| 25 | .arg("-noreset") |
| 26 | .arg("-novtswitch") |
| 27 | .arg("-nolisten") |
| 28 | .arg("tcp") |
| 29 | .arg("-auth") |
| 30 | .arg("/dev/null") // We'll set up proper auth later |
| 31 | .stdout(Stdio::null()) |
| 32 | .stderr(Stdio::piped()) |
| 33 | .spawn() |
| 34 | .context("Failed to spawn Xorg")?; |
| 35 | |
| 36 | tracing::info!( |
| 37 | %x_display, |
| 38 | vt, |
| 39 | pid = process.id(), |
| 40 | "Started Xorg" |
| 41 | ); |
| 42 | |
| 43 | let server = Self { |
| 44 | process, |
| 45 | x_display: x_display.to_string(), |
| 46 | vt, |
| 47 | }; |
| 48 | |
| 49 | // Wait for X server to become ready |
| 50 | server.wait_ready(Duration::from_secs(10))?; |
| 51 | |
| 52 | Ok(server) |
| 53 | } |
| 54 | |
| 55 | /// Wait for X server to accept connections |
| 56 | fn wait_ready(&self, timeout: Duration) -> Result<()> { |
| 57 | let start = Instant::now(); |
| 58 | |
| 59 | while start.elapsed() < timeout { |
| 60 | // Try to connect to X server |
| 61 | match x11rb::connect(Some(&self.x_display)) { |
| 62 | Ok((conn, _)) => { |
| 63 | drop(conn); |
| 64 | tracing::debug!(x_display = %self.x_display, "X server ready"); |
| 65 | return Ok(()); |
| 66 | } |
| 67 | Err(_) => { |
| 68 | std::thread::sleep(Duration::from_millis(100)); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // Check if process died |
| 73 | if !self.is_process_alive() { |
| 74 | anyhow::bail!("X server process died during startup"); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | anyhow::bail!("X server failed to become ready within {:?}", timeout) |
| 79 | } |
| 80 | |
| 81 | /// Check if the X server process is still running |
| 82 | fn is_process_alive(&self) -> bool { |
| 83 | // We can't use try_wait without &mut self, so check /proc |
| 84 | let proc_path = format!("/proc/{}", self.process.id()); |
| 85 | std::path::Path::new(&proc_path).exists() |
| 86 | } |
| 87 | |
| 88 | /// Get the x_display string (e.g., ":0") |
| 89 | pub fn x_display(&self) -> &str { |
| 90 | &self.x_display |
| 91 | } |
| 92 | |
| 93 | /// Get the VT number |
| 94 | pub fn vt(&self) -> u32 { |
| 95 | self.vt |
| 96 | } |
| 97 | |
| 98 | /// Check if X server is still running |
| 99 | pub fn is_running(&mut self) -> bool { |
| 100 | matches!(self.process.try_wait(), Ok(None)) |
| 101 | } |
| 102 | |
| 103 | /// Stop the X server gracefully |
| 104 | pub fn stop(&mut self) -> Result<()> { |
| 105 | tracing::info!(x_display = %self.x_display, "Stopping X server"); |
| 106 | |
| 107 | // Send SIGTERM first |
| 108 | let _ = nix::sys::signal::kill( |
| 109 | nix::unistd::Pid::from_raw(self.process.id() as i32), |
| 110 | nix::sys::signal::Signal::SIGTERM, |
| 111 | ); |
| 112 | |
| 113 | // Wait a bit for graceful shutdown |
| 114 | std::thread::sleep(Duration::from_millis(500)); |
| 115 | |
| 116 | // Force kill if still running |
| 117 | if self.is_running() { |
| 118 | self.process.kill().ok(); |
| 119 | } |
| 120 | |
| 121 | self.process.wait().context("Failed to wait for X server")?; |
| 122 | Ok(()) |
| 123 | } |
| 124 | |
| 125 | /// Get the process ID |
| 126 | pub fn pid(&self) -> u32 { |
| 127 | self.process.id() |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | impl Drop for XServer { |
| 132 | fn drop(&mut self) { |
| 133 | if let Err(e) = self.stop() { |
| 134 | tracing::warn!(error = %e, "Failed to stop X server cleanly"); |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | /// Find an available x_display number |
| 140 | pub fn find_available_x_display() -> Result<String> { |
| 141 | for n in 0..10 { |
| 142 | let x_display = format!(":{}", n); |
| 143 | let lock_file = format!("/tmp/.X{}-lock", n); |
| 144 | |
| 145 | if !std::path::Path::new(&lock_file).exists() { |
| 146 | return Ok(x_display); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | anyhow::bail!("No available X x_display found") |
| 151 | } |
| 152 |