diff --git a/Cargo.lock b/Cargo.lock index bb1f01897..8145032b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "ahash" version = "0.7.6" @@ -404,6 +416,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bindgen" version = "0.59.2" @@ -578,6 +596,27 @@ dependencies = [ "serde 1.0.163", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.16.7" @@ -737,6 +776,15 @@ dependencies = [ "regex", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -974,6 +1022,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.5.0" @@ -1525,6 +1579,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -2814,7 +2869,7 @@ dependencies = [ "tokio-util", "toml 0.7.3", "winapi 0.3.9", - "zstd", + "zstd 0.12.3+zstd.1.5.2", ] [[package]] @@ -2862,6 +2917,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hound" version = "3.5.0" @@ -4074,6 +4138,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -4267,6 +4337,17 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.12" @@ -4279,6 +4360,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -5175,6 +5268,7 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", + "zip", ] [[package]] @@ -5721,6 +5815,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "0.15.44" @@ -7291,19 +7391,58 @@ dependencies = [ ] [[package]] -name = "zstd" -version = "0.9.2+zstd.1.5.1" +name = "zip" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +checksum = "7e92305c174683d78035cbf1b70e18db6329cc0f1b9cae0a52ca90bf5bfe7125" dependencies = [ - "zstd-safe", + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.20", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe 6.0.5+zstd.1.5.4", ] [[package]] name = "zstd-safe" -version = "4.1.3+zstd.1.5.1" +version = "5.0.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" dependencies = [ "libc", "zstd-sys", @@ -7311,12 +7450,13 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", + "pkg-config", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f4a721073..5c9535a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ chrono = "0.4" cidr-utils = "0.5" libloading = "0.8" fon = "0.6" +zip = "0.6.5" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.15" diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index e6cfd516a..ae3889447 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,7 +10,7 @@ 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/manager.dart'; import 'package:flutter_hbb/plugin/model.dart'; import 'package:flutter_hbb/plugin/common.dart'; import 'package:flutter_hbb/plugin/widget.dart'; @@ -1449,12 +1449,10 @@ class _CheckboxState extends State<_Checkbox> { } class PluginCard extends StatefulWidget { - final PluginId pluginId; - final Desc desc; + final PluginInfo plugin; const PluginCard({ Key? key, - required this.pluginId, - required this.desc, + required this.plugin, }) : super(key: key); @override @@ -1462,40 +1460,43 @@ class PluginCard extends StatefulWidget { } 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(widget.pluginId); - await bind.pluginReload(id: widget.pluginId); + clearPlugin(pluginId); + await bind.pluginReload(id: pluginId); setState(() {}); }, ), _Checkbox( label: 'Enable', - getValue: () => bind.pluginIdIsEnabled(id: widget.pluginId), + getValue: () => bind.pluginIsEnabled(id: pluginId), setValue: (bool v) async { if (!v) { - clearPlugin(widget.pluginId); + clearPlugin(pluginId); } - await bind.pluginIdEnable(id: widget.pluginId, v: v); + await bind.pluginEnable(id: pluginId, v: v); setState(() {}); }, ), ]; - final model = getPluginModel(kLocationHostMainPlugin, widget.pluginId); + final model = getPluginModel(kLocationHostMainPlugin, pluginId); if (model != null) { children.add(PluginItem( - pluginId: widget.pluginId, + pluginId: pluginId, peerId: '', location: kLocationHostMainPlugin, pluginModel: model, isMenu: false, )); } - return _Card(title: widget.desc.name, children: children); + return _Card(title: pluginName, children: children); } } @@ -1509,14 +1510,12 @@ class _Plugin extends StatefulWidget { class _PluginState extends State<_Plugin> { // temp checkbox widget - List _buildCards(DescModel model) => [ + List _buildCards(PluginManager model) => [ _Card( title: 'Plugin', children: [], ), - ...model.all.entries - .map((entry) => PluginCard(pluginId: entry.key, desc: entry.value)) - .toList(), + ...model.plugins.map((entry) => PluginCard(plugin: entry)).toList(), ]; @override @@ -1525,8 +1524,8 @@ class _PluginState extends State<_Plugin> { return DesktopScrollWrapper( scrollController: scrollController, child: ChangeNotifierProvider.value( - value: DescModel.instance, - child: Consumer(builder: (context, model, child) { + value: pluginManager, + child: Consumer(builder: (context, model, child) { return ListView( physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e40f0615f..a91e1dc6f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,7 +17,7 @@ import 'package:flutter_hbb/models/server_model.dart'; 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/desc.dart'; +import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widget.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -230,8 +230,8 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.updateVoiceCallState(evt); } else if (name == 'fingerprint') { FingerprintState.find(peerId).value = evt['fingerprint'] ?? ''; - } else if (name == 'plugin_desc') { - updateDesc(evt); + } else if (name == 'plugin_manager') { + pluginManager.handleEvent(evt); } else if (name == 'plugin_event') { handlePluginEvent( evt, peerId, (Map e) => handleMsgBox(e, peerId)); diff --git a/flutter/lib/plugin/desc.dart b/flutter/lib/plugin/desc.dart deleted file mode 100644 index b46ec08b1..000000000 --- a/flutter/lib/plugin/desc.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'dart:convert'; -import 'dart:collection'; -import 'package:flutter/foundation.dart'; - -import './common.dart'; - -const String kValueTrue = '1'; -const String kValueFalse = '0'; - -class UiType { - String key; - String text; - String tooltip; - String action; - - UiType(this.key, this.text, this.tooltip, this.action); - - UiType.fromJson(Map json) - : key = json['key'] ?? '', - text = json['text'] ?? '', - tooltip = json['tooltip'] ?? '', - action = json['action'] ?? ''; - - static UiType? create(Map json) { - if (json['t'] == 'Button') { - return UiButton.fromJson(json['c']); - } else if (json['t'] == 'Checkbox') { - return UiCheckbox.fromJson(json['c']); - } else { - return null; - } - } -} - -class UiButton extends UiType { - String icon; - - UiButton( - {required String key, - required String text, - required this.icon, - required String tooltip, - required String action}) - : super(key, text, tooltip, action); - - UiButton.fromJson(Map json) - : icon = json['icon'] ?? '', - super.fromJson(json); -} - -class UiCheckbox extends UiType { - UiCheckbox( - {required String key, - required String text, - required String tooltip, - required String action}) - : super(key, text, tooltip, action); - - UiCheckbox.fromJson(Map json) : super.fromJson(json); -} - -class Location { - // location key: - // host|main|settings|plugin - // client|remote|toolbar|display - HashMap ui; - - Location(this.ui); - Location.fromJson(Map json) : ui = HashMap() { - (json['ui'] as Map).forEach((key, value) { - var ui = UiType.create(value); - if (ui != null) { - this.ui[ui.key] = ui; - } - }); - } -} - -class ConfigItem { - String key; - String description; - String defaultValue; - - ConfigItem(this.key, this.defaultValue, this.description); - ConfigItem.fromJson(Map json) - : key = json['key'] ?? '', - description = json['description'] ?? '', - defaultValue = json['default'] ?? ''; - - static String get trueValue => kValueTrue; - static String get falseValue => kValueFalse; - static bool isTrue(String value) => value == kValueTrue; - static bool isFalse(String value) => value == kValueFalse; -} - -class Config { - List shared; - List peer; - - Config(this.shared, this.peer); - Config.fromJson(Map json) - : shared = (json['shared'] as List) - .map((e) => ConfigItem.fromJson(e)) - .toList(), - peer = (json['peer'] as List) - .map((e) => ConfigItem.fromJson(e)) - .toList(); -} - -class Desc { - String id; - String name; - String version; - String description; - String author; - String home; - String license; - String published; - String released; - String github; - Location location; - Config config; - - Desc( - this.id, - this.name, - this.version, - this.description, - this.author, - this.home, - this.license, - this.published, - this.released, - this.github, - this.location, - this.config); - - Desc.fromJson(Map json) - : id = json['id'] ?? '', - name = json['name'] ?? '', - version = json['version'] ?? '', - description = json['description'] ?? '', - author = json['author'] ?? '', - home = json['home'] ?? '', - license = json['license'] ?? '', - published = json['published'] ?? '', - released = json['released'] ?? '', - github = json['github'] ?? '', - location = Location.fromJson(json['location']), - config = Config.fromJson(json['config']); -} - -class DescModel with ChangeNotifier { - final data = {}; - - DescModel._(); - - void _updateDesc(Map desc) { - try { - Desc d = Desc.fromJson(json.decode(desc['desc'])); - data[d.id] = d; - notifyListeners(); - } catch (e) { - debugPrint('DescModel json.decode fail(): $e'); - } - } - - Desc? _getDesc(String id) { - return data[id]; - } - - Map get all => data; - - static final DescModel _instance = DescModel._(); - static DescModel get instance => _instance; -} - -void updateDesc(Map desc) => - DescModel.instance._updateDesc(desc); -Desc? getDesc(String id) => DescModel.instance._getDesc(id); diff --git a/flutter/lib/plugin/manager.dart b/flutter/lib/plugin/manager.dart new file mode 100644 index 000000000..c445efaf0 --- /dev/null +++ b/flutter/lib/plugin/manager.dart @@ -0,0 +1,270 @@ +// The plugin manager is a singleton class that manages the plugins. +// 1. It merge metadata and the desc of plugins. + +import 'dart:collection'; +import 'package:flutter/material.dart'; + +const String kValueTrue = '1'; +const String kValueFalse = '0'; + +class ConfigItem { + String key; + String description; + String defaultValue; + + ConfigItem(this.key, this.defaultValue, this.description); + ConfigItem.fromJson(Map json) + : key = json['key'] ?? '', + description = json['description'] ?? '', + defaultValue = json['default'] ?? ''; + + static String get trueValue => kValueTrue; + static String get falseValue => kValueFalse; + static bool isTrue(String value) => value == kValueTrue; + static bool isFalse(String value) => value == kValueFalse; +} + +class UiType { + String key; + String text; + String tooltip; + String action; + + UiType(this.key, this.text, this.tooltip, this.action); + + UiType.fromJson(Map json) + : key = json['key'] ?? '', + text = json['text'] ?? '', + tooltip = json['tooltip'] ?? '', + action = json['action'] ?? ''; + + static UiType? create(Map json) { + if (json['t'] == 'Button') { + return UiButton.fromJson(json['c']); + } else if (json['t'] == 'Checkbox') { + return UiCheckbox.fromJson(json['c']); + } else { + return null; + } + } +} + +class UiButton extends UiType { + String icon; + + UiButton( + {required String key, + required String text, + required this.icon, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiButton.fromJson(Map json) + : icon = json['icon'] ?? '', + super.fromJson(json); +} + +class UiCheckbox extends UiType { + UiCheckbox( + {required String key, + required String text, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiCheckbox.fromJson(Map json) : super.fromJson(json); +} + +class Location { + // location key: + // host|main|settings|plugin + // client|remote|toolbar|display + HashMap ui; + + Location(this.ui); + Location.fromJson(Map json) : ui = HashMap() { + (json['ui'] as Map).forEach((key, value) { + var ui = UiType.create(value); + if (ui != null) { + this.ui[ui.key] = ui; + } + }); + } +} + +class PublishInfo { + PublishInfo({ + required this.lastReleased, + required this.published, + }); + + final DateTime lastReleased; + final DateTime published; +} + +class Meta { + Meta({ + required this.id, + required this.name, + required this.version, + required this.description, + required this.author, + required this.home, + required this.license, + required this.publishInfo, + required this.source, + }); + + final String id; + final String name; + final String version; + final String description; + final String author; + final String home; + final String license; + final PublishInfo publishInfo; + final String source; +} + +class SourceInfo { + String name; // 1. RustDesk github 2. Local + String url; + String description; + + SourceInfo({ + required this.name, + required this.url, + required this.description, + }); +} + +class PluginInfo with ChangeNotifier { + SourceInfo sourceInfo; + Meta meta; + String installedVersion; // It is empty if not installed. + DateTime installTime; + String invalidReason; // It is empty if valid. + + PluginInfo({ + required this.sourceInfo, + required this.meta, + required this.installedVersion, + required this.installTime, + required this.invalidReason, + }); + + void update(PluginInfo plugin) { + assert(plugin.meta.id == meta.id, 'Plugin id not match'); + if (plugin.meta.id != meta.id) { + // log error + return; + } + sourceInfo = plugin.sourceInfo; + meta = plugin.meta; + installedVersion = plugin.installedVersion; + installTime = plugin.installTime; + invalidReason = plugin.invalidReason; + notifyListeners(); + } +} + +class PluginManager with ChangeNotifier { + String failedReason = ''; // The reason of failed to load plugins. + final List _plugins = []; + + PluginManager._(); + static final PluginManager _instance = PluginManager._(); + static PluginManager get instance => _instance; + + List get plugins => _plugins; + + PluginInfo? getPlugin(String id) { + for (var p in _plugins) { + if (p.meta.id == id) { + return p; + } + } + return null; + } + + void handleEvent(Map evt) { + if (evt['plugin_list'] != null) { + _handlePluginList(evt['plugin_list']); + } else if (evt['plugin_update'] != null) { + _handlePluginUpdate(evt['plugin_update']); + } else { + debugPrint('Failed to handle manager event: $evt'); + } + } + + void _handlePluginUpdate(Map evt) { + final plugin = _getPluginFromEvent(evt); + if (plugin == null) { + return; + } + for (var i = 0; i < _plugins.length; i++) { + if (_plugins[i].meta.id == plugin.meta.id) { + _plugins[i].update(plugin); + return; + } + } + } + + void _handlePluginList(List evt) { + _plugins.clear(); + + for (var p in evt) { + final plugin = _getPluginFromEvent(p); + if (plugin == null) { + continue; + } + _plugins.add(plugin); + } + + notifyListeners(); + } + + PluginInfo? _getPluginFromEvent(Map evt) { + final s = evt['source']; + assert(s != null, 'Source is null'); + if (s == null) { + return null; + } + final source = SourceInfo( + name: s['name'], + url: s['url'] ?? '', + description: s['description'] ?? '', + ); + + final m = evt['meta']; + assert(m != null, 'Meta is null'); + if (m == null) { + return null; + } + final meta = Meta( + id: m['id'], + name: m['name'], + version: m['version'], + description: m['description'] ?? '', + author: m['author'], + home: m['home'] ?? '', + license: m['license'] ?? '', + source: m['source'] ?? '', + publishInfo: PublishInfo( + lastReleased: DateTime.parse( + m['publish_info']?['lastReleased'] ?? '1970-01-01T00+00:00'), + published: DateTime.parse( + m['publish_info']?['published'] ?? '1970-01-01T00+00:00')), + ); + return PluginInfo( + sourceInfo: source, + meta: meta, + installedVersion: evt['installed_version'], + installTime: evt['install_time'], + invalidReason: evt['invalid_reason'] ?? '', + ); + } +} + +PluginManager get pluginManager => PluginManager.instance; diff --git a/flutter/lib/plugin/model.dart b/flutter/lib/plugin/model.dart index 120f33117..44579bcd6 100644 --- a/flutter/lib/plugin/model.dart +++ b/flutter/lib/plugin/model.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import './common.dart'; -import './desc.dart'; +import './manager.dart'; final Map _locationModels = {}; final Map _optionModels = {}; diff --git a/flutter/lib/plugin/widget.dart b/flutter/lib/plugin/widget.dart index 99a8b3ace..19ad4cbc6 100644 --- a/flutter/lib/plugin/widget.dart +++ b/flutter/lib/plugin/widget.dart @@ -10,7 +10,7 @@ import 'package:get/get.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -import './desc.dart'; +import './manager.dart'; import './model.dart'; import './common.dart'; @@ -247,7 +247,7 @@ class PluginItem extends StatelessWidget { }) { final event = MsgFromUi( id: pluginId, - name: getDesc(pluginId)?.name ?? '', + name: pluginManager.getPlugin(pluginId)?.meta.name ?? '', location: location, key: key, value: diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 50b690f4a..e886ad1f5 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -16,7 +16,7 @@ bytes = { version = "1.4", features = ["serde"] } log = "0.4" env_logger = "0.10" socket2 = { version = "0.3", features = ["reuseport"] } -zstd = "0.9" +zstd = "0.12" quinn = {version = "0.9", optional = true } anyhow = "1.0" futures-util = "0.3" diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs index e7668a949..c52dd93a1 100644 --- a/libs/hbb_common/src/compress.rs +++ b/libs/hbb_common/src/compress.rs @@ -1,23 +1,28 @@ -use std::cell::RefCell; -use zstd::block::{Compressor, Decompressor}; +use std::{cell::RefCell, io}; +use zstd::bulk::{Compressor, Decompressor}; +// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), +// which is currently 22. Levels >= 20 +// Default level is ZSTD_CLEVEL_DEFAULT==3. +// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT thread_local! { - static COMPRESSOR: RefCell = RefCell::new(Compressor::new()); - static DECOMPRESSOR: RefCell = RefCell::new(Decompressor::new()); + static COMPRESSOR: RefCell>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); + static DECOMPRESSOR: RefCell>> = RefCell::new(Decompressor::new()); } -/// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), -/// which is currently 22. Levels >= 20 -/// Default level is ZSTD_CLEVEL_DEFAULT==3. -/// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT -pub fn compress(data: &[u8], level: i32) -> Vec { +pub fn compress(data: &[u8]) -> Vec { let mut out = Vec::new(); COMPRESSOR.with(|c| { if let Ok(mut c) = c.try_borrow_mut() { - match c.compress(data, level) { - Ok(res) => out = res, + match &mut *c { + Ok(c) => match c.compress(data) { + Ok(res) => out = res, + Err(err) => { + crate::log::debug!("Failed to compress: {}", err); + } + }, Err(err) => { - crate::log::debug!("Failed to compress: {}", err); + crate::log::debug!("Failed to get compressor: {}", err); } } } @@ -29,14 +34,21 @@ pub fn decompress(data: &[u8]) -> Vec { let mut out = Vec::new(); DECOMPRESSOR.with(|d| { if let Ok(mut d) = d.try_borrow_mut() { - const MAX: usize = 1024 * 1024 * 64; - const MIN: usize = 1024 * 1024; - let mut n = 30 * data.len(); - n = n.clamp(MIN, MAX); - match d.decompress(data, n) { - Ok(res) => out = res, + match &mut *d { + Ok(d) => { + const MAX: usize = 1024 * 1024 * 64; + const MIN: usize = 1024 * 1024; + let mut n = 30 * data.len(); + n = n.clamp(MIN, MAX); + match d.decompress(data, n) { + Ok(res) => out = res, + Err(err) => { + crate::log::debug!("Failed to decompress: {}", err); + } + } + } Err(err) => { - crate::log::debug!("Failed to decompress: {}", err); + crate::log::debug!("Failed to get decompressor: {}", err); } } } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 41160f49d..6b8205acf 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -10,7 +10,7 @@ use crate::{bail, get_version_number, message_proto::*, ResultType, Stream}; // https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html use crate::{ compress::{compress, decompress}, - config::{Config, COMPRESS_LEVEL}, + config::Config, }; pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { @@ -481,7 +481,7 @@ impl TransferJob { } else { self.finished_size += offset as u64; if !is_compressed_file(name) { - let tmp = compress(&buf, COMPRESS_LEVEL); + let tmp = compress(&buf); if tmp.len() < buf.len() { buf = tmp; compressed = true; diff --git a/src/common.rs b/src/common.rs index f8836e94f..7187b02cc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -19,7 +19,7 @@ use hbb_common::compress::decompress; use hbb_common::{ allow_err, compress::compress as compress_func, - config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, + config::{self, Config, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, protobuf::Enum, @@ -68,6 +68,19 @@ lazy_static::lazy_static! { static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); } +pub struct SimpleCallOnReturn { + pub b: bool, + pub f: Box, +} + +impl Drop for SimpleCallOnReturn { + fn drop(&mut self) { + if self.b { + (self.f)(); + } + } +} + pub fn global_init() -> bool { #[cfg(target_os = "linux")] { @@ -98,7 +111,7 @@ pub fn valid_for_numlock(evt: &KeyEvent) -> bool { pub fn create_clipboard_msg(content: String) -> Message { let bytes = content.into_bytes(); - let compressed = compress_func(&bytes, COMPRESS_LEVEL); + let compressed = compress_func(&bytes); let compress = compressed.len() < bytes.len(); let content = if compress { compressed } else { bytes }; let mut msg = Message::new(); diff --git a/src/core_main.rs b/src/core_main.rs index 00e7b9829..714e361c3 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -112,7 +112,7 @@ pub fn core_main() -> Option> { #[cfg(not(debug_assertions))] let load_plugins = crate::platform::is_installed(); if load_plugins { - hbb_common::allow_err!(crate::plugin::load_plugins()); + crate::plugin::init(); } } if args.is_empty() { @@ -240,6 +240,18 @@ pub fn core_main() -> Option> { #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::connection_manager::start_cm_no_ui(); return None; + } else { + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if args[0] == "--plugin-install" { + if args.len() == 3 { + crate::plugin::privileged_install_plugin(&args[1], &args[2]); + } + return None; + } else if args[0] == "--plugin-uninstall" { + // Do nothing + return None; + } } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8f795d1f9..444b3dccb 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1497,17 +1497,8 @@ pub fn plugin_reload(_id: String) { } } -pub fn plugin_id_uninstall(_id: String) { - #[cfg(feature = "plugin_framework")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - crate::plugin::unload_plugin(&_id); - allow_err!(crate::plugin::ipc::uninstall_plugin(&_id)); - } -} - #[inline] -pub fn plugin_id_enable(_id: String, _v: bool) { +pub fn plugin_enable(_id: String, _v: bool) { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -1517,14 +1508,14 @@ pub fn plugin_id_enable(_id: String, _v: bool) { _v.to_string() )); if _v { - allow_err!(crate::plugin::load_plugin(None, Some(&_id))); + allow_err!(crate::plugin::load_plugin(&_id)); } else { crate::plugin::unload_plugin(&_id); } } } -pub fn plugin_id_is_enabled(_id: String) -> SyncReturn { +pub fn plugin_is_enabled(_id: String) -> SyncReturn { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -1575,6 +1566,29 @@ pub fn plugin_sync_ui(_sync_to: String) { } } +pub fn plugin_list_reload() { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::load_plugin_list(false); + } +} + +pub fn plugin_install(id: String, b: bool) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if b { + allow_err!(crate::plugin::user_install_plugin(&id)); + } else { + // to-do: uninstall plugin + // 1. unload 2. remove configs 3. remove config files + // allow_err!(super::unload_plugin(&id)); + crate::plugin::uninstall_plugin(&id); + } + } +} + #[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 f939cf6bc..4f44ced42 100644 --- a/src/plugin/config.rs +++ b/src/plugin/config.rs @@ -280,7 +280,7 @@ impl ManagerConfig { 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))); + allow_err!(super::load_plugin(id)); } else { super::unload_plugin(id); } diff --git a/src/plugin/desc.rs b/src/plugin/desc.rs index 5aee35b96..dc196d0ea 100644 --- a/src/plugin/desc.rs +++ b/src/plugin/desc.rs @@ -46,18 +46,29 @@ pub struct Config { pub peer: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishInfo { + pub published: String, + pub last_released: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Meta { + pub id: String, + pub name: String, + pub version: String, + pub description: String, + pub author: String, + pub home: String, + pub license: String, + pub source: String, + pub publish_info: PublishInfo, +} + #[derive(Debug, Serialize, Deserialize)] pub struct Desc { - id: String, - name: String, - version: String, - description: String, - author: String, - home: String, - license: String, - published: String, - released: String, - github: String, + meta: Meta, + need_reboot: bool, location: Location, config: Config, listen_events: Vec, @@ -69,44 +80,8 @@ impl Desc { Ok(serde_json::from_str(s.to_str()?)?) } - pub fn id(&self) -> &str { - &self.id - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn version(&self) -> &str { - &self.version - } - - pub fn description(&self) -> &str { - &self.description - } - - pub fn author(&self) -> &str { - &self.author - } - - pub fn home(&self) -> &str { - &self.home - } - - pub fn license(&self) -> &str { - &self.license - } - - pub fn published(&self) -> &str { - &self.published - } - - pub fn released(&self) -> &str { - &self.released - } - - pub fn github(&self) -> &str { - &self.github + pub fn meta(&self) -> &Meta { + &self.meta } pub fn location(&self) -> &Location { diff --git a/src/plugin/ipc.rs b/src/plugin/ipc.rs index 1c41e4158..a66fd4c72 100644 --- a/src/plugin/ipc.rs +++ b/src/plugin/ipc.rs @@ -5,14 +5,25 @@ use serde_derive::{Deserialize, Serialize}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum InstallStatus { + Downloading(u8), + Installing, + Finished, + FailedCreating, + FailedDownloading, + FailedInstalling, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum Plugin { Config(String, String, Option), ManagerConfig(String, Option), ManagerPluginConfig(String, String, Option), + Load(String), Reload(String), - Uninstall(String), + InstallStatus((String, InstallStatus)), } #[tokio::main(flavor = "current_thread")] @@ -46,13 +57,13 @@ pub async fn set_manager_plugin_config(id: &str, name: &str, value: String) -> R } #[tokio::main(flavor = "current_thread")] -pub async fn reload_plugin(id: &str) -> ResultType<()> { - reload_plugin_async(id).await +pub async fn load_plugin(id: &str) -> ResultType<()> { + load_plugin_async(id).await } #[tokio::main(flavor = "current_thread")] -pub async fn uninstall_plugin(id: &str) -> ResultType<()> { - uninstall_plugin_async(id).await +pub async fn reload_plugin(id: &str) -> ResultType<()> { + reload_plugin_async(id).await } async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType> { @@ -141,16 +152,15 @@ async fn set_manager_plugin_config_async(id: &str, name: &str, value: String) -> Ok(()) } -async fn reload_plugin_async(id: &str) -> ResultType<()> { +async fn load_plugin_async(id: &str) -> ResultType<()> { let mut c = connect(1000, "").await?; - c.send(&Data::Plugin(Plugin::Reload(id.to_owned()))).await?; + c.send(&Data::Plugin(Plugin::Load(id.to_owned()))).await?; Ok(()) } -async fn uninstall_plugin_async(id: &str) -> ResultType<()> { +async fn reload_plugin_async(id: &str) -> ResultType<()> { let mut c = connect(1000, "").await?; - c.send(&Data::Plugin(Plugin::Uninstall(id.to_owned()))) - .await?; + c.send(&Data::Plugin(Plugin::Reload(id.to_owned()))).await?; Ok(()) } @@ -158,7 +168,7 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { match plugin { Plugin::Config(id, name, value) => match value { None => { - let value = crate::plugin::SharedConfig::get(&id, &name); + let value = super::SharedConfig::get(&id, &name); allow_err!( stream .send(&Data::Plugin(Plugin::Config(id, name, value))) @@ -166,12 +176,12 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { ); } Some(value) => { - allow_err!(crate::plugin::SharedConfig::set(&id, &name, &value)); + allow_err!(super::SharedConfig::set(&id, &name, &value)); } }, Plugin::ManagerConfig(name, value) => match value { None => { - let value = crate::plugin::ManagerConfig::get_option(&name); + let value = super::ManagerConfig::get_option(&name); allow_err!( stream .send(&Data::Plugin(Plugin::ManagerConfig(name, value))) @@ -179,12 +189,12 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { ); } Some(value) => { - crate::plugin::ManagerConfig::set_option(&name, &value); + super::ManagerConfig::set_option(&name, &value); } }, Plugin::ManagerPluginConfig(id, name, value) => match value { None => { - let value = crate::plugin::ManagerConfig::get_plugin_option(&id, &name); + let value = super::ManagerConfig::get_plugin_option(&id, &name); allow_err!( stream .send(&Data::Plugin(Plugin::ManagerPluginConfig(id, name, value))) @@ -192,16 +202,15 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { ); } Some(value) => { - crate::plugin::ManagerConfig::set_plugin_option(&id, &name, &value); + super::ManagerConfig::set_plugin_option(&id, &name, &value); } }, + Plugin::Load(id) => { + allow_err!(super::load_plugin(&id)); + } Plugin::Reload(id) => { - allow_err!(crate::plugin::reload_plugin(&id)); - } - Plugin::Uninstall(_id) => { - // to-do: uninstall plugin - // 1. unload 2. remove configs 3. remove config files - // allow_err!(crate::plugin::unload_plugin(&id)); + allow_err!(super::reload_plugin(&id)); } + _ => {} } } diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs new file mode 100644 index 000000000..e1d9ead9b --- /dev/null +++ b/src/plugin/manager.rs @@ -0,0 +1,370 @@ +// 1. Check update. +// 2. Install or uninstall. + +use super::{desc::Meta as PluginMeta, ipc::InstallStatus, *}; +use crate::{common::is_server, flutter}; +use hbb_common::{allow_err, bail, log, tokio}; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +const MSG_TO_UI_PLUGIN_MANAGER_LIST: &str = "plugin_list"; +const MSG_TO_UI_PLUGIN_MANAGER_UPDATE: &str = "plugin_update"; +const MSG_TO_UI_PLUGIN_MANAGER_INSTALL: &str = "plugin_install"; + +const IPC_PLUGIN_POSTFIX: &str = "_plugin"; + +lazy_static::lazy_static! { + static ref PLUGIN_INFO: Arc>> = Arc::new(Mutex::new(HashMap::new())); +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ManagerMeta { + pub version: String, + pub description: String, + pub plugins: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginSource { + pub name: String, + pub url: String, + pub description: String, +} + +#[derive(Debug, Serialize)] +pub struct PluginInfo { + pub source: PluginSource, + pub plugin: PluginMeta, + pub installed_version: String, + pub install_time: String, + pub invalid_reason: String, +} + +static PLUGIN_SOURCE_LOCAL: &str = "local"; +pub(super) static PLUGIN_SOURCE_LOCAL_URL: &str = "plugins"; + +fn get_plugin_source_list() -> Vec { + // Only one source for now. + vec![PluginSource { + name: "rustdesk".to_string(), + #[cfg(debug_assertions)] + url: PLUGIN_SOURCE_LOCAL_URL.to_string(), + #[cfg(not(debug_assertions))] + url: "https://github.com/fufesou/rustdesk-plugins".to_string(), + description: "".to_string(), + }] +} + +fn get_source_plugins() -> HashMap { + let mut plugins = HashMap::new(); + for source in get_plugin_source_list().into_iter() { + let url = format!("{}/meta.json", source.url); + match reqwest::blocking::get(&url) { + Ok(resp) => { + if !resp.status().is_success() { + log::error!( + "Failed to get plugin list from '{}', status code: {}", + url, + resp.status() + ); + } + match resp.json::() { + Ok(meta) => { + for plugin in meta.plugins.iter() { + plugins.insert( + plugin.id.clone(), + PluginInfo { + source: source.clone(), + plugin: plugin.clone(), + installed_version: "".to_string(), + install_time: "".to_string(), + invalid_reason: "".to_string(), + }, + ); + } + } + Err(e) => log::error!("Failed to parse plugin list from '{}', {}", url, e), + } + } + Err(e) => log::error!("Failed to get plugin list from '{}', {}", url, e), + } + } + plugins +} + +fn send_plugin_list_event(plugins: &HashMap) { + let mut plugin_list = plugins.values().collect::>(); + plugin_list.sort_by(|a, b| a.plugin.name.cmp(&b.plugin.name)); + if let Ok(plugin_list) = serde_json::to_string(&plugin_list) { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER); + m.insert(MSG_TO_UI_PLUGIN_MANAGER_LIST, &plugin_list); + if let Ok(event) = serde_json::to_string(&m) { + let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); + } + } +} + +pub fn load_plugin_list(load_local: bool) { + let mut plugin_info_lock = PLUGIN_INFO.lock().unwrap(); + + if load_local { + if is_server() { + allow_err!(super::plugins::load_plugins()); + return; + } + } + + let mut plugins = get_source_plugins(); + 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(); + } else { + plugins.insert( + id.to_string(), + PluginInfo { + source: PluginSource { + name: PLUGIN_SOURCE_LOCAL.to_string(), + url: PLUGIN_SOURCE_LOCAL_URL.to_string(), + description: "".to_string(), + }, + plugin: info.desc.meta().clone(), + installed_version: info.desc.meta().version.clone(), + install_time: info.install_time.clone(), + invalid_reason: "".to_string(), + }, + ); + } + } + send_plugin_list_event(&plugins); + *plugin_info_lock = plugins; +} + +pub fn install_plugin(id: &str) -> ResultType<()> { + match PLUGIN_INFO.lock().unwrap().get(id) { + Some(plugin) => { + let _plugin_url = format!( + "{}/plugins/{}/{}_{}.zip", + plugin.source.url, plugin.plugin.id, plugin.plugin.id, plugin.plugin.version + ); + #[cfg(windows)] + let _res = + crate::platform::elevate(&format!("--plugin-install '{}' '{}'", id, _plugin_url))?; + Ok(()) + } + None => { + bail!("Plugin not found: {}", id); + } + } +} + +pub(super) fn remove_plugins() { + +} + +// 1. Add to uninstall list. +// 2. Try remove. +// 2. Remove on the next start. +pub fn uninstall_plugin(id: &str) { + // to-do: add to uninstall list. + super::plugins::unload_plugin(id); +} + +fn push_install_event(id: &str, msg: &str) { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER); + m.insert("id", id); + m.insert(MSG_TO_UI_PLUGIN_MANAGER_INSTALL, msg); + if let Ok(event) = serde_json::to_string(&m) { + let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); + } +} + +async fn handle_conn(mut stream: crate::ipc::Connection) { + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::trace!("plugin ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match &data { + crate::ipc::Data::Plugin(super::ipc::Plugin::InstallStatus((id, status))) => { + match status { + InstallStatus::Downloading(n) => { + push_install_event(&id, &format!("downloading-{}", n)); + }, + InstallStatus::Installing => { + push_install_event(&id, "installing"); + } + InstallStatus::Finished => { + allow_err!(super::plugins::load_plugin(&id)); + allow_err!(super::ipc::load_plugin(id)); + push_install_event(&id, "finished"); + } + InstallStatus::FailedCreating => { + push_install_event(&id, "failed-creating"); + } + InstallStatus::FailedDownloading => { + push_install_event(&id, "failed-downloading"); + } + InstallStatus::FailedInstalling => { + push_install_event(&id, "failed-installing"); + } + } + } + _ => {} + } + } + _ => { + } + } + } + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main] +pub async fn start_ipc() { + match crate::ipc::new_listener(IPC_PLUGIN_POSTFIX).await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + tokio::spawn(handle_conn(crate::ipc::Connection::new(stream))); + } + Err(err) => { + log::error!("Couldn't get plugin client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start plugin ipc server: {}", err); + } + } +} + +// install process +pub(super) mod install { + use super::IPC_PLUGIN_POSTFIX; + use crate::{ + ipc::{connect, Data}, + plugin::ipc::{InstallStatus, Plugin}, + }; + use hbb_common::{allow_err, bail, log, tokio, ResultType}; + use std::{ + fs::File, + io::{BufReader, BufWriter, Write}, + path::PathBuf, + }; + use zip::ZipArchive; + + #[tokio::main(flavor = "current_thread")] + async fn send_install_status(id: &str, status: InstallStatus) { + allow_err!(_send_install_status(id, status).await); + } + + async fn _send_install_status(id: &str, status: InstallStatus) -> ResultType<()> { + let mut c = connect(1_000, IPC_PLUGIN_POSTFIX).await?; + c.send(&Data::Plugin(Plugin::InstallStatus(( + id.to_string(), + status, + )))) + .await?; + Ok(()) + } + + fn download_to_file(url: &str, file: File) -> ResultType<()> { + let resp = match reqwest::blocking::get(url) { + Ok(resp) => resp, + Err(e) => { + bail!("get plugin from '{}', {}", url, e); + } + }; + + if !resp.status().is_success() { + bail!("get plugin from '{}', status code: {}", url, resp.status()); + } + + let mut writer = BufWriter::new(file); + writer.write_all(resp.bytes()?.as_ref())?; + Ok(()) + } + + fn download_file(id: &str, url: &str, filename: &PathBuf) -> bool { + let file = match File::create(filename) { + Ok(f) => f, + Err(e) => { + log::error!("Failed to create plugin file: {}", e); + send_install_status(id, InstallStatus::FailedCreating); + return false; + } + }; + if let Err(e) = download_to_file(url, file) { + log::error!("Failed to download plugin '{}', {}", id, e); + send_install_status(id, InstallStatus::FailedDownloading); + return false; + } + true + } + + fn do_install_file(filename: &PathBuf, target_dir: &PathBuf) -> ResultType<()> { + let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?; + for i in 0..zip.len() { + let mut file = zip.by_index(i)?; + let file_path = target_dir.join(file.name()); + if file.name().ends_with("/") { + std::fs::create_dir_all(&file_path)?; + } else { + if let Some(p) = file_path.parent() { + if !p.exists() { + std::fs::create_dir_all(&p)?; + } + } + let mut outfile = File::create(&file_path)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + Ok(()) + } + + pub fn install_plugin(id: &str, url: &str) { + let plugin_dir = match super::super::get_plugin_dir(id) { + Ok(d) => d, + Err(e) => { + send_install_status(id, InstallStatus::FailedCreating); + log::error!("Failed to get plugin dir: {}", e); + return; + } + }; + if !plugin_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&plugin_dir) { + send_install_status(id, InstallStatus::FailedCreating); + log::error!("Failed to create plugin dir: {}", e); + return; + } + } + + let filename = plugin_dir.join(format!("{}.zip", id)); + if !download_file(id, url, &filename) { + return; + } + 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; + } + send_install_status(id, InstallStatus::Finished); + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index db27ea31c..cd5e45a38 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -1,6 +1,8 @@ -use hbb_common::{libc, ResultType}; +use hbb_common::{libc, tokio, ResultType}; use std::{ + env, ffi::{c_char, c_int, c_void, CStr}, + path::PathBuf, ptr::null, }; @@ -10,20 +12,26 @@ mod config; pub mod desc; mod errno; pub mod ipc; +mod manager; pub mod native; pub mod native_handlers; mod plog; mod plugins; +pub use manager::{ + install::install_plugin as privileged_install_plugin, install_plugin as user_install_plugin, + load_plugin_list, uninstall_plugin, +}; pub use plugins::{ handle_client_event, handle_listen_event, handle_server_event, handle_ui_event, load_plugin, - load_plugins, reload_plugin, sync_ui, unload_plugin, unload_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"; +const MSG_TO_UI_TYPE_PLUGIN_MANAGER: &str = "plugin_manager"; pub const EVENT_ON_CONN_CLIENT: &str = "on_conn_client"; pub const EVENT_ON_CONN_SERVER: &str = "on_conn_server"; @@ -32,6 +40,8 @@ pub const EVENT_ON_CONN_CLOSE_SERVER: &str = "on_conn_close_server"; pub use config::{ManagerConfig, PeerConfig, SharedConfig}; +use crate::common::is_server; + /// Common plugin return. /// /// [Note] @@ -77,6 +87,25 @@ impl PluginReturn { } } +pub fn init() { + std::thread::spawn(move || manager::start_ipc()); + if is_server() { + manager::remove_plugins(); + } + load_plugin_list(true); +} + +#[inline] +fn get_plugins_dir() -> ResultType { + // to-do: linux and macos + Ok(PathBuf::from(env::var("ProgramData")?).join(manager::PLUGIN_SOURCE_LOCAL_URL)) +} + +#[inline] +fn get_plugin_dir(id: &str) -> ResultType { + Ok(get_plugins_dir()?.join(id)) +} + #[inline] fn cstr_to_string(cstr: *const c_char) -> ResultType { Ok(String::from_utf8(unsafe { diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index 1c8211e21..01a60c1bd 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -15,6 +15,7 @@ use std::{ ffi::{c_char, c_void}, path::PathBuf, sync::{Arc, RwLock}, + time::SystemTime, }; const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; @@ -26,9 +27,10 @@ lazy_static::lazy_static! { static ref PLUGINS: Arc>> = Default::default(); } -struct PluginInfo { - path: String, - desc: Desc, +pub(super) struct PluginInfo { + pub path: String, + pub install_time: String, + pub desc: Desc, } /// Initialize the plugins. @@ -136,6 +138,11 @@ struct Callbacks { native: CallbackNative, } +#[derive(Serialize)] +struct InitInfo { + is_server: bool, +} + /// The plugin initialize data. /// version: The version of the plugin, can't be nullptr. /// local_peer_id: The local peer id, can't be nullptr. @@ -143,6 +150,7 @@ struct Callbacks { #[repr(C)] struct InitData { version: *const c_char, + info: *const c_char, cbs: Callbacks, } @@ -255,34 +263,54 @@ const DYLIB_SUFFIX: &str = ".so"; #[cfg(target_os = "macos")] const DYLIB_SUFFIX: &str = ".dylib"; -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 filename = entry.file_name(); - let filename = filename.to_str().unwrap_or(""); - if filename.starts_with("plugin_") && filename.ends_with(DYLIB_SUFFIX) { - if let Err(e) = load_plugin(Some(path.to_str().unwrap_or("")), None) - { +pub(super) fn load_plugins() -> ResultType<()> { + let plugins_dir = super::get_plugins_dir()?; + if !plugins_dir.exists() { + std::fs::create_dir_all(&plugins_dir)?; + } else { + for entry in std::fs::read_dir(plugins_dir)? { + match entry { + Ok(entry) => { + let plugin_dir = entry.path(); + if plugin_dir.is_dir() { + load_plugin_dir(&plugin_dir); + } + } + Err(e) => { + log::error!("Failed to read plugins dir entry, {}", e); + } + } + } + } + Ok(()) +} + +fn load_plugin_dir(dir: &PathBuf) { + if let Ok(rd) = std::fs::read_dir(dir) { + for entry in rd { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_file() { + let filename = entry.file_name(); + let filename = filename.to_str().unwrap_or(""); + if filename.starts_with("plugin_") && filename.ends_with(DYLIB_SUFFIX) { + if let Some(path) = path.to_str() { + if let Err(e) = load_plugin_path(path) { log::error!("Failed to load plugin {}, {}", filename, e); } } } } - Err(e) => { - log::error!("Failed to read dir entry, {}", e); - } + } + Err(e) => { + log::error!( + "Failed to read '{}' dir entry, {}", + dir.file_stem().and_then(|f| f.to_str()).unwrap_or(""), + e + ); } } - Ok(()) - } - None => { - bail!("Failed to get parent dir of {}", exe); } } } @@ -309,7 +337,7 @@ pub fn reload_plugin(id: &str) -> ResultType<()> { None => bail!("Plugin {} not found", id), }; unload_plugin(id); - load_plugin(Some(&path), Some(id)) + load_plugin_path(&path) } fn load_plugin_path(path: &str) -> ResultType<()> { @@ -319,8 +347,21 @@ fn load_plugin_path(path: &str) -> ResultType<()> { // to-do validate plugin // to-do check the plugin id (make sure it does not use another plugin's id) + 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: str_to_cstr_ret(crate::VERSION), + version: ptr_version as _, + info: ptr_info as _, cbs: Callbacks { msg: callback_msg::cb_msg, get_conf: config::cb_get_conf, @@ -332,7 +373,7 @@ fn load_plugin_path(path: &str) -> ResultType<()> { plugin.init(&init_data, path)?; if change_manager() { - super::config::ManagerConfig::add_plugin(desc.id())?; + super::config::ManagerConfig::add_plugin(&desc.meta().id)?; } // update ui @@ -340,10 +381,18 @@ fn load_plugin_path(path: &str) -> ResultType<()> { update_ui_plugin_desc(&desc, None); reload_ui(&desc, None); + let install_time = PathBuf::from(path) + .metadata() + .and_then(|d| d.created()) + .unwrap_or(SystemTime::UNIX_EPOCH); + let install_time = chrono::DateTime::::from(install_time) + .format("%Y-%m-%d %H:%M:%S") + .to_string(); // add plugins - let id = desc.id().to_string(); + let id = desc.meta().id.clone(); let plugin_info = PluginInfo { path: path.to_string(), + install_time, desc, }; PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); @@ -360,20 +409,10 @@ 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)) => { - 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"); - } - } +#[inline] +pub fn load_plugin(id: &str) -> ResultType<()> { + load_plugin_dir(&super::get_plugin_dir(id)?); + Ok(()) } fn handle_event(method: &[u8], id: &str, peer: &str, event: &[u8]) -> ResultType<()> { @@ -418,7 +457,7 @@ fn _handle_listen_event(event: String, peer: String) { let mut plugins = Vec::new(); for info in PLUGIN_INFO.read().unwrap().values() { if info.desc.listen_events().contains(&event.to_string()) { - plugins.push(info.desc.id().to_string()); + plugins.push(info.desc.meta().id.clone()); } } @@ -496,7 +535,7 @@ pub fn handle_client_event(id: &str, peer: &str, event: &[u8]) -> Message { msg ); let name = match PLUGIN_INFO.read().unwrap().get(id) { - Some(plugin) => plugin.desc.name(), + Some(plugin) => &plugin.desc.meta().name, None => "???", } .to_owned(); @@ -568,7 +607,7 @@ fn reload_ui(desc: &Desc, sync_to: Option<&str>) { let make_event = |ui: &str| { let mut m = HashMap::new(); m.insert("name", MSG_TO_UI_TYPE_PLUGIN_RELOAD); - m.insert("id", desc.id()); + m.insert("id", &desc.meta().id); m.insert("location", &location); // Do not depend on the "location" and plugin desc on the ui side. // Send the ui field to ensure the ui is valid. @@ -622,6 +661,10 @@ fn update_ui_plugin_desc(desc: &Desc, sync_to: Option<&str>) { } } +pub(super) fn get_plugin_infos() -> Arc>> { + PLUGIN_INFO.clone() +} + pub(super) fn get_desc_conf(id: &str) -> Option { PLUGIN_INFO .read() diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f31ce18ee..ed34ed00c 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -6,7 +6,7 @@ use crate::common::IS_X11; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; +use hbb_common::{get_time, protobuf::EnumOrUnknown}; use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; @@ -299,8 +299,7 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> msg = cached.clone(); } else { let mut data = crate::get_cursor_data(hcursor)?; - data.colors = - hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL).into(); + data.colors = hbb_common::compress::compress(&data.colors[..]).into(); let mut tmp = Message::new(); tmp.set_cursor_data(data); msg = Arc::new(tmp);