← luna

Commit f05c344

9
files
+148
added
-89
deleted
@@ -7,6 +7,7 @@
7 7 //! - [`scale`] — [`Scale`] do przeliczania pikseli logicznych na fizyczne.
8 8 //! - [`event`] — [`Event`] i towarzyszące typy wejścia.
9 9 //! - [`motion`] — krzywe [`Easing`] i interpolacja [`lerp`] (matematyka ruchu).
10 +//! - [`runtime`] — wybór katalogu `XDG_RUNTIME_DIR` (wspólny dla kompozytora i klientów).
10 11 //! - [`style`] — tokeny wyglądu ([`style::Theme`], spacing, radius, typografia).
11 12 //!
12 13 //! Moduł nie zależy od Waylanda ani od konkretnego rasteryzatora — jest wspólnym
@@ -17,6 +18,7 @@ pub mod color;
17 18 pub mod event;
18 19 pub mod geometry;
19 20 pub mod motion;
21 +pub mod runtime;
20 22 pub mod scale;
21 23 pub mod style;
22 24 pub mod theme_file;
23 25 new file mode 100644
@@ -0,0 +1,102 @@
1 +//! Katalog runtime (`XDG_RUNTIME_DIR`) — wspólny dla kompozytora i klientów.
2 +//!
3 +//! Gniazdo Waylanda leży w `XDG_RUNTIME_DIR`. Menedżer sesji (logind/`pam_systemd`)
4 +//! zwykle ustawia tę zmienną, ale gdy tego nie zrobi (np. `pam_systemd` nie założył
5 +//! sesji), i kompozytor, i klienci muszą wybrać **ten sam** katalog — inaczej klient
6 +//! nie znajdzie gniazda i zgłosi `NoCompositor`. Ten moduł daje jedną, spójną logikę.
7 +
8 +use std::fs;
9 +use std::os::unix::fs::{DirBuilderExt, MetadataExt};
10 +use std::path::{Path, PathBuf};
11 +
12 +/// Bieżący uid — właściciel `/proc/self` (bez zależności od libc/rustix).
13 +fn current_uid() -> Option<u32> {
14 + fs::metadata("/proc/self").ok().map(|m| m.uid())
15 +}
16 +
17 +/// Czy `path` to istniejący, bezwzględny katalog należący do `uid`, tryb `0700`
18 +/// (bez dostępu dla grupy i innych)?
19 +pub fn is_private_dir_owned_by(path: &Path, uid: u32) -> bool {
20 + path.is_absolute()
21 + && fs::metadata(path)
22 + .map(|m| m.is_dir() && m.uid() == uid && m.mode() & 0o077 == 0)
23 + .unwrap_or(false)
24 +}
25 +
26 +/// Zapewnia poprawny `XDG_RUNTIME_DIR`.
27 +///
28 +/// Jeśli zmienna jest już ustawiona i wskazuje prywatny katalog naszego użytkownika —
29 +/// nic nie zmieniamy. Inaczej wybieramy `/run/user/<uid>`, a gdy ono nie istnieje —
30 +/// tworzymy prywatny katalog `0700` pod `$TMPDIR`.
31 +///
32 +/// Zwraca `Some(katalog)`, gdy zmienną trzeba było ustawić (przydatne do diagnostyki),
33 +/// `None`, gdy była już poprawna albo gdy nie dało się ustalić uid.
34 +pub fn ensure_runtime_dir() -> std::io::Result<Option<PathBuf>> {
35 + let Some(uid) = current_uid() else {
36 + return Ok(None);
37 + };
38 +
39 + if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") {
40 + if is_private_dir_owned_by(Path::new(&dir), uid) {
41 + return Ok(None);
42 + }
43 + }
44 +
45 + let run_user = PathBuf::from(format!("/run/user/{uid}"));
46 + let dir = if is_private_dir_owned_by(&run_user, uid) {
47 + run_user
48 + } else {
49 + let base = std::env::var_os("TMPDIR")
50 + .map(PathBuf::from)
51 + .filter(|p| p.is_absolute())
52 + .unwrap_or_else(|| PathBuf::from("/tmp"));
53 + let dir = base.join(format!("luna-runtime-{uid}"));
54 + let mut builder = fs::DirBuilder::new();
55 + builder.mode(0o700);
56 + match builder.create(&dir) {
57 + Ok(()) => {}
58 + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
59 + Err(e) => return Err(e),
60 + }
61 + if !is_private_dir_owned_by(&dir, uid) {
62 + return Err(std::io::Error::new(
63 + std::io::ErrorKind::PermissionDenied,
64 + format!(
65 + "{} nie jest prywatnym katalogiem uid {uid} (oczekiwano 0700)",
66 + dir.display()
67 + ),
68 + ));
69 + }
70 + dir
71 + };
72 +
73 + std::env::set_var("XDG_RUNTIME_DIR", &dir);
74 + Ok(Some(dir))
75 +}
76 +
77 +#[cfg(test)]
78 +mod tests {
79 + use super::*;
80 + use std::os::unix::fs::PermissionsExt;
81 +
82 + #[test]
83 + fn private_dir_of_our_uid_is_accepted() {
84 + let uid = current_uid().expect("uid na Linuksie");
85 + let dir = std::env::temp_dir().join(format!("luna-runtime-test-{}", std::process::id()));
86 + let mut builder = fs::DirBuilder::new();
87 + builder.mode(0o700);
88 + builder.create(&dir).unwrap();
89 +
90 + assert!(is_private_dir_owned_by(&dir, uid));
91 + // Obcy uid.
92 + assert!(!is_private_dir_owned_by(&dir, uid.wrapping_add(1)));
93 + // Zbyt otwarte uprawnienia.
94 + fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
95 + assert!(!is_private_dir_owned_by(&dir, uid));
96 + // Nieistniejąca ścieżka i ścieżka względna.
97 + assert!(!is_private_dir_owned_by(&dir.join("missing"), uid));
98 + assert!(!is_private_dir_owned_by(Path::new("relative/dir"), uid));
99 +
100 + fs::remove_dir_all(&dir).ok();
101 + }
102 +}
@@ -292,6 +292,12 @@ pub fn run<F>(config: AppConfig, render: F) -> Result<(), Box<dyn Error>>
292 292 where
293 293 F: for<'a> FnMut(&mut Frame<'a>) + 'static,
294 294 {
295 + // Klient potrzebuje `XDG_RUNTIME_DIR`, żeby znaleźć gniazdo kompozytora. Menedżer
296 + // sesji zwykle go ustawia; gdy brak (np. zepsuty pam_systemd), wybieramy wspólny
297 + // katalog — ten sam co kompozytor — żeby uniknąć `NoCompositor`.
298 + if let Ok(Some(dir)) = libluna_core::runtime::ensure_runtime_dir() {
299 + eprintln!("libLuna: XDG_RUNTIME_DIR not set, using {}", dir.display());
300 + }
295 301 let conn = Connection::connect_to_env()?;
296 302 let (globals, event_queue) = registry_queue_init(&conn)?;
297 303 let qh = event_queue.handle();
@@ -133,8 +133,9 @@ sudo ./scripts/install.sh --prefix /usr # instalacja syst
133 133 Pliki sesji: `de/session/luna-session` (launcher) i `de/session/selene.desktop`
134 134 (`share/wayland-sessions`).
135 135
136 -Klienta podłączysz ustawiając `WAYLAND_DISPLAY=wayland-0` i `XDG_RUNTIME_DIR` na ten sam
137 -katalog (renderowanie pojawi się, gdy wejdzie backend wyświetlania).
136 +Klienta podłączysz ustawiając `WAYLAND_DISPLAY=wayland-0` (`XDG_RUNTIME_DIR` klient wybierze
137 +awaryjnie sam, tak jak Selene — patrz `libluna_core::runtime`). Renderowanie pojawi się, gdy
138 +wejdzie backend wyświetlania.
138 139
139 140 ## Bezpieczeństwo / Security
140 141
@@ -20,4 +20,4 @@ libluna-platform.workspace = true
20 20 wayland-server = "0.31"
21 21 wayland-protocols = { version = "0.32", features = ["server", "unstable"] }
22 22 wayland-protocols-wlr = { version = "0.3", features = ["server"] }
23 -rustix = { version = "0.38", features = ["event", "fs", "time", "mm", "process"] }
23 +rustix = { version = "0.38", features = ["event", "fs", "time", "mm"] }
@@ -22,9 +22,7 @@ mod sync;
22 22
23 23 use std::error::Error;
24 24 use std::os::fd::{AsRawFd, BorrowedFd};
25 -use std::os::unix::fs::MetadataExt;
26 25 use std::panic::{catch_unwind, AssertUnwindSafe};
27 -use std::path::{Path, PathBuf};
28 26 use std::sync::Arc;
29 27
30 28 use libluna_core::Event;
@@ -217,91 +215,18 @@ fn main() -> Result<(), Box<dyn Error>> {
217 215 }
218 216 }
219 217
220 -/// Zapewnia, że `XDG_RUNTIME_DIR` wskazuje prywatny katalog użytkownika, zanim
221 -/// zwiążemy gniazdo Wayland.
218 +/// Zapewnia, że `XDG_RUNTIME_DIR` jest ustawiony, zanim zwiążemy gniazdo Wayland.
222 219 ///
223 -/// Menedżer sesji (logind/`pam_systemd`) normalnie ustawia tę zmienną. Gdy jej brak
224 -/// lub jest bezużyteczna (np. `pam_systemd` nie zdołał założyć sesji i zmienna nie
225 -/// trafiła do środowiska), `ListeningSocket::bind_auto` kończy się błędem
226 -/// `RuntimeDirNotSet`, a kompozytor wychodzi natychmiast — co z zewnątrz wygląda jak
227 -/// „sesja się nie uruchomiła”. Dlatego wybieramy `/run/user/<uid>`, a gdy ono nie
228 -/// istnieje — tworzymy własny katalog `0700` pod `$TMPDIR`.
220 +/// Wspólna logika (wybór `/run/user/<uid>` albo prywatnego katalogu pod `$TMPDIR`)
221 +/// mieszka w [`libluna_core::runtime`] — korzystają z niej też klienci (m.in. `luna-shell`),
222 +/// żeby trafić na to samo gniazdo. Tu tylko raportujemy wybrany katalog.
229 223 fn ensure_runtime_dir() -> Result<(), Box<dyn Error>> {
230 - let uid = rustix::process::getuid().as_raw() as u32;
231 -
232 - if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") {
233 - if is_private_dir_owned_by(&PathBuf::from(dir), uid) {
234 - return Ok(());
235 - }
236 - }
237 -
238 - let run_user = PathBuf::from(format!("/run/user/{uid}"));
239 - let dir = if is_private_dir_owned_by(&run_user, uid) {
240 - run_user
241 - } else {
242 - let base = std::env::var_os("TMPDIR")
243 - .map(PathBuf::from)
244 - .filter(|p| p.is_absolute())
245 - .unwrap_or_else(|| PathBuf::from("/tmp"));
246 - let dir = base.join(format!("selene-runtime-{uid}"));
247 - match rustix::fs::mkdir(&dir, rustix::fs::Mode::RWXU) {
248 - Ok(()) | Err(rustix::io::Errno::EXIST) => {}
249 - Err(e) => return Err(format!("mkdir {}: {e}", dir.display()).into()),
250 - }
251 - if !is_private_dir_owned_by(&dir, uid) {
252 - return Err(format!(
253 - "runtime dir {} nie należy do uid {uid} lub jest dostępny dla grupy/innych",
254 - dir.display()
255 - )
256 - .into());
257 - }
258 - dir
259 - };
260 -
261 - println!(
262 - "{} {}",
263 - libluna_i18n::tr("XDG_RUNTIME_DIR not set, using"),
264 - dir.display()
265 - );
266 - std::env::set_var("XDG_RUNTIME_DIR", &dir);
267 - Ok(())
268 -}
269 -
270 -/// Czy `path` to istniejący, bezwzględny katalog należący do `uid`, bez dostępu dla
271 -/// grupy i innych użytkowników (tryb `0700`)?
272 -fn is_private_dir_owned_by(path: &Path, uid: u32) -> bool {
273 - path.is_absolute()
274 - && std::fs::metadata(path)
275 - .map(|m| m.is_dir() && m.uid() == uid && m.mode() & 0o077 == 0)
276 - .unwrap_or(false)
277 -}
278 -
279 -#[cfg(test)]
280 -mod runtime_dir_tests {
281 - use super::*;
282 - use std::os::unix::fs::PermissionsExt as _;
283 -
284 - #[test]
285 - fn private_dir_owned_by_uid_is_accepted() {
286 - let uid = rustix::process::getuid().as_raw() as u32;
287 - let dir = std::env::temp_dir().join(format!("selene-rdir-{}", std::process::id()));
288 - std::fs::create_dir_all(&dir).unwrap();
289 - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
290 -
291 - assert!(
292 - is_private_dir_owned_by(&dir, uid),
293 - "katalog 0700 naszego uid"
224 + if let Some(dir) = libluna_core::runtime::ensure_runtime_dir()? {
225 + println!(
226 + "{} {}",
227 + libluna_i18n::tr("XDG_RUNTIME_DIR not set, using"),
228 + dir.display()
294 229 );
295 - // Obcy uid: nie jest nasz.
296 - assert!(!is_private_dir_owned_by(&dir, uid.wrapping_add(1)));
297 - // Zbyt otwarte uprawnienia: grupa/inny mają dostęp.
298 - std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
299 - assert!(!is_private_dir_owned_by(&dir, uid));
300 - // Nieistniejąca ścieżka.
301 - assert!(!is_private_dir_owned_by(&dir.join("missing"), uid));
302 - // Ścieżka względna nie nadaje się na `XDG_RUNTIME_DIR`.
303 - assert!(!is_private_dir_owned_by(Path::new("relative/dir"), uid));
304 -
305 - std::fs::remove_dir_all(&dir).ok();
306 230 }
231 + Ok(())
307 232 }
@@ -33,7 +33,7 @@ pub fn spawn(
33 33 .app_id("eu.paganlinux.Luna.Selene")
34 34 .initial_size(width, height)
35 35 .settings(ThemeSettings::default());
36 - let _ = run(config, move |frame: &mut Frame| {
36 + let result = run(config, move |frame: &mut Frame| {
37 37 // Zgłoś bieżący rozmiar monitora (okno może być zmieniane przez rodzica).
38 38 {
39 39 let mut outs = outputs.lock_or_recover();
@@ -50,6 +50,15 @@ pub fn spawn(
50 50 // Utrzymuj klatki — model okien zmienia się poza tą pętlą.
51 51 frame.request_frame();
52 52 });
53 + // Nested rysuje do okna na nadrzędnym kompozytorze Wayland. Gdy go nie ma
54 + // (np. uruchomienie na X11), nie połykamy błędu — inaczej wygląda to jak
55 + // „kompozytor działa, ale nic nie widać”.
56 + if let Err(e) = result {
57 + eprintln!(
58 + "{}: {e}",
59 + libluna_i18n::tr("nested output unavailable (no parent compositor)")
60 + );
61 + }
53 62 });
54 63 }
55 64
@@ -21,4 +21,11 @@ fi
21 21 : "${SELENE_BACKEND:=drm}"
22 22 export SELENE_BACKEND
23 23
24 +# Tryb DRM nie ma nadrzędnego kompozytora — wejście czytamy wprost z /dev/input
25 +# (wymaga dostępu do urządzeń: grupa `input` albo root).
26 +if [ "$SELENE_BACKEND" = "drm" ]; then
27 + : "${SELENE_INPUT:=evdev}"
28 + export SELENE_INPUT
29 +fi
30 +
24 31 exec selene
@@ -21,4 +21,11 @@ fi
21 21 : "${SELENE_BACKEND:=drm}"
22 22 export SELENE_BACKEND
23 23
24 +# Tryb DRM nie ma nadrzędnego kompozytora — wejście czytamy wprost z /dev/input
25 +# (wymaga dostępu do urządzeń: grupa `input` albo root).
26 +if [ "$SELENE_BACKEND" = "drm" ]; then
27 + : "${SELENE_INPUT:=evdev}"
28 + export SELENE_INPUT
29 +fi
30 +
24 31 exec selene