plugin_framework, manager, install plugin

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou 2023-05-09 19:47:26 +08:00
parent 6f5ff0ac0e
commit db71dd039d
21 changed files with 1078 additions and 372 deletions

158
Cargo.lock generated
View File

@ -17,6 +17,18 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 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]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.6" version = "0.7.6"
@ -404,6 +416,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.59.2" version = "0.59.2"
@ -578,6 +596,27 @@ dependencies = [
"serde 1.0.163", "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]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.16.7" version = "0.16.7"
@ -737,6 +776,15 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.6.1" version = "1.6.1"
@ -974,6 +1022,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.5.0" version = "0.5.0"
@ -1525,6 +1579,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@ -2814,7 +2869,7 @@ dependencies = [
"tokio-util", "tokio-util",
"toml 0.7.3", "toml 0.7.3",
"winapi 0.3.9", "winapi 0.3.9",
"zstd", "zstd 0.12.3+zstd.1.5.2",
] ]
[[package]] [[package]]
@ -2862,6 +2917,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "hound" name = "hound"
version = "3.5.0" version = "3.5.0"
@ -4074,6 +4138,12 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.5"
@ -4267,6 +4337,17 @@ dependencies = [
"windows-sys 0.45.0", "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]] [[package]]
name = "paste" name = "paste"
version = "1.0.12" version = "1.0.12"
@ -4279,6 +4360,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 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]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
@ -5175,6 +5268,7 @@ dependencies = [
"winreg 0.10.1", "winreg 0.10.1",
"winres", "winres",
"wol-rs", "wol-rs",
"zip",
] ]
[[package]] [[package]]
@ -5721,6 +5815,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "0.15.44" version = "0.15.44"
@ -7291,19 +7391,58 @@ dependencies = [
] ]
[[package]] [[package]]
name = "zstd" name = "zip"
version = "0.9.2+zstd.1.5.1" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" checksum = "7e92305c174683d78035cbf1b70e18db6329cc0f1b9cae0a52ca90bf5bfe7125"
dependencies = [ 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]] [[package]]
name = "zstd-safe" 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" 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 = [ dependencies = [
"libc", "libc",
"zstd-sys", "zstd-sys",
@ -7311,12 +7450,13 @@ dependencies = [
[[package]] [[package]]
name = "zstd-sys" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"pkg-config",
] ]
[[package]] [[package]]

View File

