diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index b4eb9bf4b..1f323b7a9 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -600,11 +600,17 @@ message SwitchSidesResponse { message SwitchBack {} -message Plugin { +message PluginRequest { string id = 1; bytes content = 2; } +message PluginResponse { + string id = 1; + string name = 2; + string msg = 3; +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -626,7 +632,8 @@ message Misc { SwitchSidesRequest switch_sides_request = 21; SwitchBack switch_back = 22; Resolution change_resolution = 24; - Plugin plugin = 25; + PluginRequest plugin_request = 25; + PluginResponse plugin_response = 26; } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 59c35006c..bbcd74477 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -311,7 +311,8 @@ impl Remote { { // Create a channel to receive error or closed message let (tx, rx) = std::sync::mpsc::channel(); - let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); + let (tx_audio_data, mut rx_audio_data) = + hbb_common::tokio::sync::mpsc::unbounded_channel(); // Create a stand-alone inner, add subscribe to audio service let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); @@ -1297,6 +1298,22 @@ impl Remote { #[cfg(feature = "flutter")] self.handler.switch_back(&self.handler.id); } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + allow_err!(crate::plugin::handle_server_event(&p.id, &p.content)); + // to-do: show message box on UI when error occurs? + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginResponse(p)) => { + let name = if p.name.is_empty() { + "plugin".to_string() + } else { + p.name + }; + self.handler.msgbox("custom-nocancel", &name, &p.msg, ""); + } _ => {} }, Some(message::Union::TestDelay(t)) => { diff --git a/src/flutter.rs b/src/flutter.rs index 6b8a9f128..02c6b16b1 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1001,6 +1001,7 @@ pub fn session_next_rgba(id: *const char) { } } +#[inline] #[no_mangle] #[cfg(feature = "flutter_texture_render")] pub fn session_register_texture(id: *const char, ptr: usize) { @@ -1012,14 +1013,17 @@ pub fn session_register_texture(id: *const char, ptr: usize) { } } +#[inline] #[no_mangle] #[cfg(not(feature = "flutter_texture_render"))] pub fn session_register_texture(_id: *const char, _ptr: usize) {} +#[inline] pub fn push_session_event(peer: &str, name: &str, event: Vec<(&str, &str)>) -> Option { SESSIONS.read().unwrap().get(peer)?.push_event(name, event) } +#[inline] pub fn push_global_event(channel: &str, event: String) -> Option { Some(GLOBAL_EVENT_STREAM.read().unwrap().get(channel)?.add(event)) } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 29df39b8a..d18971332 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -48,10 +48,12 @@ fn initialize(app_dir: &str) { } } +#[inline] pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { super::flutter::start_global_event_stream(s, app_type) } +#[inline] pub fn stop_global_event_stream(app_type: String) { super::flutter::stop_global_event_stream(app_type) } @@ -1391,6 +1393,15 @@ pub fn send_url_scheme(_url: String) { std::thread::spawn(move || crate::handle_url_scheme(_url)); } +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn plugin_event(id: String, event: Vec) { + #[cfg(feature = "plugin_framework")] + { + crate::plugin::handle_ui_event(&id, &event); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/plugin/callback_msg.rs b/src/plugin/callback_msg.rs index c3a5a24d6..42d3cf283 100644 --- a/src/plugin/callback_msg.rs +++ b/src/plugin/callback_msg.rs @@ -1,6 +1,6 @@ use super::cstr_to_string; use crate::flutter::{self, APP_TYPE_CM, APP_TYPE_MAIN, SESSIONS}; -use hbb_common::{lazy_static, log, message_proto::Plugin}; +use hbb_common::{lazy_static, log, message_proto::PluginRequest}; use serde_json; use std::{collections::HashMap, ffi::c_char, sync::Arc}; @@ -66,7 +66,7 @@ pub fn callback_msg( let content_slice = unsafe { std::slice::from_raw_parts(content as *const u8, len) }; let content_vec = Vec::from(content_slice); - let plugin = Plugin { + let plugin = PluginRequest { id, content: bytes::Bytes::from(content_vec), ..Default::default() diff --git a/src/plugin/errno.rs b/src/plugin/errno.rs new file mode 100644 index 000000000..e7419608c --- /dev/null +++ b/src/plugin/errno.rs @@ -0,0 +1,26 @@ +pub const ERR_SUCCESS: i32 = 0; + +// ====================================================== +// errors that will be handled by RustDesk + +pub const ERR_RUSTDESK_HANDLE_BASE: i32 = 10000; + +// not loaded +pub const ERR_PLUGIN_LOAD: i32 = 10001; +// not initialized +pub const ERR_PLUGIN_MSG_CB: i32 = 10101; +// invalid +pub const ERR_CALL_INVALID_METHOD: i32 = 10201; +pub const ERR_CALL_NOT_SUPPORTED_METHOD: i32 = 10202; +// failed on calling +pub const ERR_CALL_INVALID_ARGS: i32 = 10301; +pub const ERR_PEER_ID_MISMATCH: i32 = 10302; + +// ====================================================== +// errors that must be handled by plugin + +pub const ERR_PLUGIN_HANDLE_BASE: i32 = 20000; + +pub const EER_CALL_FAILED: i32 = 200021; +pub const ERR_PEER_ON_FAILED: i32 = 30012; +pub const ERR_PEER_OFF_FAILED: i32 = 30012; diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 9e3e5993d..3d1998b63 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -5,8 +5,11 @@ mod callback_msg; mod config; pub mod desc; mod plugins; +mod errno; -pub use plugins::load_plugins; +pub use plugins::{ + handle_client_event, handle_ui_event, load_plugin, load_plugins, reload_plugin, unload_plugin, +}; #[inline] fn cstr_to_string(cstr: *const c_char) -> ResultType { @@ -14,3 +17,15 @@ fn cstr_to_string(cstr: *const c_char) -> ResultType { CStr::from_ptr(cstr).to_bytes().to_vec() })?) } + +#[inline] +fn get_code_msg_from_ret(ret: *const c_void) -> (i32, String) { + assert!(ret.is_null() == false); + let code_bytes = unsafe { std::slice::from_raw_parts(ret as *const u8, 4) }; + let code = i32::from_le_bytes([code_bytes[0], code_bytes[1], code_bytes[2], code_bytes[3]]); + let msg = unsafe { CStr::from_ptr((ret as *const u8).add(4) as _) } + .to_str() + .unwrap_or("") + .to_string(); + Ok((code, msg)) +} diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index a26f7a77c..39bda5cb7 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -5,23 +5,41 @@ use std::{ sync::{Arc, RwLock}, }; -use super::{callback_msg, desc::Desc}; +use super::{callback_msg, desc::Desc, errno::*, get_code_msg_from_ret}; use crate::flutter; -use hbb_common::{bail, dlopen::symbor::Library, lazy_static, libc, log, ResultType}; +use hbb_common::{ + bail, + dlopen::symbor::Library, + lazy_static, libc, log, + message_proto::{Message, Misc, PluginResponse}, + ResultType, +}; + +const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; +const METHOD_HANDLE_PEER: &[u8; 12] = b"handle_peer\0"; lazy_static::lazy_static! { pub static ref PLUGINS: Arc>> = Default::default(); } /// Initialize the plugins. -/// Return 0 if success. -pub type PluginFuncInit = fn() -> i32; +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncInit = fn() -> *const c_void; /// Reset the plugin. -/// Return 0 if success. -pub type PluginFuncReset = fn() -> i32; +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncReset = fn() -> *const c_void; /// Clear the plugin. -/// Return 0 if success. -pub type PluginFuncClear = fn() -> i32; +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncClear = fn() -> *const c_void; /// Get the description of the plugin. /// Return the description. The plugin allocate memory with `libc::malloc` and return the pointer. pub type PluginFuncDesc = fn() -> *const c_char; @@ -37,22 +55,19 @@ type PluginFuncCallbackMsg = fn( peer: *const c_char, target: *const c_char, id: *const c_char, - content: *const c_char, + content: *const c_void, len: usize, ); pub type PluginFuncSetCallbackMsg = fn(PluginFuncCallbackMsg); /// The main function of the plugin. /// method: The method. "handle_ui" or "handle_peer" /// args: The arguments. -/// out: The output. The plugin allocate memory with `libc::malloc` and set the pointer to `out`. -/// out_len: The length of the output. -/// Return 0 if success. -pub type PluginFuncCall = fn( - method: *const c_char, - args: *const c_char, - out: *mut *mut c_char, - out_len: *mut usize, -) -> i32; +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncCall = + fn(method: *const c_char, args: *const c_void, len: usize) -> *const c_void; macro_rules! make_plugin { ($($field:ident : $tp:ty),+) => { @@ -140,7 +155,7 @@ pub fn reload_plugin(id: &str) -> ResultType<()> { load_plugin(&path) } -fn load_plugin(path: &str) -> ResultType<()> { +pub fn load_plugin(path: &str) -> ResultType<()> { let mut plugin = Plugin::new(path)?; let desc = (plugin.fn_desc)(); let desc_res = Desc::from_cstr(desc); @@ -149,6 +164,7 @@ fn load_plugin(path: &str) -> ResultType<()> { } let desc = desc_res?; let id = desc.id().to_string(); + // to-do validate plugin (plugin.fn_set_cb_msg)(callback_msg::callback_msg); update_config(&desc); reload_ui(&desc); @@ -157,6 +173,101 @@ fn load_plugin(path: &str) -> ResultType<()> { Ok(()) } +fn handle_event(method: &[u8], id: &str, event: &[u8]) -> ResultType<()> { + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => { + let ret = (plugin.fn_call)(method.as_ptr() as _, event.as_ptr(), event.len()); + if ret.is_null() { + Ok(()) + } else { + let (code, msg) = get_code_msg_from_ret(ret); + unsafe { + libc::free(ret); + } + bail!( + "Failed to handle plugin event, id: {}, method: {}, code: {}, msg: {}", + id, + std::string::String::from_utf8(method.to_vec()).unwrap_or_default(), + code, + msg + ); + } + } + None => bail!("Plugin {} not found", id), + } +} + +#[inline] +pub fn handle_ui_event(id: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_UI, id, event) +} + +#[inline] +pub fn handle_server_event(id: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_PEER, id, event) +} + +#[inline] +pub fn handle_client_event(id: &str, event: &[u8]) -> Option { + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => { + let ret = (plugin.fn_call)( + METHOD_HANDLE_PEER.as_ptr() as _, + event.as_ptr(), + event.len(), + ); + if ret.is_null() { + None + } else { + let (code, msg) = get_code_msg_from_ret(ret); + unsafe { + libc::free(ret); + } + if code > ERR_RUSTDESK_HANDLE_BASE && code < ERR_PLUGIN_HANDLE_BASE { + let name = match PLUGINS.read().unwrap().get(id) { + Some(plugin) => plugin.desc.as_ref().unwrap().name(), + None => "", + }; + match code { + ERR_CALL_NOT_SUPPORTED_METHOD => Some(make_plugin_response( + id, + name, + "plugin method is not supported", + )), + ERR_CALL_INVALID_ARGS => Some(make_plugin_response( + id, + name, + "plugin arguments is invalid", + )), + _ => Some(make_plugin_response(id, name, &msg)), + } + } else { + log::error!( + "Failed to handle client event, code: {}, msg: {}", + code, + msg + ); + None + } + } + } + None => Some(make_plugin_response(id, "", "plugin not found")), + } +} + +fn make_plugin_response(id: &str, name: &str, msg: &str) -> Message { + let mut misc = Misc::new(); + misc.set_plugin_response(PluginResponse { + id: id.to_owned(), + name: name.to_owned(), + msg: msg.to_owned(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} + fn update_config(desc: &Desc) { super::config::set_local_items(desc.id(), &desc.config().local); super::config::set_peer_items(desc.id(), &desc.config().peer); diff --git a/src/server/connection.rs b/src/server/connection.rs index f3768aacf..f2550d5e1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1270,13 +1270,16 @@ impl Connection { // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] - if !desktop_err.is_empty() && desktop_err != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { + if !desktop_err.is_empty() + && desktop_err != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY + { self.send_login_error(desktop_err).await; return true; } if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { - self.send_login_error(crate::client::LOGIN_MSG_OFFLINE).await; + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { @@ -1284,7 +1287,8 @@ impl Connection { if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") { - self.send_login_error(crate::client::LOGIN_MSG_NO_PASSWORD_ACCESS).await; + self.send_login_error(crate::client::LOGIN_MSG_NO_PASSWORD_ACCESS) + .await; } return true; } else if password::approve_mode() == ApproveMode::Password @@ -1323,8 +1327,10 @@ impl Connection { if desktop_err.is_empty() { self.try_start_cm(lr.my_id, lr.my_name, false); } else { - self.send_login_error(crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY) - .await; + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, + ) + .await; } #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] self.try_start_cm(lr.my_id, lr.my_name, false); @@ -1371,15 +1377,19 @@ impl Connection { #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] if desktop_err.is_empty() { - self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG).await; + self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) + .await; self.try_start_cm(lr.my_id, lr.my_name, false); } else { - self.send_login_error(crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG) - .await; + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, + ) + .await; } #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] { - self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG).await; + self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) + .await; self.try_start_cm(lr.my_id, lr.my_name, false); } } else { @@ -1800,6 +1810,13 @@ impl Connection { } } } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + if let Some(msg) = create::plugin::handle_client_event(&p.id, &p.content) { + self.send(msg).await; + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => {