diff --git a/Cargo.lock b/Cargo.lock index 5c4af56e9..e15641363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,7 @@ checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" dependencies = [ "dconf_rs", "detect-desktop-environment", - "dirs", + "dirs 4.0.0", "objc", "rust-ini", "web-sys", @@ -1401,6 +1401,16 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1873,6 +1883,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -3657,6 +3680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3670,6 +3694,15 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -4655,6 +4688,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "fruitbasket", "glib 0.16.5", "gtk", "hbb_common", @@ -4673,6 +4707,7 @@ dependencies = [ "mouce", "num_cpus", "objc", + "objc_id", "parity-tokio-ipc", "rdev", "repng", @@ -4713,7 +4748,7 @@ name = "rustdesk-portable-packer" version = "0.1.0" dependencies = [ "brotli", - "dirs", + "dirs 4.0.0", "embed-resource", "md5", ] @@ -6591,7 +6626,7 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "dirs", + "dirs 4.0.0", "enumflags2", "event-listener", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 1e9af30e5..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,8 @@ core-graphics = "0.22" include_dir = "0.7.2" tray-item = "0.7" # looks better than trayicon dark-light = "0.2" +fruitbasket = "0.10.0" +objc_id = "0.1.1" [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7e22e0848..9f3e2c740 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1315,6 +1315,12 @@ StreamSubscription? listenUniLinks() { /// /// * Returns true if we successfully handle the startup arguments. bool checkArguments() { + if (kBootArgs.isNotEmpty) { + final ret = parseRustdeskUri(kBootArgs.first); + if (ret) { + return true; + } + } // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args var connectIndex = kBootArgs.indexOf("--connect"); @@ -1352,7 +1358,7 @@ bool checkArguments() { bool parseRustdeskUri(String uriPath) { final uri = Uri.tryParse(uriPath); if (uri == null) { - print("uri is not valid: $uriPath"); + debugPrint("uri is not valid: $uriPath"); return false; } return callUniLinksUriHandler(uri); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index f48b612a8..1fc97f410 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,8 +11,9 @@ const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" const String kAppTypeMain = "main"; +const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 67a243eff..86cc9d89b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -211,7 +211,7 @@ void runMultiWindow( } void runConnectionManagerScreen(bool hide) async { - await initEnv(kAppTypeMain); + await initEnv(kAppTypeConnectionManager); _runApp( '', const DesktopServerPage(), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d032719e9..aae4c6a07 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,6 +199,9 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == "on_url_scheme_received") { + final url = evt['url'].toString(); + parseRustdeskUri(url); } }; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index cf2de4219..d6885bfb0 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -46,6 +47,8 @@ class PlatformFFI { static get localeName => Platform.localeName; + static get isMain => instance._appType == kAppTypeMain; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -112,8 +115,11 @@ class PlatformFFI { } _ffiBind = RustdeskImpl(dylib); if (Platform.isLinux) { - // start dbus service, no need to await - await _ffiBind.mainStartDbusServer(); + // Start a dbus service, no need to await + _ffiBind.mainStartDbusServer(); + } else if (Platform.isMacOS) { + // Start an ipc server for handling url schemes. + _ffiBind.mainStartIpcUrlServer(); } _startListenEvent(_ffiBind); // global event try { diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 7a17c3de1..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -463,6 +463,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -607,6 +608,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -643,6 +645,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; }; diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 042840569..97b46bb84 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -87,4 +87,3 @@ class MainFlutterWindow: NSWindow { }) } } - diff --git a/src/core_main.rs b/src/core_main.rs index 89a962f1d..99d0e888e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,6 @@ -use hbb_common::log; +use std::future::Future; + +use hbb_common::{log, ResultType}; /// shared by flutter and sciter main function /// @@ -346,5 +348,11 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option SyncReturn { SyncReturn(true) } +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::server::start_ipc_url_server()); +} + +/// Send a url scheme throught the ipc. +/// +/// * macOS only +pub fn send_url_scheme(url: String) { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); +} + #[cfg(target_os = "android")] pub mod server_side { + use hbb_common::log; use jni::{ + JNIEnv, objects::{JClass, JString}, sys::jstring, - JNIEnv, }; - use hbb_common::log; - use crate::start_server; #[no_mangle] diff --git a/src/ipc.rs b/src/ipc.rs index d4d803aec..d610fb84d 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -16,10 +16,10 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, tokio, + log, password_security as password, ResultType, timeout, + tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, - ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -210,6 +210,7 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, + UrlLink(String) } #[tokio::main(flavor = "current_thread")] @@ -832,3 +833,9 @@ pub async fn test_rendezvous_server() -> ResultType<()> { c.send(&Data::TestRendezvousServer).await?; Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index 381e3df90..109fc1e9a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,13 @@ -use crate::ipc::Data; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + use bytes::Bytes; + pub use connection::*; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -12,19 +17,18 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, + ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, tokio, ResultType, Stream, + sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use service::ServiceTmpl; +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use service::{GenericService, Service, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use service::ServiceTmpl; + +use crate::ipc::{connect, Data}; pub mod audio_service; cfg_if::cfg_if! { @@ -55,8 +59,6 @@ mod service; mod video_qos; pub mod video_service; -use hbb_common::tcp::new_listener; - pub type Childs = Arc>>; type ConnMap = HashMap; @@ -425,6 +427,50 @@ pub async fn start_server(is_server: bool) { } } +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + while let Some(Ok(conn)) = incoming.next().await { + let mut conn = crate::ipc::Connection::new(conn); + match conn.next_timeout(1000).await { + Ok(Some(data)) => { + match data { + Data::UrlLink(url) => { + #[cfg(feature = "flutter")] + { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM.read().unwrap().get( + crate::flutter::APP_TYPE_MAIN + ) { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); + } + } + } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + } + } + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + #[cfg(target_os = "macos")] async fn sync_and_watch_config_dir() { if crate::platform::is_root() { diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 7daef8eab..98e355dc1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -1,3 +1,5 @@ +use std::{ffi::c_void, rc::Rc}; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, @@ -8,11 +10,16 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{Object, Sel, BOOL}, + runtime::{BOOL, Object, Sel}, sel, sel_impl, }; -use sciter::{make_args, Host}; -use std::{ffi::c_void, rc::Rc}; +use objc::runtime::Class; +use objc_id::WeakId; +use sciter::{Host, make_args}; + +use hbb_common::{log, tokio}; + +use crate::ui_cm_interface::start_ipc; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -98,12 +105,21 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method(sel!(handleEvent:withReplyEvent:), handle_apple_event as extern fn(&Object, Sel, u64, u64)); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; let state = DelegateState { handler }; let handler_ptr = Box::into_raw(Box::new(state)); (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let cls = Class::get("NSAppleEventManager").unwrap(); + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; let () = msg_send![NSApp(), setDelegate: delegate]; } @@ -167,6 +183,24 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} + +extern fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { + let event = event as *mut Object; + let url = fruitbasket::parse_url_event(event); + log::debug!("an event was received: {}", url); + std::thread::spawn(move || handle_url_scheme(url)); +} + unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { let title = NSString::alloc(nil).init_str(title); let action = sel!(handleMenuItem:);