plugin_framework, test plugin manager, uninstall is not fully tested

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou 2023-05-10 23:57:46 +08:00
parent 4ee0fd9676
commit 095ac46255
12 changed files with 327 additions and 116 deletions

View File

@ -1466,15 +1466,22 @@ class _PluginState extends State<_Plugin> {
return ListView( return ListView(
physics: DraggableNeverScrollableScrollPhysics(), physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController, controller: scrollController,
children: model.plugins children: model.plugins.map((entry) => pluginCard(entry)).toList(),
.map((entry) => DesktopSettingsCard(plugin: entry))
.toList(),
).marginOnly(bottom: _kListViewBottomMargin); ).marginOnly(bottom: _kListViewBottomMargin);
}), }),
), ),
); );
} }
Widget pluginCard(PluginInfo plugin) {
return ChangeNotifierProvider.value(
value: plugin,
child: Consumer<PluginInfo>(
builder: (context, model, child) => DesktopSettingsCard(plugin: model),
),
);
}
Widget accountAction() { Widget accountAction() {
return Obx(() => _Button( return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',

View File

@ -144,17 +144,20 @@ class PluginInfo with ChangeNotifier {
SourceInfo sourceInfo; SourceInfo sourceInfo;
Meta meta; Meta meta;
String installedVersion; // It is empty if not installed. String installedVersion; // It is empty if not installed.
DateTime installTime; String failedMsg;
String invalidReason; // It is empty if valid. String invalidReason; // It is empty if valid.
PluginInfo({ PluginInfo({
required this.sourceInfo, required this.sourceInfo,
required this.meta, required this.meta,
required this.installedVersion, required this.installedVersion,
required this.installTime,
required this.invalidReason, required this.invalidReason,
this.failedMsg = '',
}); });
bool get installed => installedVersion.isNotEmpty;
bool get needUpdate => installed && installedVersion != meta.version;
void update(PluginInfo plugin) { void update(PluginInfo plugin) {
assert(plugin.meta.id == meta.id, 'Plugin id not match'); assert(plugin.meta.id == meta.id, 'Plugin id not match');
if (plugin.meta.id != meta.id) { if (plugin.meta.id != meta.id) {
@ -164,10 +167,28 @@ class PluginInfo with ChangeNotifier {
sourceInfo = plugin.sourceInfo; sourceInfo = plugin.sourceInfo;
meta = plugin.meta; meta = plugin.meta;
installedVersion = plugin.installedVersion; installedVersion = plugin.installedVersion;
installTime = plugin.installTime;
invalidReason = plugin.invalidReason; invalidReason = plugin.invalidReason;
notifyListeners(); notifyListeners();
} }
void setInstall(String msg) {
if (msg == "finished") {
msg = '';
}
failedMsg = msg;
if (msg.isEmpty) {
installedVersion = meta.version;
}
notifyListeners();
}
void setUninstall(String msg) {
failedMsg = msg;
if (msg.isEmpty) {
installedVersion = '';
}
notifyListeners();
}
} }
class PluginManager with ChangeNotifier { class PluginManager with ChangeNotifier {
@ -194,11 +215,27 @@ class PluginManager with ChangeNotifier {
_handlePluginList(evt['plugin_list']); _handlePluginList(evt['plugin_list']);
} else if (evt['plugin_update'] != null) { } else if (evt['plugin_update'] != null) {
_handlePluginUpdate(evt['plugin_update']); _handlePluginUpdate(evt['plugin_update']);
} else if (evt['plugin_install'] != null && evt['id'] != null) {
_handlePluginInstall(evt['id'], evt['plugin_install']);
} else if (evt['plugin_uninstall'] != null && evt['id'] != null) {
_handlePluginUninstall(evt['id'], evt['plugin_uninstall']);
} else { } else {
debugPrint('Failed to handle manager event: $evt'); debugPrint('Failed to handle manager event: $evt');
} }
} }
void _sortPlugins() {
plugins.sort((a, b) {
if (a.installed) {
return -1;
} else if (b.installed) {
return 1;
} else {
return 0;
}
});
}
void _handlePluginUpdate(Map<String, dynamic> evt) { void _handlePluginUpdate(Map<String, dynamic> evt) {
final plugin = _getPluginFromEvent(evt); final plugin = _getPluginFromEvent(evt);
if (plugin == null) { if (plugin == null) {
@ -207,6 +244,8 @@ class PluginManager with ChangeNotifier {
for (var i = 0; i < _plugins.length; i++) { for (var i = 0; i < _plugins.length; i++) {
if (_plugins[i].meta.id == plugin.meta.id) { if (_plugins[i].meta.id == plugin.meta.id) {
_plugins[i].update(plugin); _plugins[i].update(plugin);
_sortPlugins();
notifyListeners();
return; return;
} }
} }
@ -225,9 +264,32 @@ class PluginManager with ChangeNotifier {
} catch (e) { } catch (e) {
debugPrint('Failed to decode $e, plugin list \'$pluginList\''); debugPrint('Failed to decode $e, plugin list \'$pluginList\'');
} }
_sortPlugins();
notifyListeners(); notifyListeners();
} }
void _handlePluginInstall(String id, String msg) {
for (var i = 0; i < _plugins.length; i++) {
if (_plugins[i].meta.id == id) {
_plugins[i].setInstall(msg);
_sortPlugins();
notifyListeners();
return;
}
}
}
void _handlePluginUninstall(String id, String msg) {
for (var i = 0; i < _plugins.length; i++) {
if (_plugins[i].meta.id == id) {
_plugins[i].setUninstall(msg);
_sortPlugins();
notifyListeners();
return;
}
}
}
PluginInfo? _getPluginFromEvent(Map<String, dynamic> evt) { PluginInfo? _getPluginFromEvent(Map<String, dynamic> evt) {
final s = evt['source']; final s = evt['source'];
assert(s != null, 'Source is null'); assert(s != null, 'Source is null');
@ -273,19 +335,10 @@ class PluginManager with ChangeNotifier {
publishInfo: publishInfo:
PublishInfo(lastReleased: lastReleased, published: published), PublishInfo(lastReleased: lastReleased, published: published),
); );
late DateTime installTime;
try {
installTime =
DateTime.parse(evt['install_time'] ?? '1970-01-01T00+00:00');
} catch (e) {
installTime = DateTime.utc(1970);
}
return PluginInfo( return PluginInfo(
sourceInfo: source, sourceInfo: source,
meta: meta, meta: meta,
installedVersion: evt['installed_version'], installedVersion: evt['installed_version'],
installTime: installTime,
invalidReason: evt['invalid_reason'] ?? '', invalidReason: evt['invalid_reason'] ?? '',
); );
} }

View File

@ -28,10 +28,13 @@ class DesktopSettingsCard extends StatefulWidget {
class _DesktopSettingsCardState extends State<DesktopSettingsCard> { class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
PluginInfo get plugin => widget.plugin; PluginInfo get plugin => widget.plugin;
bool get installed => plugin.installedVersion.isNotEmpty; bool get installed => plugin.installed;
bool isEnabled = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
isEnabled = bind.pluginIsEnabled(id: plugin.meta.id);
return Row( return Row(
children: [ children: [
Flexible( Flexible(
@ -70,7 +73,7 @@ class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
child: Row( child: Row(
children: [ children: [
Text( Text(
translate(widget.plugin.meta.name), widget.plugin.meta.name,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
fontSize: _kTitleFontSize, fontSize: _kTitleFontSize,
@ -95,24 +98,25 @@ class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
return Container( return Container(
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed, onPressed: onPressed,
child: Text(label), child: Text(translate(label)),
), ),
); );
} }
Widget headerInstallEnable() { Widget headerInstallEnable() {
final installButton = headerButton(installed ? 'uninstall' : 'install', () { final installButton = headerButton(
bind.pluginInstall( installed ? 'Uninstall' : 'Install',
id: plugin.meta.id, () {
b: !installed, bind.pluginInstall(
); id: plugin.meta.id,
}); b: !installed,
);
},
);
if (installed) { if (installed) {
final needUpdate = final updateButton = plugin.needUpdate
plugin.installedVersion.compareTo(plugin.meta.version) < 0; ? headerButton('Update', () {
final updateButton = needUpdate
? headerButton('update', () {
bind.pluginInstall( bind.pluginInstall(
id: plugin.meta.id, id: plugin.meta.id,
b: !installed, b: !installed,
@ -120,10 +124,9 @@ class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
}) })
: Container(); : Container();
final isEnabled = bind.pluginIsEnabled(id: plugin.meta.id);
final enableButton = !installed final enableButton = !installed
? Container() ? Container()
: headerButton(isEnabled ? 'disable' : 'enable', () { : headerButton(isEnabled ? 'Disable' : 'Enable', () {
if (isEnabled) { if (isEnabled) {
clearPlugin(plugin.meta.id); clearPlugin(plugin.meta.id);
} }
@ -175,7 +178,7 @@ class _DesktopSettingsCardState extends State<DesktopSettingsCard> {
} }
Widget more() { Widget more() {
if (!installed) { if (!(installed && isEnabled)) {
return Container(); return Container();
} }

View File

@ -1,6 +1,6 @@
use std::{ use std::{
future::Future, future::Future,
sync::{Arc, Mutex}, sync::{Arc, Mutex, RwLock},
}; };
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@ -61,6 +61,7 @@ lazy_static::lazy_static! {
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned());
static ref SERVER_RUNNING: Arc<RwLock<bool>> = Default::default();
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -93,9 +94,14 @@ pub fn global_init() -> bool {
pub fn global_clean() {} pub fn global_clean() {}
#[inline]
pub fn set_server_running(b: bool) {
*SERVER_RUNNING.write().unwrap() = b;
}
#[inline] #[inline]
pub fn is_server() -> bool { pub fn is_server() -> bool {
*IS_SERVER *IS_SERVER || *SERVER_RUNNING.read().unwrap()
} }
#[inline] #[inline]

View File

@ -245,7 +245,7 @@ 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")))]
if args[0] == "--plugin-install" { if args[0] == "--plugin-install" {
if args.len() == 3 { if args.len() == 3 {
crate::plugin::privileged_install_plugin(&args[1], &args[2]); crate::plugin::install_plugin_with_url(&args[1], &args[2]);
} }
return None; return None;
} else if args[0] == "--plugin-uninstall" { } else if args[0] == "--plugin-uninstall" {

View File

@ -1580,12 +1580,11 @@ pub fn plugin_install(id: String, b: bool) {
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
{ {
if b { if b {
allow_err!(crate::plugin::user_install_plugin(&id)); if let Err(e) = crate::plugin::install_plugin(&id) {
log::error!("Failed to install plugin '{}': {}", id, e);
}
} else { } else {
// to-do: uninstall plugin crate::plugin::uninstall_plugin(&id, true);
// 1. unload 2. remove configs 3. remove config files
// allow_err!(super::unload_plugin(&id));
crate::plugin::uninstall_plugin(&id);
} }
} }
} }

View File

@ -36,6 +36,16 @@ fn path_plugins(id: &str) -> PathBuf {
HbbConfig::path("plugins").join(id) HbbConfig::path("plugins").join(id)
} }
pub fn remove(id: &str) {
CONFIG_SHARED.lock().unwrap().remove(id);
CONFIG_PEERS.lock().unwrap().remove(id);
// allow_err is Ok here.
allow_err!(ManagerConfig::remove_plugin(id));
if let Err(e) = fs::remove_dir_all(path_plugins(id)) {
log::error!("Failed to remove plugin '{}' directory: {}", id, e);
}
}
impl Deref for SharedConfig { impl Deref for SharedConfig {
type Target = HashMap<String, String>; type Target = HashMap<String, String>;
@ -207,6 +217,7 @@ impl PeerConfig {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PluginStatus { pub struct PluginStatus {
pub enabled: bool, pub enabled: bool,
pub uninstalled: bool,
} }
const MANAGER_VERSION: &str = "0.1.0"; const MANAGER_VERSION: &str = "0.1.0";
@ -269,7 +280,13 @@ impl ManagerConfig {
if let Some(status) = lock.plugins.get_mut(id) { if let Some(status) = lock.plugins.get_mut(id) {
status.enabled = enabled; status.enabled = enabled;
} else { } else {
lock.plugins.insert(id.to_owned(), PluginStatus { enabled }); lock.plugins.insert(
id.to_owned(),
PluginStatus {
enabled,
uninstalled: false,
},
);
} }
hbb_common::config::store_path(Self::path(), &*lock) hbb_common::config::store_path(Self::path(), &*lock)
} }
@ -292,29 +309,50 @@ impl ManagerConfig {
#[inline] #[inline]
pub fn add_plugin(id: &str) -> ResultType<()> { pub fn add_plugin(id: &str) -> ResultType<()> {
let mut lock = CONFIG_MANAGER.lock().unwrap(); let mut lock = CONFIG_MANAGER.lock().unwrap();
lock.plugins lock.plugins.insert(
.insert(id.to_owned(), PluginStatus { enabled: true }); id.to_owned(),
PluginStatus {
enabled: true,
uninstalled: false,
},
);
hbb_common::config::store_path(Self::path(), &*lock) hbb_common::config::store_path(Self::path(), &*lock)
} }
#[inline] #[inline]
pub fn remove_plugin(id: &str, uninstall: bool) -> ResultType<()> { pub fn remove_plugin(id: &str) -> ResultType<()> {
let mut lock = CONFIG_MANAGER.lock().unwrap(); let mut lock = CONFIG_MANAGER.lock().unwrap();
lock.plugins.remove(id); lock.plugins.remove(id);
hbb_common::config::store_path(Self::path(), &*lock)?; hbb_common::config::store_path(Self::path(), &*lock)
if uninstall {
allow_err!(fs::remove_dir_all(path_plugins(id)));
}
Ok(())
} }
pub fn remove_plugins(uninstall: bool) { #[inline]
pub fn is_uninstalled(id: &str) -> bool {
CONFIG_MANAGER
.lock()
.unwrap()
.plugins
.get(id)
.map(|p| p.uninstalled)
.unwrap_or(false)
}
#[inline]
pub fn set_uninstall(id: &str, uninstall: bool) -> ResultType<()> {
let mut lock = CONFIG_MANAGER.lock().unwrap(); let mut lock = CONFIG_MANAGER.lock().unwrap();
lock.plugins.clear(); if let Some(status) = lock.plugins.get_mut(id) {
allow_err!(hbb_common::config::store_path(Self::path(), &*lock)); status.uninstalled = uninstall;
if uninstall { } else {
allow_err!(fs::remove_dir_all(HbbConfig::path("plugins"))); lock.plugins.insert(
id.to_owned(),
PluginStatus {
enabled: true,
uninstalled: uninstall,
},
);
} }
hbb_common::config::store_path(Self::path(), &*lock)?;
Ok(())
} }
} }

View File

@ -24,6 +24,7 @@ pub enum Plugin {
Load(String), Load(String),
Reload(String), Reload(String),
InstallStatus((String, InstallStatus)), InstallStatus((String, InstallStatus)),
Uninstall(String),
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
@ -66,6 +67,11 @@ pub async fn reload_plugin(id: &str) -> ResultType<()> {
reload_plugin_async(id).await reload_plugin_async(id).await
} }
#[tokio::main(flavor = "current_thread")]
pub async fn uninstall_plugin(id: &str) -> ResultType<()> {
uninstall_plugin_async(id).await
}
async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType<Option<String>> { async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType<Option<String>> {
let mut c = connect(ms_timeout, "").await?; let mut c = connect(ms_timeout, "").await?;
c.send(&Data::Plugin(Plugin::Config( c.send(&Data::Plugin(Plugin::Config(
@ -164,6 +170,13 @@ async fn reload_plugin_async(id: &str) -> ResultType<()> {
Ok(()) Ok(())
} }
async fn uninstall_plugin_async(id: &str) -> ResultType<()> {
let mut c = connect(1000, "").await?;
c.send(&Data::Plugin(Plugin::Uninstall(id.to_owned())))
.await?;
Ok(())
}
pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { 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 {
@ -206,11 +219,15 @@ pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) {
} }
}, },
Plugin::Load(id) => { Plugin::Load(id) => {
allow_err!(super::config::ManagerConfig::set_uninstall(&id, false));
allow_err!(super::load_plugin(&id)); allow_err!(super::load_plugin(&id));
} }
Plugin::Reload(id) => { Plugin::Reload(id) => {
allow_err!(super::reload_plugin(&id)); allow_err!(super::reload_plugin(&id));
} }
Plugin::Uninstall(id) => {
super::manager::uninstall_plugin(&id, false);
}
_ => {} _ => {}
} }
} }

View File

@ -8,12 +8,14 @@ use serde_derive::{Deserialize, Serialize};
use serde_json; use serde_json;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
const MSG_TO_UI_PLUGIN_MANAGER_LIST: &str = "plugin_list"; 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_UPDATE: &str = "plugin_update";
const MSG_TO_UI_PLUGIN_MANAGER_INSTALL: &str = "plugin_install"; const MSG_TO_UI_PLUGIN_MANAGER_INSTALL: &str = "plugin_install";
const MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL: &str = "plugin_uninstall";
const IPC_PLUGIN_POSTFIX: &str = "_plugin"; const IPC_PLUGIN_POSTFIX: &str = "_plugin";
@ -40,7 +42,6 @@ pub struct PluginInfo {
pub source: PluginSource, pub source: PluginSource,
pub meta: PluginMeta, pub meta: PluginMeta,
pub installed_version: String, pub installed_version: String,
pub install_time: String,
pub invalid_reason: String, pub invalid_reason: String,
} }
@ -78,7 +79,6 @@ fn get_source_plugins() -> HashMap<String, PluginInfo> {
source: source.clone(), source: source.clone(),
meta: meta.clone(), meta: meta.clone(),
installed_version: "".to_string(), installed_version: "".to_string(),
install_time: "".to_string(),
invalid_reason: "".to_string(), invalid_reason: "".to_string(),
}, },
); );
@ -110,9 +110,18 @@ fn send_plugin_list_event(plugins: &HashMap<String, PluginInfo>) {
pub fn load_plugin_list() { pub fn load_plugin_list() {
let mut plugin_info_lock = PLUGIN_INFO.lock().unwrap(); let mut plugin_info_lock = PLUGIN_INFO.lock().unwrap();
let mut plugins = get_source_plugins(); let mut plugins = get_source_plugins();
for (id, info) in super::plugins::get_plugin_infos().read().unwrap().iter() {
// A big read lock is needed to prevent race conditions.
// Loading plugin list may be slow.
// Users may call uninstall plugin in the middle.
let plugin_infos = super::plugins::get_plugin_infos();
let plugin_infos_read_lock = plugin_infos.read().unwrap();
for (id, info) in plugin_infos_read_lock.iter() {
if info.uninstalled {
continue;
}
if let Some(p) = plugins.get_mut(id) { if let Some(p) = plugins.get_mut(id) {
p.install_time = info.install_time.clone();
p.installed_version = info.desc.meta().version.clone(); p.installed_version = info.desc.meta().version.clone();
p.invalid_reason = "".to_string(); p.invalid_reason = "".to_string();
} else { } else {
@ -126,7 +135,6 @@ pub fn load_plugin_list() {
}, },
meta: info.desc.meta().clone(), meta: info.desc.meta().clone(),
installed_version: info.desc.meta().version.clone(), installed_version: info.desc.meta().version.clone(),
install_time: info.install_time.clone(),
invalid_reason: "".to_string(), invalid_reason: "".to_string(),
}, },
); );
@ -139,14 +147,34 @@ pub fn load_plugin_list() {
pub fn install_plugin(id: &str) -> ResultType<()> { pub fn install_plugin(id: &str) -> ResultType<()> {
match PLUGIN_INFO.lock().unwrap().get(id) { match PLUGIN_INFO.lock().unwrap().get(id) {
Some(plugin) => { Some(plugin) => {
let _plugin_url = format!(
"{}/plugins/{}/{}_{}.zip",
plugin.source.url, plugin.meta.id, plugin.meta.id, plugin.meta.version
);
// to-do: Support args with space in quotes. 'arg 1' and "arg 2"
#[cfg(windows)] #[cfg(windows)]
let _res = {
crate::platform::elevate(&format!("--plugin-install {} {}", id, _plugin_url))?; let mut same_plugin_exists = false;
if let Some(version) = super::plugins::get_version(id) {
if version == plugin.meta.version {
same_plugin_exists = true;
}
}
// to-do: Support args with space in quotes. 'arg 1' and "arg 2"
let plugin_url = format!(
"{}/plugins/{}/{}_{}.zip",
plugin.source.url, plugin.meta.id, plugin.meta.id, plugin.meta.version
);
let args = if same_plugin_exists {
format!("--plugin-install {}", id)
} else {
format!("--plugin-install {} {}", id, plugin_url)
};
let allowed = crate::platform::elevate(&args)?;
if allowed && same_plugin_exists {
super::ipc::load_plugin(id)?;
super::plugins::load_plugin(id)?;
super::plugins::mark_uninstalled(id, false);
push_install_event(id, "finished");
}
}
Ok(()) Ok(())
} }
None => { None => {
@ -155,26 +183,93 @@ pub fn install_plugin(id: &str) -> ResultType<()> {
} }
} }
pub(super) fn remove_plugins() {} fn get_uninstalled_plugins() -> ResultType<Vec<String>> {
let plugins_dir = super::get_plugins_dir()?;
// 1. Add to uninstall list. let mut plugins = Vec::new();
// 2. Try remove. if plugins_dir.exists() {
// 2. Remove on the next start. for entry in std::fs::read_dir(plugins_dir)? {
pub fn uninstall_plugin(id: &str) { match entry {
// to-do: add to uninstall list. Ok(entry) => {
super::plugins::unload_plugin(id); let plugin_dir = entry.path();
if plugin_dir.is_dir() {
if let Some(id) = plugin_dir.file_name().and_then(|n| n.to_str()) {
if super::config::ManagerConfig::is_uninstalled(id) {
plugins.push(id.to_string());
}
}
}
}
Err(e) => {
log::error!("Failed to read plugins dir entry, {}", e);
}
}
}
}
Ok(plugins)
} }
fn push_install_event(id: &str, msg: &str) { pub(super) fn remove_plugins() -> ResultType<()> {
for id in get_uninstalled_plugins()?.iter() {
super::config::remove(id as _);
if let Ok(dir) = super::get_plugin_dir(id as _) {
allow_err!(fs::remove_dir_all(dir));
}
}
Ok(())
}
pub fn uninstall_plugin(id: &str, called_by_ui: bool) {
if called_by_ui {
match crate::platform::elevate(&format!("--plugin-uninstall {}", id)) {
Ok(true) => {
if let Err(e) = super::ipc::uninstall_plugin(id) {
log::error!("Failed to uninstall plugin '{}': {}", id, e);
push_uninstall_event(id, "failed");
return;
}
super::plugins::unload_plugin(id);
super::plugins::mark_uninstalled(id, true);
super::config::remove(id);
push_uninstall_event(id, "");
}
Ok(false) => {
return;
}
Err(e) => {
log::error!("Failed to uninstall plugin '{}': {}", id, e);
push_uninstall_event(id, "failed");
return;
}
}
}
if is_server() {
super::plugins::unload_plugin(&id);
// allow_err is Ok here.
allow_err!(super::config::ManagerConfig::set_uninstall(&id, true));
}
}
fn push_event(id: &str, r#type: &str, msg: &str) {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER); m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER);
m.insert("id", id); m.insert("id", id);
m.insert(MSG_TO_UI_PLUGIN_MANAGER_INSTALL, msg); m.insert(r#type, msg);
if let Ok(event) = serde_json::to_string(&m) { if let Ok(event) = serde_json::to_string(&m) {
let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone());
} }
} }
#[inline]
fn push_uninstall_event(id: &str, msg: &str) {
push_event(id, MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL, msg);
}
#[inline]
fn push_install_event(id: &str, msg: &str) {
push_event(id, MSG_TO_UI_PLUGIN_MANAGER_INSTALL, msg);
}
async fn handle_conn(mut stream: crate::ipc::Connection) { async fn handle_conn(mut stream: crate::ipc::Connection) {
loop { loop {
tokio::select! { tokio::select! {
@ -197,7 +292,7 @@ async fn handle_conn(mut stream: crate::ipc::Connection) {
InstallStatus::Finished => { InstallStatus::Finished => {
allow_err!(super::plugins::load_plugin(&id)); allow_err!(super::plugins::load_plugin(&id));
allow_err!(super::ipc::load_plugin_async(id).await); allow_err!(super::ipc::load_plugin_async(id).await);
load_plugin_list(); std::thread::spawn(load_plugin_list);
push_install_event(&id, "finished"); push_install_event(&id, "finished");
} }
InstallStatus::FailedCreating => { InstallStatus::FailedCreating => {
@ -329,7 +424,7 @@ pub(super) mod install {
Ok(()) Ok(())
} }
pub fn install_plugin(id: &str, url: &str) { pub fn install_plugin_with_url(id: &str, url: &str) {
let plugin_dir = match super::super::get_plugin_dir(id) { let plugin_dir = match super::super::get_plugin_dir(id) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {

View File

@ -1,4 +1,4 @@
use hbb_common::{allow_err, libc, log, ResultType}; use hbb_common::{libc, log, ResultType};
use std::{ use std::{
env, env,
ffi::{c_char, c_int, c_void, CStr}, ffi::{c_char, c_int, c_void, CStr},
@ -19,12 +19,11 @@ mod plog;
mod plugins; mod plugins;
pub use manager::{ pub use manager::{
install::install_plugin as privileged_install_plugin, install_plugin as user_install_plugin, install::install_plugin_with_url, install_plugin, load_plugin_list, uninstall_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,
reload_plugin, sync_ui, unload_plugin, unload_plugins, reload_plugin, sync_ui, unload_plugin,
}; };
const MSG_TO_UI_TYPE_PLUGIN_EVENT: &str = "plugin_event"; const MSG_TO_UI_TYPE_PLUGIN_EVENT: &str = "plugin_event";
@ -92,9 +91,13 @@ pub fn init() {
if !is_server() { if !is_server() {
std::thread::spawn(move || manager::start_ipc()); std::thread::spawn(move || manager::start_ipc());
} else { } else {
manager::remove_plugins(); if let Err(e) = manager::remove_plugins() {
log::error!("Failed to remove plugins: {}", e);
}
}
if let Err(e) = plugins::load_plugins() {
log::error!("Failed to load plugins: {}", e);
} }
allow_err!(plugins::load_plugins());
} }
#[inline] #[inline]

View File

@ -3,7 +3,7 @@ use super::{desc::Desc, errno::*, *};
use crate::common::is_server; use crate::common::is_server;
use crate::flutter; use crate::flutter;
use hbb_common::{ use hbb_common::{
allow_err, bail, bail,
dlopen::symbor::Library, dlopen::symbor::Library,
lazy_static, log, lazy_static, log,
message_proto::{Message, Misc, PluginFailure, PluginRequest}, message_proto::{Message, Misc, PluginFailure, PluginRequest},
@ -15,7 +15,6 @@ 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";
@ -29,7 +28,7 @@ lazy_static::lazy_static! {
pub(super) struct PluginInfo { pub(super) struct PluginInfo {
pub path: String, pub path: String,
pub install_time: String, pub uninstalled: bool,
pub desc: Desc, pub desc: Desc,
} }
@ -307,7 +306,7 @@ fn load_plugin_dir(dir: &PathBuf) {
Err(e) => { Err(e) => {
log::error!( log::error!(
"Failed to read '{}' dir entry, {}", "Failed to read '{}' dir entry, {}",
dir.file_stem().and_then(|f| f.to_str()).unwrap_or(""), dir.file_name().and_then(|f| f.to_str()).unwrap_or(""),
e e
); );
} }
@ -316,20 +315,18 @@ fn load_plugin_dir(dir: &PathBuf) {
} }
} }
pub fn unload_plugins() {
log::info!("Plugins unloaded");
PLUGINS.write().unwrap().clear();
if change_manager() {
super::config::ManagerConfig::remove_plugins(false);
}
}
pub fn unload_plugin(id: &str) { pub fn unload_plugin(id: &str) {
log::info!("Plugin {} unloaded", id); log::info!("Plugin {} unloaded", id);
PLUGINS.write().unwrap().remove(id); PLUGINS.write().unwrap().remove(id);
if change_manager() { }
allow_err!(super::config::ManagerConfig::remove_plugin(id, false));
} pub(super) fn mark_uninstalled(id: &str, uninstalled: bool) {
log::info!("Plugin {} uninstall", id);
PLUGIN_INFO
.write()
.unwrap()
.get_mut(id)
.map(|info| info.uninstalled = uninstalled);
} }
pub fn reload_plugin(id: &str) -> ResultType<()> { pub fn reload_plugin(id: &str) -> ResultType<()> {
@ -364,7 +361,7 @@ fn load_plugin_path(path: &str) -> ResultType<()> {
}; };
plugin.init(&init_data, path)?; plugin.init(&init_data, path)?;
if change_manager() { if is_server() {
super::config::ManagerConfig::add_plugin(&desc.meta().id)?; super::config::ManagerConfig::add_plugin(&desc.meta().id)?;
} }
@ -372,18 +369,11 @@ fn load_plugin_path(path: &str) -> ResultType<()> {
// Ui may be not ready now, so we need to update again once ui is ready. // Ui may be not ready now, so we need to update again once ui is ready.
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.meta().id.clone(); let id = desc.meta().id.clone();
let plugin_info = PluginInfo { let plugin_info = PluginInfo {
path: path.to_string(), path: path.to_string(),
install_time, uninstalled: false,
desc, desc,
}; };
PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info);
@ -582,15 +572,6 @@ fn make_plugin_failure(id: &str, name: &str, msg: &str) -> Message {
msg_out msg_out
} }
#[inline]
fn change_manager() -> bool {
#[cfg(debug_assertions)]
let change_manager = true;
#[cfg(not(debug_assertions))]
let change_manager = is_server();
change_manager
}
fn reload_ui(desc: &Desc, sync_to: Option<&str>) { fn reload_ui(desc: &Desc, sync_to: Option<&str>) {
for (location, ui) in desc.location().ui.iter() { for (location, ui) in desc.location().ui.iter() {
if let Ok(ui) = serde_json::to_string(&ui) { if let Ok(ui) = serde_json::to_string(&ui) {
@ -641,3 +622,11 @@ pub(super) fn get_desc_conf(id: &str) -> Option<super::desc::Config> {
.get(id) .get(id)
.map(|info| info.desc.config().clone()) .map(|info| info.desc.config().clone())
} }
pub(super) fn get_version(id: &str) -> Option<String> {
PLUGIN_INFO
.read()
.unwrap()
.get(id)
.map(|info| info.desc.meta().version.clone())
}

View File

@ -394,6 +394,7 @@ pub async fn start_server(is_server: bool) {
} }
if is_server { if is_server {
crate::common::set_server_running(true);
std::thread::spawn(move || { std::thread::spawn(move || {
if let Err(err) = crate::ipc::start("") { if let Err(err) = crate::ipc::start("") {
log::error!("Failed to start ipc: {}", err); log::error!("Failed to start ipc: {}", err);