diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9fb3d0a75..9d5ab47da 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -12,6 +12,8 @@ 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:flutter_hbb/plugin/common.dart'; +import 'package:flutter_hbb/plugin/widget.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -1394,6 +1396,103 @@ class _AccountState extends State<_Account> { } } +class _Checkbox extends StatefulWidget { + final String label; + final bool Function() getValue; + final Future Function(bool) setValue; + + const _Checkbox( + {Key? key, + required this.label, + required this.getValue, + required this.setValue}) + : super(key: key); + + @override + State<_Checkbox> createState() => _CheckboxState(); +} + +class _CheckboxState extends State<_Checkbox> { + var value = false; + + @override + initState() { + super.initState(); + value = widget.getValue(); + } + + @override + Widget build(BuildContext context) { + onChanged(bool b) async { + await widget.setValue(b); + setState(() { + value = widget.getValue(); + }); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (_) => onChanged(!value), + ).marginOnly(right: 5), + Expanded( + child: Text(translate(widget.label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value), + ); + } +} + +class PluginCard extends StatefulWidget { + final PluginId pluginId; + final Desc desc; + const PluginCard({ + Key? key, + required this.pluginId, + required this.desc, + }) : super(key: key); + + @override + State createState() => PluginCardState(); +} + +class PluginCardState extends State { + @override + Widget build(BuildContext context) { + final children = [ + _Button( + 'Reload', + () => bind.pluginReload(id: widget.pluginId), + ), + _Checkbox( + label: 'Enable', + getValue: () => bind.pluginIdIsEnabled(id: widget.pluginId), + setValue: (bool v) async { + if (!v) { + clearPlugin(widget.pluginId); + } + await bind.pluginIdEnable(id: widget.pluginId, v: v); + }, + ), + ]; + final model = getPluginModel(kLocationHostMainPlugin, widget.pluginId); + if (model != null) { + children.add(PluginItem( + pluginId: widget.pluginId, + peerId: '', + location: kLocationHostMainPlugin, + pluginModel: model, + isMenu: false, + )); + } + return _Card(title: widget.desc.name, children: children); + } +} + class _Plugin extends StatefulWidget { const _Plugin({Key? key}) : super(key: key); @@ -1403,44 +1502,15 @@ class _Plugin extends StatefulWidget { 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 = [ + List _buildCards(DescModel model) => [ _Card( title: 'Plugin', children: [ - _checkbox( - 'Enable', - () => bind.pluginIsEnabled() ?? false, - (bool v) async { + _Checkbox( + label: 'Enable', + getValue: () => bind.pluginIsEnabled() ?? false, + setValue: (bool v) async { if (!v) { clearLocations(); } @@ -1449,28 +1519,14 @@ class _PluginState extends State<_Plugin> { ), ], ), + ...model.all.entries + .map((entry) => PluginCard(pluginId: entry.key, desc: entry.value)) + .toList(), ]; - 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; - } + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); return DesktopScrollWrapper( scrollController: scrollController, child: ChangeNotifierProvider.value( @@ -1479,7 +1535,7 @@ class _PluginState extends State<_Plugin> { return ListView( physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, - children: buildCards(model), + children: _buildCards(model), ).marginOnly(bottom: _kListViewBottomMargin); }), ), diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index fa263e8d0..83cefb047 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -657,7 +657,7 @@ class _DisplayMenu extends StatefulWidget { final FFI ffi; final MenubarState state; final Function(bool) setFullscreen; - final LocationItem pluginItem; + final Widget pluginItem; _DisplayMenu( {Key? key, required this.id, @@ -668,6 +668,7 @@ class _DisplayMenu extends StatefulWidget { id, ffi, kLocationClientRemoteToolbarDisplay, + true, ), super(key: key); diff --git a/flutter/lib/plugin/common.dart b/flutter/lib/plugin/common.dart index 7f92ea5a8..d984c68ea 100644 --- a/flutter/lib/plugin/common.dart +++ b/flutter/lib/plugin/common.dart @@ -3,8 +3,7 @@ import 'dart:convert'; typedef PluginId = String; // ui location -const String kLocationHostMainDisplayOthers = - 'host|main|settings|display|others'; +const String kLocationHostMainPlugin = 'host|main|settings|plugin'; const String kLocationClientRemoteToolbarDisplay = 'client|remote|toolbar|display'; diff --git a/flutter/lib/plugin/desc.dart b/flutter/lib/plugin/desc.dart index c27fc656e..b46ec08b1 100644 --- a/flutter/lib/plugin/desc.dart +++ b/flutter/lib/plugin/desc.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:collection'; import 'package:flutter/foundation.dart'; +import './common.dart'; + const String kValueTrue = '1'; const String kValueFalse = '0'; @@ -59,7 +61,7 @@ class UiCheckbox extends UiType { class Location { // location key: - // host|main|settings|display|others + // host|main|settings|plugin // client|remote|toolbar|display HashMap ui; @@ -76,14 +78,12 @@ class Location { class ConfigItem { String key; - String value; String description; String defaultValue; - ConfigItem(this.key, this.value, this.defaultValue, this.description); + ConfigItem(this.key, this.defaultValue, this.description); ConfigItem.fromJson(Map json) : key = json['key'] ?? '', - value = json['value'] ?? '', description = json['description'] ?? '', defaultValue = json['default'] ?? ''; @@ -151,7 +151,7 @@ class Desc { } class DescModel with ChangeNotifier { - final data = {}; + final data = {}; DescModel._(); @@ -169,7 +169,7 @@ class DescModel with ChangeNotifier { return data[id]; } - Map get all => data; + Map get all => data; static final DescModel _instance = DescModel._(); static DescModel get instance => _instance; diff --git a/flutter/lib/plugin/model.dart b/flutter/lib/plugin/model.dart index f7bf1f740..aeb3072a2 100644 --- a/flutter/lib/plugin/model.dart +++ b/flutter/lib/plugin/model.dart @@ -55,16 +55,17 @@ class LocationModel with ChangeNotifier { } void addLocationUi(String location, PluginId id, UiType ui) { - _locationModels[location]?.add(id, ui); -} - -LocationModel addLocation(String location) { if (_locationModels[location] == null) { _locationModels[location] = LocationModel(); } - return _locationModels[location]!; + _locationModels[location]?.add(id, ui); } +LocationModel? getLocationModel(String location) => _locationModels[location]; + +PluginModel? getPluginModel(String location, PluginId id) => + _locationModels[location]?.pluginModels[id]; + void clearPlugin(PluginId pluginId) { for (var element in _locationModels.values) { element.pluginModels.remove(pluginId); @@ -77,7 +78,7 @@ void clearLocations() { } } -OptionModel addOptionModel( +OptionModel getOptionModel( String location, PluginId pluginId, String peer, String key) { final k = OptionModel.key(location, pluginId, peer, key); if (_optionModels[k] == null) { diff --git a/flutter/lib/plugin/widget.dart b/flutter/lib/plugin/widget.dart index ae1482860..69a3abda3 100644 --- a/flutter/lib/plugin/widget.dart +++ b/flutter/lib/plugin/widget.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; +import 'package:get/get.dart'; // to-do: do not depend on desktop import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -12,11 +14,15 @@ import './desc.dart'; import './model.dart'; import './common.dart'; +// dup to flutter\lib\desktop\pages\desktop_setting_page.dart +const double _kCheckBoxLeftMargin = 10; + class LocationItem extends StatelessWidget { final String peerId; final FFI ffi; final String location; final LocationModel locationModel; + final bool isMenu; LocationItem({ Key? key, @@ -24,19 +30,23 @@ class LocationItem extends StatelessWidget { required this.ffi, required this.location, required this.locationModel, + required this.isMenu, }) : super(key: key); bool get isEmpty => locationModel.isEmpty; - static LocationItem createLocationItem( - String peerId, FFI ffi, String location) { - final model = addLocation(location); - return LocationItem( - peerId: peerId, - ffi: ffi, - location: location, - locationModel: model, - ); + static Widget createLocationItem( + String peerId, FFI ffi, String location, bool isMenu) { + final model = getLocationModel(location); + return model == null + ? Container() + : LocationItem( + peerId: peerId, + ffi: ffi, + location: location, + locationModel: model, + isMenu: isMenu, + ); } @override @@ -59,23 +69,26 @@ class LocationItem extends StatelessWidget { ffi: ffi, location: location, pluginModel: model, + isMenu: isMenu, ); } class PluginItem extends StatelessWidget { final PluginId pluginId; final String peerId; - final FFI ffi; + final FFI? ffi; final String location; final PluginModel pluginModel; + final isMenu; PluginItem({ Key? key, required this.pluginId, required this.peerId, - required this.ffi, + this.ffi, required this.location, required this.pluginModel, + required this.isMenu, }) : super(key: key); bool get isEmpty => pluginModel.isEmpty; @@ -95,74 +108,88 @@ class PluginItem extends StatelessWidget { } Widget _buildItem(UiType ui) { - late Widget child; + Widget? child; switch (ui.runtimeType) { case UiButton: - child = _buildMenuButton(ui as UiButton); + if (isMenu) { + if (ffi != null) { + child = _buildMenuButton(ui as UiButton, ffi!); + } + } else { + child = _buildButton(ui as UiButton); + } break; case UiCheckbox: - child = _buildCheckboxMenuButton(ui as UiCheckbox); + if (isMenu) { + if (ffi != null) { + child = _buildCheckboxMenuButton(ui as UiCheckbox, ffi!); + } + } else { + child = _buildCheckbox(ui as UiCheckbox); + } break; default: - child = Container(); + break; } // to-do: add plugin icon and tooltip - return child; + return child ?? Container(); } - Uint8List _makeEvent( - String key, { - bool? v, - }) { - final event = MsgFromUi( - id: pluginId, - name: getDesc(pluginId)?.name ?? '', - location: location, - key: key, - value: - v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '', - action: '', - ); - return Uint8List.fromList(event.toString().codeUnits); - } - - Widget _buildMenuButton(UiButton ui) { - return MenuButton( + Widget _buildButton(UiButton ui) { + return TextButton( onPressed: () => bind.pluginEvent( id: pluginId, peer: peerId, event: _makeEvent(ui.key), ), - // to-do: support trailing icon, but it will cause tree shake error. - // ``` - // This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations: - // Target release_macos_bundle_flutter_assets failed: Exception: Avoid non-constant invocations of IconData or try to build again with --no-tree-shake-icons. - // ``` - // - // trailingIcon: Icon( - // IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')), - // - // to-do: RustDesk translate or plugin translate ? child: Text(ui.text), - ffi: ffi, ); } - String? getOption(OptionModel model, String key) { - var v = model.value; - if (v == null) { - if (peerId.isEmpty) { - v = bind.pluginGetSharedOption(id: pluginId, key: key); - } else { - v = bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: key); + Widget _buildCheckbox(UiCheckbox ui) { + getChild(OptionModel model) { + final v = _getOption(model, ui.key); + if (v == null) { + // session or plugin not found + return Container(); } + + onChanged(bool value) { + bind.pluginEvent( + id: pluginId, + peer: peerId, + event: _makeEvent(ui.key, v: value), + ); + } + + final value = ConfigItem.isTrue(v); + return GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (_) => onChanged(!value), + ).marginOnly(right: 5), + Expanded( + child: Text(translate(ui.text)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value), + ); } - return v; + + return ChangeNotifierProvider.value( + value: getOptionModel(location, pluginId, peerId, ui.key), + child: Consumer( + builder: (context, model, child) => getChild(model), + ), + ); } - Widget _buildCheckboxMenuButton(UiCheckbox ui) { + Widget _buildCheckboxMenuButton(UiCheckbox ui, FFI ffi) { getChild(OptionModel model) { - final v = getOption(model, ui.key); + final v = _getOption(model, ui.key); if (v == null) { // session or plugin not found return Container(); @@ -184,16 +211,63 @@ class PluginItem extends StatelessWidget { ); } - final optionModel = addOptionModel(location, pluginId, peerId, ui.key); return ChangeNotifierProvider.value( - value: optionModel, + value: getOptionModel(location, pluginId, peerId, ui.key), child: Consumer( - builder: (context, model, child) { - return getChild(model); - }, + builder: (context, model, child) => getChild(model), ), ); } + + Widget _buildMenuButton(UiButton ui, FFI ffi) { + return MenuButton( + onPressed: () => bind.pluginEvent( + id: pluginId, + peer: peerId, + event: _makeEvent(ui.key), + ), + // to-do: support trailing icon, but it will cause tree shake error. + // ``` + // This application cannot tree shake icons fonts. It has non-constant instances of IconData at the following locations: + // Target release_macos_bundle_flutter_assets failed: Exception: Avoid non-constant invocations of IconData or try to build again with --no-tree-shake-icons. + // ``` + // + // trailingIcon: Icon( + // IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')), + // + // to-do: RustDesk translate or plugin translate ? + child: Text(ui.text), + ffi: ffi, + ); + } + + Uint8List _makeEvent( + String key, { + bool? v, + }) { + final event = MsgFromUi( + id: pluginId, + name: getDesc(pluginId)?.name ?? '', + location: location, + key: key, + value: + v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '', + action: '', + ); + return Uint8List.fromList(event.toString().codeUnits); + } + + String? _getOption(OptionModel model, String key) { + var v = model.value; + if (v == null) { + if (peerId.isEmpty) { + v = bind.pluginGetSharedOption(id: pluginId, key: key); + } else { + v = bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: key); + } + } + return v; + } } void handleReloading(Map evt, String peer) { diff --git a/src/common.rs b/src/common.rs index c2f02aa5a..b5cd2dafc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -59,6 +59,10 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +lazy_static::lazy_static! { + static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); @@ -76,6 +80,11 @@ pub fn global_init() -> bool { pub fn global_clean() {} +#[inline] +pub fn is_server() -> bool { + *IS_SERVER +} + #[inline] pub fn valid_for_numlock(evt: &KeyEvent) -> bool { if let Some(key_event::Union::ControlKey(ck)) = evt.union { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0e16e7353..a2fc1a489 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1466,6 +1466,7 @@ pub fn plugin_reload(_id: String) { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { + allow_err!(crate::plugin::ipc::reload_plugin(&_id,)); allow_err!(crate::plugin::reload_plugin(&_id)); } } @@ -1474,7 +1475,8 @@ pub fn plugin_id_uninstall(_id: String) { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { - // to-do: uninstall plugin + crate::plugin::unload_plugin(&_id); + allow_err!(crate::plugin::ipc::uninstall_plugin(&_id)); } } @@ -1485,11 +1487,11 @@ pub fn plugin_id_enable(_id: String, _v: bool) { { allow_err!(crate::plugin::ipc::set_manager_plugin_config( &_id, - "enable", + "enabled", _v.to_string() )); if _v { - allow_err!(crate::plugin::reload_plugin(&_id)); + allow_err!(crate::plugin::load_plugin(None, Some(&_id))); } else { crate::plugin::unload_plugin(&_id); } @@ -1522,7 +1524,7 @@ pub fn plugin_enable(_v: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] { allow_err!(crate::plugin::ipc::set_manager_config( - "enable", + "enabled", _v.to_string() )); if _v { diff --git a/src/plugin/callback_msg.rs b/src/plugin/callback_msg.rs index 1e8775883..1ab19e180 100644 --- a/src/plugin/callback_msg.rs +++ b/src/plugin/callback_msg.rs @@ -60,14 +60,14 @@ struct MsgToConfig { /// id: The id of this plugin. /// content: The content. /// len: The length of the content. -pub fn callback_msg( +pub fn cb_msg( peer: *const c_char, target: *const c_char, id: *const c_char, content: *const c_void, len: usize, ) { - macro_rules! callback_msg_field { + macro_rules! cb_msg_field { ($field: ident) => { let $field = match cstr_to_string($field) { Err(e) => { @@ -78,9 +78,9 @@ pub fn callback_msg( }; }; } - callback_msg_field!(peer); - callback_msg_field!(target); - callback_msg_field!(id); + cb_msg_field!(peer); + cb_msg_field!(target); + cb_msg_field!(id); match &target as _ { MSG_TO_PEER_TARGET => { diff --git a/src/plugin/config.rs b/src/plugin/config.rs index bce60dbc9..a0b288486 100644 --- a/src/plugin/config.rs +++ b/src/plugin/config.rs @@ -1,18 +1,20 @@ -use super::desc::ConfigItem; +use super::{cstr_to_string, str_to_cstr_ret}; use hbb_common::{allow_err, bail, config::Config as HbbConfig, lazy_static, log, ResultType}; use serde_derive::{Deserialize, Serialize}; use std::{ + collections::HashMap, + ffi::c_char, + fs, ops::{Deref, DerefMut}, + path::PathBuf, + ptr, str::FromStr, sync::{Arc, Mutex}, - {collections::HashMap, path::PathBuf}, }; lazy_static::lazy_static! { static ref CONFIG_SHARED: Arc>> = Default::default(); - static ref CONFIG_SHARED_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)) @@ -68,25 +70,34 @@ impl SharedConfig { } #[inline] - pub fn load(id: &str) { + fn load(id: &str) { + let mut lock = CONFIG_SHARED.lock().unwrap(); + if lock.contains_key(id) { + return; + } let conf = hbb_common::config::load_path::>(Self::path(id)); let mut conf = SharedConfig(conf); - if let Some(items) = CONFIG_SHARED_ITEMS.lock().unwrap().get(id) { - for item in items { + if let Some(desc_conf) = super::plugins::get_desc_conf(id) { + for item in desc_conf.shared.iter() { if !conf.contains_key(&item.key) { conf.insert(item.key.to_owned(), item.default.to_owned()); } } } - CONFIG_SHARED.lock().unwrap().insert(id.to_owned(), conf); + lock.insert(id.to_owned(), conf); + } + + #[inline] + fn load_if_not_exists(id: &str) { + if CONFIG_SHARED.lock().unwrap().contains_key(id) { + return; + } + Self::load(id); } #[inline] pub fn get(id: &str, key: &str) -> Option { - if let Some(conf) = CONFIG_SHARED.lock().unwrap().get(id) { - return conf.get(key).map(|s| s.to_owned()); - } - Self::load(id); + Self::load_if_not_exists(id); CONFIG_SHARED .lock() .unwrap() @@ -97,12 +108,16 @@ impl SharedConfig { #[inline] pub fn set(id: &str, key: &str, value: &str) -> ResultType<()> { + Self::load_if_not_exists(id); match CONFIG_SHARED.lock().unwrap().get_mut(id) { Some(config) => { config.insert(key.to_owned(), value.to_owned()); hbb_common::config::store_path(Self::path(id), config) } - None => bail!("No such plugin {}", id), + None => { + // unreachable + bail!("No such plugin {}", id) + } } } } @@ -116,35 +131,47 @@ impl PeerConfig { } #[inline] - pub fn load(id: &str, peer: &str) { + fn load(id: &str, peer: &str) { + let mut lock = CONFIG_PEERS.lock().unwrap(); + if let Some(peers) = lock.get(id) { + if peers.contains_key(peer) { + return; + } + } + let conf = hbb_common::config::load_path::>(Self::path(id, peer)); let mut conf = PeerConfig(conf); - if let Some(items) = CONFIG_PEER_ITEMS.lock().unwrap().get(id) { - for item in items { + if let Some(desc_conf) = super::plugins::get_desc_conf(id) { + for item in desc_conf.peer.iter() { if !conf.contains_key(&item.key) { conf.insert(item.key.to_owned(), item.default.to_owned()); } } } - - if let Some(peers) = CONFIG_PEERS.lock().unwrap().get_mut(id) { + + if let Some(peers) = lock.get_mut(id) { peers.insert(peer.to_owned(), conf); return; } let mut peers = HashMap::new(); peers.insert(peer.to_owned(), conf); - CONFIG_PEERS.lock().unwrap().insert(id.to_owned(), peers); + lock.insert(id.to_owned(), peers); + } + + #[inline] + fn load_if_not_exists(id: &str, peer: &str) { + if let Some(peers) = CONFIG_PEERS.lock().unwrap().get(id) { + if peers.contains_key(peer) { + return; + } + } + Self::load(id, peer); } #[inline] pub fn get(id: &str, peer: &str, key: &str) -> Option { - if let Some(peers) = CONFIG_PEERS.lock().unwrap().get(id) { - if let Some(conf) = peers.get(peer) { - return conf.get(key).map(|s| s.to_owned()); - } - } - Self::load(id, peer); + Self::load_if_not_exists(id, peer); CONFIG_PEERS .lock() .unwrap() @@ -156,35 +183,26 @@ impl PeerConfig { #[inline] pub fn set(id: &str, peer: &str, key: &str, value: &str) -> ResultType<()> { + Self::load_if_not_exists(id, peer); match CONFIG_PEERS.lock().unwrap().get_mut(id) { Some(peers) => match peers.get_mut(peer) { Some(config) => { config.insert(key.to_owned(), value.to_owned()); hbb_common::config::store_path(Self::path(id, peer), config) } - None => bail!("No such peer {}", peer), + None => { + // unreachable + bail!("No such peer {}", peer) + } }, - None => bail!("No such plugin {}", id), + None => { + // unreachable + bail!("No such plugin {}", id) + } } } } -#[inline] -pub(super) fn set_shared_items(id: &str, items: &Vec) { - CONFIG_SHARED_ITEMS - .lock() - .unwrap() - .insert(id.to_owned(), items.clone()); -} - -#[inline] -pub(super) fn set_peer_items(id: &str, items: &Vec) { - CONFIG_PEER_ITEMS - .lock() - .unwrap() - .insert(id.to_owned(), items.clone()); -} - #[derive(Debug, Serialize, Deserialize)] pub struct PluginStatus { pub enabled: bool, @@ -234,21 +252,31 @@ impl ManagerConfig { } } - #[inline] - pub fn set_option(key: &str, value: &str) -> ResultType<()> { + fn set_option_enabled(enabled: bool) -> ResultType<()> { let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.enabled = enabled; + hbb_common::config::store_path(Self::path(), &*lock) + } + + fn set_option_not_enabled(key: &str, value: &str) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.options.insert(key.to_owned(), value.to_owned()); + hbb_common::config::store_path(Self::path(), &*lock) + } + + #[inline] + pub fn set_option(key: &str, value: &str) { if key == "enabled" { let enabled = bool::from_str(value).unwrap_or(false); - lock.enabled = enabled; + allow_err!(Self::set_option_enabled(enabled)); if enabled { allow_err!(super::load_plugins()); } else { super::unload_plugins(); } } else { - lock.options.insert(key.to_owned(), value.to_owned()); + allow_err!(Self::set_option_not_enabled(key, value)); } - hbb_common::config::store_path(Self::path(), &*lock) } #[inline] @@ -261,27 +289,31 @@ impl ManagerConfig { } } - pub fn set_plugin_option(id: &str, key: &str, value: &str) -> ResultType<()> { + fn set_plugin_option_enabled(id: &str, enabled: bool) -> ResultType<()> { let mut lock = CONFIG_MANAGER.lock().unwrap(); if let Some(status) = lock.plugins.get_mut(id) { - match key { - "enabled" => { - let enabled = bool::from_str(value).unwrap_or(false); - status.enabled = enabled; - if enabled { - allow_err!(super::load_plugin(None, Some(id))); - } else { - super::unload_plugin(id); - } - } - _ => bail!("No such option {}", key), - } + status.enabled = enabled; } else { - bail!("No such plugin {}", id) + lock.plugins.insert(id.to_owned(), PluginStatus { enabled }); } hbb_common::config::store_path(Self::path(), &*lock) } + pub fn set_plugin_option(id: &str, key: &str, value: &str) { + match key { + "enabled" => { + let enabled = bool::from_str(value).unwrap_or(false); + allow_err!(Self::set_plugin_option_enabled(id, enabled)); + if enabled { + allow_err!(super::load_plugin(None, Some(id))); + } else { + super::unload_plugin(id); + } + } + _ => log::error!("No such option {}", key), + } + } + #[inline] pub fn add_plugin(id: &str) -> ResultType<()> { let mut lock = CONFIG_MANAGER.lock().unwrap(); @@ -291,11 +323,58 @@ impl ManagerConfig { } #[inline] - pub fn remove_plugin(id: &str) -> ResultType<()> { + pub fn remove_plugin(id: &str, uninstall: bool) -> ResultType<()> { let mut lock = CONFIG_MANAGER.lock().unwrap(); lock.plugins.remove(id); hbb_common::config::store_path(Self::path(), &*lock)?; - // to-do: remove plugin config dir + if uninstall { + allow_err!(fs::remove_dir_all(path_plugins(id))); + } Ok(()) } + + pub fn remove_plugins(uninstall: bool) { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins.clear(); + allow_err!(hbb_common::config::store_path(Self::path(), &*lock)); + if uninstall { + allow_err!(fs::remove_dir_all(HbbConfig::path("plugins"))); + } + } +} + +// Return shared config if peer is nullptr. +pub(super) fn cb_get_conf( + peer: *const c_char, + id: *const c_char, + key: *const c_char, +) -> *const c_char { + match (cstr_to_string(id), cstr_to_string(key)) { + (Ok(id), Ok(key)) => { + if peer.is_null() { + SharedConfig::load_if_not_exists(&id); + if let Some(conf) = CONFIG_SHARED.lock().unwrap().get(&id) { + if let Some(value) = conf.get(&key) { + return str_to_cstr_ret(value); + } + } + } else { + match cstr_to_string(peer) { + Ok(peer) => { + PeerConfig::load_if_not_exists(&id, &peer); + if let Some(conf) = CONFIG_PEERS.lock().unwrap().get(&id) { + if let Some(conf) = conf.get(&peer) { + if let Some(value) = conf.get(&key) { + return str_to_cstr_ret(value); + } + } + } + } + Err(_) => {} + } + } + } + _ => {} + } + ptr::null() } diff --git a/src/plugin/desc.rs b/src/plugin/desc.rs index 553185010..8308f6f14 100644 --- a/src/plugin/desc.rs +++ b/src/plugin/desc.rs @@ -40,7 +40,7 @@ pub struct ConfigItem { pub description: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub shared: Vec, pub peer: Vec, diff --git a/src/plugin/ipc.rs b/src/plugin/ipc.rs index 501a4d8eb..1c41e4158 100644 --- a/src/plugin/ipc.rs +++ b/src/plugin/ipc.rs @@ -12,7 +12,7 @@ pub enum Plugin { ManagerConfig(String, Option), ManagerPluginConfig(String, String, Option), Reload(String), - Uninstall, + Uninstall(String), } #[tokio::main(flavor = "current_thread")] @@ -45,6 +45,16 @@ pub async fn set_manager_plugin_config(id: &str, name: &str, value: String) -> R set_manager_plugin_config_async(id, name, value).await } +#[tokio::main(flavor = "current_thread")] +pub async fn reload_plugin(id: &str) -> ResultType<()> { + reload_plugin_async(id).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn uninstall_plugin(id: &str) -> ResultType<()> { + uninstall_plugin_async(id).await +} + async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType> { let mut c = connect(ms_timeout, "").await?; c.send(&Data::Plugin(Plugin::Config( @@ -131,6 +141,19 @@ async fn set_manager_plugin_config_async(id: &str, name: &str, value: String) -> Ok(()) } +async fn reload_plugin_async(id: &str) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Reload(id.to_owned()))).await?; + Ok(()) +} + +async fn uninstall_plugin_async(id: &str) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Uninstall(id.to_owned()))) + .await?; + Ok(()) +} + pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { match plugin { Plugin::Config(id, name, value) => match value { @@ -156,7 +179,7 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { ); } Some(value) => { - allow_err!(crate::plugin::ManagerConfig::set_option(&name, &value)); + crate::plugin::ManagerConfig::set_option(&name, &value); } }, Plugin::ManagerPluginConfig(id, name, value) => match value { @@ -169,16 +192,16 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { ); } Some(value) => { - allow_err!(crate::plugin::ManagerConfig::set_plugin_option( - &id, &name, &value - )); + crate::plugin::ManagerConfig::set_plugin_option(&id, &name, &value); } }, Plugin::Reload(id) => { allow_err!(crate::plugin::reload_plugin(&id)); } - Plugin::Uninstall => { + Plugin::Uninstall(_id) => { // to-do: uninstall plugin + // 1. unload 2. remove configs 3. remove config files + // allow_err!(crate::plugin::unload_plugin(&id)); } } } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index a530cfff3..678feb071 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -1,4 +1,4 @@ -use hbb_common::ResultType; +use hbb_common::{libc, ResultType}; use std::ffi::{c_char, c_void, CStr}; mod callback_msg; @@ -27,6 +27,21 @@ fn cstr_to_string(cstr: *const c_char) -> ResultType { })?) } +#[inline] +pub fn str_to_cstr_ret(s: &str) -> *const c_char { + let mut s = s.as_bytes().to_vec(); + s.push(0); + unsafe { + let r = libc::malloc(s.len()) as *mut c_char; + libc::memcpy( + r as *mut libc::c_void, + s.as_ptr() as *const libc::c_void, + s.len(), + ); + r + } +} + #[inline] fn get_code_msg_from_ret(ret: *const c_void) -> (i32, String) { assert!(ret.is_null() == false); @@ -38,3 +53,12 @@ fn get_code_msg_from_ret(ret: *const c_void) -> (i32, String) { .to_string(); (code, msg) } + +#[inline] +fn free_c_ptr(ret: *mut c_void) { + if !ret.is_null() { + unsafe { + libc::free(ret); + } + } +} diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index 1ee6a74cc..a69ea4052 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -1,3 +1,14 @@ +use super::{desc::Desc, errno::*, *}; +#[cfg(not(debug_assertions))] +use crate::common::is_server; +use crate::{flutter, ui_interface::get_id}; +use hbb_common::{ + allow_err, bail, + dlopen::symbor::Library, + lazy_static, log, + message_proto::{Message, Misc, PluginResponse}, + ResultType, +}; use std::{ collections::HashMap, ffi::{c_char, c_void}, @@ -5,16 +16,6 @@ use std::{ sync::{Arc, RwLock}, }; -use super::{desc::Desc, errno::*, *}; -use crate::{flutter, ui_interface::get_id}; -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"; @@ -58,19 +59,27 @@ pub type PluginFuncDesc = fn() -> *const c_char; /// id: The id of this plugin. /// content: The content. /// len: The length of the content. -type PluginFuncCallbackMsg = fn( +type CallbackMsg = fn( peer: *const c_char, target: *const c_char, id: *const c_char, content: *const c_void, len: usize, ); -pub type PluginFuncSetCallbackMsg = fn(PluginFuncCallbackMsg); /// Callback to get the id of local peer id. /// The returned string is utf8 string(null terminated). /// Don't free the returned ptr. -type GetIdFuncCallback = fn() -> *const c_char; -pub type PluginFuncGetIdCallback = fn(GetIdFuncCallback); +type CallbackGetId = fn() -> *const c_char; +/// Callback to get the config. +/// peer, key are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// id: The id of this plugin. +/// key: The key of the config. +/// +/// The returned string is utf8 string(null terminated) and must be freed by caller. +type CallbackGetConf = + fn(peer: *const c_char, id: *const c_char, key: *const c_char) -> *const c_char; /// The main function of the plugin. /// method: The method. "handle_ui" or "handle_peer" /// peer: The peer id. @@ -86,10 +95,21 @@ pub type PluginFuncCall = fn( len: usize, ) -> *const c_void; +#[repr(C)] +struct Callbacks { + msg: CallbackMsg, + get_id: CallbackGetId, + get_conf: CallbackGetConf, +} +type PluginFuncSetCallbacks = fn(Callbacks); + macro_rules! make_plugin { ($($field:ident : $tp:ty),+) => { + #[allow(dead_code)] pub struct Plugin { _lib: Library, + id: Option, + path: String, $($field: $tp),+ } @@ -115,9 +135,54 @@ macro_rules! make_plugin { Ok(Self { _lib: lib, + id: None, + path: path.to_string(), $( $field ),+ }) } + + fn desc(&self) -> ResultType { + let desc_ret = (self.desc)(); + let desc = Desc::from_cstr(desc_ret); + free_c_ptr(desc_ret as _); + desc + } + + fn init(&self, path: &str) -> ResultType<()> { + let init_ret = (self.init)(); + if !init_ret.is_null() { + let (code, msg) = get_code_msg_from_ret(init_ret); + free_c_ptr(init_ret as _); + bail!( + "Failed to init plugin {}, code: {}, msg: {}", + path, + code, + msg + ); + } + Ok(()) + } + + fn clear(&self, id: &str) { + let clear_ret = (self.clear)(); + if !clear_ret.is_null() { + let (code, msg) = get_code_msg_from_ret(clear_ret); + free_c_ptr(clear_ret as _); + log::error!( + "Failed to clear plugin {}, code: {}, msg: {}", + id, + code, + msg + ); + } + } + } + + impl Drop for Plugin { + fn drop(&mut self) { + let id = self.id.as_ref().unwrap_or(&self.path); + self.clear(id); + } } } } @@ -128,8 +193,7 @@ make_plugin!( clear: PluginFuncClear, desc: PluginFuncDesc, call: PluginFuncCall, - set_cb_msg: PluginFuncSetCallbackMsg, - set_cb_get_id: PluginFuncGetIdCallback + set_cbs: PluginFuncSetCallbacks ); #[cfg(target_os = "windows")] @@ -172,17 +236,18 @@ pub fn load_plugins() -> ResultType<()> { } 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); + log::info!("Plugins unloaded"); + PLUGINS.write().unwrap().clear(); + if change_manager() { + super::config::ManagerConfig::remove_plugins(false); } - plugins.clear(); } pub fn unload_plugin(id: &str) { - if let Some(plugin) = PLUGINS.write().unwrap().remove(id) { - let _ret = (plugin.clear)(); + log::info!("Plugin {} unloaded", id); + PLUGINS.write().unwrap().remove(id); + if change_manager() { + allow_err!(super::config::ManagerConfig::remove_plugin(id, false)); } } @@ -196,7 +261,7 @@ pub fn reload_plugin(id: &str) -> ResultType<()> { } #[no_mangle] -fn get_local_peer_id() -> *const c_char { +fn cb_get_local_peer_id() -> *const c_char { let mut id = (*LOCAL_PEER_ID.read().unwrap()).clone(); if id.is_empty() { let mut lock = LOCAL_PEER_ID.write().unwrap(); @@ -212,28 +277,38 @@ fn get_local_peer_id() -> *const c_char { fn load_plugin_path(path: &str) -> ResultType<()> { let plugin = Plugin::new(path)?; - let desc = (plugin.desc)(); - let desc_res = Desc::from_cstr(desc); - unsafe { - libc::free(desc as _); - } - let desc = desc_res?; - let id = desc.id().to_string(); + let desc = plugin.desc()?; + // to-do validate plugin // to-do check the plugin id (make sure it does not use another plugin's id) - (plugin.set_cb_msg)(callback_msg::callback_msg); - (plugin.set_cb_get_id)(get_local_peer_id as _); + + plugin.init(path)?; + + if change_manager() { + super::config::ManagerConfig::add_plugin(desc.id())?; + } + + // set callbacks + (plugin.set_cbs)(Callbacks { + msg: callback_msg::cb_msg, + get_id: cb_get_local_peer_id, + get_conf: config::cb_get_conf, + }); + + // update ui // Ui may be not ready now, so we need to update again once ui is ready. update_ui_plugin_desc(&desc, None); - update_config(&desc); - // Ui may be not ready now, so we need to reload again once ui is ready. reload_ui(&desc, None); + + // add plugins + let id = desc.id().to_string(); let plugin_info = PluginInfo { path: path.to_string(), desc, }; PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); PLUGINS.write().unwrap().insert(id.clone(), plugin); + log::info!("Plugin {} loaded", id); Ok(()) } @@ -248,10 +323,13 @@ pub fn sync_ui(sync_to: String) { 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, Some(id)) => { + let path = match PLUGIN_INFO.read().unwrap().get(id) { + Some(plugin) => plugin.path.clone(), + None => bail!("Plugin {} not found", id), + }; + load_plugin_path(&path) + } (None, None) => { bail!("path and id are both None"); } @@ -273,9 +351,7 @@ fn handle_event(method: &[u8], id: &str, peer: &str, event: &[u8]) -> ResultType Ok(()) } else { let (code, msg) = get_code_msg_from_ret(ret); - unsafe { - libc::free(ret as _); - } + free_c_ptr(ret as _); bail!( "Failed to handle plugin event, id: {}, method: {}, code: {}, msg: {}", id, @@ -315,9 +391,7 @@ pub fn handle_client_event(id: &str, peer: &str, event: &[u8]) -> Option ERR_RUSTDESK_HANDLE_BASE && code < ERR_PLUGIN_HANDLE_BASE { let name = match PLUGIN_INFO.read().unwrap().get(id) { Some(plugin) => plugin.desc.name(), @@ -364,9 +438,13 @@ fn make_plugin_response(id: &str, name: &str, msg: &str) -> Message { msg_out } -fn update_config(desc: &Desc) { - super::config::set_shared_items(desc.id(), &desc.config().shared); - super::config::set_peer_items(desc.id(), &desc.config().peer); +#[inline] +fn change_manager() -> bool { + #[cfg(debug_assertions)] + let change_manager = true; + #[cfg(not(debug_assertions))] + let change_manager = is_server(); + change_manager } fn reload_ui(desc: &Desc, sync_to: Option<&str>) { @@ -417,22 +495,22 @@ fn update_ui_plugin_desc(desc: &Desc, sync_to: Option<&str>) { let event = serde_json::to_string(&m).unwrap_or("".to_owned()); match sync_to { Some(channel) => { - let _res = flutter::push_global_event(channel, serde_json::to_string(&m).unwrap()); + let _res = flutter::push_global_event(channel, event.clone()); } None => { - let _res = flutter::push_global_event( - flutter::APP_TYPE_MAIN, - serde_json::to_string(&m).unwrap(), - ); - let _res = flutter::push_global_event( - flutter::APP_TYPE_DESKTOP_REMOTE, - serde_json::to_string(&m).unwrap(), - ); - let _res = flutter::push_global_event( - flutter::APP_TYPE_CM, - serde_json::to_string(&m).unwrap(), - ); + let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); + let _res = + flutter::push_global_event(flutter::APP_TYPE_DESKTOP_REMOTE, event.clone()); + let _res = flutter::push_global_event(flutter::APP_TYPE_CM, event.clone()); } } } } + +pub(super) fn get_desc_conf(id: &str) -> Option { + PLUGIN_INFO + .read() + .unwrap() + .get(id) + .map(|info| info.desc.config().clone()) +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index eac62d836..129c47042 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1,4 +1,6 @@ use super::*; +#[cfg(target_os = "macos")] +use crate::common::is_server; #[cfg(target_os = "linux")] use crate::common::IS_X11; #[cfg(target_os = "macos")] @@ -377,7 +379,6 @@ pub fn try_stop_record_cursor_pos() { #[cfg(target_os = "macos")] lazy_static::lazy_static! { static ref QUEUE: Queue = Queue::main(); - static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } #[cfg(target_os = "macos")] @@ -520,7 +521,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { }; } #[cfg(target_os = "macos")] - if !*IS_SERVER { + if !is_server() { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_mouse_(&evt)); @@ -902,7 +903,7 @@ pub async fn lock_screen() { pub fn handle_key(evt: &KeyEvent) { #[cfg(target_os = "macos")] - if !*IS_SERVER { + if !is_server() { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_key_(&evt)); @@ -928,7 +929,7 @@ fn reset_input() { #[cfg(target_os = "macos")] pub fn reset_input_ondisconn() { - if !*IS_SERVER { + if !is_server() { QUEUE.exec_async(reset_input); } else { reset_input();