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