mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 02:10:59 +03:00
add the base crate and repoint the moved modules at it (#16107)
* add the base crate and repoint the moved modules at it `libs/base` (crate `base`) takes the parts of hbb_common that only this app uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177 `config::keys` constants. hbb_common keeps what the server names, and the 32 keys it reads itself are re-exported from `base::config::keys` so call sites still see the full set through one path. Sources move verbatim. The only edits inside them are `crate::` prefixes that now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are byte-identical. The crate stays on edition 2018, the edition the moved code was written under. `log`, `lazy_static` and `anyhow` become direct dependencies so the bare paths in that code resolve exactly as before, and its winapi features are spelled out rather than left to feature unification. Two call sites outside Rust and Cargo had to follow the move: the Android protobuf source dir, which still pointed at hbb_common/protos for message.proto, and the three AGENTS.md entries that named hbb_common for options, protos and file transfer. `scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at hbb_common it would still have compiled, silently dropping the Wayland socket-probe fallback, so that forward is verified by a build with and without the feature. `config::keys` carries a test asserting its names stay disjoint from the ones hbb_common kept: the glob re-export and the local constants share a namespace, and Rust prefers the local item silently, so a name added to both sides would otherwise let client and server disagree with no diagnostic. Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key constants diffed name-for-name and value-for-value; the generated protobuf types compared before and after; every `#[cfg]` gate on a moved import checked against its original; and every file that was `rustfmt`-clean before this change still is, compared against master file by file. Windows is checked by inspection only -- it cannot be compiled here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * one `use` per crate, and write the rule down `fs.rs` came out of the move with two ungated `use hbb_common::` statements, because the original single `use crate::{...}` had to give up `message_proto` to the new crate and the rest was left in a second block. Fold it back into one. A scan of the whole tree for the same shape finds nothing else: every other file with more than one top-level `use base::` or `use hbb_common::` is split by a `#[cfg]` that does not cover the whole block, or by `pub use` next to `use`. Those are the cases that cannot merge, so AGENTS.md now states both the rule and the exemption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
618
libs/base/src/platform/linux.rs
Normal file
618
libs/base/src/platform/linux.rs
Normal file
@@ -0,0 +1,618 @@
|
||||
use hbb_common::ResultType;
|
||||
// Kept in hbb_common because `config::patch()` needs the shell lookup; re-exported
|
||||
// here so the long-standing `platform::linux::CMD_SH` paths are unchanged.
|
||||
pub use hbb_common::sh::{run_cmds_trim_newline, CMD_LOGINCTL, CMD_PS, CMD_SH};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use users::{get_current_uid, get_user_by_uid, os::unix::UserExt};
|
||||
|
||||
use sctk::{
|
||||
output::OutputData,
|
||||
output::{OutputHandler, OutputState},
|
||||
reexports::client::protocol::wl_output::WlOutput,
|
||||
reexports::client::{globals, Proxy},
|
||||
reexports::client::{Connection, QueueHandle},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DISTRO: Distro = Distro::new();
|
||||
}
|
||||
|
||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||
pub const DISPLAY_SERVER_X11: &str = "x11";
|
||||
pub const DISPLAY_DESKTOP_KDE: &str = "KDE";
|
||||
|
||||
pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||
|
||||
pub struct Distro {
|
||||
pub name: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
impl Distro {
|
||||
fn new() -> Self {
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
Self { name, version_id }
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated. Use `base::platform::linux::is_kde_session()` instead for now.
|
||||
// Or we need to set the correct environment variable in the server process.
|
||||
#[inline]
|
||||
pub fn is_kde() -> bool {
|
||||
if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) {
|
||||
env == DISPLAY_DESKTOP_KDE
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use `base::platform::linux::is_kde()` here.
|
||||
// It's not correct in the server process.
|
||||
pub fn is_kde_session() -> bool {
|
||||
std::process::Command::new(CMD_SH.as_str())
|
||||
.arg("-c")
|
||||
.arg("pgrep -f kded[0-9]+")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()
|
||||
.map(|o| !o.stdout.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_gdm_user(username: &str) -> bool {
|
||||
username == "gdm" || username == "sddm"
|
||||
// || username == "lightgdm"
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_desktop_wayland() -> bool {
|
||||
get_display_server() == DISPLAY_SERVER_WAYLAND
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_x11_or_headless() -> bool {
|
||||
!is_desktop_wayland()
|
||||
}
|
||||
|
||||
// -1
|
||||
const INVALID_SESSION: &str = "4294967295";
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
// Check for forced display server environment variable first
|
||||
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
|
||||
return forced_display;
|
||||
}
|
||||
|
||||
// Check if `loginctl` can be called successfully
|
||||
if run_loginctl(None).is_err() {
|
||||
return DISPLAY_SERVER_X11.to_owned();
|
||||
}
|
||||
|
||||
let mut session = get_values_of_seat0(&[0])[0].clone();
|
||||
if session.is_empty() {
|
||||
// loginctl has not given the expected output. try something else.
|
||||
if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
|
||||
// could also execute "cat /proc/self/sessionid"
|
||||
session = sid;
|
||||
}
|
||||
if session.is_empty() {
|
||||
session = run_cmds("cat /proc/self/sessionid").unwrap_or_default();
|
||||
if session == INVALID_SESSION {
|
||||
session = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
if session.is_empty() {
|
||||
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
|
||||
} else {
|
||||
get_display_server_of_session(&session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_display_server_of_session(session: &str) -> String {
|
||||
let mut display_server = if let Ok(output) =
|
||||
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
|
||||
// Check session type of the session
|
||||
{
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.replace("Type=", "")
|
||||
.trim_end()
|
||||
.into()
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
if display_server.is_empty() || display_server == "tty" || display_server == "unspecified" {
|
||||
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
|
||||
if !sestype.is_empty() {
|
||||
return sestype.to_lowercase();
|
||||
}
|
||||
}
|
||||
display_server = "x11".to_owned();
|
||||
}
|
||||
display_server.to_lowercase()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn line_values(indices: &[usize], line: &str) -> Vec<String> {
|
||||
indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, false)
|
||||
}
|
||||
|
||||
// Ignore "3 sessions listed."
|
||||
fn ignore_loginctl_line(line: &str) -> bool {
|
||||
line.contains("sessions") || line.split(" ").count() < 4
|
||||
}
|
||||
|
||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
let d = get_display_server_of_session(sid);
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& d == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if d == "tty" || d == "unspecified" {
|
||||
continue;
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_values(indices, "")
|
||||
}
|
||||
|
||||
pub fn is_active(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("active")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active_and_seat0(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("State=active")
|
||||
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Check both "Lock" and "Switch user"
|
||||
pub fn is_session_locked(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid, "--property=LockedHint"])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("LockedHint=yes")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// **Note** that the return value here, the last character is '\n'.
|
||||
// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end.
|
||||
pub fn run_cmds(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new(CMD_SH.as_str())
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||
if std::env::var("FLATPAK_ID").is_ok() {
|
||||
let mut l_args = CMD_LOGINCTL.to_string();
|
||||
if let Some(a) = args.as_ref() {
|
||||
l_args = format!("{} {}", l_args, a.join(" "));
|
||||
}
|
||||
let res = std::process::Command::new("flatpak-spawn")
|
||||
.args(vec![String::from("--host"), l_args])
|
||||
.output();
|
||||
if res.is_ok() {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
let mut cmd = std::process::Command::new(CMD_LOGINCTL.as_str());
|
||||
if let Some(a) = args {
|
||||
return cmd.args(a).output();
|
||||
}
|
||||
cmd.output()
|
||||
}
|
||||
|
||||
/// forever: may not work
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
||||
let cmds: HashMap<&str, Vec<&str>> = HashMap::from([
|
||||
("notify-send", [title, msg].to_vec()),
|
||||
(
|
||||
"zenity",
|
||||
[
|
||||
"--info",
|
||||
"--timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
"--title",
|
||||
title,
|
||||
"--text",
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
("kdialog", ["--title", title, "--msgbox", msg].to_vec()),
|
||||
(
|
||||
"xmessage",
|
||||
[
|
||||
"-center",
|
||||
"-timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
title,
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
]);
|
||||
for (k, v) in cmds {
|
||||
if Command::new(k).args(v).spawn().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
hbb_common::bail!("failed to post system message");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde_derive::Serialize, serde_derive::Deserialize)]
|
||||
pub struct WaylandDisplayInfo {
|
||||
pub name: String,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub logical_size: Option<(i32, i32)>,
|
||||
pub refresh_rate: i32,
|
||||
/// Output rotation in degrees (0/90/180/270), from `wl_output.geometry`. The mode keeps its
|
||||
/// unrotated dimensions and `logical_size` arrives already swapped, so without this field a
|
||||
/// rotated output is indistinguishable from a scaled one. Flipped variants map to their
|
||||
/// rotation. Defaulted so a serialized snapshot from an older probe child still deserializes.
|
||||
#[serde(default)]
|
||||
pub transform: i32,
|
||||
}
|
||||
|
||||
/// The isolated socket-probe fallback, in its own file and behind the `wayland_probe` feature so
|
||||
/// the base Wayland path never compiles it. The DRM login-screen build turns it on.
|
||||
#[cfg(feature = "wayland_probe")]
|
||||
pub mod wayland_probe;
|
||||
#[cfg(feature = "wayland_probe")]
|
||||
pub use wayland_probe::{wayland_display_probe_child_main, WAYLAND_DISPLAY_PROBE_ARG};
|
||||
|
||||
// Retrieves information about all connected displays via the Wayland protocol.
|
||||
pub fn get_wayland_displays() -> ResultType<Vec<WaylandDisplayInfo>> {
|
||||
// Read before connecting: `connect_to_env` consumes `WAYLAND_SOCKET`. Only the probe fallback
|
||||
// needs this, so it is computed only when that feature is compiled in.
|
||||
#[cfg(feature = "wayland_probe")]
|
||||
let named_endpoint = wayland_probe::env_names_wayland_endpoint();
|
||||
match Connection::connect_to_env() {
|
||||
Ok(conn) => collect_wayland_displays(&conn),
|
||||
// Without the feature, the connect error is final, exactly as before this fallback existed.
|
||||
#[cfg(not(feature = "wayland_probe"))]
|
||||
Err(err) => Err(err.into()),
|
||||
#[cfg(feature = "wayland_probe")]
|
||||
Err(err) => wayland_probe::wayland_displays_from_runtime_dir(named_endpoint)
|
||||
.map_err(|fallback_err| anyhow::anyhow!("{err}; {fallback_err}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// `wl_output::Transform` as degrees. Flipped variants report their rotation ONLY: wayland
|
||||
/// defines them as a vertical-axis mirror followed by the rotation, and the mirror half is
|
||||
/// dropped here - a consumer correcting frames by this value serves a flipped output mirrored.
|
||||
/// Said once in the log rather than silently, because no compositor of ours produces a flipped
|
||||
/// output to measure the mirror half against; carrying it must wait for a measured producer.
|
||||
fn transform_degrees(t: sctk::reexports::client::protocol::wl_output::Transform) -> i32 {
|
||||
use sctk::reexports::client::protocol::wl_output::Transform;
|
||||
match t {
|
||||
Transform::Normal => 0,
|
||||
Transform::_90 => 90,
|
||||
Transform::_180 => 180,
|
||||
Transform::_270 => 270,
|
||||
Transform::Flipped | Transform::Flipped90 | Transform::Flipped180
|
||||
| Transform::Flipped270 => {
|
||||
static FLIPPED_WARNED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
if !FLIPPED_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
log::warn!(
|
||||
"an output reports a flipped transform ({t:?}); only its rotation is \
|
||||
corrected, the mirror is not"
|
||||
);
|
||||
}
|
||||
match t {
|
||||
Transform::Flipped90 => 90,
|
||||
Transform::Flipped180 => 180,
|
||||
Transform::Flipped270 => 270,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_wayland_displays(conn: &Connection) -> ResultType<Vec<WaylandDisplayInfo>> {
|
||||
struct WaylandEnv {
|
||||
registry_state: RegistryState,
|
||||
output_state: OutputState,
|
||||
}
|
||||
|
||||
impl OutputHandler for WaylandEnv {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: WlOutput) {}
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for WaylandEnv {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
|
||||
sctk::registry_handlers![OutputState];
|
||||
}
|
||||
|
||||
sctk::delegate_output!(WaylandEnv);
|
||||
sctk::delegate_registry!(WaylandEnv);
|
||||
|
||||
let (globals, mut event_queue) = globals::registry_queue_init(conn)?;
|
||||
let queue_handle = event_queue.handle();
|
||||
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
let output_state = OutputState::new(&globals, &queue_handle);
|
||||
|
||||
let mut environment = WaylandEnv {
|
||||
registry_state,
|
||||
output_state,
|
||||
};
|
||||
|
||||
event_queue.roundtrip(&mut environment)?;
|
||||
|
||||
let outputs: Vec<_> = environment.output_state.outputs().collect();
|
||||
let mut display_infos = Vec::new();
|
||||
|
||||
for output in outputs {
|
||||
if let Some(output_data) = output.data::<OutputData>() {
|
||||
output_data.with_output_info(|info| {
|
||||
if let Some(mode) = info.modes.iter().find(|m| m.current) {
|
||||
// wlroots compositors leave wl_output.geometry at (0, 0) for every output and
|
||||
// publish the real layout only through xdg-output, so taking `location` there
|
||||
// stacks the whole desktop on the origin. Mutter fills both, so this stays a
|
||||
// no-op on GNOME.
|
||||
let (x, y) = info.logical_position.unwrap_or(info.location);
|
||||
let (width, height) = mode.dimensions;
|
||||
let refresh_rate = mode.refresh_rate;
|
||||
let name = info.name.clone().unwrap_or_default();
|
||||
let logical_size = info.logical_size;
|
||||
let transform = transform_degrees(info.transform);
|
||||
display_infos.push(WaylandDisplayInfo {
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
logical_size,
|
||||
refresh_rate,
|
||||
transform,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(display_infos)
|
||||
}
|
||||
|
||||
/// Escape a string for safe use in shell commands by wrapping in single quotes.
|
||||
///
|
||||
/// This function handles the edge case of single quotes within the string by:
|
||||
/// 1. Ending the current single-quoted section
|
||||
/// 2. Adding an escaped single quote
|
||||
/// 3. Starting a new single-quoted section
|
||||
///
|
||||
/// Example: "it's here" -> "'it'\''s here'"
|
||||
#[inline]
|
||||
pub fn shell_quote(s: &str) -> String {
|
||||
format!("'{}'", s.replace("'", "'\\''"))
|
||||
}
|
||||
|
||||
/// Get the current user's home directory via getpwuid (trusted source).
|
||||
///
|
||||
/// This function uses the system's password database (via `getpwuid`) to retrieve
|
||||
/// the home directory, avoiding the security risk of relying on the `HOME`
|
||||
/// environment variable which can be manipulated by untrusted input.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(PathBuf)` if the home directory was found and exists
|
||||
/// - `None` if the user lookup failed or the directory doesn't exist
|
||||
///
|
||||
/// # Security
|
||||
/// This function is designed to be safe against confused-deputy attacks where
|
||||
/// an attacker might manipulate environment variables to influence privileged
|
||||
/// operations.
|
||||
pub fn get_home_dir_trusted() -> Option<PathBuf> {
|
||||
let uid = get_current_uid();
|
||||
match get_user_by_uid(uid) {
|
||||
Some(user) => {
|
||||
let home = user.home_dir();
|
||||
if Path::is_dir(home) {
|
||||
Some(PathBuf::from(home))
|
||||
} else {
|
||||
log::warn!(
|
||||
"Home directory for uid {} does not exist or is not a directory: {:?}",
|
||||
uid,
|
||||
home
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!("Failed to get user info for uid {}", uid);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transform_degrees_maps_all_eight_variants() {
|
||||
use sctk::reexports::client::protocol::wl_output::Transform;
|
||||
// Flipped variants report their rotation: the frame still needs that turn to read
|
||||
// upright, and the mirror half has no producer among desktop compositors to test.
|
||||
for (t, deg) in [
|
||||
(Transform::Normal, 0),
|
||||
(Transform::_90, 90),
|
||||
(Transform::_180, 180),
|
||||
(Transform::_270, 270),
|
||||
(Transform::Flipped, 0),
|
||||
(Transform::Flipped90, 90),
|
||||
(Transform::Flipped180, 180),
|
||||
(Transform::Flipped270, 270),
|
||||
] {
|
||||
assert_eq!(transform_degrees(t), deg, "{t:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_info_without_transform_defaults_to_zero() {
|
||||
// A snapshot serialized by an older probe child carries no transform field; it must
|
||||
// deserialize with 0 rather than fail, or a greeter-side child update becomes a
|
||||
// lockstep upgrade.
|
||||
let old = r#"{"name":"HDMI-1","x":0,"y":0,"width":1920,"height":1080,"logical_size":null,"refresh_rate":60}"#;
|
||||
let info: WaylandDisplayInfo = serde_json::from_str(old).unwrap();
|
||||
assert_eq!(info.transform, 0);
|
||||
let roundtrip: WaylandDisplayInfo =
|
||||
serde_json::from_str(&serde_json::to_string(&info).unwrap()).unwrap();
|
||||
assert_eq!(roundtrip.transform, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_cmds_trim_newline() {
|
||||
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
||||
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
||||
assert_eq!(
|
||||
run_cmds_trim_newline("whoami").unwrap() + "\n",
|
||||
run_cmds("whoami").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// Test get_home_dir_trusted: returns valid path and ignores HOME env var
|
||||
#[test]
|
||||
fn test_get_home_dir_trusted() {
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
|
||||
// Set HOME to a fake/malicious path
|
||||
std::env::set_var("HOME", "/tmp/fake_malicious_home");
|
||||
let result = get_home_dir_trusted();
|
||||
|
||||
// Restore original HOME
|
||||
match original_home {
|
||||
Some(home) => std::env::set_var("HOME", home),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
|
||||
// Verify: returns valid path that is NOT the fake HOME
|
||||
if let Some(path) = result {
|
||||
assert!(path.is_absolute(), "Path should be absolute: {:?}", path);
|
||||
assert!(path.is_dir(), "Path should be a directory: {:?}", path);
|
||||
assert_ne!(
|
||||
path.to_string_lossy(),
|
||||
"/tmp/fake_malicious_home",
|
||||
"Should not use HOME env var"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test shell_quote with normal strings
|
||||
#[test]
|
||||
fn test_shell_quote_normal() {
|
||||
assert_eq!(shell_quote("hello"), "'hello'");
|
||||
assert_eq!(shell_quote("/home/user"), "'/home/user'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with spaces
|
||||
#[test]
|
||||
fn test_shell_quote_spaces() {
|
||||
assert_eq!(shell_quote("/home/my user/file"), "'/home/my user/file'");
|
||||
assert_eq!(shell_quote("path with spaces"), "'path with spaces'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with single quotes (the tricky case)
|
||||
#[test]
|
||||
fn test_shell_quote_single_quotes() {
|
||||
assert_eq!(shell_quote("it's"), "'it'\\''s'");
|
||||
assert_eq!(shell_quote("don't stop"), "'don'\\''t stop'");
|
||||
}
|
||||
|
||||
/// Test shell_quote with shell metacharacters
|
||||
#[test]
|
||||
fn test_shell_quote_metacharacters() {
|
||||
// These should all be safely quoted
|
||||
assert_eq!(shell_quote("test;rm -rf /"), "'test;rm -rf /'");
|
||||
assert_eq!(shell_quote("$(whoami)"), "'$(whoami)'");
|
||||
assert_eq!(shell_quote("`id`"), "'`id`'");
|
||||
assert_eq!(shell_quote("a && b"), "'a && b'");
|
||||
assert_eq!(shell_quote("a | b"), "'a | b'");
|
||||
}
|
||||
}
|
||||
349
libs/base/src/platform/linux/wayland_probe.rs
Normal file
349
libs/base/src/platform/linux/wayland_probe.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
//! Isolated Wayland display probe: enumerates a compositor over a runtime-directory socket when
|
||||
//! the environment names no endpoint (a greeter's `--server` and the root service are given no
|
||||
//! compositor variables). Gated behind the `wayland_probe` feature so the base Wayland path is
|
||||
//! untouched — a consumer that does not build the DRM login-screen backend never compiles this,
|
||||
//! and `get_wayland_displays` keeps its original behavior of returning the connect error.
|
||||
|
||||
use super::{collect_wayland_displays, get_values_of_seat0_with_gdm_wayland, WaylandDisplayInfo};
|
||||
use hbb_common::{bail, ResultType};
|
||||
use sctk::reexports::client::Connection;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const RUNTIME_DIR_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
|
||||
|
||||
/// The argument the consumer binary must dispatch to `wayland_display_probe_child_main` before
|
||||
/// any other startup work; see that function for why the probe is its own process.
|
||||
pub const WAYLAND_DISPLAY_PROBE_ARG: &str = "--wayland-display-probe";
|
||||
|
||||
/// First stdout line of a probe child. A binary that does not dispatch the arg never prints it.
|
||||
const WAYLAND_PROBE_MAGIC: &str = "wayland-display-probe-v1";
|
||||
|
||||
/// Latched on a failed handshake: a consumer that does not dispatch the probe arg runs its NORMAL
|
||||
/// startup instead, and this path re-enters every enumeration cycle.
|
||||
static PROBE_UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
static RUNTIME_DIR_PROBE_BUSY: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Clears the in-flight flag on every exit path of the parent, error arms included.
|
||||
struct ProbeBusyGuard;
|
||||
|
||||
impl Drop for ProbeBusyGuard {
|
||||
fn drop(&mut self) {
|
||||
RUNTIME_DIR_PROBE_BUSY.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry point of the isolated probe process. The consumer binary dispatches
|
||||
/// `WAYLAND_DISPLAY_PROBE_ARG` here first, before config, logging or any other startup work.
|
||||
///
|
||||
/// Its own process because the release profile builds with panic=abort: sctk panics on malformed
|
||||
/// protocol bytes, and in-process that abort takes the whole server down. Here it takes down only
|
||||
/// this child, which the parent reports as a failed probe. The seat0 lookup also runs in here, so
|
||||
/// the parent's single deadline bounds the loginctl reads too.
|
||||
pub fn wayland_display_probe_child_main() -> ! {
|
||||
use std::io::Write;
|
||||
// The handshake first, so the parent can tell this entry point ran and not a consumer binary
|
||||
// that fell through to its normal startup.
|
||||
println!("{WAYLAND_PROBE_MAGIC}");
|
||||
let _ = std::io::stdout().flush();
|
||||
let code = match seat0_runtime_dir()
|
||||
.and_then(|dir| {
|
||||
drop_to_dir_owner(&dir)?;
|
||||
probe_runtime_dir(&dir)
|
||||
})
|
||||
.and_then(|displays| serde_json::to_string(&displays).map_err(anyhow::Error::from))
|
||||
{
|
||||
Ok(json) => {
|
||||
println!("{json}");
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
let _ = std::io::stdout().flush();
|
||||
std::process::exit(code)
|
||||
}
|
||||
|
||||
static ENDPOINT_WAS_NAMED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Whether the environment ever named a wayland endpoint in this process. Empty is not a name.
|
||||
///
|
||||
/// Read before `connect_to_env`, which removes `WAYLAND_SOCKET` from the environment on both its
|
||||
/// success and its bad-fd path; and latched, so a consumed variable cannot turn a process that WAS
|
||||
/// pointed at a compositor into one that is free to go looking for another.
|
||||
pub(super) fn env_names_wayland_endpoint() -> bool {
|
||||
use std::sync::atomic::Ordering;
|
||||
let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"]
|
||||
.iter()
|
||||
.any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty()));
|
||||
if named {
|
||||
ENDPOINT_WAS_NAMED.store(true, Ordering::Release);
|
||||
}
|
||||
ENDPOINT_WAS_NAMED.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// The probe parses compositor-controlled protocol data; a root service must not do that as
|
||||
/// root. Before touching the socket, become the runtime directory's owner — and refuse to probe
|
||||
/// at all if the drop fails, since staying root is the one unacceptable outcome.
|
||||
fn drop_to_dir_owner(dir: &Path) -> ResultType<()> {
|
||||
if unsafe { libc::geteuid() } != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let meta = std::fs::metadata(dir)?;
|
||||
let (uid, gid) = (meta.uid(), meta.gid());
|
||||
if uid == 0 {
|
||||
// Root's own session: there is no boundary to cross and nothing to drop to.
|
||||
return Ok(());
|
||||
}
|
||||
unsafe {
|
||||
if libc::setgroups(0, std::ptr::null()) != 0
|
||||
|| libc::setgid(gid) != 0
|
||||
|| libc::setuid(uid) != 0
|
||||
|| libc::setuid(0) == 0
|
||||
{
|
||||
bail!("could not drop privileges for the socket probe");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `/run/user/<uid>` of the active seat0 session, a greeter included.
|
||||
///
|
||||
/// Derived from the uid rather than read from `XDG_RUNTIME_DIR`: the root service is given no such
|
||||
/// variable, and `get_home_dir_trusted` refuses to trust the environment for the same reason.
|
||||
fn seat0_runtime_dir() -> ResultType<PathBuf> {
|
||||
let uid = get_values_of_seat0_with_gdm_wayland(&[1]).remove(0);
|
||||
if uid.is_empty() || !uid.bytes().all(|b| b.is_ascii_digit()) {
|
||||
bail!("no active seat0 session to take a runtime directory from");
|
||||
}
|
||||
Ok(PathBuf::from(format!("/run/user/{uid}")))
|
||||
}
|
||||
|
||||
/// The wayland sockets present in `dir`, lowest display number first.
|
||||
///
|
||||
/// Scanned rather than guessed: `wl_display_add_socket_auto` takes the first FREE name up to
|
||||
/// `wayland-32`, and a greeter is where leftovers accumulate across compositor restarts. Only that
|
||||
/// name pattern, because the same directory holds pipewire and dbus sockets.
|
||||
fn wayland_sockets_in(dir: &Path) -> Vec<PathBuf> {
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
let mut paths: Vec<PathBuf> = match std::fs::read_dir(dir) {
|
||||
Ok(entries) => entries
|
||||
.flatten()
|
||||
.filter(|entry| {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
name.starts_with("wayland-")
|
||||
&& !name.ends_with(".lock")
|
||||
&& entry.file_type().map(|t| t.is_socket()).unwrap_or(false)
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
paths.sort_by_key(|path| {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(|name| name.strip_prefix("wayland-"))
|
||||
.and_then(|number| number.parse::<u32>().ok())
|
||||
.unwrap_or(u32::MAX)
|
||||
});
|
||||
paths
|
||||
}
|
||||
|
||||
/// Enumerate through a socket in the seat0 runtime directory, for the case where nothing named an
|
||||
/// endpoint: a greeter's `--server` and the root service are given no compositor variables, so
|
||||
/// nothing tells the enumerator where a compositor that IS running lives. An endpoint that WAS
|
||||
/// named and failed must not silently reattach to a different compositor.
|
||||
///
|
||||
/// In a subprocess and bounded, because the caller holds a process-wide lock across the call while
|
||||
/// `connect(2)` parks on a full backlog and sctk's roundtrip polls without a deadline; and because
|
||||
/// sctk panics on malformed output events, which the release profile's panic=abort turns into an
|
||||
/// abort of the whole server. A child dies alone, and on the deadline it is killed instead of
|
||||
/// leaking a thread. The seat0 lookup runs inside the child, under the same deadline.
|
||||
pub(super) fn wayland_displays_from_runtime_dir(
|
||||
named_endpoint: bool,
|
||||
) -> ResultType<Vec<WaylandDisplayInfo>> {
|
||||
use std::sync::atomic::Ordering;
|
||||
if named_endpoint {
|
||||
bail!("an explicit wayland endpoint is set and did not connect");
|
||||
}
|
||||
if PROBE_UNSUPPORTED.load(Ordering::Acquire) {
|
||||
bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}");
|
||||
}
|
||||
if RUNTIME_DIR_PROBE_BUSY.swap(true, Ordering::AcqRel) {
|
||||
bail!("an earlier probe has not returned");
|
||||
}
|
||||
let _busy = ProbeBusyGuard;
|
||||
let exe = std::env::current_exe()?;
|
||||
// Its own process group, so the deadline can kill loginctl descendants along with the child,
|
||||
// and so no surviving descendant can hold the pipes open past the reads below.
|
||||
use std::os::unix::process::CommandExt;
|
||||
let mut child = std::process::Command::new(exe)
|
||||
.arg(WAYLAND_DISPLAY_PROBE_ARG)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.process_group(0)
|
||||
.spawn()?;
|
||||
let probe_pgid = child.id() as libc::pid_t;
|
||||
let kill_probe_group = || unsafe {
|
||||
let _ = libc::kill(-probe_pgid, libc::SIGKILL);
|
||||
};
|
||||
let deadline = std::time::Instant::now() + RUNTIME_DIR_PROBE_TIMEOUT;
|
||||
let status = loop {
|
||||
match child.try_wait()? {
|
||||
Some(status) => {
|
||||
kill_probe_group();
|
||||
break status;
|
||||
}
|
||||
None if std::time::Instant::now() >= deadline => {
|
||||
kill_probe_group();
|
||||
// The direct pid too, not only its group: if the child left the group its own
|
||||
// kill would miss it, and the wait below would then block on a live child. A
|
||||
// pid-targeted SIGKILL is uncatchable, so wait() is bounded either way.
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
// An unwired binary runs its normal startup, and a long-running one (the
|
||||
// server itself) lands HERE rather than at the handshake check below — latch
|
||||
// on this path too, or every enumeration cycle spawns a full consumer
|
||||
// process. Judged by what the child already wrote: a real probe prints the
|
||||
// magic line first and flushes, so its absence after a whole deadline means
|
||||
// this is not a probe. Only buffered bytes are read — a blocking read could
|
||||
// hang on a grandchild that inherited the write end.
|
||||
match first_buffered_line(child.stdout.take()) {
|
||||
// The pipe could not be inspected at all: no evidence, no latch.
|
||||
None => {
|
||||
bail!("the wayland socket probe timed out and its output was uninspectable")
|
||||
}
|
||||
Some(head) if head.as_deref() == Some(WAYLAND_PROBE_MAGIC) => {
|
||||
bail!("the wayland socket probe did not answer and was killed");
|
||||
}
|
||||
Some(_) => {
|
||||
PROBE_UNSUPPORTED.store(true, Ordering::Release);
|
||||
bail!("the wayland socket probe timed out without the handshake; probe disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => std::thread::sleep(std::time::Duration::from_millis(25)),
|
||||
}
|
||||
};
|
||||
// Drained non-blocking, not read_to_string: the child exited so its output is already
|
||||
// buffered, but a descendant that escaped the process group could still hold a write end open
|
||||
// and an EOF-seeking read would then hang here forever.
|
||||
let stdout = drain_nonblocking(child.stdout.take()).unwrap_or_default();
|
||||
let stderr = drain_nonblocking(child.stderr.take()).unwrap_or_default();
|
||||
let mut lines = stdout.lines();
|
||||
if lines.next() != Some(WAYLAND_PROBE_MAGIC) {
|
||||
// Not a probe: the binary ran its normal startup. Latch, or this path would spawn one
|
||||
// full consumer process per enumeration cycle.
|
||||
PROBE_UNSUPPORTED.store(true, Ordering::Release);
|
||||
bail!("this binary does not dispatch {WAYLAND_DISPLAY_PROBE_ARG}; probe disabled");
|
||||
}
|
||||
if !status.success() {
|
||||
let detail = stderr.trim();
|
||||
if detail.is_empty() {
|
||||
// panic=abort or a signal leaves stderr empty; the status is then the only cause.
|
||||
bail!("wayland socket probe failed: {status}");
|
||||
}
|
||||
bail!("wayland socket probe failed ({status}): {detail}");
|
||||
}
|
||||
let displays: Vec<WaylandDisplayInfo> =
|
||||
match serde_json::from_str(lines.next().unwrap_or_default()) {
|
||||
Ok(displays) => displays,
|
||||
Err(err) => bail!("wayland socket probe answered a malformed list: {err}"),
|
||||
};
|
||||
// The child already refuses an empty list; refuse it here too, so a truncated pipe cannot
|
||||
// become a cached-for-life empty enumeration.
|
||||
if displays.is_empty() {
|
||||
bail!("wayland socket probe returned no outputs");
|
||||
}
|
||||
log::debug!(
|
||||
"wayland: {} output(s) via the probe subprocess",
|
||||
displays.len()
|
||||
);
|
||||
Ok(displays)
|
||||
}
|
||||
|
||||
/// Everything already buffered in the pipe, read strictly non-blocking and capped: a descendant
|
||||
/// that escaped the probe's process group can hold a write end open, so a blocking read (even
|
||||
/// after the child exits) could hang the enumeration forever. `None` means the pipe could not be
|
||||
/// INSPECTED (missing handle or fcntl failure) and must not be read as evidence of anything;
|
||||
/// `Some` is whatever bytes were buffered, whether or not EOF arrived.
|
||||
fn drain_nonblocking<R: std::io::Read + std::os::fd::AsRawFd>(pipe: Option<R>) -> Option<String> {
|
||||
let mut pipe = pipe?;
|
||||
let fd = pipe.as_raw_fd();
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Capped so a descendant that keeps writing cannot spin this read forever.
|
||||
const CAP: usize = 64 * 1024;
|
||||
let mut out = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match pipe.read(&mut buf) {
|
||||
Ok(0) => break, // EOF: the write end is fully closed
|
||||
Ok(n) => {
|
||||
out.extend_from_slice(&buf[..n]);
|
||||
if out.len() >= CAP {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
// WouldBlock: what is buffered is drained (a descendant may still hold the writer).
|
||||
// Any other error: stop with what we have.
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
Some(String::from_utf8_lossy(&out).into_owned())
|
||||
}
|
||||
|
||||
/// The first line the child buffered, for the timeout latch decision. `Some(None)` is an
|
||||
/// inspected-but-empty buffer (genuine absence of the handshake); outer `None` is uninspectable.
|
||||
fn first_buffered_line(pipe: Option<std::process::ChildStdout>) -> Option<Option<String>> {
|
||||
drain_nonblocking(pipe).map(|s| s.lines().next().map(str::to_owned))
|
||||
}
|
||||
|
||||
fn probe_runtime_dir(dir: &Path) -> ResultType<Vec<WaylandDisplayInfo>> {
|
||||
use std::os::unix::net::UnixStream;
|
||||
let mut errs = Vec::new();
|
||||
for path in wayland_sockets_in(dir) {
|
||||
match UnixStream::connect(&path)
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|s| Connection::from_socket(s).map_err(anyhow::Error::from))
|
||||
.and_then(|conn| collect_wayland_displays(&conn))
|
||||
{
|
||||
// The caller caches an empty list as ground truth for the process lifetime, and a
|
||||
// compositor still probing its monitors is exactly what this path connects to.
|
||||
Ok(displays) if displays.is_empty() => {
|
||||
errs.push(format!("{}: no outputs yet", path.display()))
|
||||
}
|
||||
Ok(displays) => {
|
||||
// Which socket answered, when nothing in the environment named one.
|
||||
log::debug!(
|
||||
"wayland: {} output(s) from {}, found by scanning",
|
||||
displays.len(),
|
||||
path.display()
|
||||
);
|
||||
return Ok(displays);
|
||||
}
|
||||
Err(err) => errs.push(format!("{}: {err}", path.display())),
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
"no usable wayland socket in {} ({})",
|
||||
dir.display(),
|
||||
if errs.is_empty() {
|
||||
"none present".to_owned()
|
||||
} else {
|
||||
errs.join("; ")
|
||||
}
|
||||
)
|
||||
}
|
||||
55
libs/base/src/platform/macos.rs
Normal file
55
libs/base/src/platform/macos.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use hbb_common::ResultType;
|
||||
use osascript;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AlertParams {
|
||||
title: String,
|
||||
message: String,
|
||||
alert_type: String,
|
||||
buttons: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AlertResult {
|
||||
#[serde(rename = "buttonReturned")]
|
||||
button: String,
|
||||
}
|
||||
|
||||
/// Firstly run the specified app, then alert a dialog. Return the clicked button value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `app` - The app to execute the script.
|
||||
/// * `alert_type` - Alert type. . informational, warning, critical
|
||||
/// * `title` - The alert title.
|
||||
/// * `message` - The alert message.
|
||||
/// * `buttons` - The buttons to show.
|
||||
pub fn alert(
|
||||
app: String,
|
||||
alert_type: String,
|
||||
title: String,
|
||||
message: String,
|
||||
buttons: Vec<String>,
|
||||
) -> ResultType<String> {
|
||||
let script = osascript::JavaScript::new(&format!(
|
||||
"
|
||||
var App = Application('{}');
|
||||
App.includeStandardAdditions = true;
|
||||
return App.displayAlert($params.title, {{
|
||||
message: $params.message,
|
||||
'as': $params.alert_type,
|
||||
buttons: $params.buttons,
|
||||
}});
|
||||
",
|
||||
app
|
||||
));
|
||||
|
||||
let result: AlertResult = script.execute_with_params(AlertParams {
|
||||
title,
|
||||
message,
|
||||
alert_type,
|
||||
buttons,
|
||||
})?;
|
||||
Ok(result.button)
|
||||
}
|
||||
82
libs/base/src/platform/mod.rs
Normal file
82
libs/base/src/platform/mod.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use hbb_common::{config::Config, log};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
let mut stack = vec![];
|
||||
backtrace::trace(|frame| {
|
||||
backtrace::resolve_frame(frame, |symbol| {
|
||||
if let Some(name) = symbol.name() {
|
||||
stack.push(name.to_string());
|
||||
}
|
||||
});
|
||||
true // keep going to the next frame
|
||||
});
|
||||
let mut info = String::default();
|
||||
if stack.iter().any(|s| {
|
||||
s.contains(&"nouveau_pushbuf_kick")
|
||||
|| s.to_lowercase().contains("nvidia")
|
||||
|| s.contains("gdk_window_end_draw_frame")
|
||||
|| s.contains("glGetString")
|
||||
}) {
|
||||
Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
|
||||
info = "Always use software rendering will be set.".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
if stack.iter().any(|s| {
|
||||
s.to_lowercase().contains("nvidia")
|
||||
|| s.to_lowercase().contains("amf")
|
||||
|| s.to_lowercase().contains("mfx")
|
||||
|| s.contains("cuProfilerStop")
|
||||
}) {
|
||||
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
|
||||
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
log::error!(
|
||||
"Got signal {} and exit. stack:\n{}",
|
||||
sig,
|
||||
stack.join("\n").to_string()
|
||||
);
|
||||
if !info.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::system_message(
|
||||
"RustDesk",
|
||||
&format!("Got signal {} and exit.{}", sig, info),
|
||||
true,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
if let Some(callback) = &GLOBAL_CALLBACK {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn register_breakdown_handler<T>(callback: T)
|
||||
where
|
||||
T: Fn() + 'static,
|
||||
{
|
||||
unsafe {
|
||||
GLOBAL_CALLBACK = Some(Box::new(callback));
|
||||
libc::signal(libc::SIGSEGV, breakdown_signal_handler as _);
|
||||
}
|
||||
}
|
||||
198
libs/base/src/platform/windows.rs
Normal file
198
libs/base/src/platform/windows.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
use winapi::{
|
||||
shared::minwindef::{DWORD, FALSE, TRUE},
|
||||
um::{
|
||||
handleapi::CloseHandle,
|
||||
pdh::{
|
||||
PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx,
|
||||
PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE,
|
||||
PDH_HCOUNTER, PDH_HQUERY,
|
||||
},
|
||||
synchapi::{CreateEventA, WaitForSingleObject},
|
||||
sysinfoapi::VerSetConditionMask,
|
||||
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
|
||||
winnt::{
|
||||
HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
|
||||
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CPU_USAGE_ONE_MINUTE: Arc<Mutex<Option<(f64, Instant)>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
|
||||
#[repr(transparent)]
|
||||
pub struct RAIIHandle(pub HANDLE);
|
||||
|
||||
impl Drop for RAIIHandle {
|
||||
fn drop(&mut self) {
|
||||
// This never gives problem except when running under a debugger.
|
||||
unsafe { CloseHandle(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub(self) struct RAIIPDHQuery(pub PDH_HQUERY);
|
||||
|
||||
impl Drop for RAIIPDHQuery {
|
||||
fn drop(&mut self) {
|
||||
unsafe { PdhCloseQuery(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_cpu_performance_monitor() {
|
||||
// Code from:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex
|
||||
// Why value lower than taskManager:
|
||||
// https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43
|
||||
// Therefore we should compare with Precess Explorer rather than taskManager
|
||||
|
||||
let f = || unsafe {
|
||||
// load avg or cpu usage, test with prime95.
|
||||
// Prefer cpu usage because we can get accurate value from Precess Explorer.
|
||||
// const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0";
|
||||
const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0";
|
||||
const SAMPLE_INTERVAL: DWORD = 2; // 2 second
|
||||
|
||||
let mut ret;
|
||||
let mut query: PDH_HQUERY = std::mem::zeroed();
|
||||
ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhOpenQueryA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let _query = RAIIPDHQuery(query);
|
||||
let mut counter: PDH_HCOUNTER = std::mem::zeroed();
|
||||
ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter);
|
||||
if ret != 0 {
|
||||
log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
ret = PdhCollectQueryData(query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryData failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let mut _counter_type: DWORD = 0;
|
||||
let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed();
|
||||
let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _);
|
||||
if event.is_null() {
|
||||
log::error!("CreateEventA failed");
|
||||
return;
|
||||
}
|
||||
let _event: RAIIHandle = RAIIHandle(event);
|
||||
ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut queue: VecDeque<f64> = VecDeque::new();
|
||||
let mut recent_valid: VecDeque<bool> = VecDeque::new();
|
||||
loop {
|
||||
// latest one minute
|
||||
if queue.len() == 31 {
|
||||
queue.pop_front();
|
||||
}
|
||||
if recent_valid.len() == 31 {
|
||||
recent_valid.pop_front();
|
||||
}
|
||||
// allow get value within one minute
|
||||
if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 {
|
||||
let sum: f64 = queue.iter().map(|f| f.to_owned()).sum();
|
||||
let avg = sum / (queue.len() as f64);
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now()));
|
||||
} else {
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = None;
|
||||
}
|
||||
if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) {
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
if PdhGetFormattedCounterValue(
|
||||
counter,
|
||||
PDH_FMT_DOUBLE,
|
||||
&mut _counter_type,
|
||||
&mut counter_value,
|
||||
) != 0
|
||||
|| counter_value.CStatus != 0
|
||||
{
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
queue.push_back(counter_value.u.doubleValue().clone());
|
||||
recent_valid.push_back(true);
|
||||
}
|
||||
};
|
||||
use std::sync::Once;
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
std::thread::spawn(f);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cpu_uage_one_minute() -> Option<f64> {
|
||||
let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone();
|
||||
if let Some((v, instant)) = v {
|
||||
if instant.elapsed().as_secs() < 30 {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn sync_cpu_usage(cpu_usage: Option<f64>) {
|
||||
let v = match cpu_usage {
|
||||
Some(cpu_usage) => Some((cpu_usage, Instant::now())),
|
||||
None => None,
|
||||
};
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = v;
|
||||
log::info!("cpu usage synced: {:?}", cpu_usage);
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||
// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780
|
||||
pub fn is_windows_version_or_greater(
|
||||
os_major: u32,
|
||||
os_minor: u32,
|
||||
build_number: u32,
|
||||
service_pack_major: u32,
|
||||
service_pack_minor: u32,
|
||||
) -> bool {
|
||||
let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() };
|
||||
osvi.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOEXW>() as DWORD;
|
||||
osvi.dwMajorVersion = os_major as _;
|
||||
osvi.dwMinorVersion = os_minor as _;
|
||||
osvi.dwBuildNumber = build_number as _;
|
||||
osvi.wServicePackMajor = service_pack_major as _;
|
||||
osvi.wServicePackMinor = service_pack_minor as _;
|
||||
|
||||
let result = unsafe {
|
||||
let mut condition_mask = 0;
|
||||
let op = VER_GREATER_EQUAL;
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op);
|
||||
|
||||
VerifyVersionInfoW(
|
||||
&mut osvi as *mut OSVERSIONINFOEXW,
|
||||
VER_MAJORVERSION
|
||||
| VER_MINORVERSION
|
||||
| VER_BUILDNUMBER
|
||||
| VER_SERVICEPACKMAJOR
|
||||
| VER_SERVICEPACKMINOR,
|
||||
condition_mask,
|
||||
)
|
||||
};
|
||||
|
||||
result == TRUE
|
||||
}
|
||||
Reference in New Issue
Block a user