@ -72,6 +72,7 @@ chrono = "0.4"
cidr-utils = "0.5" cidr-utils = "0.5"
libloading = "0.8" libloading = "0.8"
fon = "0.6" fon = "0.6"
zip = "0.6.5"
[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
cpal = "0.15" cpal = "0.15"

View File

@ -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/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_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/model.dart';
import 'package:flutter_hbb/plugin/common.dart'; import 'package:flutter_hbb/plugin/common.dart';
import 'package:flutter_hbb/plugin/widget.dart'; import 'package:flutter_hbb/plugin/widget.dart';
@ -1449,12 +1449,10 @@ class _CheckboxState extends State<_Checkbox> {
} }
class PluginCard extends StatefulWidget { class PluginCard extends StatefulWidget {
final PluginId pluginId; final PluginInfo plugin;
final Desc desc;
const PluginCard({ const PluginCard({
Key? key, Key? key,
required this.pluginId, required this.plugin,
required this.desc,
}) : super(key: key); }) : super(key: key);
@override @override
@ -1462,40 +1460,43 @@ class PluginCard extends StatefulWidget {
} }
class PluginCardState extends State<PluginCard> { class PluginCardState extends State<PluginCard> {
PluginId get pluginId => widget.plugin.meta.id;
String get pluginName => widget.plugin.meta.name;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final children = [ final children = [
_Button( _Button(
'Reload', 'Reload',
() async { () async {
clearPlugin(widget.pluginId); clearPlugin(pluginId);
await bind.pluginReload(id: widget.pluginId); await bind.pluginReload(id: pluginId);
setState(() {}); setState(() {});
}, },
), ),
_Checkbox( _Checkbox(
label: 'Enable', label: 'Enable',
getValue: () => bind.pluginIdIsEnabled(id: widget.pluginId), getValue: () => bind.pluginIsEnabled(id: pluginId),
setValue: (bool v) async { setValue: (bool v) async {
if (!v) { if (!v) {
clearPlugin(widget.pluginId); clearPlugin(pluginId);
} }
await bind.pluginIdEnable(id: widget.pluginId, v: v); await bind.pluginEnable(id: pluginId, v: v);
setState(() {}); setState(() {});
}, },
), ),
]; ];
final model = getPluginModel(kLocationHostMainPlugin, widget.pluginId); final model = getPluginModel(kLocationHostMainPlugin, pluginId);
if (model != null) { if (model != null) {
children.add(PluginItem( children.add(PluginItem(
pluginId: widget.pluginId, pluginId: pluginId,
peerId: '', peerId: '',
location: kLocationHostMainPlugin, location: kLocationHostMainPlugin,
pluginModel: model, pluginModel: model,
isMenu: false, 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> { class _PluginState extends State<_Plugin> {
// temp checkbox widget // temp checkbox widget
List<Widget> _buildCards(DescModel model) => [ List<Widget> _buildCards(PluginManager model) => [
_Card( _Card(
title: 'Plugin', title: 'Plugin',
children: [], children: [],
), ),
...model.all.entries ...model.plugins.map((entry) => PluginCard(plugin: entry)).toList(),
.map((entry) => PluginCard(pluginId: entry.key, desc: entry.value))
.toList(),
]; ];
@override @override
@ -1525,8 +1524,8 @@ class _PluginState extends State<_Plugin> {
return DesktopScrollWrapper( return DesktopScrollWrapper(
scrollController: scrollController, scrollController: scrollController,
child: ChangeNotifierProvider.value( child: ChangeNotifierProvider.value(
value: DescModel.instance, value: pluginManager,
child: Consumer<DescModel>(builder: (context, model, child) { child: Consumer<PluginManager>(builder: (context, model, child) {
return ListView( return ListView(
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,

View File

@ -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/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/event.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/plugin/widget.dart';
import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
@ -230,8 +230,8 @@ class FfiModel with ChangeNotifier {
parent.target?.serverModel.updateVoiceCallState(evt); parent.target?.serverModel.updateVoiceCallState(evt);
} else if (name == 'fingerprint') { } else if (name == 'fingerprint') {
FingerprintState.find(peerId).value = evt['fingerprint'] ?? ''; FingerprintState.find(peerId).value = evt['fingerprint'] ?? '';
} else if (name == 'plugin_desc') { } else if (name == 'plugin_manager') {
updateDesc(evt); pluginManager.handleEvent(evt);
} else if (name == 'plugin_event') { } else if (name == 'plugin_event') {
handlePluginEvent( handlePluginEvent(
evt, peerId, (Map<String, dynamic> e) => handleMsgBox(e, peerId)); evt, peerId, (Map<String, dynamic> e) => handleMsgBox(e, peerId));

View File

@ -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<String, dynamic> json)
: key = json['key'] ?? '',
text = json['text'] ?? '',
tooltip = json['tooltip'] ?? '',
action = json['action'] ?? '';
static UiType? create(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) : super.fromJson(json);
}
class Location {
// location key:
// host|main|settings|plugin
// client|remote|toolbar|display
HashMap<String, UiType> ui;
Location(this.ui);
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
(json['ui'] as Map<String, dynamic>).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<String, dynamic> 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<ConfigItem> shared;
List<ConfigItem> peer;
Config(this.shared, this.peer);
Config.fromJson(Map<String, dynamic> json)
: shared = (json['shared'] as List<dynamic>)
.map((e) => ConfigItem.fromJson(e))
.toList(),
peer = (json['peer'] as List<dynamic>)
.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<String, dynamic> 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 = <PluginId, Desc>{};
DescModel._();
void _updateDesc(Map<String, dynamic> 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<PluginId, Desc> get all => data;
static final DescModel _instance = DescModel._();
static DescModel get instance => _instance;
}
void updateDesc(Map<String, dynamic> desc) =>
DescModel.instance._updateDesc(desc);
Desc? getDesc(String id) => DescModel.instance._getDesc(id);

View File

@ -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<String, dynamic> 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<String, dynamic> json)
: key = json['key'] ?? '',
text = json['text'] ?? '',
tooltip = json['tooltip'] ?? '',
action = json['action'] ?? '';
static UiType? create(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) : super.fromJson(json);
}
class Location {
// location key:
// host|main|settings|plugin
// client|remote|toolbar|display
HashMap<String, UiType> ui;
Location(this.ui);
Location.fromJson(Map<String, dynamic> json) : ui = HashMap() {
(json['ui'] as Map<String, dynamic>).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<PluginInfo> _plugins = [];
PluginManager._();
static final PluginManager _instance = PluginManager._();
static PluginManager get instance => _instance;
List<PluginInfo> get plugins => _plugins;
PluginInfo? getPlugin(String id) {
for (var p in _plugins) {
if (p.meta.id == id) {
return p;
}
}
return null;
}
void handleEvent(Map<String, dynamic> 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<String, dynamic> 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<dynamic> evt) {
_plugins.clear();
for (var p in evt) {
final plugin = _getPluginFromEvent(p);
if (plugin == null) {
continue;
}
_plugins.add(plugin);
}
notifyListeners();
}
PluginInfo? _getPluginFromEvent(Map<String, dynamic> 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;

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './common.dart'; import './common.dart';
import './desc.dart'; import './manager.dart';
final Map<String, LocationModel> _locationModels = {}; final Map<String, LocationModel> _locationModels = {};
final Map<String, OptionModel> _optionModels = {}; final Map<String, OptionModel> _optionModels = {};

View File

@ -10,7 +10,7 @@ import 'package:get/get.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import './desc.dart'; import './manager.dart';
import './model.dart'; import './model.dart';
import './common.dart'; import './common.dart';
@ -247,7 +247,7 @@ class PluginItem extends StatelessWidget {
}) { }) {
final event = MsgFromUi( final event = MsgFromUi(
id: pluginId, id: pluginId,
name: getDesc(pluginId)?.name ?? '', name: pluginManager.getPlugin(pluginId)?.meta.name ?? '',
location: location, location: location,
key: key, key: key,
value: value:

View File

@ -16,7 +16,7 @@ bytes = { version = "1.4", features = ["serde"] }
log = "0.4" log = "0.4"
env_logger = "0.10" env_logger = "0.10"
socket2 = { version = "0.3", features = ["reuseport"] } socket2 = { version = "0.3", features = ["reuseport"] }
zstd = "0.9" zstd = "0.12"
quinn = {version = "0.9", optional = true } quinn = {version = "0.9", optional = true }
anyhow = "1.0" anyhow = "1.0"
futures-util = "0.3" futures-util = "0.3"

View File

@ -1,23 +1,28 @@
use std::cell::RefCell; use std::{cell::RefCell, io};
use zstd::block::{Compressor, Decompressor}; 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! { thread_local! {
static COMPRESSOR: RefCell<Compressor> = RefCell::new(Compressor::new()); static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
static DECOMPRESSOR: RefCell<Decompressor> = RefCell::new(Decompressor::new()); static DECOMPRESSOR: RefCell<io::Result<Decompressor<'static>>> = RefCell::new(Decompressor::new());
} }
/// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), pub fn compress(data: &[u8]) -> Vec<u8> {
/// 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<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
COMPRESSOR.with(|c| { COMPRESSOR.with(|c| {
if let Ok(mut c) = c.try_borrow_mut() { if let Ok(mut c) = c.try_borrow_mut() {
match c.compress(data, level) { match &mut *c {
Ok(res) => out = res, Ok(c) => match c.compress(data) {
Ok(res) => out = res,
Err(err) => {
crate::log::debug!("Failed to compress: {}", err);
}
},
Err(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<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
DECOMPRESSOR.with(|d| { DECOMPRESSOR.with(|d| {
if let Ok(mut d) = d.try_borrow_mut() { if let Ok(mut d) = d.try_borrow_mut() {
const MAX: usize = 1024 * 1024 * 64; match &mut *d {
const MIN: usize = 1024 * 1024; Ok(d) => {
let mut n = 30 * data.len(); const MAX: usize = 1024 * 1024 * 64;
n = n.clamp(MIN, MAX); const MIN: usize = 1024 * 1024;
match d.decompress(data, n) { let mut n = 30 * data.len();
Ok(res) => out = res, 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) => { Err(err) => {
crate::log::debug!("Failed to decompress: {}", err); crate::log::debug!("Failed to get decompressor: {}", err);
} }
} }
} }

View File

@ -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 // https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
use crate::{ use crate::{
compress::{compress, decompress}, compress::{compress, decompress},
config::{Config, COMPRESS_LEVEL}, config::Config,
}; };
pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType<FileDirectory> { pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType<FileDirectory> {
@ -481,7 +481,7 @@ impl TransferJob {
} else { } else {
self.finished_size += offset as u64; self.finished_size += offset as u64;
if !is_compressed_file(name) { if !is_compressed_file(name) {
let tmp = compress(&buf, COMPRESS_LEVEL); let tmp = compress(&buf);
if tmp.len() < buf.len() { if tmp.len() < buf.len() {
buf = tmp; buf = tmp;
compressed = true; compressed = true;

View File

@ -19,7 +19,7 @@ use hbb_common::compress::decompress;
use hbb_common::{ use hbb_common::{
allow_err, allow_err,
compress::compress as compress_func, compress::compress as compress_func,
config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, config::{self, Config, RENDEZVOUS_TIMEOUT},
get_version_number, log, get_version_number, log,
message_proto::*, message_proto::*,
protobuf::Enum, protobuf::Enum,
@ -68,6 +68,19 @@ lazy_static::lazy_static! {
static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(())); static ref ARBOARD_MTX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
} }
pub struct SimpleCallOnReturn {
pub b: bool,
pub f: Box<dyn Fn() + 'static>,
}
impl Drop for SimpleCallOnReturn {
fn drop(&mut self) {
if self.b {
(self.f)();
}
}
}
pub fn global_init() -> bool { pub fn global_init() -> bool {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
@ -98,7 +111,7 @@ pub fn valid_for_numlock(evt: &KeyEvent) -> bool {
pub fn create_clipboard_msg(content: String) -> Message { pub fn create_clipboard_msg(content: String) -> Message {
let bytes = content.into_bytes(); let bytes = content.into_bytes();
let compressed = compress_func(&bytes, COMPRESS_LEVEL); let compressed = compress_func(&bytes);
let compress = compressed.len() < bytes.len(); let compress = compressed.len() < bytes.len();
let content = if compress { compressed } else { bytes }; let content = if compress { compressed } else { bytes };
let mut msg = Message::new(); let mut msg = Message::new();

View File

@ -112,7 +112,7 @@ pub fn core_main() -> Option<Vec<String>> {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
let load_plugins = crate::platform::is_installed(); let load_plugins = crate::platform::is_installed();
if load_plugins { if load_plugins {
hbb_common::allow_err!(crate::plugin::load_plugins()); crate::plugin::init();
} }
} }
if args.is_empty() { if args.is_empty() {
@ -240,6 +240,18 @@ pub fn core_main() -> Option<Vec<String>> {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::flutter::connection_manager::start_cm_no_ui(); crate::flutter::connection_manager::start_cm_no_ui();
return None; 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()); //_async_logger_holder.map(|x| x.flush());

View File

@ -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] #[inline]
pub fn plugin_id_enable(_id: String, _v: bool) { pub fn plugin_enable(_id: String, _v: bool) {
#[cfg(feature = "plugin_framework")] #[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[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() _v.to_string()
)); ));
if _v { if _v {
allow_err!(crate::plugin::load_plugin(None, Some(&_id))); allow_err!(crate::plugin::load_plugin(&_id));
} else { } else {
crate::plugin::unload_plugin(&_id); crate::plugin::unload_plugin(&_id);
} }
} }
} }
pub fn plugin_id_is_enabled(_id: String) -> SyncReturn<bool> { pub fn plugin_is_enabled(_id: String) -> SyncReturn<bool> {
#[cfg(feature = "plugin_framework")] #[cfg(feature = "plugin_framework")]
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[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")] #[cfg(target_os = "android")]
pub mod server_side { pub mod server_side {
use hbb_common::{config, log}; use hbb_common::{config, log};

View File

@ -280,7 +280,7 @@ impl ManagerConfig {
let enabled = bool::from_str(value).unwrap_or(false); let enabled = bool::from_str(value).unwrap_or(false);
allow_err!(Self::set_plugin_option_enabled(id, enabled)); allow_err!(Self::set_plugin_option_enabled(id, enabled));
if enabled { if enabled {
allow_err!(super::load_plugin(None, Some(id))); allow_err!(super::load_plugin(id));
} else { } else {
super::unload_plugin(id); super::unload_plugin(id);
} }

View File

@ -46,18 +46,29 @@ pub struct Config {
pub peer: Vec<ConfigItem>, pub peer: Vec<ConfigItem>,
} }
#[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)] #[derive(Debug, Serialize, Deserialize)]
pub struct Desc { pub struct Desc {
id: String, meta: Meta,
name: String, need_reboot: bool,
version: String,
description: String,
author: String,
home: String,
license: String,
published: String,
released: String,
github: String,
location: Location, location: Location,
config: Config, config: Config,
listen_events: Vec<String>, listen_events: Vec<String>,
@ -69,44 +80,8 @@ impl Desc {
Ok(serde_json::from_str(s.to_str()?)?) Ok(serde_json::from_str(s.to_str()?)?)
} }
pub fn id(&self) -> &str { pub fn meta(&self) -> &Meta {
&self.id &self.meta
}
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 location(&self) -> &Location { pub fn location(&self) -> &Location {

View File

@ -5,14 +5,25 @@ use serde_derive::{Deserialize, Serialize};
#[cfg(not(windows))] #[cfg(not(windows))]
use std::{fs::File, io::prelude::*}; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")] #[serde(tag = "t", content = "c")]
pub enum Plugin { pub enum Plugin {
Config(String, String, Option<String>), Config(String, String, Option<String>),
ManagerConfig(String, Option<String>), ManagerConfig(String, Option<String>),
ManagerPluginConfig(String, String, Option<String>), ManagerPluginConfig(String, String, Option<String>),
Load(String),
Reload(String), Reload(String),
Uninstall(String), InstallStatus((String, InstallStatus)),
} }
#[tokio::main(flavor = "current_thread")] #[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")] #[tokio::main(flavor = "current_thread")]
pub async fn reload_plugin(id: &str) -> ResultType<()> { pub async fn load_plugin(id: &str) -> ResultType<()> {
reload_plugin_async(id).await load_plugin_async(id).await
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
pub async fn uninstall_plugin(id: &str) -> ResultType<()> { pub async fn reload_plugin(id: &str) -> ResultType<()> {
uninstall_plugin_async(id).await reload_plugin_async(id).await
} }
async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType<Option<String>> { async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType<Option<String>> {
@ -141,16 +152,15 @@ async fn set_manager_plugin_config_async(id: &str, name: &str, value: String) ->
Ok(()) Ok(())
} }
async fn reload_plugin_async(id: &str) -> ResultType<()> { async fn load_plugin_async(id: &str) -> ResultType<()> {
let mut c = connect(1000, "").await?; 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(()) Ok(())
} }
async fn uninstall_plugin_async(id: &str) -> ResultType<()> { async fn reload_plugin_async(id: &str) -> ResultType<()> {
let mut c = connect(1000, "").await?; let mut c = connect(1000, "").await?;
c.send(&Data::Plugin(Plugin::Uninstall(id.to_owned()))) c.send(&Data::Plugin(Plugin::Reload(id.to_owned()))).await?;
.await?;
Ok(()) Ok(())
} }
@ -158,7 +168,7 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) {
match plugin { match plugin {
Plugin::Config(id, name, value) => match value { Plugin::Config(id, name, value) => match value {
None => { None => {
let value = crate::plugin::SharedConfig::get(&id, &name); let value = super::SharedConfig::get(&id, &name);
allow_err!( allow_err!(
stream stream
.send(&Data::Plugin(Plugin::Config(id, name, value))) .send(&Data::Plugin(Plugin::Config(id, name, value)))
@ -166,12 +176,12 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) {
); );
} }
Some(value) => { 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 { Plugin::ManagerConfig(name, value) => match value {
None => { None => {
let value = crate::plugin::ManagerConfig::get_option(&name); let value = super::ManagerConfig::get_option(&name);
allow_err!( allow_err!(
stream stream
.send(&Data::Plugin(Plugin::ManagerConfig(name, value))) .send(&Data::Plugin(Plugin::ManagerConfig(name, value)))
@ -179,12 +189,12 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) {
); );
} }
Some(value) => { Some(value) => {
crate::plugin::ManagerConfig::set_option(&name, &value); super::ManagerConfig::set_option(&name, &value);
} }
}, },
Plugin::ManagerPluginConfig(id, name, value) => match value { Plugin::ManagerPluginConfig(id, name, value) => match value {
None => { None => {
let value = crate::plugin::ManagerConfig::get_plugin_option(&id, &name); let value = super::ManagerConfig::get_plugin_option(&id, &name);
allow_err!( allow_err!(
stream stream
.send(&Data::Plugin(Plugin::ManagerPluginConfig(id, name, value))) .send(&Data::Plugin(Plugin::ManagerPluginConfig(id, name, value)))
@ -192,16 +202,15 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) {
); );
} }
Some(value) => { 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) => { Plugin::Reload(id) => {
allow_err!(crate::plugin::reload_plugin(&id)); allow_err!(super::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));
} }
_ => {}
} }
} }

370
src/plugin/manager.rs Normal file
View File

@ -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<Mutex<HashMap<String, PluginInfo>>> = Arc::new(Mutex::new(HashMap::new()));
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ManagerMeta {
pub version: String,
pub description: String,
pub plugins: Vec<PluginMeta>,
}
#[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<PluginSource> {
// 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<String, PluginInfo> {
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::<ManagerMeta>() {
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<String, PluginInfo>) {
let mut plugin_list = plugins.values().collect::<Vec<_>>();
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);
}
}

View File

@ -1,6 +1,8 @@
use hbb_common::{libc, ResultType}; use hbb_common::{libc, tokio, ResultType};
use std::{ use std::{
env,
ffi::{c_char, c_int, c_void, CStr}, ffi::{c_char, c_int, c_void, CStr},
path::PathBuf,
ptr::null, ptr::null,
}; };
@ -10,20 +12,26 @@ mod config;
pub mod desc; pub mod desc;
mod errno; mod errno;
pub mod ipc; pub mod ipc;
mod manager;
pub mod native; pub mod native;
pub mod native_handlers; pub mod native_handlers;
mod plog; mod plog;
mod plugins; 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::{ pub use plugins::{
handle_client_event, handle_listen_event, handle_server_event, handle_ui_event, load_plugin, 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_DESC: &str = "plugin_desc";
const MSG_TO_UI_TYPE_PLUGIN_EVENT: &str = "plugin_event"; 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_RELOAD: &str = "plugin_reload";
const MSG_TO_UI_TYPE_PLUGIN_OPTION: &str = "plugin_option"; 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_CLIENT: &str = "on_conn_client";
pub const EVENT_ON_CONN_SERVER: &str = "on_conn_server"; 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}; pub use config::{ManagerConfig, PeerConfig, SharedConfig};
use crate::common::is_server;
/// Common plugin return. /// Common plugin return.
/// ///
/// [Note] /// [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<PathBuf> {
// 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<PathBuf> {
Ok(get_plugins_dir()?.join(id))
}
#[inline] #[inline]
fn cstr_to_string(cstr: *const c_char) -> ResultType<String> { fn cstr_to_string(cstr: *const c_char) -> ResultType<String> {
Ok(String::from_utf8(unsafe { Ok(String::from_utf8(unsafe {

View File

@ -15,6 +15,7 @@ use std::{
ffi::{c_char, c_void}, ffi::{c_char, c_void},
path::PathBuf, path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::SystemTime,
}; };
const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0";
@ -26,9 +27,10 @@ lazy_static::lazy_static! {
static ref PLUGINS: Arc<RwLock<HashMap<String, Plugin>>> = Default::default(); static ref PLUGINS: Arc<RwLock<HashMap<String, Plugin>>> = Default::default();
} }
struct PluginInfo { pub(super) struct PluginInfo {
path: String, pub path: String,
desc: Desc, pub install_time: String,
pub desc: Desc,
} }
/// Initialize the plugins. /// Initialize the plugins.
@ -136,6 +138,11 @@ struct Callbacks {
native: CallbackNative, native: CallbackNative,
} }
#[derive(Serialize)]
struct InitInfo {
is_server: bool,
}
/// The plugin initialize data. /// The plugin initialize data.
/// version: The version of the plugin, can't be nullptr. /// version: The version of the plugin, can't be nullptr.
/// local_peer_id: The local peer id, can't be nullptr. /// local_peer_id: The local peer id, can't be nullptr.
@ -143,6 +150,7 @@ struct Callbacks {
#[repr(C)] #[repr(C)]
struct InitData { struct InitData {
version: *const c_char, version: *const c_char,
info: *const c_char,
cbs: Callbacks, cbs: Callbacks,
} }
@ -255,34 +263,54 @@ const DYLIB_SUFFIX: &str = ".so";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const DYLIB_SUFFIX: &str = ".dylib"; const DYLIB_SUFFIX: &str = ".dylib";
pub fn load_plugins() -> ResultType<()> { pub(super) fn load_plugins() -> ResultType<()> {
let exe = std::env::current_exe()?.to_string_lossy().to_string(); let plugins_dir = super::get_plugins_dir()?;
match PathBuf::from(&exe).parent() { if !plugins_dir.exists() {
Some(dir) => { std::fs::create_dir_all(&plugins_dir)?;
for entry in std::fs::read_dir(dir)? { } else {
match entry { for entry in std::fs::read_dir(plugins_dir)? {
Ok(entry) => { match entry {
let path = entry.path(); Ok(entry) => {
if path.is_file() { let plugin_dir = entry.path();
let filename = entry.file_name(); if plugin_dir.is_dir() {
let filename = filename.to_str().unwrap_or(""); load_plugin_dir(&plugin_dir);
if filename.starts_with("plugin_") && filename.ends_with(DYLIB_SUFFIX) { }
if let Err(e) = load_plugin(Some(path.to_str().unwrap_or("")), None) }
{ 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); 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), None => bail!("Plugin {} not found", id),
}; };
unload_plugin(id); unload_plugin(id);
load_plugin(Some(&path), Some(id)) load_plugin_path(&path)
} }
fn load_plugin_path(path: &str) -> ResultType<()> { fn load_plugin_path(path: &str) -> ResultType<()> {
@ -319,8 +347,21 @@ fn load_plugin_path(path: &str) -> ResultType<()> {
// to-do validate plugin // to-do validate plugin
// to-do check the plugin id (make sure it does not use another plugin's id) // 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 { let init_data = InitData {
version: str_to_cstr_ret(crate::VERSION), version: ptr_version as _,
info: ptr_info as _,
cbs: Callbacks { cbs: Callbacks {
msg: callback_msg::cb_msg, msg: callback_msg::cb_msg,
get_conf: config::cb_get_conf, get_conf: config::cb_get_conf,
@ -332,7 +373,7 @@ fn load_plugin_path(path: &str) -> ResultType<()> {
plugin.init(&init_data, path)?; plugin.init(&init_data, path)?;
if change_manager() { if change_manager() {
super::config::ManagerConfig::add_plugin(desc.id())?; super::config::ManagerConfig::add_plugin(&desc.meta().id)?;
} }
// update ui // update ui
@ -340,10 +381,18 @@ fn load_plugin_path(path: &str) -> ResultType<()> {
update_ui_plugin_desc(&desc, None); update_ui_plugin_desc(&desc, None);
reload_ui(&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::<chrono::Local>::from(install_time)
.format("%Y-%m-%d %H:%M:%S")
.to_string();
// add plugins // add plugins
let id = desc.id().to_string(); let id = desc.meta().id.clone();
let plugin_info = PluginInfo { let plugin_info = PluginInfo {
path: path.to_string(), path: path.to_string(),
install_time,
desc, desc,
}; };
PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); 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<()> { #[inline]
match (path, id) { pub fn load_plugin(id: &str) -> ResultType<()> {
(Some(path), _) => load_plugin_path(path), load_plugin_dir(&super::get_plugin_dir(id)?);
(None, Some(id)) => { Ok(())
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");
}
}
} }
fn handle_event(method: &[u8], id: &str, peer: &str, event: &[u8]) -> ResultType<()> { 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(); let mut plugins = Vec::new();
for info in PLUGIN_INFO.read().unwrap().values() { for info in PLUGIN_INFO.read().unwrap().values() {
if info.desc.listen_events().contains(&event.to_string()) { 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 msg
); );
let name = match PLUGIN_INFO.read().unwrap().get(id) { let name = match PLUGIN_INFO.read().unwrap().get(id) {
Some(plugin) => plugin.desc.name(), Some(plugin) => &plugin.desc.meta().name,
None => "???", None => "???",
} }
.to_owned(); .to_owned();
@ -568,7 +607,7 @@ fn reload_ui(desc: &Desc, sync_to: Option<&str>) {
let make_event = |ui: &str| { let make_event = |ui: &str| {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert("name", MSG_TO_UI_TYPE_PLUGIN_RELOAD); m.insert("name", MSG_TO_UI_TYPE_PLUGIN_RELOAD);
m.insert("id", desc.id()); m.insert("id", &desc.meta().id);
m.insert("location", &location); m.insert("location", &location);
// Do not depend on the "location" and plugin desc on the ui side. // Do not depend on the "location" and plugin desc on the ui side.
// Send the ui field to ensure the ui is valid. // 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<RwLock<HashMap<String, PluginInfo>>> {
PLUGIN_INFO.clone()
}
pub(super) fn get_desc_conf(id: &str) -> Option<super::desc::Config> { pub(super) fn get_desc_conf(id: &str) -> Option<super::desc::Config> {
PLUGIN_INFO PLUGIN_INFO
.read() .read()

View File

@ -6,7 +6,7 @@ use crate::common::IS_X11;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use dispatch::Queue; use dispatch::Queue;
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; 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}; use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput};
@ -299,8 +299,7 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()>
msg = cached.clone(); msg = cached.clone();
} else { } else {
let mut data = crate::get_cursor_data(hcursor)?; let mut data = crate::get_cursor_data(hcursor)?;
data.colors = data.colors = hbb_common::compress::compress(&data.colors[..]).into();
hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL).into();
let mut tmp = Message::new(); let mut tmp = Message::new();
tmp.set_cursor_data(data); tmp.set_cursor_data(data);
msg = Arc::new(tmp); msg = Arc::new(tmp);