Files
rustdesk/src/platform/macos.rs
2025-08-17 10:04:40 +08:00

1083 lines
37 KiB
Rust

// https://developer.apple.com/documentation/appkit/nscursor
// https://github.com/servo/core-foundation-rs
// https://github.com/rust-windowing/winit
use super::{CursorData, ResultType};
use cocoa::{
appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*},
base::{id, nil, BOOL, NO, YES},
foundation::{NSDictionary, NSPoint, NSSize, NSString},
};
use core_foundation::{
array::{CFArrayGetCount, CFArrayGetValueAtIndex},
dictionary::CFDictionaryRef,
string::CFStringRef,
};
use core_graphics::{
display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo},
window::{kCGWindowName, kCGWindowOwnerPID},
};
use hbb_common::{
anyhow::anyhow,
bail, log,
message_proto::{DisplayInfo, Resolution},
sysinfo::{Pid, Process, ProcessRefreshKind, System},
};
use include_dir::{include_dir, Dir};
use objc::rc::autoreleasepool;
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
use scrap::{libc::c_void, quartz::ffi::*};
use std::{
collections::HashMap,
os::unix::process::CommandExt,
path::{Path, PathBuf},
process::{Command, Stdio},
};
static PRIVILEGES_SCRIPTS_DIR: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
static mut LATEST_SEED: i32 = 0;
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
extern "C" {
fn CGSCurrentCursorSeed() -> i32;
fn CGEventCreate(r: *const c_void) -> *const c_void;
fn CGEventGetLocation(e: *const c_void) -> CGPoint;
static kAXTrustedCheckOptionPrompt: CFStringRef;
fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
fn InputMonitoringAuthStatus(_: BOOL) -> BOOL;
fn IsCanScreenRecording(_: BOOL) -> BOOL;
fn CanUseNewApiForScreenCaptureCheck() -> BOOL;
fn MacCheckAdminAuthorization() -> BOOL;
fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL;
fn MacGetModes(
display: u32,
widths: *mut u32,
heights: *mut u32,
hidpis: *mut BOOL,
max: u32,
numModes: *mut u32,
) -> BOOL;
fn majorVersion() -> u32;
fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL;
fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> BOOL;
}
pub fn major_version() -> u32 {
unsafe { majorVersion() }
}
pub fn is_process_trusted(prompt: bool) -> bool {
autoreleasepool(|| unsafe_is_process_trusted(prompt))
}
fn unsafe_is_process_trusted(prompt: bool) -> bool {
unsafe {
let value = if prompt { YES } else { NO };
let value: id = msg_send![class!(NSNumber), numberWithBool: value];
let options = NSDictionary::dictionaryWithObject_forKey_(
nil,
value,
kAXTrustedCheckOptionPrompt as _,
);
AXIsProcessTrustedWithOptions(options as _) == YES
}
}
pub fn is_can_input_monitoring(prompt: bool) -> bool {
unsafe {
let value = if prompt { YES } else { NO };
InputMonitoringAuthStatus(value) == YES
}
}
pub fn is_can_screen_recording(prompt: bool) -> bool {
autoreleasepool(|| unsafe_is_can_screen_recording(prompt))
}
// macOS >= 10.15
// https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/
// remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk
fn unsafe_is_can_screen_recording(prompt: bool) -> bool {
// we got some report that we show no permission even after set it, so we try to use new api for screen recording check
// the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15),
// but also some said it has bug on 10.16, so we just use it on 11.0.
unsafe {
if CanUseNewApiForScreenCaptureCheck() == YES {
return IsCanScreenRecording(if prompt { YES } else { NO }) == YES;
}
}
let mut can_record_screen: bool = false;
unsafe {
let our_pid: i32 = std::process::id() as _;
let our_pid: id = msg_send![class!(NSNumber), numberWithInteger: our_pid];
let window_list =
CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
let n = CFArrayGetCount(window_list);
let dock = NSString::alloc(nil).init_str("Dock");
for i in 0..n {
let w: id = CFArrayGetValueAtIndex(window_list, i) as _;
let name: id = msg_send![w, valueForKey: kCGWindowName as id];
if name.is_null() {
continue;
}
let pid: id = msg_send![w, valueForKey: kCGWindowOwnerPID as id];
let is_me: BOOL = msg_send![pid, isEqual: our_pid];
if is_me == YES {
continue;
}
let pid: i32 = msg_send![pid, intValue];
let p: id = msg_send![
class!(NSRunningApplication),
runningApplicationWithProcessIdentifier: pid
];
if p.is_null() {
// ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
continue;
}
let url: id = msg_send![p, executableURL];
let exe_name: id = msg_send![url, lastPathComponent];
if exe_name.is_null() {
continue;
}
let is_dock: BOOL = msg_send![exe_name, isEqual: dock];
if is_dock == YES {
// ignore the Dock, which provides the desktop picture
continue;
}
can_record_screen = true;
break;
}
}
if !can_record_screen && prompt {
use scrap::{Capturer, Display};
if let Ok(d) = Display::primary() {
Capturer::new(d).ok();
}
}
can_record_screen
}
pub fn install_service() -> bool {
is_installed_daemon(false)
}
// Remember to check if `update_daemon_agent()` need to be changed if changing `is_installed_daemon()`.
// 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.
pub fn is_installed_daemon(prompt: bool) -> bool {
let daemon = format!("{}_service.plist", crate::get_full_name());
let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
if !prompt {
// in macos 13, there is new way to check if they are running or enabled, https://developer.apple.com/documentation/servicemanagement/updating-helper-executables-from-earlier-versions-of-macos#Respond-to-changes-in-System-Settings
if !std::path::Path::new(&format!("/Library/LaunchDaemons/{}", daemon)).exists() {
return false;
}
if !std::path::Path::new(&agent_plist_file).exists() {
return false;
}
return true;
}
let Some(install_script) = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt") else {
return false;
};
let Some(install_script_body) = install_script.contents_utf8().map(correct_app_name) else {
return false;
};
let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else {
return false;
};
let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else {
return false;
};
let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else {
return false;
};
let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else {
return false;
};
std::thread::spawn(move || {
match std::process::Command::new("osascript")
.arg("-e")
.arg(install_script_body)
.arg(daemon_plist_body)
.arg(agent_plist_body)
.arg(&get_active_username())
.status()
{
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {
let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", agent_plist_file, installed);
if installed {
log::info!("launch server");
std::process::Command::new("launchctl")
.args(&["load", "-w", &agent_plist_file])
.status()
.ok();
}
}
}
});
false
}
fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync: bool) {
let update_script_file = "update.scpt";
let Some(update_script) = PRIVILEGES_SCRIPTS_DIR.get_file(update_script_file) else {
return;
};
let Some(update_script_body) = update_script.contents_utf8().map(correct_app_name) else {
return;
};
let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else {
return;
};
let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else {
return;
};
let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else {
return;
};
let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else {
return;
};
let func = move || {
let mut binding = std::process::Command::new("osascript");
let mut cmd = binding
.arg("-e")
.arg(update_script_body)
.arg(daemon_plist_body)
.arg(agent_plist_body)
.arg(&get_active_username())
.arg(std::process::id().to_string())
.arg(update_source_dir);
match cmd.status() {
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {
let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", &agent_plist_file, installed);
if installed {
// Unload first, or load may not work if already loaded.
// We hope that the load operation can immediately trigger a start.
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
let status = std::process::Command::new("launchctl")
.args(&["load", "-w", &agent_plist_file])
.status();
log::info!("launch server, status: {:?}", &status);
}
}
}
};
if sync {
func();
} else {
std::thread::spawn(func);
}
}
fn correct_app_name(s: &str) -> String {
let s = s.replace("rustdesk", &crate::get_app_name().to_lowercase());
let mut s = s.replace("RustDesk", &crate::get_app_name());
if let Some(bundleid) = get_bundle_id() {
s = s.replace("com.carriez.rustdesk", &bundleid);
}
s
}
pub fn uninstall_service(show_new_window: bool, sync: bool) -> bool {
// to-do: do together with win/linux about refactory start/stop service
if !is_installed_daemon(false) {
return false;
}
let Some(script_file) = PRIVILEGES_SCRIPTS_DIR.get_file("uninstall.scpt") else {
return false;
};
let Some(script_body) = script_file.contents_utf8().map(correct_app_name) else {
return false;
};
let func = move || {
match std::process::Command::new("osascript")
.arg("-e")
.arg(script_body)
.status()
{
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {
let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
let uninstalled = !std::path::Path::new(&agent_plist_file).exists();
log::info!(
"Agent file {} uninstalled: {}",
agent_plist_file,
uninstalled
);
if uninstalled {
if !show_new_window {
let _ = crate::ipc::close_all_instances();
// leave ipc a little time
std::thread::sleep(std::time::Duration::from_millis(300));
}
crate::ipc::set_option("stop-service", "Y");
std::process::Command::new("launchctl")
.args(&["remove", &format!("{}_server", crate::get_full_name())])
.status()
.ok();
if show_new_window {
std::process::Command::new("open")
.arg("-n")
.arg(&format!("/Applications/{}.app", crate::get_app_name()))
.spawn()
.ok();
// leave open a little time
std::thread::sleep(std::time::Duration::from_millis(300));
}
quit_gui();
}
}
}
};
if sync {
func();
} else {
std::thread::spawn(func);
}
true
}
pub fn get_cursor_pos() -> Option<(i32, i32)> {
unsafe {
let e = CGEventCreate(0 as _);
let point = CGEventGetLocation(e);
CFRelease(e);
Some((point.x as _, point.y as _))
}
/*
let mut pt: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
let screen: id = unsafe { msg_send![class!(NSScreen), currentScreenForMouseLocation] };
let frame: NSRect = unsafe { msg_send![screen, frame] };
pt.x -= frame.origin.x;
pt.y -= frame.origin.y;
Some((pt.x as _, pt.y as _))
*/
}
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
autoreleasepool(|| unsafe_get_focused_display(displays))
}
fn unsafe_get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
unsafe {
let main_screen: id = msg_send![class!(NSScreen), mainScreen];
let screen: id = msg_send![main_screen, deviceDescription];
let id: id =
msg_send![screen, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")];
let display_name: u32 = msg_send![id, unsignedIntValue];
displays
.iter()
.position(|d| d.name == display_name.to_string())
}
}
pub fn get_cursor() -> ResultType<Option<u64>> {
autoreleasepool(|| unsafe_get_cursor())
}
fn unsafe_get_cursor() -> ResultType<Option<u64>> {
unsafe {
let seed = CGSCurrentCursorSeed();
if seed == LATEST_SEED {
return Ok(None);
}
LATEST_SEED = seed;
}
let c = get_cursor_id()?;
Ok(Some(c.1))
}
pub fn reset_input_cache() {
unsafe {
LATEST_SEED = 0;
}
}
fn get_cursor_id() -> ResultType<(id, u64)> {
unsafe {
let c: id = msg_send![class!(NSCursor), currentSystemCursor];
if c == nil {
bail!("Failed to call [NSCursor currentSystemCursor]");
}
let hotspot: NSPoint = msg_send![c, hotSpot];
let img: id = msg_send![c, image];
if img == nil {
bail!("Failed to call [NSCursor image]");
}
let size: NSSize = msg_send![img, size];
let tif: id = msg_send![img, TIFFRepresentation];
if tif == nil {
bail!("Failed to call [NSImage TIFFRepresentation]");
}
let rep: id = msg_send![class!(NSBitmapImageRep), imageRepWithData: tif];
if rep == nil {
bail!("Failed to call [NSBitmapImageRep imageRepWithData]");
}
let rep_size: NSSize = msg_send![rep, size];
let mut hcursor =
size.width + size.height + hotspot.x + hotspot.y + rep_size.width + rep_size.height;
let x = (rep_size.width * hotspot.x / size.width) as usize;
let y = (rep_size.height * hotspot.y / size.height) as usize;
for i in 0..2 {
let mut x2 = x + i;
if x2 >= rep_size.width as usize {
x2 = rep_size.width as usize - 1;
}
let mut y2 = y + i;
if y2 >= rep_size.height as usize {
y2 = rep_size.height as usize - 1;
}
let color: id = msg_send![rep, colorAtX:x2 y:y2];
if color != nil {
let r: f64 = msg_send![color, redComponent];
let g: f64 = msg_send![color, greenComponent];
let b: f64 = msg_send![color, blueComponent];
let a: f64 = msg_send![color, alphaComponent];
hcursor += (r + g + b + a) * (255 << i) as f64;
}
}
Ok((c, hcursor as _))
}
}
pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
autoreleasepool(|| unsafe_get_cursor_data(hcursor))
}
// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c
fn unsafe_get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
unsafe {
let (c, hcursor2) = get_cursor_id()?;
if hcursor != hcursor2 {
bail!("cursor changed");
}
let hotspot: NSPoint = msg_send![c, hotSpot];
let img: id = msg_send![c, image];
let size: NSSize = msg_send![img, size];
let reps: id = msg_send![img, representations];
if reps == nil {
bail!("Failed to call [NSImage representations]");
}
let nreps: usize = msg_send![reps, count];
if nreps == 0 {
bail!("Get empty [NSImage representations]");
}
let rep: id = msg_send![reps, objectAtIndex: 0];
/*
let n: id = msg_send![class!(NSNumber), numberWithFloat:1.0];
let props: id = msg_send![class!(NSDictionary), dictionaryWithObject:n forKey:NSString::alloc(nil).init_str("NSImageCompressionFactor")];
let image_data: id = msg_send![rep, representationUsingType:2 properties:props];
let () = msg_send![image_data, writeToFile:NSString::alloc(nil).init_str("cursor.jpg") atomically:0];
*/
let mut colors: Vec<u8> = Vec::new();
colors.reserve((size.height * size.width) as usize * 4);
// TIFF is rgb colorspace, no need to convert
// let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace];
for y in 0..(size.height as _) {
for x in 0..(size.width as _) {
let color: id = msg_send![rep, colorAtX:x as cocoa::foundation::NSInteger y:y as cocoa::foundation::NSInteger];
// let color: id = msg_send![color, colorUsingColorSpace: cs];
if color == nil {
continue;
}
let r: f64 = msg_send![color, redComponent];
let g: f64 = msg_send![color, greenComponent];
let b: f64 = msg_send![color, blueComponent];
let a: f64 = msg_send![color, alphaComponent];
colors.push((r * 255.) as _);
colors.push((g * 255.) as _);
colors.push((b * 255.) as _);
colors.push((a * 255.) as _);
}
}
Ok(CursorData {
id: hcursor,
colors: colors.into(),
hotx: hotspot.x as _,
hoty: hotspot.y as _,
width: size.width as _,
height: size.height as _,
..Default::default()
})
}
}
fn get_active_user(t: &str) -> String {
if let Ok(output) = std::process::Command::new("ls")
.args(vec![t, "/dev/console"])
.output()
{
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some(n) = line.split_whitespace().nth(2) {
return n.to_owned();
}
}
}
"".to_owned()
}
pub fn get_active_username() -> String {
get_active_user("-l")
}
pub fn get_active_userid() -> String {
get_active_user("-n")
}
pub fn get_active_user_home() -> Option<PathBuf> {
let username = get_active_username();
if !username.is_empty() {
let home = PathBuf::from(format!("/Users/{}", username));
if home.exists() {
return Some(home);
}
}
None
}
pub fn is_prelogin() -> bool {
get_active_userid() == "0"
}
// https://stackoverflow.com/questions/11505255/osx-check-if-the-screen-is-locked
// No "CGSSessionScreenIsLocked" can be found when macOS is not locked.
//
// `ioreg -n Root -d1` returns `"CGSSessionScreenIsLocked"=Yes`
// `ioreg -n Root -d1 -a` returns
// ```
// ...
// <key>CGSSessionScreenIsLocked</key>
// <true/>
// ...
// ```
pub fn is_locked() -> bool {
match std::process::Command::new("ioreg")
.arg("-n")
.arg("Root")
.arg("-d1")
.output()
{
Ok(output) => {
let output_str = String::from_utf8_lossy(&output.stdout);
// Although `"CGSSessionScreenIsLocked"=Yes` was printed on my macOS,
// I also check `"CGSSessionScreenIsLocked"=true` for better compability.
output_str.contains("\"CGSSessionScreenIsLocked\"=Yes")
|| output_str.contains("\"CGSSessionScreenIsLocked\"=true")
}
Err(e) => {
log::error!("Failed to query ioreg for the lock state: {}", e);
false
}
}
}
pub fn is_root() -> bool {
crate::username() == "root"
}
pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
let uid = get_active_userid();
let cmd = std::env::current_exe()?;
let mut args = vec!["asuser", &uid, cmd.to_str().unwrap_or("")];
args.append(&mut arg.clone());
let task = std::process::Command::new("launchctl").args(args).spawn()?;
Ok(Some(task))
}
pub fn lock_screen() {
std::process::Command::new(
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
)
.arg("-suspend")
.output()
.ok();
}
pub fn start_os_service() {
log::info!("Username: {}", crate::username());
if let Err(err) = crate::ipc::start("_service") {
log::error!("Failed to start ipc_service: {}", err);
}
/* // mouse/keyboard works in prelogin now with launchctl asuser.
// below can avoid multi-users logged in problem, but having its own below problem.
// Not find a good way to start --cm without root privilege (affect file transfer).
// one way is to start with `launchctl asuser <uid> open -n -a /Applications/RustDesk.app/ --args --cm`,
// this way --cm is started with the user privilege, but we will have problem to start another RustDesk.app
// with open in explorer.
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let mut uid = "".to_owned();
let mut server: Option<std::process::Child> = None;
if let Err(err) = ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
}) {
println!("Failed to set Ctrl-C handler: {}", err);
}
while running.load(Ordering::SeqCst) {
let tmp = get_active_userid();
let mut start_new = false;
if tmp != uid && !tmp.is_empty() {
uid = tmp;
log::info!("active uid: {}", uid);
if let Some(ps) = server.as_mut() {
hbb_common::allow_err!(ps.kill());
}
}
if let Some(ps) = server.as_mut() {
match ps.try_wait() {
Ok(Some(_)) => {
server = None;
start_new = true;
}
_ => {}
}
} else {
start_new = true;
}
if start_new {
match run_as_user("--server") {
Ok(Some(ps)) => server = Some(ps),
Err(err) => {
log::error!("Failed to start server: {}", err);
}
_ => { /*no happen*/ }
}
}
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
}
if let Some(ps) = server.take().as_mut() {
hbb_common::allow_err!(ps.kill());
}
log::info!("Exit");
*/
}
pub fn toggle_blank_screen(_v: bool) {
// https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily
}
pub fn block_input(_v: bool) -> (bool, String) {
(true, "".to_owned())
}
pub fn is_installed() -> bool {
if let Ok(p) = std::env::current_exe() {
return p
.to_str()
.unwrap_or_default()
.starts_with(&format!("/Applications/{}.app", crate::get_app_name()));
}
false
}
pub fn quit_gui() {
unsafe {
let () = msg_send!(NSApp(), terminate: nil);
};
}
pub fn update_me() -> ResultType<()> {
let is_installed_daemon = is_installed_daemon(false);
let option_stop_service = "stop-service";
let is_service_stopped = hbb_common::config::option2bool(
option_stop_service,
&crate::ui_interface::get_option(option_stop_service),
);
let cmd = std::env::current_exe()?;
// RustDesk.app/Contents/MacOS/RustDesk
let app_dir = cmd
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.map(|d| d.to_string_lossy().to_string());
let Some(app_dir) = app_dir else {
bail!("Unknown app directory of current exe file: {:?}", cmd);
};
if is_installed_daemon && !is_service_stopped {
let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
update_daemon_agent(agent_plist_file, app_dir, true);
} else {
// `kill -9` may not work without "administrator privileges"
let update_body = format!(
r#"
do shell script "
pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app
" with prompt "RustDesk wants to update itself" with administrator privileges
"#,
std::process::id(),
app_dir,
get_active_username()
);
match Command::new("osascript")
.arg("-e")
.arg(update_body)
.status()
{
Ok(status) if !status.success() => {
log::error!("osascript execution failed with status: {}", status);
}
Err(e) => {
log::error!("run osascript failed: {}", e);
}
_ => {}
}
}
std::process::Command::new("open")
.arg("-n")
.arg(&format!("/Applications/{}.app", crate::get_app_name()))
.spawn()
.ok();
// leave open a little time
std::thread::sleep(std::time::Duration::from_millis(300));
Ok(())
}
pub fn update_to(file: &str) -> ResultType<()> {
update_extracted(UPDATE_TEMP_DIR)?;
Ok(())
}
pub fn extract_update_dmg(file: &str) {
let mut evt: HashMap<&str, String> =
HashMap::from([("name", "extract-update-dmg".to_string())]);
match extract_dmg(file, UPDATE_TEMP_DIR) {
Ok(_) => {
log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR);
}
Err(e) => {
evt.insert("err", e.to_string());
log::error!("Failed to extract dmg file {}: {}", file, e);
}
}
let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned());
#[cfg(feature = "flutter")]
crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt);
}
fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> {
let mount_point = "/Volumes/RustDeskUpdate";
let target_path = Path::new(target_dir);
if target_path.exists() {
std::fs::remove_dir_all(target_path)?;
}
std::fs::create_dir_all(target_path)?;
Command::new("hdiutil")
.args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path])
.status()?;
struct DmgGuard(&'static str);
impl Drop for DmgGuard {
fn drop(&mut self) {
let _ = Command::new("hdiutil")
.args(&["detach", self.0, "-force"])
.status();
}
}
let _guard = DmgGuard(mount_point);
let app_name = "RustDesk.app";
let src_path = format!("{}/{}", mount_point, app_name);
let dest_path = format!("{}/{}", target_dir, app_name);
let copy_status = Command::new("ditto")
.args(&[&src_path, &dest_path])
.status()?;
if !copy_status.success() {
bail!("Failed to copy application {:?}", copy_status);
}
if !Path::new(&dest_path).exists() {
bail!(
"Copy operation failed - destination not found at {}",
dest_path
);
}
Ok(())
}
fn update_extracted(target_dir: &str) -> ResultType<()> {
let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir);
let _child = unsafe {
Command::new(&exe_path)
.arg("--update")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.pre_exec(|| {
hbb_common::libc::setsid();
Ok(())
})
.spawn()?
};
Ok(())
}
pub fn get_double_click_time() -> u32 {
// to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823
500 as _
}
pub fn hide_dock() {
unsafe {
NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory);
}
}
#[inline]
fn get_server_start_time_of(p: &Process, path: &Path) -> Option<i64> {
let cmd = p.cmd();
if cmd.len() <= 1 {
return None;
}
if &cmd[1] != "--server" {
return None;
}
let Ok(cur) = std::fs::canonicalize(p.exe()) else {
return None;
};
if &cur != path {
return None;
}
Some(p.start_time() as _)
}
#[inline]
fn get_server_start_time(sys: &mut System, path: &Path) -> Option<(i64, Pid)> {
sys.refresh_processes_specifics(ProcessRefreshKind::new());
for (_, p) in sys.processes() {
if let Some(t) = get_server_start_time_of(p, path) {
return Some((t, p.pid() as _));
}
}
None
}
pub fn handle_application_should_open_untitled_file() {
hbb_common::log::debug!("icon clicked on finder");
let x = std::env::args().nth(1).unwrap_or_default();
if x == "--server" || x == "--cm" || x == "--tray" {
std::thread::spawn(move || crate::handle_url_scheme("".to_lowercase()));
}
}
/// Get all resolutions of the display. The resolutions are:
/// 1. Sorted by width and height in descending order, with duplicates removed.
/// 2. Filtered out if the width is less than 800 (800x600) if there are too many (e.g., >15).
/// 3. Contain HiDPI resolutions and the real resolutions.
///
/// We don't need to distinguish between HiDPI and real resolutions.
/// When the controlling side changes the resolution, it will call `change_resolution_directly()`.
/// `change_resolution_directly()` will try to use the HiDPI resolution first.
/// This is how teamviewer does it for now.
///
/// If we need to distinguish HiDPI and real resolutions, we can add a flag to the `Resolution` struct.
pub fn resolutions(name: &str) -> Vec<Resolution> {
let mut v = vec![];
if let Ok(display) = name.parse::<u32>() {
let mut num = 0;
unsafe {
if YES == MacGetModeNum(display, &mut num) {
let (mut widths, mut heights, mut _hidpis) =
(vec![0; num as _], vec![0; num as _], vec![NO; num as _]);
let mut real_num = 0;
if YES
== MacGetModes(
display,
widths.as_mut_ptr(),
heights.as_mut_ptr(),
_hidpis.as_mut_ptr(),
num,
&mut real_num,
)
{
if real_num <= num {
v = (0..real_num)
.map(|i| Resolution {
width: widths[i as usize] as _,
height: heights[i as usize] as _,
..Default::default()
})
.collect::<Vec<_>>();
// Sort by (w, h), desc
v.sort_by(|a, b| {
if a.width == b.width {
b.height.cmp(&a.height)
} else {
b.width.cmp(&a.width)
}
});
// Remove duplicates
v.dedup_by(|a, b| a.width == b.width && a.height == b.height);
// Filter out the ones that are less than width 800 (800x600) if there are too many.
// We can also do this filtering on the client side, but it is better not to change the client side to reduce the impact.
if v.len() > 15 {
// Most width > 800, so it's ok to remove the small ones.
v.retain(|r| r.width >= 800);
}
if v.len() > 15 {
// Ignore if the length is still too long.
}
}
}
}
}
}
v
}
pub fn current_resolution(name: &str) -> ResultType<Resolution> {
let display = name.parse::<u32>().map_err(|e| anyhow!(e))?;
unsafe {
let (mut width, mut height) = (0, 0);
if NO == MacGetMode(display, &mut width, &mut height) {
bail!("MacGetMode failed");
}
Ok(Resolution {
width: width as _,
height: height as _,
..Default::default()
})
}
}
pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> {
let display = name.parse::<u32>().map_err(|e| anyhow!(e))?;
unsafe {
if NO == MacSetMode(display, width as _, height as _, true) {
bail!("MacSetMode failed");
}
}
Ok(())
}
pub fn check_super_user_permission() -> ResultType<bool> {
unsafe { Ok(MacCheckAdminAuthorization() == YES) }
}
pub fn elevate(args: Vec<&str>, prompt: &str) -> ResultType<bool> {
let cmd = std::env::current_exe()?;
match cmd.to_str() {
Some(cmd) => {
let mut cmd_with_args = cmd.to_string();
for arg in args {
cmd_with_args = format!("{} {}", cmd_with_args, arg);
}
let script = format!(
r#"do shell script "{}" with prompt "{}" with administrator privileges"#,
cmd_with_args, prompt
);
match std::process::Command::new("osascript")
.arg("-e")
.arg(script)
.arg(&get_active_username())
.status()
{
Err(e) => {
bail!("Failed to run osascript: {}", e);
}
Ok(status) => Ok(status.success() && status.code() == Some(0)),
}
}
None => {
bail!("Failed to get current exe str");
}
}
}
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(),
)
}
pub fn set_display(&mut self, display: bool) -> ResultType<()> {
self.0
.as_mut()
.map(|h| h.set_display(display))
.ok_or(anyhow!("no AwakeHandle"))?
}
}
fn get_bundle_id() -> Option<String> {
unsafe {
let bundle: id = msg_send![class!(NSBundle), mainBundle];
if bundle.is_null() {
return None;
}
let bundle_id: id = msg_send![bundle, bundleIdentifier];
if bundle_id.is_null() {
return None;
}
let c_str: *const std::os::raw::c_char = msg_send![bundle_id, UTF8String];
if c_str.is_null() {
return None;
}
let bundle_id_str = std::ffi::CStr::from_ptr(c_str)
.to_string_lossy()
.to_string();
Some(bundle_id_str)
}
}