⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
kde_session.rs

1//! KDE Plasma 6 session manager
2//!
3//! Orchestrates launching KDE Plasma 6 as the default desktop session.
4//! Initializes the desktop subsystem, hands off the framebuffer to KWin,
5//! launches the KDE init script as a user process, and restores the text
6//! console when the session ends.
7//!
8//! If the KDE session exits within a few seconds (indicating startup
9//! failure), automatically falls back to the built-in desktop environment.
10
11#![allow(dead_code)]
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16/// Minimum session lifetime (in approximate loop iterations) before we
17/// consider the session to have started successfully. If the KDE init
18/// script exits before this threshold, we treat it as a startup failure
19/// and fall back to the built-in DE.
20///
21/// Each iteration of the wait loop is roughly 10ms via a busy-wait yield,
22/// so 500 iterations ~ 5 seconds.
23const MIN_SESSION_LIFETIME_ITERS: u64 = 500;
24
25/// KDE init script path (used when /bin/sh is available).
26const KDE_INIT_SCRIPT: &str = "/usr/share/veridian/veridian-kde-init.sh";
27
28/// KWin Wayland compositor binary path (direct exec, no shell needed).
29const KWIN_WAYLAND: &str = "/usr/bin/kwin_wayland";
30
31/// D-Bus daemon binary path.
32const DBUS_DAEMON: &str = "/usr/bin/dbus-daemon";
33
34/// Environment variables passed to the KDE init script.
35const KDE_ENV: &[&str] = &[
36    "HOME=/root",
37    "USER=root",
38    "SHELL=/bin/sh",
39    "PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
40    "XDG_RUNTIME_DIR=/run/user/0",
41    "XDG_SESSION_TYPE=wayland",
42    "XDG_CURRENT_DESKTOP=KDE",
43    "WAYLAND_DISPLAY=wayland-0",
44    "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
45    "QT_QPA_PLATFORM=veridian",
46    "LANG=en_US.UTF-8",
47];
48
49/// Start a KDE Plasma 6 session.
50///
51/// This function blocks until the KDE session ends (user logout) or
52/// KDE fails to start. On failure, it falls back to the built-in DE.
53///
54/// # Flow
55/// 1. Initialize desktop subsystem (Wayland, fonts, etc.)
56/// 2. Disable fbcon output (KWin will drive the framebuffer)
57/// 3. Launch KDE init script via `load_user_program`
58/// 4. Wait for the process to complete (blocks kernel thread)
59/// 5. Restore fbcon on return
60///
61/// If the process exits too quickly, falls back to built-in DE.
62#[cfg(all(feature = "alloc", target_arch = "x86_64"))]
63pub fn start_kde_session() {
64    println!("[KDE] Starting KDE Plasma 6 session...");
65
66    // Step 1: Initialize desktop subsystem if not already done.
67    // If already initialized (e.g. by the boot sequence), that's fine --
68    // the built-in DE was showing and we're replacing it with KDE.
69    match crate::desktop::init() {
70        Ok(()) => println!("[KDE] Desktop subsystem initialized"),
71        Err(crate::error::KernelError::InvalidState { .. }) => {
72            println!("[KDE] Desktop already initialized, switching to KDE session");
73        }
74        Err(e) => {
75            println!("[KDE] Desktop subsystem init failed: {:?}", e);
76            println!("[KDE] Falling back to built-in desktop...");
77            crate::desktop::renderer::start_desktop();
78            return;
79        }
80    }
81
82    // Step 2: Disable fbcon -- KWin will take over DRM/framebuffer
83    crate::graphics::fbcon::disable_output();
84    println!("[KDE] Framebuffer console disabled (KWin will drive display)");
85
86    // Step 3: Launch KDE session (tries init script, then direct exec)
87    let pid = match launch_kde_init() {
88        Ok(pid) => {
89            println!("[KDE] KDE process launched (PID {})", pid.0);
90            pid
91        }
92        Err(e) => {
93            println!("[KDE] Failed to launch KDE session: {:?}", e);
94            println!("[KDE] Falling back to built-in desktop...");
95            crate::graphics::fbcon::enable_output();
96            crate::graphics::fbcon::mark_all_dirty_and_flush();
97            crate::desktop::renderer::start_desktop();
98            return;
99        }
100    };
101
102    // Step 4: Record start time and run the user process (blocks)
103    let start_iter = get_iteration_counter();
104    run_kde_process(pid);
105    let end_iter = get_iteration_counter();
106
107    // Step 5: Restore fbcon
108    crate::graphics::fbcon::enable_output();
109    crate::graphics::fbcon::mark_all_dirty_and_flush();
110    println!("[KDE] Session ended, text console restored");
111
112    // Step 6: Check if session exited too quickly (startup failure)
113    let elapsed = end_iter.saturating_sub(start_iter);
114    if elapsed < MIN_SESSION_LIFETIME_ITERS {
115        println!(
116            "[KDE] Session exited unexpectedly (within ~{}s of launch)",
117            elapsed / 100
118        );
119        println!("[KDE] Falling back to built-in desktop...");
120        crate::desktop::renderer::start_desktop();
121    }
122}
123
124/// Launch the KDE session as a user process.
125///
126/// Tries three strategies in order:
127/// 1. Shell-based: `/bin/sh` running the init script (full orchestration)
128/// 2. Direct KWin: load `kwin_wayland` directly (no shell needed)
129/// 3. Direct D-Bus: load `dbus-daemon` as a smoke test
130///
131/// Strategy 1 requires a real `/bin/sh` in the rootfs. Strategies 2-3
132/// load cross-compiled static ELF binaries directly from BlockFS.
133#[cfg(feature = "alloc")]
134fn launch_kde_init() -> Result<crate::process::ProcessId, crate::error::KernelError> {
135    // Strategy 1: Try shell-based init script (full KDE startup sequence)
136    let shell_argv: &[&str] = &["sh", KDE_INIT_SCRIPT, "--from-kernel"];
137    if let Ok(pid) = crate::userspace::load_user_program("/bin/sh", shell_argv, KDE_ENV) {
138        println!("[KDE] Launched via init script (/bin/sh)");
139        return Ok(pid);
140    }
141
142    // Strategy 2: Direct-exec kwin_wayland (no shell needed)
143    let kwin_argv: &[&str] = &["kwin_wayland", "--no-lockscreen"];
144    if let Ok(pid) = crate::userspace::load_user_program(KWIN_WAYLAND, kwin_argv, KDE_ENV) {
145        println!("[KDE] Launched kwin_wayland directly");
146        return Ok(pid);
147    }
148    println!("[KDE] kwin_wayland not loadable, trying dbus-daemon...");
149
150    // Strategy 3: Direct-exec dbus-daemon as smoke test
151    let dbus_argv: &[&str] = &["dbus-daemon", "--session", "--nofork"];
152    if let Ok(pid) = crate::userspace::load_user_program(DBUS_DAEMON, dbus_argv, KDE_ENV) {
153        println!("[KDE] Launched dbus-daemon (smoke test)");
154        return Ok(pid);
155    }
156
157    Err(crate::error::KernelError::NotFound {
158        resource: "KDE binaries (kwin_wayland, dbus-daemon)",
159        id: 0,
160    })
161}
162
163/// Run a KDE process using the same pattern as
164/// `bootstrap::run_user_process_scheduled`.
165///
166/// This blocks the current kernel thread until the process exits, then
167/// cleans up page tables and reaps the zombie process entry.
168#[cfg(all(feature = "alloc", target_arch = "x86_64"))]
169fn run_kde_process(pid: crate::process::ProcessId) {
170    use crate::process::get_process;
171
172    // Save page table root before running (needed for cleanup after exit)
173    let saved_pt_root = if let Some(proc) = get_process(pid) {
174        proc.memory_space.lock().get_page_table()
175    } else {
176        0
177    };
178
179    // Look up first thread ID for boot-current tracking
180    let tid = if let Some(proc) = get_process(pid) {
181        let threads = proc.threads.lock();
182        threads.values().next().map(|t| t.tid)
183    } else {
184        None
185    };
186
187    // Run the process (blocks until it exits)
188    if let Some(tid) = tid {
189        crate::process::set_boot_current(pid, tid);
190        crate::bootstrap::run_user_process(pid);
191        crate::process::clear_boot_current();
192    } else {
193        crate::bootstrap::run_user_process(pid);
194    }
195
196    // Boot CR3 is now restored. Free page table hierarchy frames.
197    // If the process called exec(), the page table was replaced -- free both.
198    let current_pt_root = if let Some(proc) = get_process(pid) {
199        proc.memory_space.lock().get_page_table()
200    } else {
201        0
202    };
203
204    if current_pt_root != 0 && current_pt_root != saved_pt_root {
205        crate::mm::vas::free_user_page_table_frames(current_pt_root);
206    }
207    if saved_pt_root != 0 {
208        crate::mm::vas::free_user_page_table_frames(saved_pt_root);
209    }
210
211    // Clear page_table_root to prevent double-free
212    if let Some(proc) = get_process(pid) {
213        proc.memory_space.lock().set_page_table(0);
214    }
215
216    // Reap zombie process
217    if let Some(proc) = get_process(pid) {
218        let state = proc.get_state();
219        if state == crate::process::ProcessState::Zombie
220            || state == crate::process::ProcessState::Dead
221        {
222            crate::process::table::remove_process(pid);
223        }
224    }
225}
226
227/// Simple monotonic counter for measuring elapsed time.
228///
229/// Uses a static atomic counter incremented by a periodic timer or
230/// approximated via TSC reads.
231fn get_iteration_counter() -> u64 {
232    #[cfg(target_arch = "x86_64")]
233    {
234        // Use TSC as a rough monotonic counter
235        // SAFETY: RDTSC is always available on x86_64
236        unsafe { core::arch::x86_64::_rdtsc() / 1_000_000 }
237    }
238    #[cfg(not(target_arch = "x86_64"))]
239    {
240        0
241    }
242}
243
244/// Stub for non-x86_64 architectures.
245#[cfg(not(all(feature = "alloc", target_arch = "x86_64")))]
246pub fn start_kde_session() {
247    crate::println!("[KDE] KDE session not supported on this architecture");
248    crate::println!("[KDE] Falling back to built-in desktop...");
249    crate::desktop::renderer::start_desktop();
250}
251
252// =========================================================================
253// Tests
254// =========================================================================
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_kde_env_has_required_vars() {
262        let has_display = KDE_ENV.iter().any(|e| e.starts_with("WAYLAND_DISPLAY="));
263        let has_desktop = KDE_ENV
264            .iter()
265            .any(|e| e.starts_with("XDG_CURRENT_DESKTOP="));
266        let has_qpa = KDE_ENV.iter().any(|e| e.starts_with("QT_QPA_PLATFORM="));
267        assert!(has_display, "WAYLAND_DISPLAY must be set");
268        assert!(has_desktop, "XDG_CURRENT_DESKTOP must be set");
269        assert!(has_qpa, "QT_QPA_PLATFORM must be set");
270    }
271
272    #[test]
273    fn test_min_session_lifetime() {
274        // 500 iterations * ~10ms = ~5 seconds
275        assert!(MIN_SESSION_LIFETIME_ITERS >= 100);
276    }
277
278    #[test]
279    fn test_kde_init_script_path() {
280        assert!(KDE_INIT_SCRIPT.starts_with("/usr/share/"));
281        assert!(KDE_INIT_SCRIPT.ends_with(".sh"));
282    }
283
284    #[test]
285    fn test_kde_binary_paths() {
286        assert!(KWIN_WAYLAND.starts_with("/usr/bin/"));
287        assert!(DBUS_DAEMON.starts_with("/usr/bin/"));
288    }
289}