diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index c083421fd..821edd27d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,6 +10,8 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/plugin/desc.dart'; +import 'package:flutter_hbb/plugin/model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -71,16 +73,6 @@ class DesktopSettingPage extends StatefulWidget { class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - final List<_TabInfo> settingTabs = <_TabInfo>[ - _TabInfo('General', Icons.settings_outlined, Icons.settings), - _TabInfo('Security', Icons.enhanced_encryption_outlined, - Icons.enhanced_encryption), - _TabInfo('Network', Icons.link_outlined, Icons.link), - _TabInfo('Display', Icons.desktop_windows_outlined, Icons.desktop_windows), - _TabInfo('Account', Icons.person_outline, Icons.person), - _TabInfo('About', Icons.info_outline, Icons.info) - ]; - late PageController controller; late RxInt selectedIndex; @@ -104,6 +96,39 @@ class _DesktopSettingPageState extends State Get.delete(tag: _kSettingPageIndexTag); } + List<_TabInfo> _settingTabs() { + final List<_TabInfo> settingTabs = <_TabInfo>[ + _TabInfo('General', Icons.settings_outlined, Icons.settings), + _TabInfo('Security', Icons.enhanced_encryption_outlined, + Icons.enhanced_encryption), + _TabInfo('Network', Icons.link_outlined, Icons.link), + _TabInfo( + 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows), + _TabInfo('Account', Icons.person_outline, Icons.person), + _TabInfo('About', Icons.info_outline, Icons.info) + ]; + if (bind.pluginFeatureIsEnabled()) { + settingTabs.insert( + 4, _TabInfo('Plugin', Icons.extension_outlined, Icons.extension)); + } + return settingTabs; + } + + List _children() { + final children = [ + _General(), + _Safety(), + _Network(), + _Display(), + _Account(), + _About(), + ]; + if (bind.pluginFeatureIsEnabled()) { + children.insert(4, _Plugin()); + } + return children; + } + @override Widget build(BuildContext context) { super.build(context); @@ -116,7 +141,7 @@ class _DesktopSettingPageState extends State child: Column( children: [ _header(), - Flexible(child: _listView(tabs: settingTabs)), + Flexible(child: _listView(tabs: _settingTabs())), ], ), ), @@ -129,14 +154,7 @@ class _DesktopSettingPageState extends State child: PageView( controller: controller, physics: DraggableNeverScrollableScrollPhysics(), - children: const [ - _General(), - _Safety(), - _Network(), - _Display(), - _Account(), - _About(), - ], + children: _children(), )), ), ) @@ -1376,6 +1394,98 @@ class _AccountState extends State<_Account> { } } +class _Plugin extends StatefulWidget { + const _Plugin({Key? key}) : super(key: key); + + @override + State<_Plugin> createState() => _PluginState(); +} + +class _PluginState extends State<_Plugin> { + // temp checkbox widget + Widget _checkbox( + String label, + bool Function() getValue, + Future Function(bool) setValue, + ) { + final value = getValue(); + onChanged(bool b) async { + await setValue(b); + setState(() {}); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox( + value: bind.pluginIsEnabled(), + onChanged: (_) => onChanged(!value), + ).marginOnly(right: 5), + Expanded( + child: Text(translate(label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value)); + } + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + buildCards(DescModel model) { + final cards = [ + _Card(title: 'Plugin', children: [ + _checkbox('Enable', bind.pluginIsEnabled, (bool v) async { + if (!v) { + clearLocations(); + } + await bind.pluginEnable(v: v); + }), + ]), + ]; + model.all.forEach((key, value) { + cards.add(_Card(title: key, children: [ + _Button('Reload', () { + bind.pluginReload(id: key); + }), + _checkbox('Enable', () => bind.pluginIdIsEnabled(id: key), + (bool v) async { + if (!v) { + clearPlugin(key); + } + await bind.pluginIdEnable(id: key, v: v); + }), + ])); + }); + return cards; + } + + return DesktopScrollWrapper( + scrollController: scrollController, + child: ChangeNotifierProvider.value( + value: DescModel.instance, + child: Consumer(builder: (context, model, child) { + return ListView( + physics: DraggableNeverScrollableScrollPhysics(), + controller: scrollController, + children: buildCards(model), + ).marginOnly(bottom: _kListViewBottomMargin); + }), + ), + ); + } + + Widget accountAction() { + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); + } +} + class _About extends StatefulWidget { const _About({Key? key}) : super(key: key); diff --git a/flutter/lib/plugin/desc.dart b/flutter/lib/plugin/desc.dart index 8c0de4625..409fc5265 100644 --- a/flutter/lib/plugin/desc.dart +++ b/flutter/lib/plugin/desc.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; const String kValueTrue = '1'; const String kValueFalse = '0'; @@ -154,13 +155,27 @@ class Desc { .toList()); } -final mapPluginDesc = {}; +class DescModel with ChangeNotifier { + final data = {}; -void updateDesc(Map desc) { - Desc d = Desc.fromJson(desc); - mapPluginDesc[d.id] = d; + DescModel._(); + + void _updateDesc(Map desc) { + Desc d = Desc.fromJson(desc); + data[d.id] = d; + notifyListeners(); + } + + Desc? _getDesc(String id) { + return data[id]; + } + + Map get all => data; + + static final DescModel _instance = DescModel._(); + static DescModel get instance => _instance; } -Desc? getDesc(String id) { - return mapPluginDesc[id]; -} +void updateDesc(Map desc) => + DescModel.instance._updateDesc(desc); +Desc? getDesc(String id) => DescModel.instance._getDesc(id); diff --git a/flutter/lib/plugin/model.dart b/flutter/lib/plugin/model.dart index eb5fa78b7..f7bf1f740 100644 --- a/flutter/lib/plugin/model.dart +++ b/flutter/lib/plugin/model.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import './common.dart'; import './desc.dart'; -final Map locationModels = {}; -final Map optionModels = {}; +final Map _locationModels = {}; +final Map _optionModels = {}; class OptionModel with ChangeNotifier { String? v; @@ -46,31 +46,48 @@ class LocationModel with ChangeNotifier { } } + void clear() { + pluginModels.clear(); + notifyListeners(); + } + bool get isEmpty => pluginModels.isEmpty; } void addLocationUi(String location, PluginId id, UiType ui) { - locationModels[location]?.add(id, ui); + _locationModels[location]?.add(id, ui); } LocationModel addLocation(String location) { - if (locationModels[location] == null) { - locationModels[location] = LocationModel(); + if (_locationModels[location] == null) { + _locationModels[location] = LocationModel(); + } + return _locationModels[location]!; +} + +void clearPlugin(PluginId pluginId) { + for (var element in _locationModels.values) { + element.pluginModels.remove(pluginId); + } +} + +void clearLocations() { + for (var element in _locationModels.values) { + element.clear(); } - return locationModels[location]!; } OptionModel addOptionModel( String location, PluginId pluginId, String peer, String key) { final k = OptionModel.key(location, pluginId, peer, key); - if (optionModels[k] == null) { - optionModels[k] = OptionModel(); + if (_optionModels[k] == null) { + _optionModels[k] = OptionModel(); } - return optionModels[k]!; + return _optionModels[k]!; } void updateOption( String location, PluginId id, String peer, String key, String value) { final k = OptionModel.key(location, id, peer, key); - optionModels[k]?.value = value; + _optionModels[k]?.value = value; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1a025fbaa..db8c65a89 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1414,7 +1414,7 @@ pub fn plugin_get_session_option( #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { - return SyncReturn(crate::plugin::PeerConfig::get(&_id, &_peer, &_key)); + SyncReturn(crate::plugin::PeerConfig::get(&_id, &_peer, &_key)) } #[cfg(any( not(feature = "plugin_framework"), @@ -1422,7 +1422,7 @@ pub fn plugin_get_session_option( target_os = "ios" ))] { - return SyncReturn(None); + SyncReturn(None) } } @@ -1440,7 +1440,7 @@ pub fn plugin_get_local_option(_id: String, _key: String) -> SyncReturn SyncReturn SyncReturn { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn( + crate::plugin::ManagerConfig::get_plugin_status(&_id, |s| s.enabled).unwrap_or(false), + ) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(false) + } +} + +pub fn plugin_enable(v: bool) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::ManagerConfig::set_enabled(v)); + if v { + allow_err!(crate::plugin::load_plugins()); + } else { + crate::plugin::unload_plugins(); + } + } +} + +pub fn plugin_is_enabled() -> SyncReturn { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(crate::plugin::ManagerConfig::is_enabled()) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(false) + } +} + +pub fn plugin_feature_is_enabled() -> SyncReturn { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(true) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(false) + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/plugin/config.rs b/src/plugin/config.rs index 928cad68b..dc754dabd 100644 --- a/src/plugin/config.rs +++ b/src/plugin/config.rs @@ -1,3 +1,5 @@ +use crate::plugins::Plugin; + use super::desc::ConfigItem; use hbb_common::{bail, config::Config as HbbConfig, lazy_static, ResultType}; use serde_derive::{Deserialize, Serialize}; @@ -12,6 +14,10 @@ lazy_static::lazy_static! { static ref CONFIG_LOCAL_ITEMS: Arc>>> = Default::default(); static ref CONFIG_PEERS: Arc>> = Default::default(); static ref CONFIG_PEER_ITEMS: Arc>>> = Default::default(); + static ref CONFIG_MANAGER: Arc> = { + let conf = hbb_common::config::load_path::(ManagerConfig::path()); + Arc::new(Mutex::new(conf)) + }; } pub(super) const CONFIG_TYPE_LOCAL: &str = "local"; @@ -178,3 +184,78 @@ pub(super) fn set_peer_items(id: &str, items: &Vec) { .unwrap() .insert(id.to_owned(), items.clone()); } + +#[derive(Debug, Serialize, Deserialize)] +pub struct PluginStatus { + pub enabled: bool, +} + +const MANAGER_VERSION: &str = "0.1.0"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ManagerConfig { + pub version: String, + pub enabled: bool, + pub plugins: HashMap, +} + +impl Default for ManagerConfig { + fn default() -> Self { + Self { + version: "0.1.0".to_owned(), + enabled: true, + plugins: HashMap::new(), + } + } +} + +// Do not care about the `store_path` error, no need to store the old value and restore if failed. +impl ManagerConfig { + #[inline] + fn path() -> PathBuf { + HbbConfig::path("plugins").join("manager.toml") + } + + #[inline] + pub fn is_enabled() -> bool { + CONFIG_MANAGER.lock().unwrap().enabled + } + + #[inline] + pub fn set_enabled(enabled: bool) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.enabled = enabled; + hbb_common::config::store_path(Self::path(), &*lock) + } + + #[inline] + pub fn get_plugin_status(id: &str, f: fn(&PluginStatus) -> T) -> Option { + let lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins.get(id).map(f) + } + + pub fn set_plugin_enabled(id: &str, enabled: bool) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + if let Some(status) = lock.plugins.get_mut(id) { + status.enabled = enabled; + hbb_common::config::store_path(Self::path(), &*lock) + } else { + bail!("No such plugin {}", id) + } + } + + #[inline] + pub fn add_plugin(id: &str) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins + .insert(id.to_owned(), PluginStatus { enabled: true }); + hbb_common::config::store_path(Self::path(), &*lock) + } + + #[inline] + pub fn remove_plugin(id: &str) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins.remove(id); + hbb_common::config::store_path(Self::path(), &*lock) + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index e0de6386e..fd854666e 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -9,7 +9,7 @@ mod plugins; pub use plugins::{ handle_client_event, handle_server_event, handle_ui_event, load_plugin, load_plugins, - reload_plugin, unload_plugin, + reload_plugin, unload_plugin, unload_plugins, }; const MSG_TO_UI_TYPE_PLUGIN_DESC: &str = "plugin_desc"; @@ -17,7 +17,7 @@ const MSG_TO_UI_TYPE_PLUGIN_EVENT: &str = "plugin_event"; const MSG_TO_UI_TYPE_PLUGIN_RELOAD: &str = "plugin_reload"; const MSG_TO_UI_TYPE_PLUGIN_OPTION: &str = "plugin_option"; -pub use config::{LocalConfig, PeerConfig}; +pub use config::{LocalConfig, ManagerConfig, PeerConfig}; #[inline] fn cstr_to_string(cstr: *const c_char) -> ResultType { diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index 9948136a7..27a401974 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ffi::{c_char, c_void}, - path::Path, + path::PathBuf, sync::{Arc, RwLock}, }; @@ -19,10 +19,16 @@ const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; const METHOD_HANDLE_PEER: &[u8; 12] = b"handle_peer\0"; lazy_static::lazy_static! { + static ref PLUGIN_INFO: Arc>> = Default::default(); pub static ref PLUGINS: Arc>> = Default::default(); pub static ref LOCAL_PEER_ID: Arc> = Default::default(); } +struct PluginInfo { + path: String, + desc: Desc, +} + /// Initialize the plugins. /// /// Return null ptr if success. @@ -84,8 +90,6 @@ macro_rules! make_plugin { ($($field:ident : $tp:ty),+) => { pub struct Plugin { _lib: Library, - path: String, - desc_v: Option, $($field: $tp),+ } @@ -111,8 +115,6 @@ macro_rules! make_plugin { Ok(Self { _lib: lib, - path: path.to_string(), - desc_v: None, $( $field ),+ }) } @@ -130,26 +132,43 @@ make_plugin!( set_cb_get_id: PluginFuncGetIdCallback ); -pub fn load_plugins>(dir: P) -> ResultType<()> { - for entry in std::fs::read_dir(dir)? { - match entry { - Ok(entry) => { - let path = entry.path(); - if path.is_file() { - let path = path.to_str().unwrap_or(""); - if path.ends_with(".so") { - if let Err(e) = load_plugin(path) { - log::error!("{e}"); +pub fn load_plugins() -> ResultType<()> { + let exe = std::env::current_exe()?.to_string_lossy().to_string(); + match PathBuf::from(&exe).parent() { + Some(dir) => { + for entry in std::fs::read_dir(dir)? { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_file() { + let path = path.to_str().unwrap_or(""); + if path.ends_with(".so") { + if let Err(e) = load_plugin(Some(path), None) { + log::error!("{e}"); + } + } } } + Err(e) => { + log::error!("Failed to read dir entry, {}", e); + } } } - Err(e) => { - log::error!("Failed to read dir entry, {}", e); - } + Ok(()) + } + None => { + bail!("Failed to get parent dir of {}", exe); } } - Ok(()) +} + +pub fn unload_plugins() { + let mut plugins = PLUGINS.write().unwrap(); + for (id, plugin) in plugins.iter() { + let _ret = (plugin.clear)(); + log::info!("Plugin {} unloaded", id); + } + plugins.clear(); } pub fn unload_plugin(id: &str) { @@ -159,12 +178,12 @@ pub fn unload_plugin(id: &str) { } pub fn reload_plugin(id: &str) -> ResultType<()> { - let path = match PLUGINS.read().unwrap().get(id) { + let path = match PLUGIN_INFO.read().unwrap().get(id) { Some(plugin) => plugin.path.clone(), None => bail!("Plugin {} not found", id), }; unload_plugin(id); - load_plugin(&path) + load_plugin(Some(&path), Some(id)) } #[no_mangle] @@ -182,8 +201,8 @@ fn get_local_peer_id() -> *const c_char { id.as_ptr() as _ } -pub fn load_plugin(path: &str) -> ResultType<()> { - let mut plugin = Plugin::new(path)?; +fn load_plugin_path(path: &str) -> ResultType<()> { + let plugin = Plugin::new(path)?; let desc = (plugin.desc)(); let desc_res = Desc::from_cstr(desc); unsafe { @@ -198,11 +217,28 @@ pub fn load_plugin(path: &str) -> ResultType<()> { update_ui_plugin_desc(&desc); update_config(&desc); reload_ui(&desc); - plugin.desc_v = Some(desc); + let plugin_info = PluginInfo { + path: path.to_string(), + desc, + }; + PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); PLUGINS.write().unwrap().insert(id, plugin); Ok(()) } +pub fn load_plugin(path: Option<&str>, id: Option<&str>) -> ResultType<()> { + match (path, id) { + (Some(path), _) => load_plugin_path(path), + (None, Some(id)) => match PLUGIN_INFO.read().unwrap().get(id) { + Some(plugin) => load_plugin_path(&plugin.path), + None => bail!("Plugin {} not found", id), + }, + (None, None) => { + bail!("path and id are both None"); + } + } +} + fn handle_event(method: &[u8], id: &str, peer: &str, event: &[u8]) -> ResultType<()> { let mut peer: String = peer.to_owned(); peer.push('\0'); @@ -264,19 +300,23 @@ pub fn handle_client_event(id: &str, peer: &str, event: &[u8]) -> Option ERR_RUSTDESK_HANDLE_BASE && code < ERR_PLUGIN_HANDLE_BASE { - let name = plugin.desc_v.as_ref().unwrap().name(); + let name = match PLUGIN_INFO.read().unwrap().get(id) { + Some(plugin) => plugin.desc.name(), + None => "???", + } + .to_owned(); match code { ERR_CALL_NOT_SUPPORTED_METHOD => Some(make_plugin_response( id, - name, + &name, "plugin method is not supported", )), ERR_CALL_INVALID_ARGS => Some(make_plugin_response( id, - name, + &name, "plugin arguments is invalid", )), - _ => Some(make_plugin_response(id, name, &msg)), + _ => Some(make_plugin_response(id, &name, &msg)), } } else { log::error!(