diff --git a/README.md b/README.md index 48f4c22..fbb3bf2 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ binary, so normal installation does not require a Rust toolchain. | Brightness, volume, settings, and update commands | ✅ | ✅ | ✅ | ✅ | | Brightness desktop dialog | ✅ | ✅ | ✅ | ✅ | -The default `auto` backend uses GNOME when compatible, then falls back to -`swayidle` when installed. Native Wayland is currently opt-in: +The default `auto` backend prefers a complete GNOME session, then native +Wayland when the compositor provides `ext_idle_notifier_v1` version 2 or newer +and at least one seat. It uses `swayidle` only as a deprecated compatibility +fallback when native monitoring is unavailable. Inspect the decision with: ```bash -lg-buddy settings set screen.backend wayland +lg-buddy settings describe screen.backend ``` See the [user guide](docs/user-guide.md#automatic-screen-blanking) for backend @@ -44,14 +46,14 @@ The native `lg_webos` control path does not require Python. Native-only packages can omit the Python client, `venv`, and `pip`. The current fresh-install flow still provisions `bscpylgtv` as a compatibility fallback and installs the brightness dialog, so release-bundle installation checks for Python 3 with a -`venv` that provisions `pip`, plus `zenity`. `swayidle` is required only when -using that desktop backend. +`venv` that provisions `pip`, plus `zenity`. `swayidle` is needed only by an +existing explicit selection or as the deprecated compatibility fallback. ### Debian, Ubuntu, and Pop!_OS ```bash sudo apt install python3-venv python3-pip zenity -# Optional swayidle backend: +# Deprecated compatibility fallback only: sudo apt install swayidle ``` @@ -59,7 +61,7 @@ sudo apt install swayidle ```bash sudo dnf install python3 python3-pip python3-virtualenv zenity -# Optional swayidle backend: +# Deprecated compatibility fallback only: sudo dnf install swayidle ``` @@ -67,7 +69,7 @@ sudo dnf install swayidle ```bash sudo pacman -S python python-pip python-virtualenv zenity -# Optional swayidle backend: +# Deprecated compatibility fallback only: sudo pacman -S swayidle ``` diff --git a/configure.sh b/configure.sh index f8a4269..d5f27a9 100755 --- a/configure.sh +++ b/configure.sh @@ -161,7 +161,7 @@ if [ "${LG_BUDDY_NONINTERACTIVE:-0}" = "1" ]; then exit 1 } validate_backend "$screen_backend" || { - echo "LG_BUDDY_SCREEN_BACKEND must be one of auto, gnome, wayland, or swayidle." + echo "LG_BUDDY_SCREEN_BACKEND must be auto, gnome, wayland, or the deprecated compatibility value swayidle." exit 1 } validate_screen_idle_blank "$screen_idle_blank" || { @@ -310,7 +310,11 @@ else echo " 1) auto" echo " 2) gnome" echo " 3) wayland" - echo " 4) swayidle" + backend_choice_range="1-3" + if [ "$existing_config_loaded" -eq 1 ] && [ "$current_screen_backend" = "swayidle" ]; then + echo " 4) swayidle (deprecated compatibility backend; preserve existing selection)" + backend_choice_range="1-4" + fi case "$current_screen_backend" in auto) default_backend_choice="1" ;; @@ -321,13 +325,19 @@ else esac while true; do - BACKEND_CHOICE="$(prompt_with_default "Enter number (1-4)" "$default_backend_choice")" + BACKEND_CHOICE="$(prompt_with_default "Enter number ($backend_choice_range)" "$default_backend_choice")" case "$BACKEND_CHOICE" in 1) screen_backend="auto"; break ;; 2) screen_backend="gnome"; break ;; 3) screen_backend="wayland"; break ;; - 4) screen_backend="swayidle"; break ;; - *) echo " Please enter a number between 1 and 4." ;; + 4) + if [ "$backend_choice_range" = "1-4" ]; then + screen_backend="swayidle" + break + fi + echo " Please enter a number between 1 and 3." + ;; + *) echo " Please enter a number in $backend_choice_range." ;; esac done @@ -383,6 +393,9 @@ echo " System Sleep/Wake: $system_sleep_wake_policy" echo " Update Checks: $update_auto_check" echo " Update Channel: $update_channel" echo " Config File: $CONFIG_FILE" +if [ "$screen_backend" = "swayidle" ]; then + echo " Warning: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0; use auto or wayland." +fi echo "" if [ "${LG_BUDDY_NONINTERACTIVE:-0}" != "1" ]; then diff --git a/crates/lg-buddy/src/backend.rs b/crates/lg-buddy/src/backend.rs index 367e11f..05ed10b 100644 --- a/crates/lg-buddy/src/backend.rs +++ b/crates/lg-buddy/src/backend.rs @@ -1,3 +1,4 @@ +use std::cell::{Cell, RefCell}; use std::env; use std::error::Error; use std::fmt; @@ -10,7 +11,12 @@ use crate::sources::desktop::gnome::{ GNOME_IDLE_MONITOR_NAME, GNOME_REQUIRED_SERVICES_REASON, GNOME_SCREEN_SAVER_NAME, GNOME_SHELL_NAME, }; -use crate::sources::desktop::wayland::probe_wayland_capabilities; +use crate::sources::desktop::wayland::{ + connect_wayland, probe_wayland_capabilities_on, WaylandProviderCapabilities, +}; + +pub const SWAYIDLE_DEPRECATION_NOTICE: &str = + "swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0; use auto or wayland"; const GNOME_SHELL_WAIT_TIMEOUT: Duration = Duration::from_secs(2); @@ -34,7 +40,11 @@ impl Error for BackendSelectionError {} #[derive(Debug, Clone, PartialEq, Eq)] pub enum BackendDetectionError { - NoSupportedBackend, + NoSupportedBackend { + gnome_reason: String, + wayland_reason: String, + swayidle_reason: String, + }, UnavailableBackend { backend: ScreenBackend, reason: String, @@ -48,12 +58,14 @@ pub enum BackendDetectionError { impl fmt::Display for BackendDetectionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NoSupportedBackend => { - write!( - f, - "no supported backend detected; install swayidle or run under a compatible GNOME session" - ) - } + Self::NoSupportedBackend { + gnome_reason, + wayland_reason, + swayidle_reason, + } => write!( + f, + "no supported backend detected; GNOME unavailable: {gnome_reason}; native Wayland unavailable: {wayland_reason}; deprecated swayidle compatibility unavailable: {swayidle_reason}" + ), Self::UnavailableBackend { backend, reason } => { write!(f, "backend `{}` is unavailable: {reason}", backend.as_str()) } @@ -68,18 +80,57 @@ impl fmt::Display for BackendDetectionError { impl Error for BackendDetectionError {} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BackendResolution { + backend: ScreenBackend, + fallback_reason: Option, +} + +impl BackendResolution { + pub fn backend(&self) -> ScreenBackend { + self.backend + } + + pub fn fallback_reason(&self) -> Option<&str> { + self.fallback_reason.as_deref() + } + + pub(crate) fn selected(backend: ScreenBackend, fallback_reason: Option) -> Self { + Self { + backend, + fallback_reason, + } + } +} + pub trait BackendProbe { fn has_command(&self, command: &str) -> bool; fn gnome_shell_available(&self) -> bool; fn gnome_screen_saver_available(&self) -> bool; fn gnome_idle_monitor_available(&self) -> bool; - fn wayland_capabilities(&self) -> Result<(), String> { + fn wayland_capabilities(&self) -> Result { Err("native Wayland capability probing is unavailable".to_string()) } + fn swayidle_fallback_available(&self) -> Result<(), String> { + if self.has_command("swayidle") { + Ok(()) + } else { + Err("swayidle command not found".to_string()) + } + } } -#[derive(Debug, Default, Clone, Copy)] -pub struct SystemBackendProbe; +#[derive(Default)] +pub struct SystemBackendProbe { + wayland_connection: RefCell>, + inherited_wayland_socket_consumed: Cell, +} + +impl SystemBackendProbe { + pub fn take_wayland_connection(&mut self) -> Option { + self.wayland_connection.get_mut().take() + } +} impl BackendProbe for SystemBackendProbe { fn has_command(&self, command: &str) -> bool { @@ -115,10 +166,36 @@ impl BackendProbe for SystemBackendProbe { bus.name_has_owner(GNOME_IDLE_MONITOR_NAME).unwrap_or(false) } - fn wayland_capabilities(&self) -> Result<(), String> { - probe_wayland_capabilities() - .map(|_| ()) - .map_err(|err| err.to_string()) + fn wayland_capabilities(&self) -> Result { + let connection = match self.wayland_connection.borrow().as_ref() { + Some(connection) => connection.clone(), + None => { + let inherited_socket_without_display = env::var_os("WAYLAND_SOCKET").is_some() + && env::var_os("WAYLAND_DISPLAY").is_none(); + let result = connect_wayland().map_err(|err| err.to_string()); + if inherited_socket_without_display && env::var_os("WAYLAND_SOCKET").is_none() { + self.inherited_wayland_socket_consumed.set(true); + } + result? + } + }; + let capabilities = + probe_wayland_capabilities_on(connection.clone()).map_err(|err| err.to_string())?; + *self.wayland_connection.borrow_mut() = Some(connection); + Ok(capabilities) + } + + fn swayidle_fallback_available(&self) -> Result<(), String> { + if !self.has_command("swayidle") { + return Err("swayidle command not found".to_string()); + } + if self.inherited_wayland_socket_consumed.get() { + return Err( + "native probing consumed the session's one-shot WAYLAND_SOCKET, so swayidle cannot reconnect; configure swayidle explicitly to bypass native probing" + .to_string(), + ); + } + Ok(()) } } @@ -148,42 +225,68 @@ pub fn configured_backend_from_sources( pub fn detect_backend_from_system( configured: ScreenBackend, ) -> Result { - detect_backend_with_probe(&SystemBackendProbe, configured) + resolve_backend_from_system(configured).map(|resolution| resolution.backend()) +} + +pub fn resolve_backend_from_system( + configured: ScreenBackend, +) -> Result { + resolve_backend_with_probe(&SystemBackendProbe::default(), configured) } pub fn detect_backend_with_probe( probe: &impl BackendProbe, configured: ScreenBackend, ) -> Result { + resolve_backend_with_probe(probe, configured).map(|resolution| resolution.backend()) +} + +pub fn resolve_backend_with_probe( + probe: &impl BackendProbe, + configured: ScreenBackend, +) -> Result { match configured { ScreenBackend::Auto => { - if probe.gnome_shell_available() { - if probe.gnome_screen_saver_available() && probe.gnome_idle_monitor_available() { - return Ok(ScreenBackend::Gnome); - } - - if probe.has_command("swayidle") { - return Ok(ScreenBackend::Swayidle); - } - - return Err(BackendDetectionError::UnavailableBackend { - backend: ScreenBackend::Gnome, - reason: GNOME_REQUIRED_SERVICES_REASON.to_string(), - }); + let gnome_shell_available = probe.gnome_shell_available(); + if gnome_shell_available + && probe.gnome_screen_saver_available() + && probe.gnome_idle_monitor_available() + { + return Ok(BackendResolution::selected(ScreenBackend::Gnome, None)); } - if probe.has_command("swayidle") { - return Ok(ScreenBackend::Swayidle); + let gnome_reason = if gnome_shell_available { + GNOME_REQUIRED_SERVICES_REASON.to_string() + } else { + "GNOME Shell is not available".to_string() + }; + + match probe.wayland_capabilities() { + Ok(_) => Ok(BackendResolution::selected( + ScreenBackend::Wayland, + Some(format!("GNOME unavailable: {gnome_reason}")), + )), + Err(wayland_reason) => match probe.swayidle_fallback_available() { + Ok(()) => Ok(BackendResolution::selected( + ScreenBackend::Swayidle, + Some(format!( + "GNOME unavailable: {gnome_reason}; native Wayland unavailable: {wayland_reason}" + )), + )), + Err(swayidle_reason) => Err(BackendDetectionError::NoSupportedBackend { + gnome_reason, + wayland_reason, + swayidle_reason, + }), + }, } - - Err(BackendDetectionError::NoSupportedBackend) } ScreenBackend::Gnome => { if probe.gnome_shell_available() && probe.gnome_screen_saver_available() && probe.gnome_idle_monitor_available() { - Ok(ScreenBackend::Gnome) + Ok(BackendResolution::selected(ScreenBackend::Gnome, None)) } else { Err(BackendDetectionError::UnavailableBackend { backend: ScreenBackend::Gnome, @@ -193,14 +296,14 @@ pub fn detect_backend_with_probe( } ScreenBackend::Wayland => probe .wayland_capabilities() - .map(|()| ScreenBackend::Wayland) + .map(|_| BackendResolution::selected(ScreenBackend::Wayland, None)) .map_err(|reason| BackendDetectionError::UnavailableBackend { backend: ScreenBackend::Wayland, reason, }), ScreenBackend::Swayidle => { if probe.has_command("swayidle") { - Ok(ScreenBackend::Swayidle) + Ok(BackendResolution::selected(ScreenBackend::Swayidle, None)) } else { Err(BackendDetectionError::MissingRequiredCommand { backend: ScreenBackend::Swayidle, @@ -226,10 +329,11 @@ fn command_in_path(command: &str) -> bool { #[cfg(test)] mod tests { use super::{ - configured_backend_from_sources, detect_backend_with_probe, BackendDetectionError, - BackendProbe, BackendSelectionError, + configured_backend_from_sources, detect_backend_with_probe, resolve_backend_with_probe, + BackendDetectionError, BackendProbe, BackendSelectionError, }; use crate::config::ScreenBackend; + use crate::sources::desktop::wayland::WaylandProviderCapabilities; #[derive(Debug, Clone, Copy)] struct FakeProbe { @@ -237,6 +341,21 @@ mod tests { gnome_screen_saver_available: bool, gnome_idle_monitor_available: bool, has_swayidle: bool, + swayidle_fallback_reason: Option<&'static str>, + wayland_capabilities: Result, + } + + impl Default for FakeProbe { + fn default() -> Self { + Self { + gnome_shell_available: false, + gnome_screen_saver_available: false, + gnome_idle_monitor_available: false, + has_swayidle: false, + swayidle_fallback_reason: None, + wayland_capabilities: Err("no Wayland compositor is available"), + } + } } impl BackendProbe for FakeProbe { @@ -258,9 +377,23 @@ mod tests { fn gnome_idle_monitor_available(&self) -> bool { self.gnome_idle_monitor_available } + + fn wayland_capabilities(&self) -> Result { + self.wayland_capabilities.map_err(str::to_string) + } + + fn swayidle_fallback_available(&self) -> Result<(), String> { + if !self.has_swayidle { + return Err("swayidle command not found".to_string()); + } + match self.swayidle_fallback_reason { + Some(reason) => Err(reason.to_string()), + None => Ok(()), + } + } } - struct WaylandProbe(Result<(), &'static str>); + struct WaylandProbe(Result); impl BackendProbe for WaylandProbe { fn has_command(&self, _command: &str) -> bool { @@ -279,11 +412,18 @@ mod tests { false } - fn wayland_capabilities(&self) -> Result<(), String> { + fn wayland_capabilities(&self) -> Result { self.0.map_err(str::to_string) } } + fn native_wayland_capabilities() -> WaylandProviderCapabilities { + WaylandProviderCapabilities { + idle_notifier_version: 2, + seat_count: 1, + } + } + #[test] fn env_override_wins_over_config_backend() { let backend = configured_backend_from_sources(Some("swayidle"), Some(ScreenBackend::Gnome)) @@ -334,6 +474,7 @@ mod tests { gnome_screen_saver_available: true, gnome_idle_monitor_available: true, has_swayidle: true, + ..FakeProbe::default() }; let backend = @@ -342,6 +483,45 @@ mod tests { assert_eq!(backend, ScreenBackend::Gnome); } + #[test] + fn auto_selects_native_wayland_before_swayidle() { + let probe = FakeProbe { + has_swayidle: true, + wayland_capabilities: Ok(native_wayland_capabilities()), + ..FakeProbe::default() + }; + + let resolution = resolve_backend_with_probe(&probe, ScreenBackend::Auto) + .expect("detect native Wayland backend"); + + assert_eq!(resolution.backend(), ScreenBackend::Wayland); + assert_eq!( + resolution.fallback_reason(), + Some("GNOME unavailable: GNOME Shell is not available") + ); + } + + #[test] + fn auto_selects_native_wayland_when_gnome_is_incomplete() { + let probe = FakeProbe { + gnome_shell_available: true, + gnome_screen_saver_available: true, + gnome_idle_monitor_available: false, + has_swayidle: true, + wayland_capabilities: Ok(native_wayland_capabilities()), + ..FakeProbe::default() + }; + + let resolution = resolve_backend_with_probe(&probe, ScreenBackend::Auto) + .expect("fall back from incomplete GNOME to native Wayland"); + + assert_eq!(resolution.backend(), ScreenBackend::Wayland); + assert!(resolution + .fallback_reason() + .unwrap() + .contains("org.gnome.Mutter.IdleMonitor")); + } + #[test] fn auto_falls_back_to_swayidle() { let probe = FakeProbe { @@ -349,6 +529,7 @@ mod tests { gnome_screen_saver_available: false, gnome_idle_monitor_available: false, has_swayidle: true, + ..FakeProbe::default() }; let backend = detect_backend_with_probe(&probe, ScreenBackend::Auto) @@ -364,12 +545,20 @@ mod tests { gnome_screen_saver_available: false, gnome_idle_monitor_available: false, has_swayidle: false, + ..FakeProbe::default() }; let err = detect_backend_with_probe(&probe, ScreenBackend::Auto) .expect_err("missing backend should fail"); - assert_eq!(err, BackendDetectionError::NoSupportedBackend); + assert_eq!( + err, + BackendDetectionError::NoSupportedBackend { + gnome_reason: "GNOME Shell is not available".to_string(), + wayland_reason: "no Wayland compositor is available".to_string(), + swayidle_reason: "swayidle command not found".to_string(), + } + ); } #[test] @@ -379,6 +568,7 @@ mod tests { gnome_screen_saver_available: false, gnome_idle_monitor_available: false, has_swayidle: true, + ..FakeProbe::default() }; let err = detect_backend_with_probe(&probe, ScreenBackend::Gnome) @@ -402,6 +592,7 @@ mod tests { gnome_screen_saver_available: true, gnome_idle_monitor_available: false, has_swayidle: false, + ..FakeProbe::default() }; let err = detect_backend_with_probe(&probe, ScreenBackend::Auto) @@ -409,15 +600,43 @@ mod tests { assert_eq!( err, - BackendDetectionError::UnavailableBackend { - backend: ScreenBackend::Gnome, - reason: + BackendDetectionError::NoSupportedBackend { + gnome_reason: "GNOME Shell, org.gnome.ScreenSaver, and org.gnome.Mutter.IdleMonitor are required" .to_string(), + wayland_reason: "no Wayland compositor is available".to_string(), + swayidle_reason: "swayidle command not found".to_string(), } ); } + #[test] + fn auto_refuses_an_unsafe_swayidle_fallback_without_disabling_explicit_swayidle() { + let probe = FakeProbe { + has_swayidle: true, + swayidle_fallback_reason: Some( + "native probing consumed the session's one-shot WAYLAND_SOCKET", + ), + ..FakeProbe::default() + }; + + let err = detect_backend_with_probe(&probe, ScreenBackend::Auto) + .expect_err("unsafe automatic fallback should fail"); + assert_eq!( + err, + BackendDetectionError::NoSupportedBackend { + gnome_reason: "GNOME Shell is not available".to_string(), + wayland_reason: "no Wayland compositor is available".to_string(), + swayidle_reason: "native probing consumed the session's one-shot WAYLAND_SOCKET" + .to_string(), + } + ); + + let explicit = detect_backend_with_probe(&probe, ScreenBackend::Swayidle) + .expect("explicit swayidle should bypass native fallback safety"); + assert_eq!(explicit, ScreenBackend::Swayidle); + } + #[test] fn auto_falls_back_to_swayidle_when_gnome_idle_monitor_is_missing() { let probe = FakeProbe { @@ -425,12 +644,16 @@ mod tests { gnome_screen_saver_available: true, gnome_idle_monitor_available: false, has_swayidle: true, + ..FakeProbe::default() }; - let backend = detect_backend_with_probe(&probe, ScreenBackend::Auto) + let resolution = resolve_backend_with_probe(&probe, ScreenBackend::Auto) .expect("fallback to swayidle when GNOME is incomplete"); - assert_eq!(backend, ScreenBackend::Swayidle); + assert_eq!(resolution.backend(), ScreenBackend::Swayidle); + let reason = resolution.fallback_reason().unwrap(); + assert!(reason.contains("org.gnome.Mutter.IdleMonitor")); + assert!(reason.contains("native Wayland unavailable: no Wayland compositor is available")); } #[test] @@ -440,6 +663,7 @@ mod tests { gnome_screen_saver_available: true, gnome_idle_monitor_available: false, has_swayidle: true, + ..FakeProbe::default() }; let err = detect_backend_with_probe(&probe, ScreenBackend::Gnome) @@ -463,6 +687,7 @@ mod tests { gnome_screen_saver_available: true, gnome_idle_monitor_available: true, has_swayidle: false, + ..FakeProbe::default() }; let err = detect_backend_with_probe(&probe, ScreenBackend::Swayidle) @@ -500,8 +725,11 @@ mod tests { #[test] fn forced_wayland_is_selected_when_the_native_protocol_surface_is_available() { - let backend = detect_backend_with_probe(&WaylandProbe(Ok(())), ScreenBackend::Wayland) - .expect("forced Wayland should be available"); + let backend = detect_backend_with_probe( + &WaylandProbe(Ok(native_wayland_capabilities())), + ScreenBackend::Wayland, + ) + .expect("forced Wayland should be available"); assert_eq!(backend, ScreenBackend::Wayland); } diff --git a/crates/lg-buddy/src/session/runner.rs b/crates/lg-buddy/src/session/runner.rs index e39e276..7865370 100644 --- a/crates/lg-buddy/src/session/runner.rs +++ b/crates/lg-buddy/src/session/runner.rs @@ -14,8 +14,8 @@ use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; use crate::backend::{ - configured_backend_from_env_or_config, detect_backend_from_system, BackendDetectionError, - BackendSelectionError, + configured_backend_from_env_or_config, resolve_backend_with_probe, BackendDetectionError, + BackendResolution, BackendSelectionError, SystemBackendProbe, SWAYIDLE_DEPRECATION_NOTICE, }; use crate::commands::{run_sleep_pre_for_event, run_system_resume}; use crate::config::{ @@ -41,7 +41,7 @@ use crate::sources::desktop::gnome::{ screen_saver_owner_changed, GnomeBackend, SystemGnomeProbe, GNOME_SCREEN_SAVER_INTERFACE, GNOME_SCREEN_SAVER_PATH, GNOME_SHELL_NAME, }; -use crate::sources::desktop::wayland::{connect_wayland, run_wayland_activity_monitor}; +use crate::sources::desktop::wayland::run_wayland_activity_monitor; use crate::sources::linux::logind::{ acquire_sleep_delay_inhibitor, add_logind_signal_match, map_prepare_for_sleep_signal, }; @@ -509,21 +509,13 @@ fn lifecycle_policy_enabled_from_config(config_path: &Path) -> Result( +fn prepare_monitor_backend( + probe: &mut SystemBackendProbe, configured: ScreenBackend, - detect: F, -) -> Result -where - F: FnOnce(ScreenBackend) -> Result, -{ - if configured == ScreenBackend::Wayland { - // The provider validates the protocol and seats while opening its - // production connection. A separate probe would consume an inherited - // WAYLAND_SOCKET before the provider can use it. - Ok(ScreenBackend::Wayland) - } else { - detect(configured) - } +) -> Result<(BackendResolution, Option), BackendDetectionError> { + let resolution = resolve_backend_with_probe(probe, configured)?; + let connection = probe.take_wayland_connection(); + Ok((resolution, connection)) } fn run_monitor_with_executor( @@ -531,37 +523,17 @@ fn run_monitor_with_executor( executor: E, ) -> Result<(), SessionRunnerError> { let screen_idle_blank_enabled = screen_idle_blank_enabled_from_config()?; - let configured = if screen_idle_blank_enabled { - Some( - configured_backend_from_env_or_config() - .map_err(SessionRunnerError::BackendSelection)?, - ) - } else { - None - }; - let mut wayland_connection = if configured == Some(ScreenBackend::Wayland) { - // wayland-client consumes an inherited WAYLAND_SOCKET by mutating the - // process environment. Do that while monitor startup is single-threaded. - Some(connect_wayland().map_err(|err| SessionRunnerError::Failed { - backend: ScreenBackend::Wayland, - message: err.to_string(), - })?) - } else { - None - }; - - let _session_service = match spawn_session_notification_service() { - Ok(service) => Some(service), - Err(err) => { - writeln!( - writer, - "LG Buddy Monitor: session notification service unavailable: {err}" - )?; - None - } - }; - if !screen_idle_blank_enabled { + let _session_service = match spawn_session_notification_service() { + Ok(service) => Some(service), + Err(err) => { + writeln!( + writer, + "LG Buddy Monitor: session notification service unavailable: {err}" + )?; + None + } + }; writeln!( writer, "LG Buddy Monitor: screen idle blanking is disabled by config." @@ -569,47 +541,91 @@ fn run_monitor_with_executor( return run_passive_session_agent(writer); } + let initial_configured = + configured_backend_from_env_or_config().map_err(SessionRunnerError::BackendSelection)?; let mut executor = Some(executor); - let mut initial_configured = configured; let started = Instant::now(); let test_timeout = resolve_gnome_monitor_test_timeout(); + let mut probe = SystemBackendProbe::default(); + let initial_resolution = prepare_monitor_backend(&mut probe, initial_configured); + let mut initial_attempt = Some((initial_configured, initial_resolution)); - loop { - if test_timeout_reached(started, test_timeout) { - return Ok(()); + // Native probing must consume an inherited WAYLAND_SOCKET before this + // thread starts. The same probe is retained for every later retry so the + // automatic fallback policy cannot forget that one-shot socket state. + let _session_service = match spawn_session_notification_service() { + Ok(service) => Some(service), + Err(err) => { + writeln!( + writer, + "LG Buddy Monitor: session notification service unavailable: {err}" + )?; + None } + }; - let configured = match initial_configured.take() { - Some(configured) => configured, - None => configured_backend_from_env_or_config() - .map_err(SessionRunnerError::BackendSelection)?, + loop { + let (configured, resolution) = match initial_attempt.take() { + Some(attempt) => attempt, + None => { + if test_timeout_reached(started, test_timeout) { + return Ok(()); + } + let configured = configured_backend_from_env_or_config() + .map_err(SessionRunnerError::BackendSelection)?; + let resolution = prepare_monitor_backend(&mut probe, configured); + (configured, resolution) + } }; - match select_monitor_backend(configured, detect_backend_from_system) { - Ok(ScreenBackend::Gnome) => { - let mut dispatcher = - SessionEventDispatcher::new(executor.take().expect("executor available")); - return run_gnome_monitor(writer, &mut dispatcher); - } - Ok(ScreenBackend::Wayland) => { - let mut dispatcher = - SessionEventDispatcher::new(executor.take().expect("executor available")); - let connection = wayland_connection.take().ok_or_else(|| { - SessionRunnerError::Failed { - backend: ScreenBackend::Wayland, - message: "native Wayland was selected after threaded monitor startup; restart the monitor to acquire its connection safely" - .to_string(), + match resolution { + Ok((resolution, mut wayland_connection)) => { + if configured == ScreenBackend::Auto { + writeln!( + writer, + "LG Buddy Monitor: auto resolved to {}.", + resolution.backend().as_str() + )?; + if let Some(reason) = resolution.fallback_reason() { + writeln!(writer, "LG Buddy Monitor: fallback reason: {reason}")?; } - })?; - return run_wayland_monitor(writer, &mut dispatcher, connection); - } - Ok(ScreenBackend::Swayidle) => return run_swayidle_monitor(writer), - Ok(ScreenBackend::Auto) => { - return Err(SessionRunnerError::Failed { - backend: ScreenBackend::Auto, - message: "auto backend should be resolved before starting the runner" - .to_string(), - }); + } + if resolution.backend() == ScreenBackend::Swayidle { + writeln!( + writer, + "LG Buddy Monitor: warning: {SWAYIDLE_DEPRECATION_NOTICE}." + )?; + } + + match resolution.backend() { + ScreenBackend::Gnome => { + let mut dispatcher = SessionEventDispatcher::new( + executor.take().expect("executor available"), + ); + return run_gnome_monitor(writer, &mut dispatcher); + } + ScreenBackend::Wayland => { + let mut dispatcher = SessionEventDispatcher::new( + executor.take().expect("executor available"), + ); + let connection = wayland_connection.take().ok_or_else(|| { + SessionRunnerError::Failed { + backend: ScreenBackend::Wayland, + message: "native Wayland was selected without retaining its verified connection" + .to_string(), + } + })?; + return run_wayland_monitor(writer, &mut dispatcher, connection); + } + ScreenBackend::Swayidle => return run_swayidle_monitor(writer), + ScreenBackend::Auto => { + return Err(SessionRunnerError::Failed { + backend: ScreenBackend::Auto, + message: "auto backend should be resolved before starting the runner" + .to_string(), + }); + } + } } Err(err) => { writeln!( @@ -1586,7 +1602,7 @@ mod tests { gamepad_device_event_refresh_requested, gamepad_refresh_due, handle_inactivity_observation, handle_inactivity_timeout, normalize_idle_timeout_secs, poll_gnome_idle_monitor_once, run_lifecycle_monitor_with_bus, run_native_session_monitor, schedule_gamepad_refresh, - select_monitor_backend, shell_quote, GamepadDeviceEventMonitor, GamepadDeviceEventRefresh, + shell_quote, GamepadDeviceEventMonitor, GamepadDeviceEventRefresh, GamepadDiagnosticEmitter, LatestInactivityObservation, RunnerMessage, SessionActionExecutor, SessionEventDispatcher, TimedInactivityObservation, TrustedScreenSaverSignals, GAMEPAD_ACTIVITY_REFRESH_RETRY_INTERVAL, @@ -1620,27 +1636,6 @@ mod tests { LOCK.get_or_init(|| Mutex::new(())) } - #[test] - fn explicit_wayland_monitor_selection_does_not_probe_a_second_connection() { - let backend = select_monitor_backend(ScreenBackend::Wayland, |_| { - panic!("explicit Wayland monitor selection must not run capability detection") - }) - .expect("select explicit Wayland backend"); - - assert_eq!(backend, ScreenBackend::Wayland); - } - - #[test] - fn other_monitor_backend_selection_still_uses_detection() { - let backend = select_monitor_backend(ScreenBackend::Auto, |configured| { - assert_eq!(configured, ScreenBackend::Auto); - Ok(ScreenBackend::Swayidle) - }) - .expect("resolve automatic backend"); - - assert_eq!(backend, ScreenBackend::Swayidle); - } - #[derive(Debug, Default)] struct FakeActionExecutor { screen_off_calls: usize, diff --git a/crates/lg-buddy/src/settings.rs b/crates/lg-buddy/src/settings.rs index 64b7349..d94e441 100644 --- a/crates/lg-buddy/src/settings.rs +++ b/crates/lg-buddy/src/settings.rs @@ -1,7 +1,5 @@ use std::io; -use crate::config::ScreenBackend; - mod command; mod formatter; mod model; @@ -134,8 +132,11 @@ impl SettingsCommandRunner { } } - fn with_screen_backend_resolution(mut self, resolution: Option) -> Self { - self.screen_backend = screen::BackendPresentation::Resolved(resolution); + fn with_screen_backend_presentation( + mut self, + presentation: screen::BackendPresentation, + ) -> Self { + self.screen_backend = presentation; self } @@ -155,7 +156,7 @@ impl SettingsCommandRunner { self.formatter.write_describe_with_backend( writer, &[setting], - self.screen_backend, + &self.screen_backend, ) } None => { @@ -163,7 +164,7 @@ impl SettingsCommandRunner { self.formatter.write_describe_with_backend( writer, &settings, - self.screen_backend, + &self.screen_backend, ) } }, @@ -206,13 +207,14 @@ pub fn run_settings_command( writer: &mut W, ) -> Result<(), SettingsError> { let store = SettingsStore::load_from_env()?; + let configured_backend = store + .effective_by_name("screen.backend") + .ok() + .and_then(|setting| setting.value()) + .map(|value| value.to_string()); + let presentation = screen::presentation_for_command(&command, configured_backend.as_deref()); let runner = SettingsCommandRunner::new(store); - let runner = match screen::presentation_for_command(&command) { - screen::BackendPresentation::Raw => runner, - screen::BackendPresentation::Resolved(resolution) => { - runner.with_screen_backend_resolution(resolution) - } - }; + let runner = runner.with_screen_backend_presentation(presentation); runner.run(command, writer) } @@ -491,7 +493,7 @@ screen.backend default: auto mutability: read-write supported operations: get, describe, set, unset - allowed values: auto, gnome, wayland, swayidle + allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend) apply: restart-user-screen-service description: Screen backend selection for user-session blanking and restore behavior. diff --git a/crates/lg-buddy/src/settings/formatter.rs b/crates/lg-buddy/src/settings/formatter.rs index e3637db..10e85d6 100644 --- a/crates/lg-buddy/src/settings/formatter.rs +++ b/crates/lg-buddy/src/settings/formatter.rs @@ -43,14 +43,14 @@ impl SettingsFormatter { writer: &mut W, settings: &[EffectiveSetting], ) -> Result<(), SettingsError> { - self.write_describe_with_backend(writer, settings, screen::BackendPresentation::Raw) + self.write_describe_with_backend(writer, settings, &screen::BackendPresentation::Raw) } pub(super) fn write_describe_with_backend( &self, writer: &mut W, settings: &[EffectiveSetting], - screen_backend: screen::BackendPresentation, + screen_backend: &screen::BackendPresentation, ) -> Result<(), SettingsError> { for (index, setting) in settings.iter().enumerate() { if index > 0 { @@ -124,19 +124,26 @@ impl SettingsFormatter { &self, writer: &mut W, setting: &EffectiveSetting, - screen_backend: screen::BackendPresentation, + screen_backend: &screen::BackendPresentation, ) -> Result<(), SettingsError> { let definition = setting.definition(); writeln!(writer, "{}", setting.key_name()).map_err(output_error)?; writeln!(writer, " storage key: {}", setting.storage_key()).map_err(output_error)?; writeln!(writer, " type: {}", definition.value_type().as_str()).map_err(output_error)?; - writeln!( - writer, - " current: {}", - format_described_value(setting, screen_backend) - ) - .map_err(output_error)?; + writeln!(writer, " current: {}", format_described_value(setting)).map_err(output_error)?; + if setting.key_name() == "screen.backend" { + let configured = format_effective_value(setting); + if let Some((resolved, fallback_reason)) = + screen::resolution_details(&configured, screen_backend) + { + writeln!(writer, " resolved backend: {resolved}").map_err(output_error)?; + writeln!(writer, " fallback reason: {fallback_reason}").map_err(output_error)?; + } + if let Some(notice) = screen::deprecation_notice(&configured) { + writeln!(writer, " deprecation: {notice}.").map_err(output_error)?; + } + } writeln!(writer, " source: {}", setting.source().as_str()).map_err(output_error)?; writeln!(writer, " default: {}", definition.default_value_label()) .map_err(output_error)?; @@ -154,7 +161,7 @@ impl SettingsFormatter { writeln!( writer, " allowed values: {}", - format_described_enum_values(setting, enum_type.values(), screen_backend) + format_described_enum_values(setting, enum_type.values()) ) .map_err(output_error)?; if !enum_type.aliases().is_empty() { @@ -200,30 +207,23 @@ pub(super) fn format_effective_value(setting: &EffectiveSetting) -> String { .unwrap_or_else(|| "".to_string()) } -fn format_described_value( - setting: &EffectiveSetting, - screen_backend: screen::BackendPresentation, -) -> String { +fn format_described_value(setting: &EffectiveSetting) -> String { let value = format_effective_value(setting); if setting.key_name() == "screen.backend" { - screen::format_backend_choice(&value, screen_backend) + screen::format_backend_choice(&value) } else { value } } -fn format_described_enum_values( - setting: &EffectiveSetting, - values: &[&str], - screen_backend: screen::BackendPresentation, -) -> String { +fn format_described_enum_values(setting: &EffectiveSetting, values: &[&str]) -> String { if setting.key_name() != "screen.backend" { return values.join(", "); } values .iter() - .map(|value| screen::format_backend_choice(value, screen_backend)) + .map(|value| screen::format_backend_choice(value)) .collect::>() .join(", ") } diff --git a/crates/lg-buddy/src/settings/screen.rs b/crates/lg-buddy/src/settings/screen.rs index 09d2cd9..b852633 100644 --- a/crates/lg-buddy/src/settings/screen.rs +++ b/crates/lg-buddy/src/settings/screen.rs @@ -1,4 +1,4 @@ -use crate::backend::detect_backend_from_system; +use crate::backend::{resolve_backend_from_system, BackendResolution, SWAYIDLE_DEPRECATION_NOTICE}; use crate::config::{ScreenBackend, DEFAULT_IDLE_TIMEOUT, MAX_IDLE_TIMEOUT}; use super::{ @@ -77,42 +77,71 @@ pub(super) const RESTORE_POLICY: SettingDefinition = SettingDefinition { description: "Screen restore policy after LG Buddy blanks the configured screen.", }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum BackendPresentation { Raw, - Resolved(Option), + Resolved(Result), } -pub(super) fn presentation_for_command(command: &SettingsCommand) -> BackendPresentation { +pub(super) fn presentation_for_command( + command: &SettingsCommand, + configured: Option<&str>, +) -> BackendPresentation { let describes_backend = match command { SettingsCommand::Describe(None) => true, SettingsCommand::Describe(Some(key)) => key == "screen.backend", _ => false, }; - if describes_backend { - BackendPresentation::Resolved(detect_backend_from_system(ScreenBackend::Auto).ok()) - } else { - BackendPresentation::Raw + let Some(configured) = configured.and_then(|value| value.parse::().ok()) else { + return BackendPresentation::Raw; + }; + if !describes_backend { + return BackendPresentation::Raw; } + + BackendPresentation::Resolved( + resolve_backend_from_system(configured).map_err(|err| err.to_string()), + ) } -pub(super) fn format_backend_choice(value: &str, presentation: BackendPresentation) -> String { - if value != ScreenBackend::Auto.as_str() { - return value.to_string(); +pub(super) fn format_backend_choice(value: &str) -> String { + if value == ScreenBackend::Swayidle.as_str() { + format!("{value} (deprecated compatibility backend)") + } else { + value.to_string() } +} +pub(super) fn resolution_details( + configured: &str, + presentation: &BackendPresentation, +) -> Option<(String, String)> { match presentation { - BackendPresentation::Raw => value.to_string(), - BackendPresentation::Resolved(Some(backend)) => { - format!("{value} ({})", backend.as_str()) - } - BackendPresentation::Resolved(None) => { - format!("{value} (no backend currently available)") + BackendPresentation::Raw => None, + BackendPresentation::Resolved(Ok(resolution)) => Some(( + resolution.backend().as_str().to_string(), + resolution + .fallback_reason() + .unwrap_or_else(|| { + if configured == ScreenBackend::Auto.as_str() { + "none; preferred backend is available" + } else { + "none; explicit selection does not fall back" + } + }) + .to_string(), + )), + BackendPresentation::Resolved(Err(reason)) => { + Some(("unavailable".to_string(), reason.clone())) } } } +pub(super) fn deprecation_notice(configured: &str) -> Option<&'static str> { + (configured == ScreenBackend::Swayidle.as_str()).then_some(SWAYIDLE_DEPRECATION_NOTICE) +} + pub(super) fn apply_service_restart( service_controller: &C, ) -> Result { @@ -175,10 +204,14 @@ mod tests { } #[test] - fn settings_runner_describe_annotates_auto_backend_without_changing_get() { + fn settings_runner_describe_distinguishes_auto_resolution_without_changing_get() { let store = ConfigEnvReader::parse("/tmp/config.env", "screen_backend=auto\n").into_store(); - let runner = SettingsCommandRunner::new(store) - .with_screen_backend_resolution(Some(ScreenBackend::Gnome)); + let runner = SettingsCommandRunner::new(store).with_screen_backend_presentation( + BackendPresentation::Resolved(Ok(BackendResolution::selected( + ScreenBackend::Gnome, + None, + ))), + ); let mut output = Vec::new(); runner @@ -189,8 +222,12 @@ mod tests { .unwrap(); let output = String::from_utf8(output).unwrap(); - assert!(output.contains(" current: auto (gnome)\n")); - assert!(output.contains(" allowed values: auto (gnome), gnome, wayland, swayidle\n")); + assert!(output.contains(" current: auto\n")); + assert!(output.contains(" resolved backend: gnome\n")); + assert!(output.contains(" fallback reason: none; preferred backend is available\n")); + assert!(output.contains( + " allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend)\n" + )); let mut raw_output = Vec::new(); runner @@ -205,7 +242,11 @@ mod tests { #[test] fn settings_runner_describe_reports_when_auto_has_no_available_backend() { let store = ConfigEnvReader::parse("/tmp/config.env", "screen_backend=auto\n").into_store(); - let runner = SettingsCommandRunner::new(store).with_screen_backend_resolution(None); + let runner = SettingsCommandRunner::new(store).with_screen_backend_presentation( + BackendPresentation::Resolved( + Err("native Wayland protocol is unavailable".to_string()), + ), + ); let mut output = Vec::new(); runner @@ -216,12 +257,65 @@ mod tests { .unwrap(); let output = String::from_utf8(output).unwrap(); - assert!(output.contains(" current: auto (no backend currently available)\n")); + assert!(output.contains(" current: auto\n")); + assert!(output.contains(" resolved backend: unavailable\n")); + assert!(output.contains(" fallback reason: native Wayland protocol is unavailable\n")); assert!(output.contains( - " allowed values: auto (no backend currently available), gnome, wayland, swayidle\n" + " allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend)\n" )); } + #[test] + fn settings_runner_marks_explicit_swayidle_as_deprecated() { + let store = + ConfigEnvReader::parse("/tmp/config.env", "screen_backend=swayidle\n").into_store(); + let runner = SettingsCommandRunner::new(store).with_screen_backend_presentation( + BackendPresentation::Resolved(Ok(BackendResolution::selected( + ScreenBackend::Swayidle, + None, + ))), + ); + let mut output = Vec::new(); + + runner + .run( + SettingsCommand::Describe(Some("screen.backend".to_string())), + &mut output, + ) + .unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert!(output.contains(" current: swayidle (deprecated compatibility backend)\n")); + assert!(output.contains(" resolved backend: swayidle\n")); + assert!(output.contains(" fallback reason: none; explicit selection does not fall back\n")); + assert!(output.contains(" deprecation: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0; use auto or wayland.\n")); + } + + #[test] + fn settings_runner_reports_an_explicit_wayland_capability_limit() { + let store = + ConfigEnvReader::parse("/tmp/config.env", "screen_backend=wayland\n").into_store(); + let runner = SettingsCommandRunner::new(store).with_screen_backend_presentation( + BackendPresentation::Resolved(Err( + "backend `wayland` is unavailable: the compositor advertises ext_idle_notifier_v1 version 1; version 2 or newer is required" + .to_string(), + )), + ); + let mut output = Vec::new(); + + runner + .run( + SettingsCommand::Describe(Some("screen.backend".to_string())), + &mut output, + ) + .unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert!(output.contains(" current: wayland\n")); + assert!(output.contains(" resolved backend: unavailable\n")); + assert!(output.contains("ext_idle_notifier_v1 version 1; version 2 or newer is required\n")); + } + #[test] fn settings_runner_sets_value_and_restarts_active_screen_service() { let path = unique_test_path("set"); diff --git a/crates/lg-buddy/src/sources/desktop/swayidle.rs b/crates/lg-buddy/src/sources/desktop/swayidle.rs index e56dc4e..ac5c10c 100644 --- a/crates/lg-buddy/src/sources/desktop/swayidle.rs +++ b/crates/lg-buddy/src/sources/desktop/swayidle.rs @@ -73,7 +73,7 @@ pub struct SystemSwayidleProbe; impl SwayidleProbe for SystemSwayidleProbe { fn swayidle_available(&self) -> bool { - let probe = SystemBackendProbe; + let probe = SystemBackendProbe::default(); probe.has_command("swayidle") } diff --git a/crates/lg-buddy/src/sources/desktop/wayland.rs b/crates/lg-buddy/src/sources/desktop/wayland.rs index af32659..13f5b9c 100644 --- a/crates/lg-buddy/src/sources/desktop/wayland.rs +++ b/crates/lg-buddy/src/sources/desktop/wayland.rs @@ -302,6 +302,32 @@ where } } +#[derive(Default)] +struct WaylandCapabilityProbeState { + registry_facts: RegistryFacts, +} + +impl Dispatch for WaylandCapabilityProbeState { + fn event( + state: &mut Self, + _: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => state.registry_facts.add(name, interface.as_str(), version), + wl_registry::Event::GlobalRemove { name } => state.registry_facts.remove(name), + _ => {} + } + } +} + impl Dispatch for WaylandProviderState where F: FnMut(Instant) -> bool + 'static, @@ -390,11 +416,18 @@ pub(crate) fn connect_wayland() -> Result { Connection::connect_to_env().map_err(|err| WaylandProviderError::Connection(err.to_string())) } -pub(crate) fn probe_wayland_capabilities( +pub(crate) fn probe_wayland_capabilities_on( + connection: Connection, ) -> Result { - let connection = connect_wayland()?; - let (_, _, capabilities) = initialize_provider(connection, |_| true)?; - Ok(capabilities) + let display = connection.display(); + let mut event_queue = connection.new_event_queue(); + let queue_handle = event_queue.handle(); + let _registry = display.get_registry(&queue_handle, ()); + let mut state = WaylandCapabilityProbeState::default(); + event_queue + .roundtrip(&mut state) + .map_err(|err| WaylandProviderError::Dispatch(err.to_string()))?; + state.registry_facts.capabilities() } pub(crate) fn run_wayland_activity_monitor( diff --git a/crates/lg-buddy/tests/cucumber_support/world.rs b/crates/lg-buddy/tests/cucumber_support/world.rs index cc51771..2d2d18d 100644 --- a/crates/lg-buddy/tests/cucumber_support/world.rs +++ b/crates/lg-buddy/tests/cucumber_support/world.rs @@ -1,6 +1,7 @@ use crate::support::{ - ExecutableScript, MockBscpylgtv, MockNmOnline, MockSessionBusIdleMonitor, MockSwayidle, - MockSystemLogind, RuntimeStateLayout, TestConfigFile, TestEnv, + prime_isolated_path_dependencies, ExecutableScript, MockBscpylgtv, MockNmOnline, + MockSessionBusIdleMonitor, MockSwayidle, MockSystemLogind, RuntimeStateLayout, TestConfigFile, + TestEnv, }; use crate::web_os::{MockWebOsTv, MockWebOsTvSnapshot, MockWebOsVersion, VALID_WEBOS_ACCESS_TOKEN}; use cucumber::World; @@ -387,7 +388,11 @@ exit 1\n", } pub fn isolate_path(&mut self) { + prime_isolated_path_dependencies(); self.ensure_env().set("PATH", ""); + self.ensure_env().remove("WAYLAND_DISPLAY"); + self.ensure_env().remove("WAYLAND_SOCKET"); + self.ensure_env().remove("XDG_RUNTIME_DIR"); } pub fn set_backend_override(&mut self, backend: &str) { diff --git a/crates/lg-buddy/tests/features/detect_backend.feature b/crates/lg-buddy/tests/features/detect_backend.feature index 136c0d2..1b50742 100644 --- a/crates/lg-buddy/tests/features/detect_backend.feature +++ b/crates/lg-buddy/tests/features/detect_backend.feature @@ -10,7 +10,7 @@ Feature: Detect backend Then the command succeeds And stdout is "gnome" - Scenario: swayidle is selected when GNOME is unavailable + Scenario: swayidle is selected when GNOME and native Wayland are unavailable Given a temporary LG Buddy config using input HDMI_2 And the executable PATH is isolated And swayidle is installed @@ -35,3 +35,4 @@ Feature: Detect backend When I run the command "detect-backend" Then the command fails And stderr contains "org.gnome.Mutter.IdleMonitor" + And stderr contains "native Wayland unavailable" diff --git a/crates/lg-buddy/tests/features/monitor_gnome.feature b/crates/lg-buddy/tests/features/monitor_gnome.feature index 6474ed8..196dd81 100644 --- a/crates/lg-buddy/tests/features/monitor_gnome.feature +++ b/crates/lg-buddy/tests/features/monitor_gnome.feature @@ -17,6 +17,15 @@ Feature: GNOME monitor And the TV client did not receive "turn_screen_off" And the TV client did not receive "turn_screen_on" + Scenario: unavailable idle backends do not suppress the session notification service + Given a temporary LG Buddy config using input HDMI_2 + And the executable PATH is isolated + And GNOME monitor stays open for 0.1 seconds + When I run the command "monitor" + Then the command succeeds + And stdout contains "session notification service unavailable" + And stdout contains "screen idle backend unavailable" + Scenario: GNOME ScreenSaver idle does not bypass the LG Buddy timeout Given a temporary LG Buddy config using input HDMI_2 And the idle timeout is 2 seconds diff --git a/crates/lg-buddy/tests/features/monitor_swayidle.feature b/crates/lg-buddy/tests/features/monitor_swayidle.feature index f49ec45..8fe2450 100644 --- a/crates/lg-buddy/tests/features/monitor_swayidle.feature +++ b/crates/lg-buddy/tests/features/monitor_swayidle.feature @@ -12,6 +12,7 @@ Feature: swayidle monitor And swayidle will emit an idle timeout When I run the command "monitor" Then the command succeeds + And stdout contains "swayidle is a deprecated compatibility backend" And the TV client received "get_input" And the TV client received "turn_screen_off" And the session marker exists diff --git a/crates/lg-buddy/tests/features/settings.feature b/crates/lg-buddy/tests/features/settings.feature index 6232b40..57c50ff 100644 --- a/crates/lg-buddy/tests/features/settings.feature +++ b/crates/lg-buddy/tests/features/settings.feature @@ -45,34 +45,51 @@ Feature: Settings CLI And stdout contains "settings" And stdout does not contain "detect-backend" - Scenario: settings describe annotates auto with the resolved GNOME backend + Scenario: settings describe distinguishes auto from the resolved GNOME backend Given a temporary LG Buddy config using input HDMI_2 And GNOME Shell is available And the executable PATH is isolated When I run the command "settings describe screen.backend" Then the command succeeds - And stdout contains "current: auto (gnome)" - And stdout contains "allowed values: auto (gnome), gnome, wayland, swayidle" + And stdout contains "current: auto" + And stdout contains "resolved backend: gnome" + And stdout contains "fallback reason: none; preferred backend is available" + And stdout contains "allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend)" When I run the command "settings get screen.backend" Then the command succeeds And stdout is "auto" - Scenario: settings describe annotates auto with the swayidle fallback + Scenario: settings describe explains the swayidle compatibility fallback Given a temporary LG Buddy config using input HDMI_2 And the executable PATH is isolated And swayidle is installed When I run the command "settings describe screen.backend" Then the command succeeds - And stdout contains "current: auto (swayidle)" - And stdout contains "allowed values: auto (swayidle), gnome, wayland, swayidle" + And stdout contains "current: auto" + And stdout contains "resolved backend: swayidle" + And stdout contains "native Wayland unavailable" + And stdout contains "allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend)" Scenario: settings describe remains available without a detected backend Given a temporary LG Buddy config using input HDMI_2 And the executable PATH is isolated When I run the command "settings describe screen.backend" Then the command succeeds - And stdout contains "current: auto (no backend currently available)" - And stdout contains "allowed values: auto (no backend currently available), gnome, wayland, swayidle" + And stdout contains "current: auto" + And stdout contains "resolved backend: unavailable" + And stdout contains "native Wayland unavailable" + And stdout contains "allowed values: auto, gnome, wayland, swayidle (deprecated compatibility backend)" + + Scenario: settings describe marks an explicit swayidle selection as deprecated + Given a temporary LG Buddy config using input HDMI_2 + And the executable PATH is isolated + And systemd apply actions are skipped + When I run the command "settings set screen.backend swayidle" + Then the command succeeds + When I run the command "settings describe screen.backend" + Then the command succeeds + And stdout contains "current: swayidle (deprecated compatibility backend)" + And stdout contains "planned for removal in LG Buddy 2.0.0" Scenario: settings describe shows required TV operations Given a temporary LG Buddy config using input HDMI_2 diff --git a/crates/lg-buddy/tests/support/mod.rs b/crates/lg-buddy/tests/support/mod.rs index 4d624cd..7d78738 100644 --- a/crates/lg-buddy/tests/support/mod.rs +++ b/crates/lg-buddy/tests/support/mod.rs @@ -1574,6 +1574,12 @@ fn env_lock() -> &'static Mutex<()> { ENV_LOCK.get_or_init(|| Mutex::new(())) } +#[allow(dead_code)] +pub fn prime_isolated_path_dependencies() { + let _ = python3_path(); + let _ = dbus_daemon_path(); +} + fn python3_path() -> PathBuf { static PYTHON3_PATH: OnceLock = OnceLock::new(); diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index e0113ea..75cadc8 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -284,7 +284,7 @@ The intended split is: - native Wake-on-LAN packet generation and UDP send - `backend.rs` - backend selection and detection - - `auto`, `gnome`, explicit `wayland`, and `swayidle` support + - `auto`, `gnome`, native `wayland`, and deprecated `swayidle` compatibility - `session.rs` - backend-neutral session event model - capability surface for desktop backends @@ -623,10 +623,10 @@ Selection order: Detection behavior: - `auto` prefers GNOME when the current session satisfies the full GNOME contract and the session bus is reachable -- otherwise falls back to `swayidle` if installed -- explicit `wayland` validates `ext_idle_notifier_v1` version 2 or newer plus - at least one advertised seat and does not fall back -- `auto` does not select native Wayland yet +- native `wayland` validates `ext_idle_notifier_v1` version 2 or newer plus at + least one advertised seat; explicit selection does not fall back +- `auto` prefers complete GNOME, then compatible native Wayland, then the + deprecated `swayidle` compatibility backend when installed - other forced backends validate their required services or commands ## TV Integration Boundary @@ -840,9 +840,11 @@ asymmetric: handled by the NetworkManager pre-down gate plus logind lifecycle service instead -`swayidle` remains the external-tool compatibility backend while native -Wayland is explicit opt-in. Automatic native selection and later deprecation of -the delegated path are separate work. +`swayidle` remains an explicit and automatic compatibility fallback during the +1.x migration window, but emits a deprecation notice and is not offered by +fresh interactive configuration. Removal is planned for 2.0.0 after native +Wayland remains field-validated across supported compositors and unsupported +sessions have precise diagnostics. ## Configuration and Override Surface diff --git a/docs/development.md b/docs/development.md index 376dd9c..084dd8b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -18,7 +18,7 @@ testing release bundles also requires: Backend-specific tools used in development and local testing: -- `swayidle` for the `swayidle` monitor backend +- `swayidle` only when testing the deprecated compatibility backend - readable `/dev/input/event*` devices for local gamepad activity testing - readable `/dev/hidraw*` devices when testing the Logitech G923 raw HID fallback diff --git a/docs/runtime-event-handler-map.md b/docs/runtime-event-handler-map.md index 58cfd04..dde6cf3 100644 --- a/docs/runtime-event-handler-map.md +++ b/docs/runtime-event-handler-map.md @@ -97,8 +97,8 @@ Examples: ### Native Inactivity Path -The native inactivity path is used by GNOME and the explicit native Wayland -backend. Both feed activity facts into the same inactivity model instead of +The native inactivity path is used by GNOME and native Wayland, including +Wayland selected by `auto`. Both feed activity facts into the same inactivity model instead of delegating blank/restore commands to an external tool. ```text @@ -153,8 +153,9 @@ swayidle timeout/resume currently starts `swayidle` with direct `screen off` and `screen on` commands. Those richer hook events are not consumed by the monitor runner. -This path exists for current non-GNOME Wayland support. It is delegated, but it -is not a separate screen-policy quirks mode: `swayidle` re-enters LG Buddy +This deprecated path remains for existing explicit selections and as an +automatic compatibility fallback on unsupported native sessions. It is +delegated, but it is not a separate screen-policy quirks mode: `swayidle` re-enters LG Buddy through the same CLI/API command surface as manual `screen off` and `screen on`. Retiring it means replacing delegated timeout/resume execution with native idle/activity facts that feed the same inactivity engine used by the current @@ -283,9 +284,9 @@ The current architecture has the Linux lifecycle sources, screen policy, lifecycle policy, runtime phase guard, and source adapter namespace in place. Remaining work should stay scoped: -1. Keep native Wayland idle replacement separate from the logind lifecycle path. -2. Keep `swayidle` available while native Wayland remains explicit opt-in; - automatic promotion and deprecation are separate work. +1. Keep native Wayland monitoring separate from the logind lifecycle path. +2. Keep `swayidle` working without rewriting existing configuration throughout + the documented 1.x compatibility window. 3. Preserve the one-lifecycle-owner invariant in installer, release-bundle, and uninstall tests. 4. Treat future platform lifecycle providers, such as a possible macOS provider, diff --git a/docs/session-backend-model.md b/docs/session-backend-model.md index 3cfffd5..c1aa182 100644 --- a/docs/session-backend-model.md +++ b/docs/session-backend-model.md @@ -168,8 +168,8 @@ facts; it does not acquire gamepad responsibility. ### Native Wayland -The explicit `wayland` backend requires `ext_idle_notifier_v1` version 2 or -newer and at least one advertised `wl_seat`. It monitors every seat, including +The native `wayland` backend requires `ext_idle_notifier_v1` version 2 or newer +and at least one advertised `wl_seat`. It monitors every seat, including seats that currently advertise no input capabilities, using zero-timeout idle notifications. `resumed` maps to desktop activity; `idled` remains observational, so only LG Buddy's inactivity deadline can trigger blanking. @@ -177,8 +177,8 @@ observational, so only LG Buddy's inactivity deadline can trigger blanking. Seats are added and removed dynamically. Connection or dispatch loss, removal of the bound notifier, or removal of the last seat is fatal to the provider and causes the user service to retry. Explicit selection reports capability errors -without falling back. `auto` does not select this backend yet, and `swayidle` -remains available. +without falling back. `auto` selects native Wayland after the complete GNOME +contract and before the deprecated `swayidle` compatibility backend. ### `swayidle` @@ -195,6 +195,9 @@ Current mapping: Notes: +- `swayidle` is deprecated, remains accepted for existing explicit selections, + and is planned for removal in 2.0.0 after the native provider remains + field-validated across supported compositors and the 1.x migration window. - `swayidle` does not provide a clear equivalent of GNOME's `WakeRequested`. - `swayidle` does not provide a Mutter-style early activity surface. - LG Buddy owns the configured timeout value for this backend. diff --git a/docs/testing-strategy.md b/docs/testing-strategy.md index 92e9d05..53df140 100644 --- a/docs/testing-strategy.md +++ b/docs/testing-strategy.md @@ -242,12 +242,13 @@ Examples: - gamepad activity integration with the LG Buddy inactivity deadline - screen runtime-phase eligibility over the private logind system-bus seam -Native Wayland changes also require manual opt-in checks on Plasma/KWin and at -least one other target compositor. Verify that explicit `wayland` detection and -monitor startup succeed, unsupported capability or connection cases fail with -a precise diagnostic, and `auto` retains its existing GNOME-then-`swayidle` -selection. Release-facing changes must keep the static x86_64 musl build and -release-bundle smoke test green. +Native Wayland changes also require manual checks on Plasma/KWin and at least +one other target compositor. Verify that explicit and automatic `wayland` +detection and monitor startup succeed, unsupported capability or connection +cases report a precise fallback reason, and `auto` retains the +GNOME-then-native-Wayland-then-`swayidle` order. Release-facing changes must +keep the static x86_64 musl build and release-bundle smoke test green, including +preservation and deprecation reporting for an existing `swayidle` config. ### Gamepad activity diff --git a/docs/user-guide.md b/docs/user-guide.md index 8d8f4fd..4ba1df6 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -83,7 +83,7 @@ Current settings are: | `tv.mac` | TV MAC address used for Wake-on-LAN. | | `tv.input` | Input LG Buddy manages, such as `HDMI_2`. | | `tv.platform` | TV control implementation: `bscpylgtv` or experimental `lg_webos`. | -| `screen.backend` | Desktop idle backend: `auto`, `gnome`, `wayland`, or `swayidle`. | +| `screen.backend` | Desktop idle backend: `auto`, `gnome`, `wayland`, or deprecated compatibility value `swayidle`. | | `screen.idle_blank` | Enable or disable automatic idle blanking. | | `screen.idle_timeout` | Seconds of inactivity before blanking; defaults to 300. | | `screen.restore_policy` | `conservative` or `aggressive` restore behavior. | @@ -129,10 +129,10 @@ The restore policies are: | Backend | When to use it | | --- | --- | -| `auto` | Default. Uses GNOME when the session is compatible, otherwise `swayidle` when installed. It does not select native Wayland yet. | +| `auto` | Default. Prefers compatible GNOME, then compatible native Wayland, then the deprecated `swayidle` fallback when installed. | | `gnome` | A GNOME Shell session with the required GNOME idle services. | -| `wayland` | A compatible recent Wayland compositor. This backend is currently opt-in. | -| `swayidle` | A Wayland session with `swayidle` installed. | +| `wayland` | Force native monitoring on a compositor that advertises `ext_idle_notifier_v1` version 2 or newer and at least one `wl_seat`. | +| `swayidle` | Deprecated compatibility backend for existing installations and older compositors. Fresh interactive configuration does not offer it. | Select a backend persistently: @@ -147,8 +147,9 @@ lg-buddy settings unset screen.backend ``` An explicitly selected backend reports a compatibility error rather than -silently switching to another backend. If native Wayland is unavailable, use -`auto` or `swayidle` instead. +silently switching to another backend. `auto` reports why it moved past GNOME +or native Wayland. Existing explicit `swayidle` selections remain valid and are +never silently rewritten, but emit a deprecation notice. Check the selected backend and user service: @@ -158,8 +159,16 @@ systemctl --user status LG_Buddy_screen.service journalctl --user -u LG_Buddy_screen.service --since today ``` -For `auto`, `settings describe` also shows the backend currently detected, such -as `auto (gnome)` or `auto (swayidle)`. +For `auto`, `settings describe` prints the configured selection, resolved +backend, and fallback reason separately. Unsupported native sessions report +the compositor connection or protocol limitation before using `swayidle` or +reporting that no backend is available. + +The `swayidle` compatibility window lasts through the 1.x release line, with +removal planned for 2.0.0. Removal requires native Wayland monitoring to remain +field-validated on supported non-GNOME compositors, precise unsupported-session +diagnostics, and a released migration window in which existing configurations +continue to run without being rewritten. ### Gamepad Activity diff --git a/install.sh b/install.sh index f97a10f..244a2cb 100755 --- a/install.sh +++ b/install.sh @@ -453,8 +453,10 @@ else fi ;; swayidle) - if command -v swayidle &>/dev/null; then - echo " [OK] swayidle (configured backend)" + echo " [WARNING] swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0" + echo " Select auto or wayland when the compositor supports ext_idle_notifier_v1 version 2 or newer." + if command -v swayidle >/dev/null 2>&1; then + echo " [OK] swayidle (configured compatibility backend)" SCREEN_MONITOR_AVAILABLE=1 SCREEN_MONITOR_RUNTIME_BACKEND="swayidle" else @@ -463,19 +465,23 @@ else fi ;; *) - if command -v swayidle &>/dev/null; then - echo " [OK] swayidle (wlroots/COSMIC backend)" - SCREEN_MONITOR_AVAILABLE=1 - else - echo " [OPTIONAL] swayidle (required for wlroots/COSMIC backend)" - fi - - SCREEN_MONITOR_RUNTIME_BACKEND="$("$RUNTIME_BINARY" detect-backend 2>/dev/null || true)" - if [ -n "$SCREEN_MONITOR_RUNTIME_BACKEND" ]; then + SCREEN_MONITOR_DIAGNOSTICS="$("$RUNTIME_BINARY" settings describe screen.backend 2>/dev/null || true)" + SCREEN_MONITOR_RUNTIME_BACKEND="$(printf '%s\n' "$SCREEN_MONITOR_DIAGNOSTICS" | sed -n 's/^ resolved backend: //p' | tail -n1)" + SCREEN_MONITOR_FALLBACK_REASON="$(printf '%s\n' "$SCREEN_MONITOR_DIAGNOSTICS" | sed -n 's/^ fallback reason: //p' | tail -n1)" + if [ -n "$SCREEN_MONITOR_RUNTIME_BACKEND" ] && [ "$SCREEN_MONITOR_RUNTIME_BACKEND" != "unavailable" ]; then SCREEN_MONITOR_AVAILABLE=1 echo " [OK] current session backend: $SCREEN_MONITOR_RUNTIME_BACKEND" + if [ -n "$SCREEN_MONITOR_FALLBACK_REASON" ] && [ "$SCREEN_MONITOR_FALLBACK_REASON" != "none; preferred backend is available" ]; then + echo " [INFO] fallback reason: $SCREEN_MONITOR_FALLBACK_REASON" + fi + if [ "$SCREEN_MONITOR_RUNTIME_BACKEND" = "swayidle" ]; then + echo " [WARNING] using deprecated swayidle compatibility fallback; planned for removal in LG Buddy 2.0.0" + fi else echo " [INFO] no supported backend detected in the current session" + if [ -n "$SCREEN_MONITOR_FALLBACK_REASON" ]; then + echo " $SCREEN_MONITOR_FALLBACK_REASON" + fi echo " The user-session service will retry until a supported backend is available." fi ;; diff --git a/scripts/test-cross-version-upgrade.sh b/scripts/test-cross-version-upgrade.sh index c619179..7d0bb7b 100755 --- a/scripts/test-cross-version-upgrade.sh +++ b/scripts/test-cross-version-upgrade.sh @@ -249,7 +249,7 @@ do done export LG_BUDDY_CONFIG="$CONFIG_FILE" -"$INSTALLED_BINARY" settings set screen.backend gnome +"$INSTALLED_BINARY" settings set screen.backend swayidle "$INSTALLED_BINARY" settings set screen.idle_timeout 900 "$INSTALLED_BINARY" settings set screen.restore_policy aggressive "$INSTALLED_BINARY" settings set screen.idle_blank disabled @@ -421,7 +421,7 @@ do grep -F -q "LG_BUDDY_CONFIG=$CONFIG_FILE" "$override" done -grep -q '^screen_backend=gnome$' "$CONFIG_FILE" +grep -q '^screen_backend=swayidle$' "$CONFIG_FILE" grep -q '^screen_idle_timeout=900$' "$CONFIG_FILE" grep -q '^screen_restore_policy=aggressive$' "$CONFIG_FILE" grep -q '^screen_idle_blank=disabled$' "$CONFIG_FILE" @@ -432,6 +432,8 @@ grep -q '^tvs_primary_input=HDMI_4$' "$CONFIG_FILE" grep -q '^tvs_primary_platform=lg_webos$' "$CONFIG_FILE" grep -q '^updates_auto_check=disabled$' "$CONFIG_FILE" grep -q '^updates_channel=prerelease$' "$CONFIG_FILE" +"$INSTALLED_BINARY" settings describe screen.backend \ + | grep -F -q 'deprecation: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0' python3 "$SCRIPT_DIR/release_bundle_manifest.py" validate \ --manifest "$CANDIDATE_BUNDLE/release-manifest.json" \ diff --git a/scripts/test-production-upgrade-canary.sh b/scripts/test-production-upgrade-canary.sh index 1c0f582..809c818 100755 --- a/scripts/test-production-upgrade-canary.sh +++ b/scripts/test-production-upgrade-canary.sh @@ -172,7 +172,7 @@ VENV_MARKER="$INSTALL_ROOT/usr/bin/LG_Buddy_PIP/production-canary-native-marker" [ -f "$INSTALLED_POINTER" ] || fail "Baseline config pointer was not installed." export LG_BUDDY_CONFIG="$CONFIG_FILE" -"$INSTALLED_BINARY" settings set screen.backend gnome +"$INSTALLED_BINARY" settings set screen.backend swayidle "$INSTALLED_BINARY" settings set screen.idle_blank disabled "$INSTALLED_BINARY" settings set updates.auto_check disabled "$INSTALLED_BINARY" settings set updates.channel prerelease @@ -208,6 +208,8 @@ cmp -s "$POINTER_SNAPSHOT" "$INSTALLED_POINTER" || fail "Production upgrade chan cmp -s "$TOKEN_SNAPSHOT" "$NATIVE_TOKEN_FILE" || fail "Production upgrade changed the native credential." [ -e "$VENV_MARKER" ] || fail "Production native upgrade recreated the Python environment." "$INSTALLED_BINARY" settings get updates.channel | grep -q '^prerelease$' +"$INSTALLED_BINARY" settings describe screen.backend \ + | grep -F -q 'deprecation: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0' CANDIDATE_CHECK_OUTPUT="$WORK_DIR/candidate-update-check.output" UPDATE_CACHE_FILE="$XDG_CACHE_HOME/lg-buddy/update-check.json" diff --git a/scripts/test-release-bundle.sh b/scripts/test-release-bundle.sh index 3a81e4f..c0d0eb0 100755 --- a/scripts/test-release-bundle.sh +++ b/scripts/test-release-bundle.sh @@ -342,6 +342,26 @@ printf '%s\n' "$VERSION_OUTPUT" | grep -q "^version: " printf '%s\n' "$VERSION_OUTPUT" | grep -q "^channel: " printf '%s\n' "$VERSION_OUTPUT" | grep -q "^commit: " +FRESH_CONFIG_HOME="$WORK_DIR/fresh-config-home" +FRESH_CONFIG_OUTPUT="$WORK_DIR/fresh-config.output" +mkdir -p "$FRESH_CONFIG_HOME" +( + unset LG_BUDDY_NONINTERACTIVE LG_BUDDY_SCREEN_BACKEND LG_BUDDY_CONFIG + export HOME="$FRESH_CONFIG_HOME" + export XDG_CONFIG_HOME="$FRESH_CONFIG_HOME/.config" + export LG_BUDDY_RUNTIME_BINARY="$BUNDLE_DIR/lg-buddy" + export LG_BUDDY_SKIP_SYSTEMD_ACTIONS="1" + printf '%s\n' \ + '192.0.2.10' 'aa:bb:cc:dd:ee:ff' '2' '1' 'Y' '1' '300' '1' 'Y' \ + | "$BUNDLE_DIR/configure.sh" >"$FRESH_CONFIG_OUTPUT" 2>&1 +) +grep -F -q ' 3) wayland' "$FRESH_CONFIG_OUTPUT" +if grep -F -q 'swayidle' "$FRESH_CONFIG_OUTPUT"; then + echo "Fresh interactive configuration presented swayidle." + exit 1 +fi +grep -q '^screen_backend=auto$' "$FRESH_CONFIG_HOME/.config/lg-buddy/config.env" + export HOME="$HOME_DIR" export XDG_CONFIG_HOME="$XDG_CONFIG_HOME" export LG_BUDDY_INSTALL_ROOT="$INSTALL_ROOT" @@ -445,7 +465,7 @@ printf '%s\n' "$NATIVE_PLATFORM_OUTPUT" | grep -F -q 'No stored native TV creden "$INSTALLED_BINARY" settings set tv.platform bscpylgtv grep -q '^tvs_primary_platform=bscpylgtv$' "$CONFIG_FILE" -"$INSTALLED_BINARY" settings set screen.backend gnome +"$INSTALLED_BINARY" settings set screen.backend swayidle "$INSTALLED_BINARY" settings set screen.idle_timeout 900 "$INSTALLED_BINARY" settings set screen.idle_timeout 90000 grep -q '^screen_idle_timeout=86400$' "$CONFIG_FILE" @@ -460,7 +480,7 @@ grep -q '^screen_idle_timeout=86400$' "$CONFIG_FILE" "$INSTALLED_BINARY" settings set updates.channel prerelease BACKGROUND_UPDATE_OUTPUT="$("$INSTALLED_BINARY" updates background-check)" printf '%s\n' "$BACKGROUND_UPDATE_OUTPUT" | grep -F -q 'background: skipped (automatic update checks disabled)' -grep -q '^screen_backend=gnome$' "$CONFIG_FILE" +grep -q '^screen_backend=swayidle$' "$CONFIG_FILE" grep -q '^screen_idle_blank=disabled$' "$CONFIG_FILE" grep -q '^screen_idle_timeout=900$' "$CONFIG_FILE" grep -q '^screen_restore_policy=aggressive$' "$CONFIG_FILE" @@ -475,6 +495,7 @@ grep -q '^updates_channel=prerelease$' "$CONFIG_FILE" # semantics as the Rust config parser, then persist the sanitized choice. sed -i 's/^tvs_primary_platform=bscpylgtv$/ tvs_primary_platform = bscpylgtv # legacy/' "$CONFIG_FILE" printf '%s\n' 'tvs_primary_platform = lg_webos # experimental' >>"$CONFIG_FILE" +LEGACY_CONFIGURE_OUTPUT="$WORK_DIR/legacy-configure.output" ( unset LG_BUDDY_SCREEN_BACKEND @@ -485,14 +506,16 @@ printf '%s\n' 'tvs_primary_platform = lg_webos # experimental' >>"$CONFIG_FILE" export LG_BUDDY_TV_MAC="11:22:33:44:55:66" export LG_BUDDY_INPUT="HDMI_3" cd "$BUNDLE_DIR" - ./configure.sh + ./configure.sh >"$LEGACY_CONFIGURE_OUTPUT" 2>&1 ) +grep -F -q 'Warning: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0' "$LEGACY_CONFIGURE_OUTPUT" + grep -q '^tvs_primary_ip=192.168.1.11$' "$CONFIG_FILE" grep -q '^tvs_primary_mac=11:22:33:44:55:66$' "$CONFIG_FILE" grep -q '^tvs_primary_input=HDMI_3$' "$CONFIG_FILE" grep -q '^tvs_primary_platform=lg_webos$' "$CONFIG_FILE" -grep -q '^screen_backend=gnome$' "$CONFIG_FILE" +grep -q '^screen_backend=swayidle$' "$CONFIG_FILE" grep -q '^screen_idle_blank=disabled$' "$CONFIG_FILE" grep -q '^screen_idle_timeout=900$' "$CONFIG_FILE" grep -q '^screen_restore_policy=aggressive$' "$CONFIG_FILE" @@ -717,6 +740,8 @@ cmp -s "$CONFIG_SNAPSHOT" "$CONFIG_FILE" || { echo "Upgrade changed the user configuration." exit 1 } +"$INSTALLED_BINARY" settings describe screen.backend \ + | grep -F -q 'deprecation: swayidle is a deprecated compatibility backend planned for removal in LG Buddy 2.0.0' cmp -s "$CONFIG_POINTER_SNAPSHOT" "$INSTALLED_POINTER" || { echo "Upgrade changed the installed config pointer." exit 1