rustdesk/src/platform/linux.rs
fufesou 2e11a8b458
Fix. Linux, run_cmds, trim new line (#7579)
* Fix. Linux, run_cmds, trim new line

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* add tests

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-02 14:47:13 +08:00

1433 lines
44 KiB
Rust

use super::{CursorData, ResultType};
use desktop::Desktop;
#[cfg(all(feature = "linux_headless"))]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
use hbb_common::config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS;
pub use hbb_common::platform::linux::*;
use hbb_common::{
allow_err,
anyhow::anyhow,
bail,
config::Config,
libc::{c_char, c_int, c_long, c_void},
log,
message_proto::Resolution,
regex::{Captures, Regex},
};
use std::{
cell::RefCell,
ffi::OsStr,
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Child, Command},
string::String,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant},
};
use users::{get_user_by_name, os::unix::UserExt};
use wallpaper;
type Xdo = *const c_void;
pub const PA_SAMPLE_RATE: u32 = 48000;
static mut UNMODIFIED: bool = true;
lazy_static::lazy_static! {
pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless();
}
thread_local! {
static XDO: RefCell<Xdo> = RefCell::new(unsafe { xdo_new(std::ptr::null()) });
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
}
extern "C" {
fn xdo_get_mouse_location(
xdo: Xdo,
x: *mut c_int,
y: *mut c_int,
screen_num: *mut c_int,
) -> c_int;
fn xdo_new(display: *const c_char) -> Xdo;
}
#[link(name = "X11")]
extern "C" {
fn XOpenDisplay(display_name: *const c_char) -> *mut c_void;
// fn XCloseDisplay(d: *mut c_void) -> c_int;
}
#[link(name = "Xfixes")]
extern "C" {
// fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int;
fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image;
fn XFree(data: *mut c_void);
}
// /usr/include/X11/extensions/Xfixes.h
#[repr(C)]
pub struct xcb_xfixes_get_cursor_image {
pub x: i16,
pub y: i16,
pub width: u16,
pub height: u16,
pub xhot: u16,
pub yhot: u16,
pub cursor_serial: c_long,
pub pixels: *const c_long,
}
#[inline]
#[cfg(feature = "linux_headless")]
#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
pub fn is_headless_allowed() -> bool {
Config::get_option(CONFIG_OPTION_ALLOW_LINUX_HEADLESS) == "Y"
}
#[inline]
pub fn is_login_screen_wayland() -> bool {
let values = get_values_of_seat0_with_gdm_wayland(&[0, 2]);
is_gdm_user(&values[1]) && get_display_server_of_session(&values[0]) == DISPLAY_SERVER_WAYLAND
}
#[inline]
fn sleep_millis(millis: u64) {
std::thread::sleep(Duration::from_millis(millis));
}
pub fn get_cursor_pos() -> Option<(i32, i32)> {
let mut res = None;
XDO.with(|xdo| {
if let Ok(xdo) = xdo.try_borrow_mut() {
if xdo.is_null() {
return;
}
let mut x: c_int = 0;
let mut y: c_int = 0;
unsafe {
xdo_get_mouse_location(*xdo, &mut x as _, &mut y as _, std::ptr::null_mut());
}
res = Some((x, y));
}
});
res
}
pub fn reset_input_cache() {}
pub fn get_cursor() -> ResultType<Option<u64>> {
let mut res = None;
DISPLAY.with(|conn| {
if let Ok(d) = conn.try_borrow_mut() {
if !d.is_null() {
unsafe {
let img = XFixesGetCursorImage(*d);
if !img.is_null() {
res = Some((*img).cursor_serial as u64);
XFree(img as _);
}
}
}
}
});
Ok(res)
}
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
let mut res = None;
DISPLAY.with(|conn| {
if let Ok(ref mut d) = conn.try_borrow_mut() {
if !d.is_null() {
unsafe {
let img = XFixesGetCursorImage(**d);
if !img.is_null() && hcursor == (*img).cursor_serial as u64 {
let mut cd: CursorData = Default::default();
cd.hotx = (*img).xhot as _;
cd.hoty = (*img).yhot as _;
cd.width = (*img).width as _;
cd.height = (*img).height as _;
// to-do: how about if it is 0
cd.id = (*img).cursor_serial as _;
let pixels =
std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _);
// cd.colors.resize(pixels.len() * 4, 0);
let mut cd_colors = vec![0_u8; pixels.len() * 4];
for y in 0..cd.height {
for x in 0..cd.width {
let pos = (y * cd.width + x) as usize;
let p = pixels[pos];
let a = (p >> 24) & 0xff;
let r = (p >> 16) & 0xff;
let g = (p >> 8) & 0xff;
let b = (p >> 0) & 0xff;
if a == 0 {
continue;
}
let pos = pos * 4;
cd_colors[pos] = r as _;
cd_colors[pos + 1] = g as _;
cd_colors[pos + 2] = b as _;
cd_colors[pos + 3] = a as _;
}
}
cd.colors = cd_colors.into();
res = Some(cd);
}
if !img.is_null() {
XFree(img as _);
}
}
}
}
});
match res {
Some(x) => Ok(x),
_ => bail!("Failed to get cursor image of {}", hcursor),
}
}
fn start_uinput_service() {
use crate::server::uinput::service;
std::thread::spawn(|| {
service::start_service_control();
});
std::thread::spawn(|| {
service::start_service_keyboard();
});
std::thread::spawn(|| {
service::start_service_mouse();
});
}
#[inline]
fn try_start_server_(desktop: Option<&Desktop>) -> ResultType<Option<Child>> {
match desktop {
Some(desktop) => {
let mut envs = vec![];
if !desktop.display.is_empty() {
envs.push(("DISPLAY", desktop.display.clone()));
}
if !desktop.xauth.is_empty() {
envs.push(("XAUTHORITY", desktop.xauth.clone()));
}
if !desktop.wl_display.is_empty() {
envs.push(("WAYLAND_DISPLAY", desktop.wl_display.clone()));
}
if !desktop.home.is_empty() {
envs.push(("HOME", desktop.home.clone()));
}
run_as_user(
vec!["--server"],
Some((desktop.uid.clone(), desktop.username.clone())),
envs,
)
}
None => Ok(Some(crate::run_me(vec!["--server"])?)),
}
}
#[inline]
fn start_server(desktop: Option<&Desktop>, server: &mut Option<Child>) {
match try_start_server_(desktop) {
Ok(ps) => *server = ps,
Err(err) => {
log::error!("Failed to start server: {}", err);
}
}
}
fn stop_server(server: &mut Option<Child>) {
if let Some(mut ps) = server.take() {
allow_err!(ps.kill());
sleep_millis(30);
match ps.try_wait() {
Ok(Some(_status)) => {}
Ok(None) => {
let _res = ps.wait();
}
Err(e) => log::error!("error attempting to wait: {e}"),
}
}
}
fn set_x11_env(desktop: &Desktop) {
log::info!("DISPLAY: {}", desktop.display);
log::info!("XAUTHORITY: {}", desktop.xauth);
if !desktop.display.is_empty() {
std::env::set_var("DISPLAY", &desktop.display);
}
if !desktop.xauth.is_empty() {
std::env::set_var("XAUTHORITY", &desktop.xauth);
}
}
#[inline]
fn stop_rustdesk_servers() {
let _ = run_cmds(&format!(
r##"ps -ef | grep -E 'rustdesk +--server' | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##,
));
}
#[inline]
fn stop_subprocess() {
let _ = run_cmds(&format!(
r##"ps -ef | grep '/etc/rustdesk/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##,
));
let _ = run_cmds(&format!(
r##"ps -ef | grep -E 'rustdesk +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##,
));
}
fn should_start_server(
try_x11: bool,
is_display_changed: bool,
uid: &mut String,
desktop: &Desktop,
cm0: &mut bool,
last_restart: &mut Instant,
server: &mut Option<Child>,
) -> bool {
let cm = get_cm();
let mut start_new = false;
let mut should_kill = false;
if desktop.is_headless() {
if !uid.is_empty() {
// From having a monitor to not having a monitor.
*uid = "".to_owned();
should_kill = true;
}
} else if is_display_changed || desktop.uid != *uid && !desktop.uid.is_empty() {
*uid = desktop.uid.clone();
if try_x11 {
set_x11_env(&desktop);
}
should_kill = true;
}
if !should_kill
&& !cm
&& ((*cm0 && last_restart.elapsed().as_secs() > 60)
|| last_restart.elapsed().as_secs() > 3600)
{
// restart server if new connections all closed, or every one hour,
// as a workaround to resolve "SpotUdp" (dns resolve)
// and x server get displays failure issue
should_kill = true;
log::info!("restart server");
}
if should_kill {
if let Some(ps) = server.as_mut() {
allow_err!(ps.kill());
sleep_millis(30);
*last_restart = Instant::now();
}
}
if let Some(ps) = server.as_mut() {
match ps.try_wait() {
Ok(Some(_)) => {
*server = None;
start_new = true;
}
_ => {}
}
} else {
start_new = true;
}
*cm0 = cm;
start_new
}
// to-do: stop_server(&mut user_server); may not stop child correctly
// stop_rustdesk_servers() is just a temp solution here.
fn force_stop_server() {
stop_rustdesk_servers();
sleep_millis(super::SERVICE_INTERVAL);
}
pub fn start_os_service() {
check_if_stop_service();
stop_rustdesk_servers();
stop_subprocess();
start_uinput_service();
std::thread::spawn(|| {
allow_err!(crate::ipc::start(crate::POSTFIX_SERVICE));
});
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let (mut display, mut xauth): (String, String) = ("".to_owned(), "".to_owned());
let mut desktop = Desktop::default();
let mut sid = "".to_owned();
let mut uid = "".to_owned();
let mut server: Option<Child> = None;
let mut user_server: Option<Child> = None;
if let Err(err) = ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
}) {
println!("Failed to set Ctrl-C handler: {}", err);
}
let mut cm0 = false;
let mut last_restart = Instant::now();
while running.load(Ordering::SeqCst) {
desktop.refresh();
// Duplicate logic here with should_start_server
// Login wayland will try to start a headless --server.
if desktop.username == "root" || desktop.is_login_wayland() {
// try kill subprocess "--server"
stop_server(&mut user_server);
// try start subprocess "--server"
// No need to check is_display_changed here.
if should_start_server(
true,
false,
&mut uid,
&desktop,
&mut cm0,
&mut last_restart,
&mut server,
) {
stop_subprocess();
force_stop_server();
start_server(None, &mut server);
}
} else if desktop.username != "" {
// try kill subprocess "--server"
stop_server(&mut server);
let is_display_changed = desktop.display != display || desktop.xauth != xauth;
display = desktop.display.clone();
xauth = desktop.xauth.clone();
// try start subprocess "--server"
if should_start_server(
!desktop.is_wayland(),
is_display_changed,
&mut uid,
&desktop,
&mut cm0,
&mut last_restart,
&mut user_server,
) {
stop_subprocess();
force_stop_server();
start_server(Some(&desktop), &mut user_server);
}
} else {
force_stop_server();
stop_server(&mut user_server);
stop_server(&mut server);
}
let keeps_headless = sid.is_empty() && desktop.is_headless();
let keeps_session = sid == desktop.sid;
if keeps_headless || keeps_session {
// for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling,
sleep_millis(500);
} else {
sleep_millis(super::SERVICE_INTERVAL);
}
if !desktop.is_headless() {
sid = desktop.sid.clone();
}
}
if let Some(ps) = user_server.take().as_mut() {
allow_err!(ps.kill());
}
if let Some(ps) = server.take().as_mut() {
allow_err!(ps.kill());
}
log::info!("Exit");
}
#[inline]
pub fn get_active_user_id_name() -> (String, String) {
let vec_id_name = get_values_of_seat0(&[1, 2]);
(vec_id_name[0].clone(), vec_id_name[1].clone())
}
#[inline]
pub fn get_active_userid() -> String {
get_values_of_seat0(&[1])[0].clone()
}
fn get_cm() -> bool {
if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains(&format!(
"{} --cm",
std::env::current_exe()
.unwrap_or("".into())
.to_string_lossy()
)) {
return true;
}
}
}
false
}
pub fn is_login_wayland() -> bool {
let files = ["/etc/gdm3/custom.conf", "/etc/gdm/custom.conf"];
match (
Regex::new(r"# *WaylandEnable *= *false"),
Regex::new(r"WaylandEnable *= *true"),
) {
(Ok(pat1), Ok(pat2)) => {
for file in files {
if let Ok(contents) = std::fs::read_to_string(file) {
return pat1.is_match(&contents) || pat2.is_match(&contents);
}
}
}
_ => {}
}
false
}
#[inline]
pub fn current_is_wayland() -> bool {
return is_desktop_wayland() && unsafe { UNMODIFIED };
}
// to-do: test the other display manager
fn _get_display_manager() -> String {
if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") {
if let Some(x) = x.split("/").last() {
return x.to_owned();
}
}
"gdm3".to_owned()
}
#[inline]
pub fn get_active_username() -> String {
get_values_of_seat0(&[2])[0].clone()
}
pub fn get_user_home_by_name(username: &str) -> Option<PathBuf> {
return match get_user_by_name(username) {
None => None,
Some(user) => {
let home = user.home_dir();
if Path::is_dir(home) {
Some(PathBuf::from(home))
} else {
None
}
}
};
}
pub fn get_active_user_home() -> Option<PathBuf> {
let username = get_active_username();
if !username.is_empty() {
match get_user_home_by_name(&username) {
None => {
// fallback to most common default pattern
let home = PathBuf::from(format!("/home/{}", username));
if home.exists() {
return Some(home);
}
}
Some(home) => {
return Some(home);
}
}
}
None
}
pub fn get_env_var(k: &str) -> String {
match std::env::var(k) {
Ok(v) => v,
Err(_e) => "".to_owned(),
}
}
// Headless is enabled, always return true.
pub fn is_prelogin() -> bool {
let n = get_active_userid().len();
n < 4 && n > 1
}
pub fn is_root() -> bool {
crate::username() == "root"
}
fn is_opensuse() -> bool {
if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse") {
if !res.is_empty() {
return true;
}
}
false
}
pub fn run_as_user<I, K, V>(
arg: Vec<&str>,
user: Option<(String, String)>,
envs: I,
) -> ResultType<Option<Child>>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let (uid, username) = match user {
Some(id_name) => id_name,
None => get_active_user_id_name(),
};
let cmd = std::env::current_exe()?;
if uid.is_empty() {
bail!("No valid uid");
}
let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str;
let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")];
args.append(&mut arg.clone());
// -E is required to preserve env
args.insert(0, "-E");
let task = Command::new("sudo").envs(envs).args(args).spawn()?;
Ok(Some(task))
}
pub fn get_pa_monitor() -> String {
get_pa_sources()
.drain(..)
.map(|x| x.0)
.filter(|x| x.contains("monitor"))
.next()
.unwrap_or("".to_owned())
}
pub fn get_pa_source_name(desc: &str) -> String {
get_pa_sources()
.drain(..)
.filter(|x| x.1 == desc)
.map(|x| x.0)
.next()
.unwrap_or("".to_owned())
}
pub fn get_pa_sources() -> Vec<(String, String)> {
use pulsectl::controllers::*;
let mut out = Vec::new();
match SourceController::create() {
Ok(mut handler) => {
if let Ok(devices) = handler.list_devices() {
for dev in devices.clone() {
out.push((
dev.name.unwrap_or("".to_owned()),
dev.description.unwrap_or("".to_owned()),
));
}
}
}
Err(err) => {
log::error!("Failed to get_pa_sources: {:?}", err);
}
}
out
}
pub fn get_default_pa_source() -> Option<(String, String)> {
use pulsectl::controllers::*;
match SourceController::create() {
Ok(mut handler) => {
if let Ok(dev) = handler.get_default_device() {
return Some((
dev.name.unwrap_or("".to_owned()),
dev.description.unwrap_or("".to_owned()),
));
}
}
Err(err) => {
log::error!("Failed to get_pa_source: {:?}", err);
}
}
None
}
pub fn lock_screen() {
Command::new("xdg-screensaver").arg("lock").spawn().ok();
}
pub fn toggle_blank_screen(_v: bool) {
// https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x
}
pub fn block_input(_v: bool) -> (bool, String) {
(true, "".to_owned())
}
pub fn is_installed() -> bool {
if let Ok(p) = std::env::current_exe() {
p.to_str().unwrap_or_default().starts_with("/usr")
} else {
false
}
}
#[inline]
fn get_env(name: &str, uid: &str, process: &str) -> String {
let cmd = format!("ps -u {} -f | grep -E '{}' | grep -v 'grep' | tail -1 | awk '{{print $2}}' | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, process, name, name);
if let Ok(x) = run_cmds(&cmd) {
x.trim_end().to_string()
} else {
"".to_owned()
}
}
#[inline]
fn get_env_from_pid(name: &str, pid: &str) -> String {
let cmd = format!("cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", pid, name, name);
if let Ok(x) = run_cmds(&cmd) {
x.trim_end().to_string()
} else {
"".to_owned()
}
}
#[link(name = "gtk-3")]
extern "C" {
fn gtk_main_quit();
}
pub fn quit_gui() {
unsafe { gtk_main_quit() };
}
pub fn exec_privileged(args: &[&str]) -> ResultType<Child> {
Ok(Command::new("pkexec").args(args).spawn()?)
}
pub fn check_super_user_permission() -> ResultType<bool> {
let file = "/usr/share/rustdesk/files/polkit";
let arg;
if Path::new(file).is_file() {
arg = file;
} else {
arg = "echo";
}
// https://github.com/rustdesk/rustdesk/issues/2756
if let Ok(status) = Command::new("pkexec").arg(arg).status() {
// https://github.com/rustdesk/rustdesk/issues/5205#issuecomment-1658059657s
Ok(status.code() != Some(126) && status.code() != Some(127))
} else {
Ok(true)
}
}
pub fn elevate(args: Vec<&str>) -> ResultType<bool> {
let cmd = std::env::current_exe()?;
match cmd.to_str() {
Some(cmd) => {
let mut args_with_exe = vec![cmd];
args_with_exe.append(&mut args.clone());
// -E required for opensuse
if is_opensuse() {
args_with_exe.insert(0, "-E");
}
let res = match exec_privileged(&args_with_exe)?.wait() {
Ok(status) => {
if status.success() {
true
} else {
log::error!(
"Failed to wait install process, process status: {:?}",
status
);
false
}
}
Err(e) => {
log::error!("Failed to wait install process, error: {}", e);
false
}
};
Ok(res)
}
None => {
hbb_common::bail!("Failed to get current exe as str");
}
}
}
type GtkSettingsPtr = *mut c_void;
type GObjectPtr = *mut c_void;
#[link(name = "gtk-3")]
extern "C" {
// fn gtk_init(argc: *mut c_int, argv: *mut *mut c_char);
fn gtk_settings_get_default() -> GtkSettingsPtr;
}
#[link(name = "gobject-2.0")]
extern "C" {
fn g_object_get(object: GObjectPtr, first_property_name: *const c_char, ...);
}
pub fn get_double_click_time() -> u32 {
// GtkSettings *settings = gtk_settings_get_default ();
// g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL);
unsafe {
let mut double_click_time = 0u32;
let Ok(property) = std::ffi::CString::new("gtk-double-click-time") else {
return 0;
};
let settings = gtk_settings_get_default();
g_object_get(
settings,
property.as_ptr(),
&mut double_click_time as *mut u32,
0 as *const c_void,
);
double_click_time
}
}
#[inline]
fn get_width_height_from_captures<'t>(caps: &Captures<'t>) -> Option<(i32, i32)> {
match (caps.name("width"), caps.name("height")) {
(Some(width), Some(height)) => {
match (
width.as_str().parse::<i32>(),
height.as_str().parse::<i32>(),
) {
(Ok(width), Ok(height)) => {
return Some((width, height));
}
_ => {}
}
}
_ => {}
}
None
}
#[inline]
fn get_xrandr_conn_pat(name: &str) -> String {
format!(
r"{}\s+connected.+?(?P<width>\d+)x(?P<height>\d+)\+(?P<x>\d+)\+(?P<y>\d+).*?\n",
name
)
}
pub fn resolutions(name: &str) -> Vec<Resolution> {
let resolutions_pat = r"(?P<resolutions>(\s*\d+x\d+\s+\d+.*\n)+)";
let connected_pat = get_xrandr_conn_pat(name);
let mut v = vec![];
if let Ok(re) = Regex::new(&format!("{}{}", connected_pat, resolutions_pat)) {
match run_cmds("xrandr --query | tr -s ' '") {
Ok(xrandr_output) => {
// There'are different kinds of xrandr output.
/*
1.
Screen 0: minimum 320 x 175, current 1920 x 1080, maximum 1920 x 1080
default connected 1920x1080+0+0 0mm x 0mm
1920x1080 10.00*
1280x720 25.00
1680x1050 60.00
Virtual2 disconnected (normal left inverted right x axis y axis)
Virtual3 disconnected (normal left inverted right x axis y axis)
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 193mm
1920x1080 60.01*+ 60.01 59.97 59.96 59.93
1680x1050 59.95 59.88
1600x1024 60.17
XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
HDMI-0 connected (normal left inverted right x axis y axis)
rdp0 connected primary 1920x1080+0+0 0mm x 0mm
*/
if let Some(caps) = re.captures(&xrandr_output) {
if let Some(resolutions) = caps.name("resolutions") {
let resolution_pat =
r"\s*(?P<width>\d+)x(?P<height>\d+)\s+(?P<rates>(\d+\.\d+\D*)+)\s*\n";
let Ok(resolution_re) = Regex::new(&format!(r"{}", resolution_pat)) else {
log::error!("Regex new failed");
return vec![];
};
for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) {
if let Some((width, height)) =
get_width_height_from_captures(&resolution_caps)
{
let resolution = Resolution {
width,
height,
..Default::default()
};
if !v.contains(&resolution) {
v.push(resolution);
}
}
}
}
}
}
Err(e) => log::error!("Failed to run xrandr query, {}", e),
}
}
v
}
pub fn current_resolution(name: &str) -> ResultType<Resolution> {
let xrandr_output = run_cmds("xrandr --query | tr -s ' '")?;
let re = Regex::new(&get_xrandr_conn_pat(name))?;
if let Some(caps) = re.captures(&xrandr_output) {
if let Some((width, height)) = get_width_height_from_captures(&caps) {
return Ok(Resolution {
width,
height,
..Default::default()
});
}
}
bail!("Failed to find current resolution for {}", name);
}
pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> {
Command::new("xrandr")
.args(vec![
"--output",
name,
"--mode",
&format!("{}x{}", width, height),
])
.spawn()?;
Ok(())
}
#[inline]
pub fn is_xwayland_running() -> bool {
if let Ok(output) = run_cmds("pgrep -a Xwayland") {
return output.contains("Xwayland");
}
false
}
mod desktop {
use super::*;
pub const XFCE4_PANEL: &str = "xfce4-panel";
pub const SDDM_GREETER: &str = "sddm-greeter";
const XWAYLAND: &str = "Xwayland";
const IBUS_DAEMON: &str = "ibus-daemon";
const PLASMA_KDED: &str = "kded[0-9]+";
const GNOME_GOA_DAEMON: &str = "goa-daemon";
const RUSTDESK_TRAY: &str = "rustdesk +--tray";
#[derive(Debug, Clone, Default)]
pub struct Desktop {
pub sid: String,
pub username: String,
pub uid: String,
pub protocal: String,
pub display: String,
pub xauth: String,
pub home: String,
pub is_rustdesk_subprocess: bool,
pub wl_display: String,
}
impl Desktop {
#[inline]
pub fn is_wayland(&self) -> bool {
self.protocal == DISPLAY_SERVER_WAYLAND
}
#[inline]
pub fn is_login_wayland(&self) -> bool {
super::is_gdm_user(&self.username) && self.protocal == DISPLAY_SERVER_WAYLAND
}
#[inline]
pub fn is_headless(&self) -> bool {
self.sid.is_empty() || self.is_rustdesk_subprocess
}
fn get_display_xauth_xwayland(&mut self) {
for _ in 0..5 {
let display_proc = vec![
XWAYLAND,
IBUS_DAEMON,
GNOME_GOA_DAEMON,
PLASMA_KDED,
RUSTDESK_TRAY,
];
for proc in display_proc {
self.display = get_env("DISPLAY", &self.uid, proc);
self.xauth = get_env("XAUTHORITY", &self.uid, proc);
self.wl_display = get_env("WAYLAND_DISPLAY", &self.uid, proc);
if !self.display.is_empty() && !self.xauth.is_empty() {
break;
}
}
sleep_millis(300);
}
}
fn get_display_x11(&mut self) {
for _ in 0..10 {
let display_proc = vec![
XWAYLAND,
IBUS_DAEMON,
GNOME_GOA_DAEMON,
PLASMA_KDED,
XFCE4_PANEL,
SDDM_GREETER,
];
for proc in display_proc {
self.display = get_env("DISPLAY", &self.uid, proc);
if !self.display.is_empty() {
break;
}
}
sleep_millis(300);
}
if self.display.is_empty() {
self.display = Self::get_display_by_user(&self.username);
}
if self.display.is_empty() {
self.display = ":0".to_owned();
}
self.display = self
.display
.replace(&whoami::hostname(), "")
.replace("localhost", "");
}
fn get_home(&mut self) {
self.home = "".to_string();
let cmd = format!(
"getent passwd '{}' | awk -F':' '{{print $6}}'",
&self.username
);
self.home = run_cmds_trim_newline(&cmd).unwrap_or(format!("/home/{}", &self.username));
}
fn get_xauth_from_xorg(&mut self) {
if let Ok(output) = run_cmds(&format!(
"ps -u {} -f | grep 'Xorg' | grep -v 'grep'",
&self.uid
)) {
for line in output.lines() {
let mut auth_found = false;
for v in line.split_whitespace() {
if v == "-auth" {
auth_found = true;
} else if auth_found {
if std::path::Path::new(v).is_absolute()
&& std::path::Path::new(v).exists()
{
self.xauth = v.to_string();
} else {
if let Some(pid) = line.split_whitespace().nth(1) {
let mut base_dir: String = String::from("/home"); // default pattern
let home_dir = get_env_from_pid("HOME", pid);
if home_dir.is_empty() {
if let Some(home) = get_user_home_by_name(&self.username) {
base_dir = home.as_path().to_string_lossy().to_string();
};
} else {
base_dir = home_dir;
}
if Path::new(&base_dir).exists() {
self.xauth = format!("{}/{}", base_dir, v);
};
} else {
// unreachable!
}
}
return;
}
}
}
}
}
fn get_xauth_x11(&mut self) {
// try by direct access to window manager process by name
for _ in 0..10 {
let display_proc = vec![
XWAYLAND,
IBUS_DAEMON,
GNOME_GOA_DAEMON,
PLASMA_KDED,
XFCE4_PANEL,
SDDM_GREETER,
RUSTDESK_TRAY,
];
for proc in display_proc {
self.xauth = get_env("XAUTHORITY", &self.uid, proc);
if !self.xauth.is_empty() {
break;
}
}
sleep_millis(300);
}
// get from Xorg process, parameter and environment
if self.xauth.is_empty() {
self.get_xauth_from_xorg();
}
// fallback to default file name
if self.xauth.is_empty() {
let gdm = format!("/run/user/{}/gdm/Xauthority", self.uid);
self.xauth = if std::path::Path::new(&gdm).exists() {
gdm
} else {
let username = &self.username;
match get_user_home_by_name(username) {
None => {
if username == "root" {
format!("/{}/.Xauthority", username)
} else {
let tmp = format!("/home/{}/.Xauthority", username);
if std::path::Path::new(&tmp).exists() {
tmp
} else {
format!("/var/lib/{}/.Xauthority", username)
}
}
}
Some(home) => {
format!(
"{}/.Xauthority",
home.as_path().to_string_lossy().to_string()
)
}
}
};
}
}
fn get_display_by_user(user: &str) -> String {
// log::debug!("w {}", &user);
if let Ok(output) = std::process::Command::new("w").arg(&user).output() {
for line in String::from_utf8_lossy(&output.stdout).lines() {
let mut iter = line.split_whitespace();
let b = iter.nth(2);
if let Some(b) = b {
if b.starts_with(":") {
return b.to_owned();
}
}
}
}
// above not work for gdm user
//log::debug!("ls -l /tmp/.X11-unix/");
let mut last = "".to_owned();
if let Ok(output) = std::process::Command::new("ls")
.args(vec!["-l", "/tmp/.X11-unix/"])
.output()
{
for line in String::from_utf8_lossy(&output.stdout).lines() {
let mut iter = line.split_whitespace();
let user_field = iter.nth(2);
if let Some(x) = iter.last() {
if x.starts_with("X") {
last = x.replace("X", ":").to_owned();
if user_field == Some(&user) {
return last;
}
}
}
}
}
last
}
fn set_is_subprocess(&mut self) {
self.is_rustdesk_subprocess = false;
let cmd = "ps -ef | grep 'rustdesk/xorg.conf' | grep -v grep | wc -l";
if let Ok(res) = run_cmds(cmd) {
if res.trim() != "0" {
self.is_rustdesk_subprocess = true;
}
}
}
pub fn refresh(&mut self) {
if !self.sid.is_empty() && is_active_and_seat0(&self.sid) {
// Xwayland display and xauth may not be available in a short time after login.
if is_xwayland_running() && !self.is_login_wayland() {
self.get_display_xauth_xwayland();
self.is_rustdesk_subprocess = false;
}
return;
}
let seat0_values = get_values_of_seat0(&[0, 1, 2]);
if seat0_values[0].is_empty() {
*self = Self::default();
self.is_rustdesk_subprocess = false;
return;
}
self.sid = seat0_values[0].clone();
self.uid = seat0_values[1].clone();
self.username = seat0_values[2].clone();
self.protocal = get_display_server_of_session(&self.sid).into();
if self.is_login_wayland() {
self.display = "".to_owned();
self.xauth = "".to_owned();
self.is_rustdesk_subprocess = false;
return;
}
self.get_home();
if self.is_wayland() {
if is_xwayland_running() {
self.get_display_xauth_xwayland();
} else {
self.display = "".to_owned();
self.xauth = "".to_owned();
}
self.is_rustdesk_subprocess = false;
} else {
self.get_display_x11();
self.get_xauth_x11();
self.set_is_subprocess();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_desktop_env() {
let mut d = Desktop::default();
d.refresh();
if d.username == "root" {
assert_eq!(d.home, "/root");
} else {
if !d.username.is_empty() {
let home = super::super::get_env_var("HOME");
if !home.is_empty() {
assert_eq!(d.home, home);
} else {
//
}
}
}
}
}
}
pub struct WakeLock(Option<keepawake::AwakeHandle>);
impl WakeLock {
pub fn new(display: bool, idle: bool, sleep: bool) -> Self {
WakeLock(
keepawake::Builder::new()
.display(display)
.idle(idle)
.sleep(sleep)
.create()
.ok(),
)
}
}
fn has_cmd(cmd: &str) -> bool {
std::process::Command::new("which")
.arg(cmd)
.status()
.map(|x| x.success())
.unwrap_or_default()
}
pub fn run_cmds_pkexec(cmds: &str) -> bool {
const DONE: &str = "RUN_CMDS_PKEXEC_DONE";
if let Ok(output) = std::process::Command::new("pkexec")
.arg("sh")
.arg("-c")
.arg(&format!("{cmds} echo {DONE}"))
.output()
{
let out = String::from_utf8_lossy(&output.stdout);
log::debug!("cmds: {cmds}");
log::debug!("output: {out}");
out.contains(DONE)
} else {
false
}
}
pub fn run_me_with(secs: u32) {
let exe = std::env::current_exe()
.unwrap_or("".into())
.to_string_lossy()
.to_string();
std::process::Command::new("sh")
.arg("-c")
.arg(&format!("sleep {secs}; {exe}"))
.spawn()
.ok();
}
fn switch_service(stop: bool) -> String {
let home = std::env::var("HOME").unwrap_or_default();
Config::set_option("stop-service".into(), if stop { "Y" } else { "" }.into());
if home != "/root" && !Config::get().is_empty() {
format!("cp -f {home}/.config/rustdesk/RustDesk.toml /root/.config/rustdesk/; cp -f {home}/.config/rustdesk/RustDesk2.toml /root/.config/rustdesk/;")
} else {
"".to_owned()
}
}
pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
if !has_cmd("systemctl") {
return false;
}
log::info!("Uninstalling service...");
let cp = switch_service(true);
if !run_cmds_pkexec(&format!(
"systemctl disable rustdesk; systemctl stop rustdesk; {cp}"
)) {
Config::set_option("stop-service".into(), "".into());
return true;
}
if show_new_window {
run_me_with(2);
}
std::process::exit(0);
}
pub fn install_service() -> bool {
let _installing = crate::platform::InstallingService::new();
if !has_cmd("systemctl") {
return false;
}
log::info!("Installing service...");
let cp = switch_service(false);
if !run_cmds_pkexec(&format!(
"{cp} systemctl enable rustdesk; systemctl start rustdesk;"
)) {
Config::set_option("stop-service".into(), "Y".into());
return true;
}
run_me_with(2);
std::process::exit(0);
}
fn check_if_stop_service() {
if Config::get_option("stop-service".into()) == "Y" {
allow_err!(run_cmds(
"systemctl disable rustdesk; systemctl stop rustdesk"
));
}
}
pub fn check_autostart_config() -> ResultType<()> {
let home = std::env::var("HOME").unwrap_or_default();
let path = format!("{home}/.config/autostart");
let file = format!("{path}/rustdesk.desktop");
std::fs::create_dir_all(&path).ok();
if !Path::new(&file).exists() {
// write text to the desktop file
let mut file = std::fs::File::create(&file)?;
file.write_all(
"
[Desktop Entry]
Type=Application
Exec=rustdesk --tray
NoDisplay=false
"
.as_bytes(),
)?;
}
Ok(())
}
pub struct WallPaperRemover {
old_path: String,
old_path_dark: Option<String>, // ubuntu 22.04 light/dark theme have different uri
}
impl WallPaperRemover {
pub fn new() -> ResultType<Self> {
let start = std::time::Instant::now();
let old_path = wallpaper::get().map_err(|e| anyhow!(e.to_string()))?;
let old_path_dark = wallpaper::get_dark().ok();
if old_path.is_empty() && old_path_dark.clone().unwrap_or_default().is_empty() {
bail!("already solid color");
}
wallpaper::set_from_path("").map_err(|e| anyhow!(e.to_string()))?;
wallpaper::set_dark_from_path("").ok();
log::info!(
"created wallpaper remover, old_path: {:?}, old_path_dark: {:?}, elapsed: {:?}",
old_path,
old_path_dark,
start.elapsed(),
);
Ok(Self {
old_path,
old_path_dark,
})
}
pub fn support() -> bool {
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
if wallpaper::gnome::is_compliant(&desktop) || desktop.as_str() == "XFCE" {
return wallpaper::get().is_ok();
}
false
}
}
impl Drop for WallPaperRemover {
fn drop(&mut self) {
allow_err!(wallpaper::set_from_path(&self.old_path).map_err(|e| anyhow!(e.to_string())));
if let Some(old_path_dark) = &self.old_path_dark {
allow_err!(wallpaper::set_dark_from_path(old_path_dark.as_str())
.map_err(|e| anyhow!(e.to_string())));
}
}
}
#[inline]
pub fn is_x11() -> bool {
*IS_X11
}
#[inline]
pub fn is_selinux_enforcing() -> bool {
match run_cmds("getenforce") {
Ok(output) => output.trim() == "Enforcing",
Err(_) => match run_cmds("sestatus") {
Ok(output) => {
for line in output.lines() {
if line.contains("Current mode:") {
return line.contains("enforcing");
}
}
false
}
Err(_) => false,
},
}
}