mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 10:21:03 +03:00
* fix(linux): serve the Wayland login screen the DRM backend was built for The login screen support in #15420 never worked on a real greeter. fufesou found it: the session is refused, and with the refusal commented out the client gets a failed connection instead of a screen. One premise under all of it. `get_values_of_seat0` is `_get_values_of_seat0(.., ignore_gdm_wayland = true)`, so a gdm/sddm Wayland session is skipped by construction and `get_display_server` falls back to x11. That was correct while the portal was the only backend, since the portal cannot serve a greeter at all. The DRM path never talks to the compositor, which is precisely why it can serve one, so the premise stops holding there and every x11-vs-Wayland decision in the tree answers x11 at a login screen. The central change is the memoised `IS_X11`: when it reads x11 and seat0 is a Wayland greeter, answer Wayland. That covers fifteen routing sites at once, and it is under `cfg(feature = "drm")`, so a build without the backend keeps the current answer exactly. `is_x11_for_drm` is the unmemoised form for the two retry loops that must keep asking while a boot is still naming the session, and the memoised accessor is scoped to per-frame callers in the per-session `--server`, which the service only spawns once it has identified the session. Input was the last layer and lived outside all of that. `Enigo` decides x11-vs-Wayland once in `Default::default()`, from the same seat0 lookup, and on "x11" routes every key and mouse event to xdo; with no X server that context is null and libxdo drops them without an error. So the uinput devices were created, the compositor opened them, and nothing was ever written to them. `set_is_x11` is now called where the custom devices are installed, which is only reached once `!is_x11()` is already established. The unit test pins both directions, since a one-directional test passes against the bug. With no compositor reachable, the uinput desktop rect comes from the DRM display list instead: those are the same displays being captured, so the coordinate space matches by construction. Telling the truth about a greeter also makes four compositor-probing paths reachable where the probe cannot answer; all four already treat an empty output list as "nothing to do", so they skip it and 11818 "Could not find wayland compositor" warnings in one session became 1. Tested on an sddm Plasma Wayland greeter, MacBook T2, 2880x1800: the greeter renders, typing from the client enters characters in the password field, a click at an absolute coordinate opens the greeter session combo, the service pre-warm primes in 994 us instead of timing out, and the privileged service maps no EGL during a live capture. Not proven on gdm under Wayland. Known limitations: non-ASCII characters cannot be typed at a greeter, because that path goes through the clipboard and the clipboard here is X11 only; and at a multi-monitor greeter the pointer reaches the first display only, since every DRM output reports origin (0,0) on Wayland and there is no arrangement to derive without the compositor. * fix(linux): a Wayland greeter the DRM backend can serve is not headless fufesou reported the login screen still failing on Ubuntu 24.04 with gdm3, with the client asking for OS credentials to start an X session instead of showing the greeter. Reproduced on a real gdm greeter here. Same premise as the rest of the branch, one more consumer. `DesktopManager::new` reads seat0 through `get_values_of_seat0`, which skips a gdm/sddm Wayland session by construction, so at a greeter it finds no session at all and `get_supported_display_seat0_username` returns None from its empty-username arm. That makes `is_headless()` true, so the service advertises headless and `try_start_desktop` answers `LOGIN_MSG_DESKTOP_SESSION_NOT_READY`. The corrected `IS_X11` does not reach this one: it asks who owns seat0, not which display server is running. So ask again, with the greeter visible, when the DRM backend can capture and inject into it. At query time rather than in `new()`, because the DRM probe has not necessarily settled when the desktop manager is constructed, and the answer would latch for the process lifetime. In a normal session the latched username is a real user and the extra read is skipped. * chore: drop the hbb_common bump, this branch does not need it The bump carried rustdesk/hbb_common#580, the compositor-socket fallback. Nothing here depends on it: the greeter paths in this branch are the ones that run when compositor data is unavailable, which is what the commit before this one states as a known limitation. Keeping the bump would only block the greeter fix behind a review of a separate change, and would import that change's blocking review items into this path. * fix(linux): let the uinput uid gate see the greeter that owns seat0 Input at a real greeter was rejected by our own authorization. Measured on Ubuntu 24.04 with gdm3: the root service logs Rejected unauthorized connection on uinput ipc channel: postfix=_uinput_control, peer_uid=Some(120), active_uid=None and the greeter's `--server` gets ECONNRESET out of `setup_uinput`, so no uinput device is ever created and neither keyboard nor mouse reaches the greeter. uid 120 is gdm, the owner of the only active seat0 session. `active_uid` is None because the uinput authorizer deliberately bypasses the service-loop cache and takes a fresh seat0 lookup, and the fresh read hides a Wayland greeter by construction. The cache-based gates do not have the problem: `Desktop::refresh` fills it through the greeter-visible read, which is also why capture and config sync work at a greeter while input does not. So make the fresh read agree with the cache. It keeps the property the uinput gate wants, a lookup that cannot be stale, and it still compares the peer against the uid of the session that owns seat0 -- which at a greeter is the greeter. * fix: settle the DRM probe before routing login to X11, and read seat0 fresh Two findings from the #15792 review, both verified against the code: - drm_login_screen_seat0_username asked the cached probe, so a client arriving before warm_availability publishes its verdict read "no DRM" and, with allow-linux-headless=Y, try_start_x_session could start Xorg over a live Wayland greeter. Ask the probing form instead, and only after the cheap seat0 read says a Wayland greeter is actually there: a bounded definitive verdict is affordable on a login-time path. - get_supported_display_seat0_username trusted the seat0 values cached in DesktopManager::new(), which go stale across a logout or a fast user switch: a stale non-greeter name skipped the greeter probe and was returned as the supported display owner. Read seat0 fresh on every query; every call site is connection-time, so the extra loginctl read is cheap. Regression-tested on a real sddm Wayland greeter: capture streams the greeter, the RustDesk password dialog is the only prompt, and five typed characters appeared in the greeter password field over uinput with zero "Rejected unauthorized connection" lines in the service log. * fix: ask the greeter compositor for the multi-monitor layout The display arrangement and the pointer mapping were wrong at a multi-monitor login screen, and the mechanism is measured on a two-head virtio VM: DRM has no origins, so every display was advertised at (0,0) (a stacked arrangement on the client), and the uinput range was taken from the union of the DRM modes while the compositor had arranged the outputs side by side. Both came from the same premise, written before the hbb_common socket fallback existed: "a login screen has no compositor to ask". wayland_outputs_askable() skipped the wl_output augmentation at any greeter, and update_uinput_resolution took the DRM union directly. The premise is false now: a greeter runs a compositor, and the socket fallback reaches it with no environment variables, measured answering two outputs at the VM greeter while the old gate was still routing around it. Drop the gate and take the compositor-first path everywhere. Where the fallback cannot answer, the output list comes back empty and both call sites degrade to exactly the old behavior, so a build against an older hbb_common is unchanged. * fix: augment a single display too, and probe the desktop rect off the executor Two follow-ups from the automated re-review ofcd80c3dee, both verified: - augment_with_wayland_geometry skipped the compositor below two DRM displays, but on a multi-GPU host the one connector this service can open may sit at a non-zero origin of the compositor layout, and DRM alone reports (0,0). - the desktop rect for uinput can now block for the socket probe deadline, and update_uinput_resolution runs on current-thread runtimes; move the query into spawn_blocking. The third re-review finding, the warm-up allegedly skipping Wayland greeters, is refuted: warm_availability probes while is_x11_for_drm() is false, which includes a Wayland greeter, and the greeter log of the VM run behindcd80c3deeshows the warm succeeding there. * fix: baseline the layout from the blocking task, and augment a lone output's origin The layout snapshot after the rect lookup still ran on the executor: a failed compositor lookup is not cached, so the snapshot synchronously repeated the whole socket probe there. The baseline is now computed inside the same blocking task, from the snapshot the successful lookup just cached, or omitted when only the raw DRM union was available, which keeps the #15601 remap inactive exactly where origins are unknown. A single compositor output now hands its origin to a single connector: the lone output can sit at a non-zero origin the DRM side cannot see. Scale stays 1 on purpose, matching how a single display is advertised at physical size, and more connectors than the one output stays unaugmented, since the layout-order fallback would plant that origin on a guess. Also refresh the get_primary_index doc that still said augmentation declines below two connectors. * fix: read the DRM probe as a tri-state, and keep pre-auth seat0 checks cache-only is_available() answered false both for a definitive no-DRM verdict and for a probe that had simply not settled (another probe in flight, or a failure still below the disable threshold), and the login-screen decision turned that transient false into no-greeter: try_start_x_session could put Xorg over a live greeter in exactly the window the probe needed. The machinery now answers Available/Unavailable/Unsettled, and only a definitive Unavailable routes the seat toward X11. Connection setup also ran the whole lookup pre-auth: constructing LinuxHeadlessHandle called is_headless() before authentication, holding DESKTOP_MANAGER while loginctl ran and, at a greeter, while the DRM probe waited out its handshake. An unauthenticated peer could occupy a worker for seconds and serialize every other connection on the mutex. is_headless() now answers from a snapshot refreshed off-thread, and the fresh lookup became a free function called with the manager lock released everywhere; the enforcing decisions, get_username and try_start_x_session, still read seat0 fresh. Also drops seat0_display_server, dead since the fresh-read change. * fix: respect RUSTDESK_FORCED_DISPLAY_SERVER over the greeter correction The greeter correction rewired IS_X11 and is_x11_for_drm() to Wayland whenever seat0 looks like a Wayland greeter, including when the operator explicitly forced the display server: get_display_server() kept honoring the override while the DRM routing gates contradicted it, leaving capture and input routing internally inconsistent. The correction now only adjusts the auto-detected answer. * fix: honest pre-auth snapshot, sticky negative verdict, and a complete forced-x11 gate Four defects found by an adversarial review of the two previous commits, all in their new lines: - The empty-snapshot fallback derived headless from the manager's boot-time seat0 read, which is blank at a Wayland greeter (the loginctl wrapper skips greeter sessions), so the first connection of every server process at a greeter answered headless=true, the opposite of the comment on it. No snapshot now answers NOT headless, the snapshot is seeded at start_xdesktop, and the boot-time cache is gone entirely (it had no reader left). - wait_desktop_cm_ready gated on a bool stored at construction, which can lag one seat0 transition behind and skipped the CM-ready wait right after a logout. It re-reads the snapshot at call time. - A settled Unavailable was erased at NEGATIVE_TTL expiry (state to Unknown, failure counter to zero), so a permanently helper-less box reopened the Unsettled window every 30 seconds and the login decision kept adopting a greeter nothing can serve. The verdict now stays Unavailable while an off-thread re-probe re-verifies it: a failed or empty re-probe restamps the no, and only a non-empty list flips it. - The forced-x11 gate only covered IS_X11 and is_x11_for_drm, while the seat0 adoption path still probed DRM and admitted greeter sessions whose capture and input then routed to X11. Greeter adoption now yields to an operator-forced X11, degrading to upstream behavior: the connection is refused at the login screen. * fix: keep the login request path off the probe entirely try_start_desktop runs while handling a LoginRequest, before password validation, and at a Wayland greeter its seat0 lookup reached the probing availability form: an unauthenticated peer could park a worker for the probe deadline. The greeter adoption now reads a cached tri-state that never blocks; when the state is Unknown it kicks the probe off-thread and answers Unsettled, which the login decision treats as a possibly servable greeter until it settles. Settling lives in the startup warm-up, that kick, and the TTL re-verifiers; the blocking form stays for the capture-side callers, where waiting is acceptable. * fix: run the pre-auth desktop start off the executor, guard the refresh flag, trim comments From fufesou's #15792 re-review (no blocking issues) plus a bot pass: - try_start_desktop now runs on spawn_blocking. It executes loginctl, and PAM when a session must start, while handling a LoginRequest before password validation, so a slow logind must not tie up an async request worker; the blocking pool absorbs it. - kick_seat0_refresh releases SEAT0_REFRESH_IN_FLIGHT through an RAII guard, so a panic in the refresh thread cannot freeze is_headless on a stale snapshot for the process lifetime. - drm_can_serve_login_screen stays Available-only, and the reason is now in the code: it is deliberately not symmetric with the seat0 adoption gate. Adoption yields Xorg only on a definitive Unavailable; admission accepts only on a definitive Available; both wait through an unsettled probe. Admitting there would black-screen a client on a helper-less box, so a review suggestion to make them agree is declined. - Trimmed two over-long comments to the repo's three-line rule. * fix(linux): harden DRM login-screen startup Keep unauthenticated headless checks cache-only, bound OS-session startup to one blocking task, and surface JoinError failures. Wire the isolated Wayland probe consumer and update hbb_common plus libdrmtap 0.5.4. * fix(linux): headless refresh state Signed-off-by: fufesou <linlong1266@gmail.com> * fix(linux): keep headless startup state consistent - gate concurrent desktop startup attempts - route CM IPC after refreshing desktop state - avoid blocking seat0 queries in the CM retry loop - preserve newer seat0 snapshots during overlapping refreshes - derive DRM geometry and primary display from one Wayland snapshot Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: rustdesk <info@rustdesk.com> Co-authored-by: fufesou <linlong1266@gmail.com>
590 lines
28 KiB
Rust
590 lines
28 KiB
Rust
use super::*;
|
|
use hbb_common::{allow_err, anyhow, platform::linux::DISTRO};
|
|
use scrap::{
|
|
is_cursor_embedded, set_map_err,
|
|
wayland::pipewire::{fill_displays, try_fix_logical_size},
|
|
Capturer, Display, Frame, TraitCapturer,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::io;
|
|
|
|
use crate::{
|
|
client::{
|
|
SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED,
|
|
SCRAP_X11_REQUIRED, SCRAP_XDP_PORTAL_UNAVAILABLE,
|
|
},
|
|
platform::linux::is_x11,
|
|
};
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref CAP_DISPLAY_INFO: RwLock<HashMap<usize, u64>> = RwLock::new(HashMap::new());
|
|
static ref PIPEWIRE_INITIALIZED: RwLock<bool> = RwLock::new(false);
|
|
static ref LOG_SCRAP_COUNT: Mutex<u32> = Mutex::new(0);
|
|
static ref ACTIVE_DISPLAY_COUNT: RwLock<usize> = RwLock::new(0);
|
|
}
|
|
|
|
pub fn init() {
|
|
set_map_err(map_err_scrap);
|
|
}
|
|
|
|
pub(super) fn increment_active_display_count() -> usize {
|
|
let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap();
|
|
*count += 1;
|
|
*count
|
|
}
|
|
|
|
pub(super) fn decrement_active_display_count() -> usize {
|
|
let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap();
|
|
if *count > 0 {
|
|
*count -= 1;
|
|
}
|
|
*count
|
|
}
|
|
|
|
fn map_err_scrap(err: String) -> io::Error {
|
|
// to-do: Handle error better, do not restart server
|
|
if err.starts_with("Did not receive a reply") {
|
|
log::error!("Fatal pipewire error, {}", &err);
|
|
std::process::exit(-1);
|
|
}
|
|
|
|
if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() {
|
|
if DISTRO.version_id < "21".to_owned() {
|
|
io::Error::new(io::ErrorKind::Other, SCRAP_UBUNTU_HIGHER_REQUIRED)
|
|
} else {
|
|
try_log(&err);
|
|
io::Error::new(io::ErrorKind::Other, err)
|
|
}
|
|
} else {
|
|
try_log(&err);
|
|
let err_lower = err.to_ascii_lowercase();
|
|
if err_lower.contains("org.freedesktop.portal")
|
|
|| err_lower.contains("dbus")
|
|
|| err_lower.contains("d-bus")
|
|
{
|
|
// The portal D-Bus interface is unreachable. This typically means
|
|
// xdg-desktop-portal has crashed... for more info, see: Issue #12897
|
|
io::Error::new(io::ErrorKind::Other, SCRAP_XDP_PORTAL_UNAVAILABLE)
|
|
} else if err_lower.contains("pipewire") {
|
|
io::Error::new(io::ErrorKind::Other, SCRAP_OTHER_VERSION_OR_X11_REQUIRED)
|
|
} else {
|
|
io::Error::new(io::ErrorKind::Other, SCRAP_X11_REQUIRED)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_log(err: &String) {
|
|
let mut lock_count = LOG_SCRAP_COUNT.lock().unwrap();
|
|
if *lock_count >= 1000000 {
|
|
return;
|
|
}
|
|
if *lock_count % 10000 == 0 {
|
|
log::error!("Failed scrap {}", err);
|
|
}
|
|
*lock_count += 1;
|
|
}
|
|
|
|
struct CapturerPtr(*mut Capturer);
|
|
|
|
impl Clone for CapturerPtr {
|
|
fn clone(&self) -> Self {
|
|
Self(self.0)
|
|
}
|
|
}
|
|
|
|
impl TraitCapturer for CapturerPtr {
|
|
fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result<Frame<'a>> {
|
|
unsafe { (*self.0).frame(timeout) }
|
|
}
|
|
}
|
|
|
|
struct CapDisplayInfo {
|
|
rects: Vec<((i32, i32), usize, usize)>,
|
|
displays: Vec<DisplayInfo>,
|
|
num: usize,
|
|
primary: usize,
|
|
current: usize,
|
|
capturer: CapturerPtr,
|
|
}
|
|
|
|
/// Uinput desktop rect from the DRM display list, for a login screen where no compositor can be
|
|
/// asked. `(minx, maxx, miny, maxy)`, in scanout pixels: no compositor here applied a scale, so
|
|
/// unlike `desktop_rect_of` there is no logical size to handle.
|
|
#[cfg(feature = "drm")]
|
|
fn drm_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> {
|
|
let displays = super::drm_capturer::get_display_infos()?;
|
|
if displays.is_empty() {
|
|
return None;
|
|
}
|
|
let minx = displays.iter().map(|d| d.x).min()?;
|
|
let miny = displays.iter().map(|d| d.y).min()?;
|
|
let maxx = displays.iter().map(|d| d.x + d.width).max()?;
|
|
let maxy = displays.iter().map(|d| d.y + d.height).max()?;
|
|
if maxx <= minx || maxy <= miny {
|
|
return None;
|
|
}
|
|
Some((minx, maxx, miny, maxy))
|
|
}
|
|
|
|
/// Set the uinput absolute-pointer range to the whole logical desktop so the compositor maps
|
|
/// injected coordinates 1:1 instead of stretching a single-monitor range across all outputs. The
|
|
/// PipeWire path does this inline in `check_init`; the DRM path bypasses check_init so it must do it
|
|
/// too, otherwise on a multi-monitor host the injected pointer lands on the wrong output — and the
|
|
/// hardware cursor, which lives on whichever CRTC the pointer is over, never appears on the captured
|
|
/// CRTC (the "cursor not visible" symptom). Reads the layout from the Wayland outputs, so it is
|
|
/// independent of the capture backend.
|
|
///
|
|
/// This is the DRM path's single copy of what `check_init` does inline for PipeWire, and it does the
|
|
/// same three things, for the same reasons:
|
|
///
|
|
/// - drops the cached Wayland layout first, because it can predate compositor changes made while no
|
|
/// session was active (rustdesk#15601), and on the hotplug path it is stale by definition;
|
|
/// - bounds the IPC wait, because `uinput::client::set_resolution` reads its reply with no timeout of
|
|
/// its own, so a hung uinput socket would otherwise block every video-service start on this branch
|
|
/// and wedge the hotplug worker inside `rt.block_on`, leaving `UINPUT_REFRESH_BUSY` latched true so
|
|
/// that every later hotplug refresh is silently skipped for the process lifetime;
|
|
/// - records the applied rect and snapshots the per-display layout baseline, which is what arms the
|
|
/// #15601 drift remap. Without it the remap never activates on the DRM path at all.
|
|
///
|
|
/// It stays a separate copy rather than being folded into `check_init` because `check_init` ships in
|
|
/// every Linux build and this feature must not change the drm-off one by so much as a line.
|
|
#[cfg(feature = "drm")]
|
|
pub(super) async fn update_uinput_resolution() {
|
|
if !crate::input_service::wayland_use_uinput() {
|
|
return;
|
|
}
|
|
// Compositor first at a login screen too: a greeter runs one, and the hbb_common socket
|
|
// fallback reaches it with no environment variables. The DRM union is the fallback, and it is
|
|
// a real loss to land there on a multi-monitor host: DRM has no origins, so its union rect
|
|
// mis-maps the pointer whenever the compositor arranged the outputs side by side.
|
|
//
|
|
// Off the executor: the compositor query can block for the socket probe deadline, and this
|
|
// runs on current-thread runtimes (session init and the hotplug worker). The layout baseline
|
|
// is computed in the SAME task: a failed lookup is not cached, so asking for the rects
|
|
// afterwards would rerun the whole socket probe synchronously.
|
|
let (rect, layout) = match hbb_common::tokio::task::spawn_blocking(|| {
|
|
scrap::wayland::display::clear_wayland_displays_cache();
|
|
match scrap::wayland::display::get_desktop_rect_for_uinput() {
|
|
// The lookup above just cached the displays, so the rects come from that snapshot.
|
|
Some(rect) => Some((rect, scrap::wayland::display::get_display_rects_for_uinput())),
|
|
// Raw DRM union: there is no compositor layout to baseline. Empty keeps the #15601
|
|
// remap inactive, which is right when the origins are unknown anyway.
|
|
None => drm_desktop_rect_for_uinput().map(|rect| (rect, Vec::new())),
|
|
}
|
|
})
|
|
.await
|
|
{
|
|
Ok(Some(pair)) => pair,
|
|
Ok(None) => {
|
|
log::warn!("Failed to get desktop rect for uinput");
|
|
return;
|
|
}
|
|
Err(err) => {
|
|
log::warn!("The desktop rect probe task failed: {err}");
|
|
return;
|
|
}
|
|
};
|
|
// Re-snapshot the baseline on every call: this runs at session init and after every hotplug, and
|
|
// the baseline is what the client's coordinates are measured against.
|
|
let snapshot_layout = || {
|
|
super::display_service::set_wayland_layout_baseline(layout.clone());
|
|
};
|
|
// Reprogram the device only when the range actually changes. A display stuck in a rebuild loop
|
|
// calls this about once a second, and reapplying an identical range is an IPC roundtrip plus a
|
|
// uinput device reconfiguration under a user who may be at the console.
|
|
if super::display_service::wayland_uinput_rect() == Some(rect) {
|
|
snapshot_layout();
|
|
return;
|
|
}
|
|
let (minx, maxx, miny, maxy) = rect;
|
|
log::info!("update mouse resolution: ({minx}, {maxx}), ({miny}, {maxy})");
|
|
match timeout(
|
|
3_000,
|
|
input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
|
)
|
|
.await
|
|
{
|
|
// Record the rect only after a successful apply, so a transient failure is retried on the
|
|
// next call instead of being remembered as applied.
|
|
Ok(Ok(())) => {
|
|
super::display_service::set_wayland_uinput_rect(rect);
|
|
snapshot_layout();
|
|
}
|
|
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
|
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
|
}
|
|
}
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub(super) async fn ensure_inited() -> ResultType<()> {
|
|
// DRM/KMS capture (opt-in): the root service owns the reader and the capturer self-inits over
|
|
// IPC, so there is no PipeWire recorder to initialize here. But we still must set the uinput
|
|
// desktop rect (check_init does this on the PipeWire path, and the DRM path skips check_init).
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available_cached() {
|
|
update_uinput_resolution().await;
|
|
return Ok(());
|
|
}
|
|
check_init().await
|
|
}
|
|
|
|
pub(super) fn is_inited() -> Option<Message> {
|
|
if is_x11() {
|
|
None
|
|
} else {
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available_cached() {
|
|
return None;
|
|
}
|
|
if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
|
|
let mut msg_out = Message::new();
|
|
let res = MessageBox {
|
|
msgtype: "nook-nocancel-hasclose".to_owned(),
|
|
title: "Wayland".to_owned(),
|
|
text: "Please Select the screen to be shared(Operate on the peer side).".to_owned(),
|
|
link: "".to_owned(),
|
|
..Default::default()
|
|
};
|
|
msg_out.set_message_box(res);
|
|
Some(msg_out)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) async fn check_init() -> ResultType<()> {
|
|
if !is_x11() {
|
|
if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
|
|
if crate::input_service::wayland_use_uinput() {
|
|
// The cached layout may predate compositor changes made while no session
|
|
// was active, https://github.com/rustdesk/rustdesk/issues/15601
|
|
scrap::wayland::display::clear_wayland_displays_cache();
|
|
if let Some((minx, maxx, miny, maxy)) =
|
|
scrap::wayland::display::get_desktop_rect_for_uinput()
|
|
{
|
|
log::info!(
|
|
"update mouse resolution: ({}, {}), ({}, {})",
|
|
minx,
|
|
maxx,
|
|
miny,
|
|
maxy
|
|
);
|
|
// Bound the IPC wait like the periodic refresh does, so a hung
|
|
// response can't stall session init.
|
|
match timeout(
|
|
3_000,
|
|
input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Ok(())) => {
|
|
super::display_service::set_wayland_uinput_rect((
|
|
minx, maxx, miny, maxy,
|
|
));
|
|
// Snapshot the per-display layout the client's coordinates
|
|
// will be based on, so the mouse path can correct them if
|
|
// the compositor moves a monitor mid-session.
|
|
super::display_service::set_wayland_layout_baseline(
|
|
scrap::wayland::display::get_display_rects_for_uinput(),
|
|
);
|
|
}
|
|
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
|
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
|
}
|
|
} else {
|
|
log::warn!("Failed to get desktop rect for uinput");
|
|
}
|
|
}
|
|
|
|
let mut lock = CAP_DISPLAY_INFO.write().unwrap();
|
|
if lock.is_empty() {
|
|
// Check if PipeWire is already initialized to prevent duplicate recorder creation
|
|
if *PIPEWIRE_INITIALIZED.read().unwrap() {
|
|
log::warn!("wayland_diag: Preventing duplicate PipeWire initialization");
|
|
return Ok(());
|
|
}
|
|
|
|
let mut all = Display::all()?;
|
|
log::debug!("Initializing displays with fill_displays()");
|
|
{
|
|
let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new();
|
|
let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y);
|
|
fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?;
|
|
}
|
|
log::debug!("Attempting to fix logical size with try_fix_logical_size()");
|
|
try_fix_logical_size(&mut all);
|
|
*PIPEWIRE_INITIALIZED.write().unwrap() = true;
|
|
let num = all.len();
|
|
let primary = super::display_service::get_primary_2(&all);
|
|
let mut displays = super::display_service::update_sync_displays(&all);
|
|
for display in displays.iter_mut() {
|
|
display.cursor_embedded = is_cursor_embedded();
|
|
}
|
|
|
|
let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new();
|
|
for d in &all {
|
|
rects.push((d.origin(), d.width(), d.height()));
|
|
}
|
|
|
|
log::debug!(
|
|
"#displays={}, primary={}, rects: {:?}, cpus={}/{}",
|
|
num,
|
|
primary,
|
|
rects,
|
|
num_cpus::get_physical(),
|
|
num_cpus::get()
|
|
);
|
|
|
|
// Create individual CapDisplayInfo for each display with its own capturer
|
|
for (idx, display) in all.into_iter().enumerate() {
|
|
let capturer =
|
|
Box::into_raw(Box::new(Capturer::new(display).with_context(|| {
|
|
format!("Failed to create capturer for display {}", idx)
|
|
})?));
|
|
let capturer = CapturerPtr(capturer);
|
|
|
|
let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo {
|
|
rects: rects.clone(),
|
|
displays: displays.clone(),
|
|
num,
|
|
primary,
|
|
current: idx,
|
|
capturer,
|
|
}));
|
|
|
|
lock.insert(idx, cap_display_info as u64);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn get_displays_and_primary() -> ResultType<(Vec<DisplayInfo>, usize)> {
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available_cached() {
|
|
// This function runs once per login (update_get_sync_displays_on_login is its only
|
|
// caller), and login is the moment the client is PROMISED a display list -- so refresh
|
|
// that list over a live `_drm` handshake first. The service wakes sleeping displays and
|
|
// answers with the settled truth, which is what makes an unattended box with an idled,
|
|
// DISABLED panel connectable at all: the cached list would either omit the panel (probed
|
|
// while asleep) or advertise a display with no scanout behind it (probed while awake), and
|
|
// either way the wake then firing inside the capture handshake would change the list the
|
|
// client had already been given. Properly async, so the executor is never blocked; on any
|
|
// failure the cache serves as before.
|
|
super::drm_capturer::refresh_displays_for_login().await;
|
|
let snapshot = hbb_common::tokio::task::spawn_blocking(
|
|
super::drm_capturer::get_display_infos_and_primary,
|
|
)
|
|
.await
|
|
.map_err(|err| anyhow::anyhow!("Wayland display probe task failed: {err}"))?;
|
|
if let Some(snapshot) = snapshot {
|
|
return Ok(snapshot);
|
|
}
|
|
}
|
|
check_init().await?;
|
|
// Keep one read guard so clear/reinitialization cannot split these across cache snapshots.
|
|
let cap_map = CAP_DISPLAY_INFO.read().unwrap();
|
|
if let Some(addr) = cap_map.values().next() {
|
|
let cap_display_info: *const CapDisplayInfo = *addr as _;
|
|
unsafe {
|
|
let cap_display_info = &*cap_display_info;
|
|
Ok((cap_display_info.displays.clone(), cap_display_info.primary))
|
|
}
|
|
} else {
|
|
bail!("Failed to get capturer display info");
|
|
}
|
|
}
|
|
|
|
pub fn clear() {
|
|
if is_x11() {
|
|
return;
|
|
}
|
|
// The DRM path augments its geometry from the compositor's Wayland outputs (logical origin +
|
|
// scale), which scrap caches process-wide. The PipeWire path clears that cache on session close,
|
|
// but the DRM path opens no PipeWire session, so without this it would keep matching DRM outputs
|
|
// against STALE geometry after a monitor hotplug/rotation/scale change. Invalidate it on teardown
|
|
// so the next session re-reads fresh geometry (lazily, on the next enumeration) and self-heals.
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available_cached() {
|
|
scrap::wayland::display::clear_wayland_displays_cache();
|
|
}
|
|
// NOTE: intentionally do NOT reset the DRM probe cache here. `clear()` runs on every capturer
|
|
// teardown (which happens on each video-service restart), and re-probing `_drm` from the async
|
|
// enumeration path blocks the executor long enough to trip "deadline has elapsed" and spiral
|
|
// into a restart loop. DRM availability is fixed at service start, so the cache stays valid.
|
|
let mut write_lock = CAP_DISPLAY_INFO.write().unwrap();
|
|
for (_, addr) in write_lock.iter() {
|
|
let cap_display_info: *mut CapDisplayInfo = *addr as _;
|
|
unsafe {
|
|
let _box_capturer = Box::from_raw((*cap_display_info).capturer.0);
|
|
let _box_cap_display_info = Box::from_raw(cap_display_info);
|
|
}
|
|
}
|
|
write_lock.clear();
|
|
|
|
// Reset PipeWire initialization flag to allow recreation on next init
|
|
*PIPEWIRE_INITIALIZED.write().unwrap() = false;
|
|
}
|
|
|
|
/// Initialize the PipeWire/portal capture path from the plain (sync) video thread, so a DRM display
|
|
/// that cannot be captured can fall through to PipeWire for THAT display. `ensure_inited` short-circuits
|
|
/// to the DRM branch whenever DRM is globally available, so it never runs `check_init`; this helper
|
|
/// drives the same async portal ScreenCast init directly (mirroring `ensure_inited`'s pattern). Needed
|
|
/// because `is_available()` is a GLOBAL verdict — it stays true for the still-working DRM outputs — so
|
|
/// without a per-display fallback a single failed/demoted DRM display would restart-loop the video
|
|
/// service instead of degrading to PipeWire only for itself.
|
|
#[cfg(feature = "drm")]
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn ensure_pipewire_inited() -> ResultType<()> {
|
|
check_init().await
|
|
}
|
|
|
|
pub(super) fn get_capturer_for_display(
|
|
display_idx: usize,
|
|
) -> ResultType<super::video_service::CapturerInfo> {
|
|
if is_x11() {
|
|
bail!("Do not call this function if not wayland");
|
|
}
|
|
// DRM/KMS capture path: build the capturer straight from the service `_drm` stream, bypassing
|
|
// the PipeWire CAP_DISPLAY_INFO machinery entirely. `is_available()` is a GLOBAL verdict, so a
|
|
// per-display DRM failure (an ungrabbable/demoted CRTC, or — after the phase-2 split — a
|
|
// render-node-absent seat or a convert failure on the unprivileged side) must NOT propagate out
|
|
// and restart-loop this per-display video service. Instead fall THROUGH to PipeWire for just this
|
|
// display; the other DRM outputs keep streaming over DRM.
|
|
// The ONE gate that keeps the probing form on purpose: this runs on the plain video thread,
|
|
// not an async executor, and it is the capture-build path, so a definitive verdict is worth
|
|
// seconds here. It is also what makes a cold cache recoverable at all -- warm_availability
|
|
// gives up after its attempts, so if EVERY gate were cache-only a --server that started
|
|
// before the root service would never see DRM again for the rest of its life.
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available() {
|
|
match super::drm_capturer::get_capturer_info(display_idx) {
|
|
Ok(info) => return Ok(info),
|
|
Err(e) => {
|
|
log::warn!(
|
|
"drm capturer for display {} unavailable ({:#}); falling back to PipeWire",
|
|
display_idx,
|
|
e
|
|
);
|
|
ensure_pipewire_inited()?;
|
|
}
|
|
}
|
|
}
|
|
// Resolved BEFORE the read guard below, deliberately. `get_display_infos` runs
|
|
// `augment_with_wayland_geometry`, which is a compositor output roundtrip, and `clear()` takes
|
|
// the WRITE guard on every capturer teardown -- which is exactly what is happening when a DRM
|
|
// display is demoted or flapping, i.e. precisely when this path runs. Holding the read guard
|
|
// across that roundtrip would stall every concurrent teardown for its duration, and the value
|
|
// does not depend on anything inside the guard.
|
|
#[cfg(feature = "drm")]
|
|
let drm_advertised = if super::drm_capturer::is_available_cached() {
|
|
match super::drm_capturer::get_display_infos() {
|
|
Some(list) => Some((list.get(display_idx).cloned(), list.len() == 1)),
|
|
None => Some((None, false)),
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let cap_map = CAP_DISPLAY_INFO.read().unwrap();
|
|
// Serve ONLY the exact PipeWire entry for this index. Do NOT fall back to another index's
|
|
// `CapDisplayInfo`: `CapturerPtr` is a bare `*mut Capturer` cloned by raw-pointer copy, so aliasing
|
|
// one entry to two `display_idx` values would let two video-service threads call `frame()` on the
|
|
// same `Recorder` with no lock (data race / UB), and it would also mis-map input against the wrong
|
|
// rect. DRM and PipeWire do not share an index space (the portal often exposes one whole-desktop
|
|
// stream at index 0), so a demoted non-primary DRM index has no PipeWire entry here; that case is
|
|
// handled at the source by dropping the demoted display from the advertised list (see
|
|
// drm_capturer demotion) so the client re-enumerates against a consistent list, rather than being
|
|
// papered over with a shared/mismatched capturer.
|
|
if let Some(addr) = cap_map.get(&display_idx) {
|
|
let cap_display_info: *const CapDisplayInfo = *addr as _;
|
|
unsafe {
|
|
let cap_display_info = &*cap_display_info;
|
|
let rect = cap_display_info.rects[cap_display_info.current];
|
|
// Reaching here with DRM active means get_capturer_info bailed (a demoted display) and
|
|
// we fell through to PipeWire. Serve this stream ONLY if its rect matches the
|
|
// geometry we advertised for this index. The portal typically exposes one whole-desktop
|
|
// stream, so on a multi-monitor host that rect is the FULL desktop while the advertised DRM
|
|
// geometry is a single connector -> serving it would stretch the frame and offset all
|
|
// input. Bail instead; get_display_infos advertised the display offline, so the client
|
|
// re-enumerates against a consistent list. A single-display host matches (whole-desktop ==
|
|
// that display) and is served normally. On a pure-PipeWire host is_available() is false and
|
|
// this guard is skipped, preserving upstream behavior exactly.
|
|
#[cfg(feature = "drm")]
|
|
if let Some((advertised, single_display)) = drm_advertised {
|
|
if let Some(advertised) = advertised {
|
|
// BOTH SIDES ARE PHYSICAL, so compare them raw. Traced rather than assumed,
|
|
// because it was twice "corrected" to a scale conversion that broke it:
|
|
// `rect` is built above from `Display::width()/height()`, and the WAYLAND
|
|
// variant of those returns `physical_width()/physical_height()`
|
|
// (scrap `common/wayland.rs`), i.e. `PipeWireCapturable.physical_size`.
|
|
// `try_fix_logical_size` only repairs the capturable's SEPARATE
|
|
// `logical_size` field and never touches `physical_size`, so the rect is not
|
|
// logical. The advertised DRM geometry is physical too
|
|
// (`augment_with_wayland_geometry` sets x/y/scale and deliberately leaves
|
|
// width/height as the DRM mode). Dividing one side by the scale therefore
|
|
// compares logical against physical and rejects the valid stream on exactly
|
|
// the scaled outputs it was meant to rescue.
|
|
//
|
|
// The size check is what tells one connector apart from the whole-desktop
|
|
// rect the portal usually exposes. It is skipped only when BOTH sides say
|
|
// there is a single display -- the DRM list has one entry and the PipeWire
|
|
// map has one -- because only then is "the whole-desktop stream IS this
|
|
// display" true by construction. (The portal can report a different physical
|
|
// size for a Full Workspace selection than the connector's mode, which is why
|
|
// that case needs the carve-out at all.) The DRM count alone is not enough:
|
|
// a monitor on a card the service cannot open is missing from the DRM list
|
|
// while the compositor still drives it.
|
|
let single_display = single_display && cap_display_info.num == 1;
|
|
let consistent = advertised.x == rect.0 .0
|
|
&& advertised.y == rect.0 .1
|
|
&& (single_display
|
|
|| (advertised.width as usize == rect.1
|
|
&& advertised.height as usize == rect.2));
|
|
if !consistent {
|
|
bail!(
|
|
"drm display {} demoted with no geometry-consistent PipeWire stream (advertised {}x{}+{}+{} vs stream {}x{}+{}+{}); advertised offline",
|
|
display_idx,
|
|
advertised.width,
|
|
advertised.height,
|
|
advertised.x,
|
|
advertised.y,
|
|
rect.1,
|
|
rect.2,
|
|
rect.0 .0,
|
|
rect.0 .1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(super::video_service::CapturerInfo {
|
|
origin: rect.0,
|
|
width: rect.1,
|
|
height: rect.2,
|
|
ndisplay: cap_display_info.num,
|
|
current: cap_display_info.current,
|
|
privacy_mode_id: 0,
|
|
_capturer_privacy_mode_id: 0,
|
|
capturer: Box::new(cap_display_info.capturer.clone()),
|
|
})
|
|
}
|
|
} else {
|
|
bail!(
|
|
"Failed to get capturer display info for display {}",
|
|
display_idx
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn common_get_error() -> String {
|
|
if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() {
|
|
if DISTRO.version_id < "21".to_owned() {
|
|
return "".to_owned();
|
|
}
|
|
} else {
|
|
// to-do: check other distros
|
|
}
|
|
return "".to_owned();
|
|
}
|