diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 5b03b39ef..32ca6c20d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -372,7 +372,7 @@ class _DesktopHomePageState extends State } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { return buildInstallCard( - "Warning", translate("wayland_experiment_tip"), "", () async {}, + "Warning", "wayland_experiment_tip", "", () async {}, help: 'Help', link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); } else if (bind.mainIsLoginWayland()) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b89929ad9..81cd2af7d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -312,20 +312,26 @@ class _GeneralState extends State<_General> { } Widget other() { - return _Card(title: 'Other', children: [ + final children = [ _OptionCheckBox(context, 'Confirm before closing multiple tabs', 'enable-confirm-closing-tabs'), - _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), - if (Platform.isLinux) - Tooltip( - message: translate('software_render_tip'), - child: _OptionCheckBox( - context, - "Always use software rendering", - 'allow-always-software-render', - ), - ) - ]); + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr') + ]; + if (Platform.isLinux) { + children.add(Tooltip( + message: translate('software_render_tip'), + child: _OptionCheckBox( + context, + "Always use software rendering", + 'allow-always-software-render', + ), + )); + } + if (bind.mainShowOption(key: 'allow-linux-headless')) { + children.add(_OptionCheckBox( + context, 'Allow linux headless', 'allow-linux-headless')); + } + return _Card(title: 'Other', children: children); } Widget hwcodec() { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 7bdd322d3..ce041e75c 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -32,6 +32,10 @@ pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; +// config2 options +#[cfg(target_os = "linux")] +pub const CONFIG_OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless"; + #[cfg(target_os = "macos")] lazy_static::lazy_static! { pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); diff --git a/src/client.rs b/src/client.rs index a48669899..e98e6bd3a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -90,6 +90,7 @@ pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password"; pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password"; pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; pub const LOGIN_MSG_OFFLINE: &str = "Offline"; +pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported"; #[cfg(target_os = "linux")] pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; #[cfg(target_os = "linux")] @@ -2100,7 +2101,13 @@ struct LoginErrorMsgBox { lazy_static::lazy_static! { static ref LOGIN_ERROR_MAP: Arc> = { use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT; - let map = HashMap::from([(LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{ + let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{ + msgtype: "error", + title: "Login Error", + text: "Login screen using Wayland is not supported", + link: "https://rustdesk.com/docs/en/manual/linux/#login-screen", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{ msgtype: "session-login", title: "", text: "", diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c25ac78b0..64a221141 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -614,6 +614,15 @@ pub fn main_get_error() -> String { get_error() } +pub fn main_show_option(_key: String) -> SyncReturn { + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if _key.eq(config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS) { + return SyncReturn(true) + } + SyncReturn(false) +} + pub fn main_set_option(key: String, value: String) { if key.eq("custom-rendezvous-server") { set_option(key, value); diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9c5a8b9fc..86b530342 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,5 +1,8 @@ use super::{CursorData, ResultType}; use desktop::Desktop; +#[cfg(all(feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +use hbb_common::config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS; pub use hbb_common::platform::linux::*; use hbb_common::{ allow_err, bail, @@ -69,6 +72,19 @@ pub struct xcb_xfixes_get_cursor_image { pub pixels: *const c_long, } +#[inline] +#[cfg(feature = "linux_headless")] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +pub fn is_headless_allowed() -> bool { + Config::get_option(CONFIG_OPTION_ALLOW_LINUX_HEADLESS) == "Y" +} + +#[inline] +pub fn is_login_screen_wayland() -> bool { + let values = get_values_of_seat0_with_gdm_wayland(&[0, 2]); + is_gdm_user(&values[1]) && get_display_server_of_session(&values[0]) == DISPLAY_SERVER_WAYLAND +} + #[inline] fn sleep_millis(millis: u64) { std::thread::sleep(Duration::from_millis(millis)); @@ -429,13 +445,21 @@ fn get_cm() -> bool { } pub fn is_login_wayland() -> bool { - if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { - contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") - } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") { - contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") - } else { - false + let files = ["/etc/gdm3/custom.conf", "/etc/gdm/custom.conf"]; + match ( + Regex::new(r"# *WaylandEnable *= *false"), + Regex::new(r"WaylandEnable *= *true"), + ) { + (Ok(pat1), Ok(pat2)) => { + for file in files { + if let Ok(contents) = std::fs::read_to_string(file) { + return pat1.is_match(&contents) || pat2.is_match(&contents); + } + } + } + _ => {} } + false } #[inline] diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index fe60964cc..b7caa527c 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -109,6 +109,10 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { // No need to verify password here. return "".to_owned(); } + if !username.is_empty() { + // Another user is logged in. No need to start a new xsession. + return "".to_owned(); + } if let Some(msg) = detect_headless() { return msg.to_owned(); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 3e3b6c9b3..924c0c709 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -73,6 +73,7 @@ impl RendezvousMediator { allow_err!(super::lan::start_listening()); }); } + // It is ok to run xdesktop manager when the headless function is not allowed. #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] crate::platform::linux_desktop_manager::start_xdesktop(); diff --git a/src/server/connection.rs b/src/server/connection.rs index ed51a6c29..f3e059696 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -191,10 +191,7 @@ pub struct Connection { pressed_modifiers: HashSet, #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - rx_cm_stream_ready: mpsc::Receiver<()>, - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - tx_desktop_ready: mpsc::Sender<()>, + linux_headless_handle: LinuxHeadlessHandle, closed: bool, delay_response_instant: Instant, } @@ -266,6 +263,10 @@ impl Connection { let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1); + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + let linux_headless_handle = + LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready); #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); @@ -322,10 +323,7 @@ impl Connection { pressed_modifiers: Default::default(), #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - rx_cm_stream_ready: _rx_cm_stream_ready, - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - tx_desktop_ready: _tx_desktop_ready, + linux_headless_handle, closed: false, delay_response_instant: Instant::now(), }; @@ -985,8 +983,10 @@ impl Connection { } #[cfg(feature = "linux_headless")] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if linux_desktop_manager::is_headless() { - platform_additions.insert("headless".into(), json!(true)); + if crate::platform::is_headless_allowed() { + if linux_desktop_manager::is_headless() { + platform_additions.insert("headless".into(), json!(true)); + } } if !platform_additions.is_empty() { pi.platform_additions = @@ -1009,10 +1009,15 @@ impl Connection { if dtype != crate::platform::linux::DISPLAY_SERVER_X11 && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND { - res.set_error(format!( - "Unsupported display server type \"{}\", x11 or wayland expected", - dtype - )); + let msg = if crate::platform::linux::is_login_screen_wayland() { + crate::client::LOGIN_SCREEN_WAYLAND.to_owned() + } else { + format!( + "Unsupported display server type \"{}\", x11 or wayland expected", + dtype + ) + }; + res.set_error(msg); let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; @@ -1373,28 +1378,22 @@ impl Connection { } } + #[cfg(any( + feature = "flatpak", + feature = "appimage", + not(all(target_os = "linux", feature = "linux_headless")) + ))] + let err_msg = "".to_owned(); #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - let desktop_err = match lr.os_login.as_ref() { - Some(os_login) => { - linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) - } - None => linux_desktop_manager::try_start_desktop("", ""), - }; - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - let is_headless = linux_desktop_manager::is_headless(); - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - let wait_ipc_timeout = 10_000; + let err_msg = self + .linux_headless_handle + .try_start_desktop(lr.os_login.as_ref()); // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if !desktop_err.is_empty() - && desktop_err != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY + if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { - self.send_login_error(desktop_err).await; + self.send_login_error(err_msg).await; return true; } @@ -1422,34 +1421,20 @@ impl Connection { self.send_login_error("Connection not allowed").await; return false; } else if self.is_recent_session() { - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if desktop_err.is_empty() { - #[cfg(target_os = "linux")] - if is_headless { - self.tx_desktop_ready.send(()).await.ok(); - let _res = timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; - } - self.try_start_cm(lr.my_id, lr.my_name, true); + if err_msg.is_empty() { + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + self.linux_headless_handle.wait_desktop_cm_ready().await; + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); self.send_logon_response().await; if self.port_forward_socket.is_some() { return false; } } else { - self.send_login_error(desktop_err).await; - } - #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] - { - self.try_start_cm(lr.my_id, lr.my_name, true); - self.send_logon_response().await; - if self.port_forward_socket.is_some() { - return false; - } + self.send_login_error(err_msg).await; } } else if lr.password.is_empty() { - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if desktop_err.is_empty() { + if err_msg.is_empty() { self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( @@ -1457,8 +1442,6 @@ impl Connection { ) .await; } - #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] - self.try_start_cm(lr.my_id, lr.my_name, false); } else { let mut failure = LOGIN_FAILURES .lock() @@ -1497,9 +1480,7 @@ impl Connection { .lock() .unwrap() .insert(self.ip.clone(), failure); - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if desktop_err.is_empty() { + if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) .await; self.try_start_cm(lr.my_id, lr.my_name, false); @@ -1509,40 +1490,21 @@ impl Connection { ) .await; } - #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] - { - self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) - .await; - self.try_start_cm(lr.my_id, lr.my_name, false); - } } else { if failure.0 != 0 { LOGIN_FAILURES.lock().unwrap().remove(&self.ip); } - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if desktop_err.is_empty() { - #[cfg(target_os = "linux")] - if is_headless { - self.tx_desktop_ready.send(()).await.ok(); - let _res = - timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; - } + if err_msg.is_empty() { + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + self.linux_headless_handle.wait_desktop_cm_ready().await; self.send_logon_response().await; self.try_start_cm(lr.my_id, lr.my_name, true); if self.port_forward_socket.is_some() { return false; } } else { - self.send_login_error(desktop_err).await; - } - #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] - { - self.send_logon_response().await; - self.try_start_cm(lr.my_id, lr.my_name, true); - if self.port_forward_socket.is_some() { - return false; - } + self.send_login_error(err_msg).await; } } } @@ -2361,19 +2323,14 @@ async fn start_ipc( args.push("--hide"); }; + #[allow(unused_mut)] #[cfg(target_os = "linux")] - #[cfg(not(feature = "linux_headless"))] - let user = None; - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(any(feature = "flatpak", feature = "appimage"))] - let user = None; - #[cfg(all(target_os = "linux", feature = "linux_headless"))] - #[cfg(not(any(feature = "flatpak", feature = "appimage")))] let mut user = None; + // Cm run as user, wait until desktop session is ready. #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if linux_desktop_manager::is_headless() { + if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { let mut username = linux_desktop_manager::get_username(); loop { if !username.is_empty() { @@ -2572,6 +2529,52 @@ impl Drop for Connection { } } +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +struct LinuxHeadlessHandle { + pub is_headless_allowed: bool, + pub is_headless: bool, + pub wait_ipc_timeout: u64, + pub rx_cm_stream_ready: mpsc::Receiver<()>, + pub tx_desktop_ready: mpsc::Sender<()>, +} + +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +impl LinuxHeadlessHandle { + pub fn new(rx_cm_stream_ready: mpsc::Receiver<()>, tx_desktop_ready: mpsc::Sender<()>) -> Self { + let is_headless_allowed = crate::platform::is_headless_allowed(); + let is_headless = is_headless_allowed && linux_desktop_manager::is_headless(); + Self { + is_headless_allowed, + is_headless, + wait_ipc_timeout: 10_000, + rx_cm_stream_ready, + tx_desktop_ready, + } + } + + pub fn try_start_desktop(&mut self, os_login: Option<&OSLogin>) -> String { + if self.is_headless_allowed { + match os_login { + Some(os_login) => { + linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) + } + None => linux_desktop_manager::try_start_desktop("", ""), + } + } else { + "".to_string() + } + } + + pub async fn wait_desktop_cm_ready(&mut self) { + if self.is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = timeout(self.wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + } +} + mod raii { use super::*; pub struct ConnectionID(i32);