2021-03-29 15:59:14 +08:00
|
|
|
// 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::{
|
|
|
|
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::{allow_err, bail, log};
|
2022-01-14 03:17:36 +08:00
|
|
|
use include_dir::{include_dir, Dir};
|
2021-03-29 15:59:14 +08:00
|
|
|
use objc::{class, msg_send, sel, sel_impl};
|
|
|
|
use scrap::{libc::c_void, quartz::ffi::*};
|
|
|
|
|
2022-01-14 03:17:36 +08:00
|
|
|
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
|
|
|
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
2021-03-29 15:59:14 +08:00
|
|
|
static mut LATEST_SEED: i32 = 0;
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
pub fn is_can_screen_recording(prompt: bool) -> bool {
|
|
|
|
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, true).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
can_record_screen
|
|
|
|
}
|
|
|
|
|
2022-01-13 19:23:41 +08:00
|
|
|
pub fn is_installed_daemon(prompt: bool) -> bool {
|
|
|
|
if !prompt {
|
2022-01-14 03:17:36 +08:00
|
|
|
if !std::path::Path::new("/Library/LaunchDaemons/com.carriez.rustdesk.daemon.plist")
|
|
|
|
.exists()
|
|
|
|
{
|
2022-01-13 19:23:41 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-01-14 03:17:36 +08:00
|
|
|
if !std::path::Path::new("/Library/LaunchAgents/com.carriez.rustdesk.agent.root.plist")
|
|
|
|
.exists()
|
|
|
|
{
|
2022-01-13 19:23:41 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-01-14 03:17:36 +08:00
|
|
|
if !std::path::Path::new("/Library/LaunchAgents/com.carriez.rustdesk.agent.user.plist")
|
|
|
|
.exists()
|
|
|
|
{
|
2022-01-13 19:23:41 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-01-14 03:17:36 +08:00
|
|
|
let install_script = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt").unwrap();
|
|
|
|
let install_script_body = install_script.contents_utf8().unwrap();
|
|
|
|
|
|
|
|
let daemon_plist = PRIVILEGES_SCRIPTS_DIR
|
|
|
|
.get_file("com.carriez.rustdesk.daemon.plist")
|
|
|
|
.unwrap();
|
|
|
|
let daemon_plist_body = daemon_plist.contents_utf8().unwrap();
|
|
|
|
|
|
|
|
let root_agent_plist = PRIVILEGES_SCRIPTS_DIR
|
|
|
|
.get_file("com.carriez.rustdesk.agent.root.plist")
|
|
|
|
.unwrap();
|
|
|
|
let root_agent_plist_body = root_agent_plist.contents_utf8().unwrap();
|
|
|
|
|
|
|
|
let user_agent_plist = PRIVILEGES_SCRIPTS_DIR
|
|
|
|
.get_file("com.carriez.rustdesk.agent.user.plist")
|
|
|
|
.unwrap();
|
|
|
|
let user_agent_plist_body = user_agent_plist.contents_utf8().unwrap();
|
|
|
|
|
|
|
|
match std::process::Command::new("osascript")
|
|
|
|
.arg("-e")
|
|
|
|
.arg(install_script_body)
|
|
|
|
.arg(daemon_plist_body)
|
|
|
|
.arg(root_agent_plist_body)
|
|
|
|
.arg(user_agent_plist_body)
|
|
|
|
.spawn()
|
|
|
|
{
|
|
|
|
Ok(mut proc) => proc.wait().is_ok(),
|
|
|
|
Err(e) => {
|
|
|
|
log::error!("run osascript failed: {}", e);
|
|
|
|
false
|
2022-01-14 18:28:39 +08:00
|
|
|
}
|
2022-01-13 19:23:41 +08:00
|
|
|
}
|
2022-01-14 03:17:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn launch_or_stop_daemon(launch: bool) {
|
|
|
|
let mut script_filename = "launch_service.scpt";
|
|
|
|
if !launch {
|
|
|
|
script_filename = "stop_service.scpt";
|
|
|
|
}
|
|
|
|
|
|
|
|
let script_file = PRIVILEGES_SCRIPTS_DIR.get_file(script_filename).unwrap();
|
|
|
|
let script_body = script_file.contents_utf8().unwrap();
|
2022-01-13 19:23:41 +08:00
|
|
|
|
2022-01-14 03:17:36 +08:00
|
|
|
std::process::Command::new("osascript")
|
|
|
|
.arg("-e")
|
|
|
|
.arg(script_body)
|
|
|
|
.spawn()
|
|
|
|
.ok();
|
2022-01-13 19:23:41 +08:00
|
|
|
}
|
|
|
|
|
2021-03-29 15:59:14 +08:00
|
|
|
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_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 _))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c
|
|
|
|
pub fn 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 colrspace, 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 y:y];
|
|
|
|
// 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,
|
|
|
|
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 is_prelogin() -> bool {
|
|
|
|
get_active_userid() == "0"
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_root() -> bool {
|
|
|
|
crate::username() == "root"
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn run_as_user(arg: &str) -> ResultType<Option<std::process::Child>> {
|
|
|
|
let uid = get_active_userid();
|
|
|
|
let cmd = std::env::current_exe()?;
|
|
|
|
let task = std::process::Command::new("launchctl")
|
|
|
|
.args(vec!["asuser", &uid, cmd.to_str().unwrap_or(""), arg])
|
|
|
|
.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() {
|
|
|
|
let mut server: Option<std::process::Child> = None;
|
|
|
|
let mut uid = "".to_owned();
|
|
|
|
loop {
|
|
|
|
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() {
|
|
|
|
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 crate::run_me(vec!["--server"]) {
|
|
|
|
Ok(ps) => server = Some(ps),
|
|
|
|
Err(err) => {
|
|
|
|
log::error!("Failed to start server: {}", err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn toggle_privacy_mode(_v: bool) {
|
|
|
|
// https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn block_input(_v: bool) {
|
|
|
|
//
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_installed() -> bool {
|
|
|
|
true
|
|
|
|
}
|
2022-01-13 15:26:57 +08:00
|
|
|
|
2022-01-13 19:23:41 +08:00
|
|
|
pub fn start_daemon() {
|
|
|
|
log::info!("{}", crate::username());
|
2022-01-13 15:26:57 +08:00
|
|
|
if let Err(err) = crate::ipc::start("_daemon") {
|
|
|
|
log::error!("Failed to start ipc_daemon: {}", err);
|
|
|
|
std::process::exit(-1);
|
|
|
|
}
|
|
|
|
}
|