feat, update, win, macos (#11618)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-05-04 07:32:47 +08:00
committed by GitHub
parent 62276b4f4f
commit ca00706a38
72 changed files with 2128 additions and 69 deletions

View File

@@ -13,7 +13,9 @@ use hbb_common::{
libc::{c_int, wchar_t},
log,
message_proto::{DisplayInfo, Resolution, WindowsSession},
sleep, timeout, tokio,
sleep,
sysinfo::{Pid, System},
timeout, tokio,
};
use std::{
collections::HashMap,
@@ -484,6 +486,7 @@ extern "C" {
cmd: *const u16,
session_id: DWORD,
as_user: BOOL,
show: BOOL,
token_pid: &mut DWORD,
) -> HANDLE;
fn GetSessionUserTokenWin(
@@ -669,6 +672,10 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
"\"{}\" --server",
std::env::current_exe()?.to_str().unwrap_or("")
);
launch_privileged_process(session_id, &cmd)
}
pub fn launch_privileged_process(session_id: DWORD, cmd: &str) -> ResultType<HANDLE> {
use std::os::windows::ffi::OsStrExt;
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
.encode_wide()
@@ -676,9 +683,12 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
.collect();
let wstr = wstr.as_ptr();
let mut token_pid = 0;
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE, &mut token_pid) };
let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE, FALSE, &mut token_pid) };
if h.is_null() {
log::error!("Failed to launch server: {}", io::Error::last_os_error());
log::error!(
"Failed to launch privileged process: {}",
io::Error::last_os_error()
);
if token_pid == 0 {
log::error!("No process winlogon.exe");
}
@@ -687,22 +697,43 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType<HANDL
}
pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
let cmd = format!(
"\"{}\" {}",
std::env::current_exe()?.to_str().unwrap_or(""),
arg.join(" "),
);
run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false)
}
pub fn run_exe_in_cur_session(
exe: &str,
arg: Vec<&str>,
show: bool,
) -> ResultType<Option<std::process::Child>> {
let Some(session_id) = get_current_process_session_id() else {
bail!("Failed to get current process session id");
};
run_exe_in_session(exe, arg, session_id, show)
}
pub fn run_exe_in_session(
exe: &str,
arg: Vec<&str>,
session_id: DWORD,
show: bool,
) -> ResultType<Option<std::process::Child>> {
use std::os::windows::ffi::OsStrExt;
let cmd = format!("\"{}\" {}", exe, arg.join(" "),);
let wstr: Vec<u16> = std::ffi::OsStr::new(&cmd)
.encode_wide()
.chain(Some(0).into_iter())
.collect();
let wstr = wstr.as_ptr();
let mut token_pid = 0;
let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE, &mut token_pid) };
let h = unsafe {
LaunchProcessWin(
wstr,
session_id,
TRUE,
if show { TRUE } else { FALSE },
&mut token_pid,
)
};
if h.is_null() {
if token_pid == 0 {
bail!(
@@ -800,8 +831,12 @@ pub fn set_share_rdp(enable: bool) {
}
pub fn get_current_process_session_id() -> Option<u32> {
get_session_id_of_process(unsafe { GetCurrentProcessId() })
}
pub fn get_session_id_of_process(pid: DWORD) -> Option<u32> {
let mut sid = 0;
if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut sid) == TRUE } {
if unsafe { ProcessIdToSessionId(pid, &mut sid) == TRUE } {
Some(sid)
} else {
None
@@ -1348,6 +1383,9 @@ copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\
")
};
// Remember to check if `update_me` need to be changed if changing the `cmds`.
// No need to merge the existing dup code, because the code in these two functions are too critical.
// New code should be written in a common function.
let cmds = format!(
"
{uninstall_str}
@@ -2366,6 +2404,171 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
std::process::exit(0);
}
pub fn update_me(debug: bool) -> ResultType<()> {
let app_name = crate::get_app_name();
let src_exe = std::env::current_exe()?.to_string_lossy().to_string();
let (subkey, path, _, exe) = get_install_info();
let is_installed = std::fs::metadata(&exe).is_ok();
if !is_installed {
bail!("{} is not installed.", &app_name);
}
let app_exe_name = &format!("{}.exe", &app_name);
let main_window_pids =
crate::platform::get_pids_of_process_with_args::<_, &str>(&app_exe_name, &[]);
let main_window_sessions = main_window_pids
.iter()
.map(|pid| get_session_id_of_process(pid.as_u32()))
.flatten()
.collect::<Vec<_>>();
kill_process_by_pids(&app_exe_name, main_window_pids)?;
let tray_pids = crate::platform::get_pids_of_process_with_args(&app_exe_name, &["--tray"]);
let tray_sessions = tray_pids
.iter()
.map(|pid| get_session_id_of_process(pid.as_u32()))
.flatten()
.collect::<Vec<_>>();
kill_process_by_pids(&app_exe_name, tray_pids)?;
let is_service_running = is_self_service_running();
let mut version_major = "0";
let mut version_minor = "0";
let mut version_build = "0";
let versions: Vec<&str> = crate::VERSION.split(".").collect();
if versions.len() > 0 {
version_major = versions[0];
}
if versions.len() > 1 {
version_minor = versions[1];
}
if versions.len() > 2 {
version_build = versions[2];
}
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
let size = meta.len() / 1024;
let reg_cmd = format!(
"
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\"
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
",
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
);
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
let restore_service_cmd = if is_service_running {
format!("sc start {}", &app_name)
} else {
"".to_owned()
};
// We do not try to remove all files in the old version.
// Because I don't know whether additional files will be installed here after installation, such as drivers.
// Just copy files to the installation directory works fine.
//if exist \"{path}\" rd /s /q \"{path}\"
// md \"{path}\"
//
// We need `taskkill` because:
// 1. There may be some other processes like `rustdesk --connect` are running.
// 2. Sometimes, the main window and the tray icon are showing
// while I cannot find them by `tasklist` or the methods above.
// There's should be 4 processes running: service, server, tray and main window.
// But only 2 processes are shown in the tasklist.
let cmds = format!(
"
chcp 65001
sc stop {app_name}
taskkill /F /IM {app_name}.exe{filter}
{reg_cmd}
{copy_exe}
{restore_service_cmd}
{sleep}
",
app_name = app_name,
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
sleep = if debug { "timeout 300" } else { "" },
);
run_cmds(cmds, debug, "update")?;
std::thread::sleep(std::time::Duration::from_millis(2000));
if tray_sessions.is_empty() {
log::info!("No tray process found.");
} else {
log::info!("Try to restore the tray process...");
log::info!(
"Try to restore the tray process..., sessions: {:?}",
&tray_sessions
);
for s in tray_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
}
}
}
if main_window_sessions.is_empty() {
log::info!("No main window process found.");
} else {
log::info!("Try to restore the main window process...");
std::thread::sleep(std::time::Duration::from_millis(2000));
for s in main_window_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec![], s, true));
}
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
log::info!("Update completed.");
Ok(())
}
// Double confirm the process name
fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
let name = name.to_lowercase();
let s = System::new_all();
// No need to check all names of `pids` first, and kill them then.
// It's rare case that they're not matched.
for pid in pids {
if let Some(process) = s.process(pid) {
if process.name().to_lowercase() != name {
bail!("Failed to kill the process, the names are mismatched.");
}
if !process.kill() {
bail!("Failed to kill the process");
}
} else {
bail!("Failed to kill the process, the pid is not found");
}
}
Ok(())
}
// Don't launch tray app when running with `\qn`.
// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission.
// Or launching the main window from the tray app will cause the main window to be launched with administrator permission.
// 2. We are not able to launch the tray app if the UI is in the login screen.
// `fn update_me()` can handle the above cases, but for msi update, we need to do more work to handle the above cases.
// 1. Record the tray app session ids.
// 2. Do the update.
// 3. Restore the tray app sessions.
// `1` and `3` must be done in custom actions.
// We need also to handle the command line parsing to find the tray processes.
pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
let cmds = format!(
"chcp 65001 && msiexec /i {msi} {}",
if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" }
);
run_cmds(cmds, false, "update-msi")?;
Ok(())
}
pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
Ok(write_cmds(
format!(
@@ -2450,23 +2653,6 @@ pub fn try_kill_broker() {
.spawn());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());
}
#[test]
fn test_get_unicode_char_by_vk() {
let chr = get_char_from_vk(0x41); // VK_A
assert_eq!(chr, Some('a'));
let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC
assert_eq!(chr, None)
}
}
pub fn message_box(text: &str) {
let mut text = text.to_owned();
let nodialog = std::env::var("NO_DIALOG").unwrap_or_default() == "Y";
@@ -2974,3 +3160,264 @@ fn get_pids<S: AsRef<str>>(name: S) -> ResultType<Vec<u32>> {
Ok(pids)
}
pub fn is_msi_installed() -> std::io::Result<bool> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let uninstall_key = hklm.open_subkey(format!(
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}",
crate::get_app_name()
))?;
Ok(1 == uninstall_key.get_value::<u32, _>("WindowsInstaller")?)
}
#[cfg(not(target_pointer_width = "64"))]
fn get_pids_with_args_from_wmic_output<S2: AsRef<str>>(
output: std::borrow::Cow<'_, str>,
name: &str,
args: &[S2],
) -> Vec<hbb_common::sysinfo::Pid> {
// CommandLine=
// ProcessId=33796
//
// CommandLine=
// ProcessId=34668
//
// CommandLine="C:\Program Files\RustDesk\RustDesk.exe" --tray
// ProcessId=13728
//
// CommandLine="C:\Program Files\RustDesk\RustDesk.exe"
// ProcessId=10136
let mut pids = Vec::new();
let mut proc_found = false;
for line in output.lines() {
if line.starts_with("ProcessId=") {
if proc_found {
if let Ok(pid) = line["ProcessId=".len()..].trim().parse::<u32>() {
pids.push(hbb_common::sysinfo::Pid::from_u32(pid));
}
proc_found = false;
}
} else if line.starts_with("CommandLine=") {
proc_found = false;
let cmd = line["CommandLine=".len()..].trim().to_lowercase();
if args.is_empty() {
if cmd.ends_with(&name) || cmd.ends_with(&format!("{}\"", &name)) {
proc_found = true;
}
} else {
proc_found = args.iter().all(|arg| cmd.contains(arg.as_ref()));
}
}
}
pids
}
// Note the args are not compared strictly, only check if the args are contained in the command line.
// If we want to check the args strictly, we need to parse the command line and compare each arg.
// Maybe we have to introduce some external crate like `shell_words` to do this.
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn get_pids_with_args_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
args: &[S2],
) -> Vec<hbb_common::sysinfo::Pid> {
let name = name.as_ref().to_lowercase();
std::process::Command::new("wmic.exe")
.args([
"process",
"where",
&format!("name='{}'", name),
"get",
"commandline,processid",
"/value",
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|output| {
get_pids_with_args_from_wmic_output::<S2>(
String::from_utf8_lossy(&output.stdout),
&name,
args,
)
})
.unwrap_or_default()
}
#[cfg(not(target_pointer_width = "64"))]
fn get_pids_with_first_arg_from_wmic_output(
output: std::borrow::Cow<'_, str>,
name: &str,
arg: &str,
) -> Vec<hbb_common::sysinfo::Pid> {
let mut pids = Vec::new();
let mut proc_found = false;
for line in output.lines() {
if line.starts_with("ProcessId=") {
if proc_found {
if let Ok(pid) = line["ProcessId=".len()..].trim().parse::<u32>() {
pids.push(hbb_common::sysinfo::Pid::from_u32(pid));
}
proc_found = false;
}
} else if line.starts_with("CommandLine=") {
proc_found = false;
let cmd = line["CommandLine=".len()..].trim().to_lowercase();
if cmd.is_empty() {
continue;
}
if !arg.is_empty() && cmd.starts_with(arg) {
proc_found = true;
} else {
for x in [&format!("{}\"", name), &format!("{}", name)] {
if cmd.contains(x) {
let cmd = cmd.split(x).collect::<Vec<_>>()[1..].join("");
if arg.is_empty() {
if cmd.trim().is_empty() {
proc_found = true;
}
} else if cmd.trim().starts_with(arg) {
proc_found = true;
}
break;
}
}
}
}
}
pids
}
// Note the args are not compared strictly, only check if the args are contained in the command line.
// If we want to check the args strictly, we need to parse the command line and compare each arg.
// Maybe we have to introduce some external crate like `shell_words` to do this.
#[cfg(not(target_pointer_width = "64"))]
pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
arg: S2,
) -> Vec<hbb_common::sysinfo::Pid> {
let name = name.as_ref().to_lowercase();
let arg = arg.as_ref().to_lowercase();
std::process::Command::new("wmic.exe")
.args([
"process",
"where",
&format!("name='{}'", name),
"get",
"commandline,processid",
"/value",
])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|output| {
get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(&output.stdout),
&name,
&arg,
)
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uninstall_cert() {
println!("uninstall driver certs: {:?}", cert::uninstall_cert());
}
#[test]
fn test_get_unicode_char_by_vk() {
let chr = get_char_from_vk(0x41); // VK_A
assert_eq!(chr, Some('a'));
let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC
assert_eq!(chr, None)
}
#[cfg(not(target_pointer_width = "64"))]
#[test]
fn test_get_pids_with_args_from_wmic_output() {
let output = r#"
CommandLine=
ProcessId=33796
CommandLine=
ProcessId=34668
CommandLine="C:\Program Files\testapp\TestApp.exe" --tray
ProcessId=13728
CommandLine="C:\Program Files\testapp\TestApp.exe"
ProcessId=10136
"#;
let name = "testapp.exe";
let args = vec!["--tray"];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 13728);
let args: Vec<&str> = vec![];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 10136);
let args = vec!["--other"];
let pids = super::get_pids_with_args_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
&args,
);
assert_eq!(pids.len(), 0);
}
#[cfg(not(target_pointer_width = "64"))]
#[test]
fn test_get_pids_with_first_arg_from_wmic_output() {
let output = r#"
CommandLine=
ProcessId=33796
CommandLine=
ProcessId=34668
CommandLine="C:\Program Files\testapp\TestApp.exe" --tray
ProcessId=13728
CommandLine="C:\Program Files\testapp\TestApp.exe"
ProcessId=10136
"#;
let name = "testapp.exe";
let arg = "--tray";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 13728);
let arg = "";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 1);
assert_eq!(pids[0].as_u32(), 10136);
let arg = "--other";
let pids = super::get_pids_with_first_arg_from_wmic_output(
String::from_utf8_lossy(output.as_bytes()),
name,
arg,
);
assert_eq!(pids.len(), 0);
}
}