From 4ee0fd9676031e965d64d70131d7270205d5e79f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 10 May 2023 18:58:45 +0800 Subject: [PATCH] plugin_framework, test install plugin Signed-off-by: fufesou --- .../desktop/pages/desktop_setting_page.dart | 71 +------ .../lib/desktop/widgets/remote_toolbar.dart | 2 +- flutter/lib/models/model.dart | 2 +- flutter/lib/plugin/model.dart | 28 +-- .../{widget.dart => widgets/desc_ui.dart} | 18 +- .../lib/plugin/widgets/desktop_settings.dart | 199 ++++++++++++++++++ src/flutter_ffi.rs | 3 +- src/plugin/desc.rs | 2 +- src/plugin/ipc.rs | 2 +- src/plugin/manager.rs | 58 +++-- src/plugin/mod.rs | 15 +- src/plugin/plugins.rs | 37 +--- 12 files changed, 271 insertions(+), 166 deletions(-) rename flutter/lib/plugin/{widget.dart => widgets/desc_ui.dart} (95%) create mode 100644 flutter/lib/plugin/widgets/desktop_settings.dart diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ae3889447..74617a140 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,9 +11,7 @@ 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/manager.dart'; -import 'package:flutter_hbb/plugin/model.dart'; -import 'package:flutter_hbb/plugin/common.dart'; -import 'package:flutter_hbb/plugin/widget.dart'; +import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -1448,58 +1446,6 @@ class _CheckboxState extends State<_Checkbox> { } } -class PluginCard extends StatefulWidget { - final PluginInfo plugin; - const PluginCard({ - Key? key, - required this.plugin, - }) : super(key: key); - - @override - State createState() => PluginCardState(); -} - -class PluginCardState extends State { - PluginId get pluginId => widget.plugin.meta.id; - String get pluginName => widget.plugin.meta.name; - - @override - Widget build(BuildContext context) { - final children = [ - _Button( - 'Reload', - () async { - clearPlugin(pluginId); - await bind.pluginReload(id: pluginId); - setState(() {}); - }, - ), - _Checkbox( - label: 'Enable', - getValue: () => bind.pluginIsEnabled(id: pluginId), - setValue: (bool v) async { - if (!v) { - clearPlugin(pluginId); - } - await bind.pluginEnable(id: pluginId, v: v); - setState(() {}); - }, - ), - ]; - final model = getPluginModel(kLocationHostMainPlugin, pluginId); - if (model != null) { - children.add(PluginItem( - pluginId: pluginId, - peerId: '', - location: kLocationHostMainPlugin, - pluginModel: model, - isMenu: false, - )); - } - return _Card(title: pluginName, children: children); - } -} - class _Plugin extends StatefulWidget { const _Plugin({Key? key}) : super(key: key); @@ -1508,18 +1454,9 @@ class _Plugin extends StatefulWidget { } class _PluginState extends State<_Plugin> { - // temp checkbox widget - - List _buildCards(PluginManager model) => [ - _Card( - title: 'Plugin', - children: [], - ), - ...model.plugins.map((entry) => PluginCard(plugin: entry)).toList(), - ]; - @override Widget build(BuildContext context) { + bind.pluginListReload(); final scrollController = ScrollController(); return DesktopScrollWrapper( scrollController: scrollController, @@ -1529,7 +1466,9 @@ class _PluginState extends State<_Plugin> { return ListView( physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, - children: _buildCards(model), + children: model.plugins + .map((entry) => DesktopSettingsCard(plugin: entry)) + .toList(), ).marginOnly(bottom: _kListViewBottomMargin); }), ), diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 83cefb047..fa70c1c7a 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -8,7 +8,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:flutter_hbb/plugin/widget.dart'; +import 'package:flutter_hbb/plugin/widgets/desc_ui.dart'; import 'package:flutter_hbb/plugin/common.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a91e1dc6f..e36b8d390 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -18,7 +18,7 @@ import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/event.dart'; import 'package:flutter_hbb/plugin/manager.dart'; -import 'package:flutter_hbb/plugin/widget.dart'; +import 'package:flutter_hbb/plugin/widgets/desc_ui.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; diff --git a/flutter/lib/plugin/model.dart b/flutter/lib/plugin/model.dart index 44579bcd6..4fc024e4c 100644 --- a/flutter/lib/plugin/model.dart +++ b/flutter/lib/plugin/model.dart @@ -22,16 +22,18 @@ class PluginModel with ChangeNotifier { final List uiList = []; final Map opts = {}; - void add(UiType ui) { + void add(List uiList) { bool found = false; - for (int i = 0; i < uiList.length; i++) { - if (uiList[i].key == ui.key) { - uiList[i] = ui; - found = true; + for (var ui in uiList) { + for (int i = 0; i < this.uiList.length; i++) { + if (this.uiList[i].key == ui.key) { + this.uiList[i] = ui; + found = true; + } + } + if (!found) { + this.uiList.add(ui); } - } - if (!found) { - uiList.add(ui); } notifyListeners(); } @@ -44,12 +46,12 @@ class PluginModel with ChangeNotifier { class LocationModel with ChangeNotifier { final Map pluginModels = {}; - void add(PluginId id, UiType ui) { + void add(PluginId id, List uiList) { if (pluginModels[id] != null) { - pluginModels[id]!.add(ui); + pluginModels[id]!.add(uiList); } else { var model = PluginModel(); - model.add(ui); + model.add(uiList); pluginModels[id] = model; notifyListeners(); } @@ -68,11 +70,11 @@ class LocationModel with ChangeNotifier { bool get isEmpty => pluginModels.isEmpty; } -void addLocationUi(String location, PluginId id, UiType ui) { +void addLocationUi(String location, PluginId id, List uiList) { if (_locationModels[location] == null) { _locationModels[location] = LocationModel(); } - _locationModels[location]?.add(id, ui); + _locationModels[location]?.add(id, uiList); } LocationModel? getLocationModel(String location) => _locationModels[location]; diff --git a/flutter/lib/plugin/widget.dart b/flutter/lib/plugin/widgets/desc_ui.dart similarity index 95% rename from flutter/lib/plugin/widget.dart rename to flutter/lib/plugin/widgets/desc_ui.dart index 19ad4cbc6..d67b298b5 100644 --- a/flutter/lib/plugin/widget.dart +++ b/flutter/lib/plugin/widgets/desc_ui.dart @@ -10,9 +10,9 @@ import 'package:get/get.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -import './manager.dart'; -import './model.dart'; -import './common.dart'; +import '../manager.dart'; +import '../model.dart'; +import '../common.dart'; // dup to flutter\lib\desktop\pages\desktop_setting_page.dart const double _kCheckBoxLeftMargin = 10; @@ -280,9 +280,15 @@ void handleReloading(Map evt, String peer) { return; } try { - final ui = UiType.create(json.decode(evt['ui'] as String)); - if (ui != null) { - addLocationUi(evt['location']!, evt['id']!, ui); + final uiList = []; + for (var e in json.decode(evt['ui'] as String)) { + final ui = UiType.create(e); + if (ui != null) { + uiList.add(ui); + } + } + if (uiList.isNotEmpty) { + addLocationUi(evt['location']!, evt['id']!, uiList); } } catch (e) { debugPrint('Failed handleReloading, json decode of ui, $e '); diff --git a/flutter/lib/plugin/widgets/desktop_settings.dart b/flutter/lib/plugin/widgets/desktop_settings.dart new file mode 100644 index 000000000..f1368fbef --- /dev/null +++ b/flutter/lib/plugin/widgets/desktop_settings.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/plugin/model.dart'; +import 'package:flutter_hbb/plugin/common.dart'; +import 'package:get/get.dart'; + +import '../manager.dart'; +import './desc_ui.dart'; + +// to-do: use settings from desktop_setting_page.dart +const double _kCardFixedWidth = 540; +const double _kCardLeftMargin = 15; +const double _kContentHMargin = 15; +const double _kTitleFontSize = 20; +const double _kVersionFontSize = 12; + +class DesktopSettingsCard extends StatefulWidget { + final PluginInfo plugin; + DesktopSettingsCard({ + Key? key, + required this.plugin, + }) : super(key: key); + + @override + State createState() => _DesktopSettingsCardState(); +} + +class _DesktopSettingsCardState extends State { + PluginInfo get plugin => widget.plugin; + bool get installed => plugin.installedVersion.isNotEmpty; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + header(), + body(), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), + ), + ], + ); + } + + Widget header() { + return Row( + children: [ + headerNameVersion(), + headerInstallEnable(), + ], + ).marginOnly( + left: _kContentHMargin, + top: 10, + bottom: 10, + right: _kContentHMargin, + ); + } + + Widget headerNameVersion() { + return Expanded( + child: Row( + children: [ + Text( + translate(widget.plugin.meta.name), + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kTitleFontSize, + ), + ), + SizedBox( + width: 5, + ), + Text( + plugin.meta.version, + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kVersionFontSize, + ), + ) + ], + ), + ); + } + + Widget headerButton(String label, VoidCallback onPressed) { + return Container( + child: ElevatedButton( + onPressed: onPressed, + child: Text(label), + ), + ); + } + + Widget headerInstallEnable() { + final installButton = headerButton(installed ? 'uninstall' : 'install', () { + bind.pluginInstall( + id: plugin.meta.id, + b: !installed, + ); + }); + + if (installed) { + final needUpdate = + plugin.installedVersion.compareTo(plugin.meta.version) < 0; + final updateButton = needUpdate + ? headerButton('update', () { + bind.pluginInstall( + id: plugin.meta.id, + b: !installed, + ); + }) + : Container(); + + final isEnabled = bind.pluginIsEnabled(id: plugin.meta.id); + final enableButton = !installed + ? Container() + : headerButton(isEnabled ? 'disable' : 'enable', () { + if (isEnabled) { + clearPlugin(plugin.meta.id); + } + bind.pluginEnable(id: plugin.meta.id, v: !isEnabled); + setState(() {}); + }); + return Row( + children: [ + updateButton, + SizedBox( + width: 10, + ), + installButton, + SizedBox( + width: 10, + ), + enableButton, + ], + ); + } else { + return installButton; + } + } + + Widget body() { + return Column(children: [ + author(), + description(), + more(), + ]).marginOnly( + left: _kCardLeftMargin, + top: 4, + right: _kContentHMargin, + ); + } + + Widget author() { + return Align( + alignment: Alignment.centerLeft, + child: Text(plugin.meta.author), + ); + } + + Widget description() { + return Align( + alignment: Alignment.centerLeft, + child: Text(plugin.meta.description), + ); + } + + Widget more() { + if (!installed) { + return Container(); + } + + final List children = []; + final model = getPluginModel(kLocationHostMainPlugin, plugin.meta.id); + if (model != null) { + children.add(PluginItem( + pluginId: plugin.meta.id, + peerId: '', + location: kLocationHostMainPlugin, + pluginModel: model, + isMenu: false, + )); + } + return ExpansionTile( + title: Text('Options'), + controlAffinity: ListTileControlAffinity.leading, + children: children, + ); + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4d7c21dfd..59fe91ced 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1498,7 +1498,7 @@ pub fn plugin_reload(_id: String) { } #[inline] -pub fn plugin_enable(_id: String, _v: bool) { +pub fn plugin_enable(_id: String, _v: bool) -> SyncReturn<()> { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -1512,6 +1512,7 @@ pub fn plugin_enable(_id: String, _v: bool) { } else { crate::plugin::unload_plugin(&_id); } + SyncReturn(()) } } diff --git a/src/plugin/desc.rs b/src/plugin/desc.rs index dc196d0ea..511d187a4 100644 --- a/src/plugin/desc.rs +++ b/src/plugin/desc.rs @@ -30,7 +30,7 @@ pub enum UiType { #[derive(Debug, Serialize, Deserialize)] pub struct Location { - pub ui: HashMap, + pub ui: HashMap>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/plugin/ipc.rs b/src/plugin/ipc.rs index a66fd4c72..73742cbda 100644 --- a/src/plugin/ipc.rs +++ b/src/plugin/ipc.rs @@ -152,7 +152,7 @@ async fn set_manager_plugin_config_async(id: &str, name: &str, value: String) -> Ok(()) } -async fn load_plugin_async(id: &str) -> ResultType<()> { +pub async fn load_plugin_async(id: &str) -> ResultType<()> { let mut c = connect(1000, "").await?; c.send(&Data::Plugin(Plugin::Load(id.to_owned()))).await?; Ok(()) diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs index 1828cd7e5..cfbc6216c 100644 --- a/src/plugin/manager.rs +++ b/src/plugin/manager.rs @@ -3,9 +3,7 @@ use super::{desc::Meta as PluginMeta, ipc::InstallStatus, *}; use crate::flutter; -#[cfg(not(debug_assertions))] -use hbb_common::toml; -use hbb_common::{allow_err, bail, config::load_path, log, tokio}; +use hbb_common::{allow_err, bail, log, tokio, toml}; use serde_derive::{Deserialize, Serialize}; use serde_json; use std::{ @@ -48,42 +46,15 @@ pub struct PluginInfo { static PLUGIN_SOURCE_LOCAL: &str = "local"; -#[cfg(not(debug_assertions))] fn get_plugin_source_list() -> Vec { // Only one source for now. vec![PluginSource { name: "rustdesk".to_string(), - url: "https://github.com/fufesou/rustdesk-plugins".to_string(), + url: "https://raw.githubusercontent.com/fufesou/rustdesk-plugins/main".to_string(), description: "".to_string(), }] } -#[cfg(debug_assertions)] -fn get_source_plugins() -> HashMap { - let meta_file = super::get_plugins_dir().unwrap().join("meta.toml"); - let mut plugins = HashMap::new(); - let manager_meta = load_path::(meta_file); - let source = PluginSource { - name: "rustdesk".to_string(), - url: "https://github.com/fufesou/rustdesk-plugins".to_string(), - description: "".to_string(), - }; - for meta in manager_meta.plugins.iter() { - plugins.insert( - meta.id.clone(), - PluginInfo { - source: source.clone(), - meta: meta.clone(), - installed_version: "".to_string(), - install_time: "".to_string(), - invalid_reason: "".to_string(), - }, - ); - } - plugins -} - -#[cfg(not(debug_assertions))] fn get_source_plugins() -> HashMap { let mut plugins = HashMap::new(); for source in get_plugin_source_list().into_iter() { @@ -142,7 +113,8 @@ pub fn load_plugin_list() { for (id, info) in super::plugins::get_plugin_infos().read().unwrap().iter() { if let Some(p) = plugins.get_mut(id) { p.install_time = info.install_time.clone(); - p.invalid_reason = info.desc.meta().version.clone(); + p.installed_version = info.desc.meta().version.clone(); + p.invalid_reason = "".to_string(); } else { plugins.insert( id.to_string(), @@ -171,9 +143,10 @@ pub fn install_plugin(id: &str) -> ResultType<()> { "{}/plugins/{}/{}_{}.zip", plugin.source.url, plugin.meta.id, plugin.meta.id, plugin.meta.version ); + // to-do: Support args with space in quotes. 'arg 1' and "arg 2" #[cfg(windows)] let _res = - crate::platform::elevate(&format!("--plugin-install '{}' '{}'", id, _plugin_url))?; + crate::platform::elevate(&format!("--plugin-install {} {}", id, _plugin_url))?; Ok(()) } None => { @@ -223,7 +196,8 @@ async fn handle_conn(mut stream: crate::ipc::Connection) { } InstallStatus::Finished => { allow_err!(super::plugins::load_plugin(&id)); - allow_err!(super::ipc::load_plugin(id)); + allow_err!(super::ipc::load_plugin_async(id).await); + load_plugin_list(); push_install_event(&id, "finished"); } InstallStatus::FailedCreating => { @@ -373,15 +347,31 @@ pub(super) mod install { } let filename = plugin_dir.join(format!("{}.zip", id)); + + // download if !download_file(id, url, &filename) { return; } + + let filename_to_remove = filename.clone(); + let _call_on_ret = crate::common::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + if let Err(e) = std::fs::remove_file(&filename_to_remove) { + log::error!("Failed to remove plugin file: {}", e); + } + }), + }; + + // install send_install_status(id, InstallStatus::Installing); if let Err(e) = do_install_file(&filename, &plugin_dir) { log::error!("Failed to install plugin: {}", e); send_install_status(id, InstallStatus::FailedInstalling); return; } + + // finished send_install_status(id, InstallStatus::Finished); } } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 878f0b98b..9542171f3 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -27,7 +27,6 @@ pub use plugins::{ reload_plugin, sync_ui, unload_plugin, unload_plugins, }; -const MSG_TO_UI_TYPE_PLUGIN_DESC: &str = "plugin_desc"; 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"; @@ -90,12 +89,12 @@ impl PluginReturn { } pub fn init() { - std::thread::spawn(move || manager::start_ipc()); - if is_server() { + if !is_server() { + std::thread::spawn(move || manager::start_ipc()); + } else { manager::remove_plugins(); - allow_err!(plugins::load_plugins()); } - load_plugin_list(); + allow_err!(plugins::load_plugins()); } #[inline] @@ -134,10 +133,10 @@ fn str_to_cstr_ret(s: &str) -> *const c_char { } #[inline] -fn free_c_ptr(ret: *mut c_void) { - if !ret.is_null() { +fn free_c_ptr(p: *mut c_void) { + if !p.is_null() { unsafe { - libc::free(ret); + libc::free(p); } } } diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index 01a60c1bd..ee4f1691f 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -157,6 +157,7 @@ struct InitData { impl Drop for InitData { fn drop(&mut self) { free_c_ptr(self.version as _); + free_c_ptr(self.info as _); } } @@ -350,18 +351,9 @@ fn load_plugin_path(path: &str) -> ResultType<()> { let init_info = serde_json::to_string(&InitInfo { is_server: crate::common::is_server(), })?; - let ptr_info = str_to_cstr_ret(&init_info); - let ptr_version = str_to_cstr_ret(crate::VERSION); - let _call_on_ret = crate::common::SimpleCallOnReturn { - b: true, - f: Box::new(move || { - free_c_ptr(ptr_info as _); - free_c_ptr(ptr_version as _); - }), - }; let init_data = InitData { - version: ptr_version as _, - info: ptr_info as _, + version: str_to_cstr_ret(crate::VERSION), + info: str_to_cstr_ret(&init_info) as _, cbs: Callbacks { msg: callback_msg::cb_msg, get_conf: config::cb_get_conf, @@ -378,7 +370,6 @@ fn load_plugin_path(path: &str) -> ResultType<()> { // update ui // Ui may be not ready now, so we need to update again once ui is ready. - update_ui_plugin_desc(&desc, None); reload_ui(&desc, None); let install_time = PathBuf::from(path) @@ -404,7 +395,6 @@ fn load_plugin_path(path: &str) -> ResultType<()> { pub fn sync_ui(sync_to: String) { for plugin in PLUGIN_INFO.read().unwrap().values() { - update_ui_plugin_desc(&plugin.desc, Some(&sync_to)); reload_ui(&plugin.desc, Some(&sync_to)); } } @@ -640,27 +630,6 @@ fn reload_ui(desc: &Desc, sync_to: Option<&str>) { } } -fn update_ui_plugin_desc(desc: &Desc, sync_to: Option<&str>) { - // This function is rarely used. There's no need to care about serialization efficiency here. - if let Ok(desc_str) = serde_json::to_string(desc) { - let mut m = HashMap::new(); - m.insert("name", MSG_TO_UI_TYPE_PLUGIN_DESC); - m.insert("desc", &desc_str); - let event = serde_json::to_string(&m).unwrap_or("".to_owned()); - match sync_to { - Some(channel) => { - let _res = flutter::push_global_event(channel, event.clone()); - } - None => { - 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_plugin_infos() -> Arc>> { PLUGIN_INFO.clone() }