2024-08-10 03:23:47 +00:00
using Flow.Launcher.Core.ExternalPlugins ;
2022-08-12 13:12:38 +00:00
using System ;
2020-02-21 21:12:58 +00:00
using System.Collections.Concurrent ;
2015-11-03 05:09:54 +00:00
using System.Collections.Generic ;
2014-12-26 11:36:43 +00:00
using System.IO ;
using System.Linq ;
2021-01-02 07:52:41 +00:00
using System.Threading ;
2016-05-05 20:15:13 +00:00
using System.Threading.Tasks ;
2020-04-21 09:12:17 +00:00
using Flow.Launcher.Infrastructure ;
using Flow.Launcher.Infrastructure.Logger ;
using Flow.Launcher.Infrastructure.UserSettings ;
using Flow.Launcher.Plugin ;
2021-06-21 11:56:20 +00:00
using ISavable = Flow . Launcher . Plugin . ISavable ;
2023-11-10 16:40:27 +00:00
using Flow.Launcher.Plugin.SharedCommands ;
using System.Text.Json ;
2024-06-09 12:45:23 +00:00
using Flow.Launcher.Core.Resource ;
2014-12-26 11:36:43 +00:00
2020-04-21 09:12:17 +00:00
namespace Flow.Launcher.Core.Plugin
2014-12-26 11:36:43 +00:00
{
/// <summary>
2020-04-22 10:26:09 +00:00
/// The entry for managing Flow Launcher plugins
2014-12-26 11:36:43 +00:00
/// </summary>
public static class PluginManager
{
2016-01-08 01:13:36 +00:00
private static IEnumerable < PluginPair > _contextMenuPlugins ;
2014-12-26 14:51:04 +00:00
2016-04-21 00:53:21 +00:00
public static List < PluginPair > AllPlugins { get ; private set ; }
2021-05-28 09:06:58 +00:00
public static readonly HashSet < PluginPair > GlobalPlugins = new ( ) ;
2021-07-01 07:58:50 +00:00
public static readonly Dictionary < string , PluginPair > NonGlobalPlugins = new ( ) ;
2015-11-05 20:44:14 +00:00
2015-11-01 22:59:56 +00:00
public static IPublicAPI API { private set ; get ; }
2016-06-20 23:14:32 +00:00
2023-11-10 17:34:01 +00:00
private static PluginsSettings Settings ;
2016-05-05 15:08:44 +00:00
private static List < PluginMetadata > _metadatas ;
2023-11-11 07:05:24 +00:00
private static List < string > _modifiedPlugins = new List < string > ( ) ;
2015-11-01 22:59:56 +00:00
2020-06-25 00:40:29 +00:00
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
2021-07-01 07:58:50 +00:00
private static readonly string [ ] Directories =
{
Constant . PreinstalledDirectory , DataLocation . PluginsDirectory
} ;
2016-06-21 23:42:24 +00:00
private static void DeletePythonBinding ( )
{
2020-04-21 12:54:41 +00:00
const string binding = "flowlauncher.py" ;
2020-06-25 00:40:29 +00:00
foreach ( var subDirectory in Directory . GetDirectories ( DataLocation . PluginsDirectory ) )
2016-06-21 23:42:24 +00:00
{
2020-06-25 00:40:29 +00:00
File . Delete ( Path . Combine ( subDirectory , binding ) ) ;
2014-12-26 11:36:43 +00:00
}
}
2023-04-25 14:38:36 +00:00
/// <summary>
2024-06-27 05:10:50 +00:00
/// Save json and ISavable
2023-04-25 14:38:36 +00:00
/// </summary>
2016-05-02 21:37:01 +00:00
public static void Save ( )
{
foreach ( var plugin in AllPlugins )
{
2021-06-21 11:56:20 +00:00
var savable = plugin . Plugin as ISavable ;
2016-05-02 21:37:01 +00:00
savable ? . Save ( ) ;
}
2021-06-21 02:34:07 +00:00
2021-05-13 12:49:41 +00:00
API . SavePluginSettings ( ) ;
2016-05-02 21:37:01 +00:00
}
2021-07-05 08:50:50 +00:00
public static async ValueTask DisposePluginsAsync ( )
{
2021-07-05 08:54:19 +00:00
foreach ( var pluginPair in AllPlugins )
2021-07-05 08:50:50 +00:00
{
2021-07-05 08:54:19 +00:00
switch ( pluginPair . Plugin )
2021-07-05 08:50:50 +00:00
{
case IDisposable disposable :
disposable . Dispose ( ) ;
break ;
case IAsyncDisposable asyncDisposable :
await asyncDisposable . DisposeAsync ( ) ;
break ;
}
}
}
2022-08-09 00:35:38 +00:00
public static async Task ReloadDataAsync ( )
2019-10-06 01:49:47 +00:00
{
2021-01-06 11:11:58 +00:00
await Task . WhenAll ( AllPlugins . Select ( plugin = > plugin . Plugin switch
2019-10-06 01:49:47 +00:00
{
2021-01-06 11:11:58 +00:00
IReloadable p = > Task . Run ( p . ReloadData ) ,
IAsyncReloadable p = > p . ReloadDataAsync ( ) ,
_ = > Task . CompletedTask ,
} ) . ToArray ( ) ) ;
2019-10-06 01:49:47 +00:00
}
2024-05-28 11:27:48 +00:00
public static async Task OpenExternalPreviewAsync ( string path , bool sendFailToast = true )
{
await Task . WhenAll ( AllPlugins . Select ( plugin = > plugin . Plugin switch
{
IAsyncExternalPreview p = > p . OpenPreviewAsync ( path , sendFailToast ) ,
_ = > Task . CompletedTask ,
} ) . ToArray ( ) ) ;
}
public static async Task CloseExternalPreviewAsync ( )
{
await Task . WhenAll ( AllPlugins . Select ( plugin = > plugin . Plugin switch
{
IAsyncExternalPreview p = > p . ClosePreviewAsync ( ) ,
_ = > Task . CompletedTask ,
} ) . ToArray ( ) ) ;
}
public static async Task SwitchExternalPreviewAsync ( string path , bool sendFailToast = true )
{
await Task . WhenAll ( AllPlugins . Select ( plugin = > plugin . Plugin switch
{
IAsyncExternalPreview p = > p . SwitchPreviewAsync ( path , sendFailToast ) ,
_ = > Task . CompletedTask ,
} ) . ToArray ( ) ) ;
}
public static bool UseExternalPreview ( )
{
return GetPluginsForInterface < IAsyncExternalPreview > ( ) . Any ( x = > ! x . Metadata . Disabled ) ;
}
2024-06-12 04:14:25 +00:00
public static bool AllowAlwaysPreview ( )
{
var plugin = GetPluginsForInterface < IAsyncExternalPreview > ( ) . FirstOrDefault ( x = > ! x . Metadata . Disabled ) ;
if ( plugin is null )
return false ;
return ( ( IAsyncExternalPreview ) plugin . Plugin ) . AllowAlwaysPreview ( ) ;
}
2024-05-28 11:27:48 +00:00
2016-05-05 15:08:44 +00:00
static PluginManager ( )
{
2020-06-25 00:40:29 +00:00
// validate user directory
Directory . CreateDirectory ( DataLocation . PluginsDirectory ) ;
2016-06-21 23:42:24 +00:00
// force old plugins use new python binding
DeletePythonBinding ( ) ;
2016-05-05 15:08:44 +00:00
}
2016-05-10 00:08:54 +00:00
/// <summary>
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
/// todo happlebao The API should be removed
/// </summary>
/// <param name="settings"></param>
public static void LoadPlugins ( PluginsSettings settings )
2016-03-28 00:09:40 +00:00
{
2016-05-10 00:08:54 +00:00
_metadatas = PluginConfig . Parse ( Directories ) ;
2016-05-12 01:45:35 +00:00
Settings = settings ;
Settings . UpdatePluginSettings ( _metadatas ) ;
AllPlugins = PluginsLoader . Plugins ( _metadatas , Settings ) ;
2016-05-10 00:08:54 +00:00
}
2020-02-21 21:12:58 +00:00
/// <summary>
/// Call initialize for all plugins
/// </summary>
/// <returns>return the list of failed to init plugins or null for none</returns>
2022-08-10 03:18:37 +00:00
public static async Task InitializePluginsAsync ( IPublicAPI api )
2016-05-10 00:08:54 +00:00
{
2016-03-28 00:09:40 +00:00
API = api ;
2020-02-21 21:12:58 +00:00
var failedPlugins = new ConcurrentQueue < PluginPair > ( ) ;
2021-01-02 07:52:41 +00:00
var InitTasks = AllPlugins . Select ( pair = > Task . Run ( async delegate
2014-12-26 11:36:43 +00:00
{
2020-02-21 21:12:58 +00:00
try
2014-12-26 11:36:43 +00:00
{
2021-03-23 09:25:46 +00:00
var milliseconds = await Stopwatch . DebugAsync ( $"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>" ,
2021-07-01 07:58:50 +00:00
( ) = > pair . Plugin . InitAsync ( new PluginInitContext ( pair . Metadata , API ) ) ) ;
2021-03-23 09:25:46 +00:00
2020-02-21 21:12:58 +00:00
pair . Metadata . InitTime + = milliseconds ;
2021-01-02 07:52:41 +00:00
Log . Info (
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>" ) ;
2020-02-21 21:12:58 +00:00
}
catch ( Exception e )
{
Log . Exception ( nameof ( PluginManager ) , $"Fail to Init plugin: {pair.Metadata.Name}" , e ) ;
2021-01-02 14:30:56 +00:00
pair . Metadata . Disabled = true ;
2020-02-21 21:12:58 +00:00
failedPlugins . Enqueue ( pair ) ;
}
2021-01-02 07:52:41 +00:00
} ) ) ;
await Task . WhenAll ( InitTasks ) ;
2015-01-27 14:59:03 +00:00
2016-05-05 20:15:13 +00:00
_contextMenuPlugins = GetPluginsForInterface < IContextMenu > ( ) ;
foreach ( var plugin in AllPlugins )
2015-02-04 15:16:41 +00:00
{
2021-05-27 22:44:06 +00:00
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
// has multiple global and action keywords because we will only add them here once.
foreach ( var actionKeyword in plugin . Metadata . ActionKeywords . Distinct ( ) )
2021-01-02 07:52:41 +00:00
{
switch ( actionKeyword )
{
case Query . GlobalPluginWildcardSign :
GlobalPlugins . Add ( plugin ) ;
break ;
default :
NonGlobalPlugins [ actionKeyword ] = plugin ;
break ;
}
}
2016-05-05 20:15:13 +00:00
}
2016-05-05 15:08:44 +00:00
2024-06-27 05:05:18 +00:00
InternationalizationManager . Instance . AddPluginLanguageDirectories ( GetPluginsForInterface < IPluginI18n > ( ) ) ;
2024-06-09 12:45:23 +00:00
InternationalizationManager . Instance . ChangeLanguage ( InternationalizationManager . Instance . Settings . Language ) ;
2020-02-21 21:12:58 +00:00
if ( failedPlugins . Any ( ) )
{
var failed = string . Join ( "," , failedPlugins . Select ( x = > x . Metadata . Name ) ) ;
2024-06-27 05:10:12 +00:00
API . ShowMsg (
InternationalizationManager . Instance . GetTranslation ( "failedToInitializePluginsTitle" ) ,
string . Format (
InternationalizationManager . Instance . GetTranslation ( "failedToInitializePluginsMessage" ) ,
failed
) ,
"" ,
false
) ;
2020-02-21 21:12:58 +00:00
}
2014-12-26 11:36:43 +00:00
}
2021-05-28 09:06:58 +00:00
public static ICollection < PluginPair > ValidPluginsForQuery ( Query query )
2015-11-01 17:25:44 +00:00
{
2022-09-19 20:48:28 +00:00
if ( query is null )
return Array . Empty < PluginPair > ( ) ;
2024-06-27 05:10:50 +00:00
2022-09-19 20:48:28 +00:00
if ( ! NonGlobalPlugins . ContainsKey ( query . ActionKeyword ) )
2016-03-28 00:09:40 +00:00
return GlobalPlugins ;
2024-06-27 05:10:50 +00:00
2022-09-19 20:48:28 +00:00
var plugin = NonGlobalPlugins [ query . ActionKeyword ] ;
return new List < PluginPair >
{
plugin
} ;
2014-12-26 14:51:04 +00:00
}
2016-04-21 00:53:21 +00:00
2021-06-11 04:40:07 +00:00
public static async Task < List < Result > > QueryForPluginAsync ( PluginPair pair , Query query , CancellationToken token )
2014-12-26 11:36:43 +00:00
{
2020-06-25 00:40:29 +00:00
var results = new List < Result > ( ) ;
2022-08-12 13:12:38 +00:00
var metadata = pair . Metadata ;
2015-11-01 22:59:56 +00:00
try
2014-12-26 11:36:43 +00:00
{
2022-08-12 13:12:38 +00:00
var milliseconds = await Stopwatch . DebugAsync ( $"|PluginManager.QueryForPlugin|Cost for {metadata.Name}" ,
2021-03-23 09:25:46 +00:00
async ( ) = > results = await pair . Plugin . QueryAsync ( query , token ) . ConfigureAwait ( false ) ) ;
2021-01-03 02:19:33 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-02-03 09:50:21 +00:00
if ( results = = null )
2021-06-11 04:40:07 +00:00
return null ;
2021-01-03 02:19:33 +00:00
UpdatePluginMetadata ( results , metadata , query ) ;
2016-05-22 04:30:38 +00:00
metadata . QueryCount + = 1 ;
2021-01-02 07:52:41 +00:00
metadata . AvgQueryTime =
metadata . QueryCount = = 1 ? milliseconds : ( metadata . AvgQueryTime + milliseconds ) / 2 ;
2021-01-03 02:19:33 +00:00
token . ThrowIfCancellationRequested ( ) ;
}
2021-01-03 02:37:36 +00:00
catch ( OperationCanceledException )
2021-01-03 02:19:33 +00:00
{
2021-01-06 11:33:55 +00:00
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
2021-02-16 02:50:48 +00:00
return null ;
2015-11-01 22:59:56 +00:00
}
2022-08-12 13:12:38 +00:00
catch ( Exception e )
{
2023-11-19 02:45:06 +00:00
Result r = new ( )
{
2023-11-24 18:15:29 +00:00
Title = $"{metadata.Name}: Failed to respond!" ,
SubTitle = "Select this result for more info" ,
2023-11-24 18:15:17 +00:00
IcoPath = Flow . Launcher . Infrastructure . Constant . ErrorIcon ,
2023-11-19 02:45:06 +00:00
PluginDirectory = metadata . PluginDirectory ,
ActionKeywordAssigned = query . ActionKeyword ,
PluginID = metadata . ID ,
OriginQuery = query ,
2023-11-24 18:15:58 +00:00
Action = _ = > { throw new FlowPluginException ( metadata , e ) ; } ,
Score = - 100
2023-11-19 02:45:06 +00:00
} ;
results . Add ( r ) ;
2022-08-12 13:12:38 +00:00
}
2021-01-03 02:19:33 +00:00
return results ;
2014-12-26 11:36:43 +00:00
}
2016-05-05 15:08:44 +00:00
public static void UpdatePluginMetadata ( List < Result > results , PluginMetadata metadata , Query query )
{
foreach ( var r in results )
{
r . PluginDirectory = metadata . PluginDirectory ;
r . PluginID = metadata . ID ;
r . OriginQuery = query ;
2020-03-31 11:00:09 +00:00
2024-06-27 05:10:50 +00:00
// ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
// Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
2020-03-31 11:00:09 +00:00
if ( metadata . ActionKeywords . Count = = 1 )
r . ActionKeywordAssigned = query . ActionKeyword ;
2016-05-05 15:08:44 +00:00
}
}
2014-12-26 11:36:43 +00:00
/// <summary>
/// get specified plugin, return null if not found
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2015-11-05 20:44:14 +00:00
public static PluginPair GetPluginForId ( string id )
2014-12-26 11:36:43 +00:00
{
2015-11-01 23:32:17 +00:00
return AllPlugins . FirstOrDefault ( o = > o . Metadata . ID = = id ) ;
2014-12-26 11:36:43 +00:00
}
2015-02-05 10:43:05 +00:00
2015-11-05 20:44:14 +00:00
public static IEnumerable < PluginPair > GetPluginsForInterface < T > ( ) where T : IFeatures
2015-02-05 14:20:42 +00:00
{
2024-08-11 10:56:03 +00:00
// Handle scenario where this is called before all plugins are instantiated, e.g. language change on startup
2024-08-10 03:23:47 +00:00
return AllPlugins ? . Where ( p = > p . Plugin is T ) ? ? Array . Empty < PluginPair > ( ) ;
2015-02-05 14:20:42 +00:00
}
2015-02-07 15:49:46 +00:00
2015-11-05 20:44:14 +00:00
public static List < Result > GetContextMenusForPlugin ( Result result )
2015-02-07 15:49:46 +00:00
{
2021-07-01 08:03:03 +00:00
var results = new List < Result > ( ) ;
2016-01-08 01:13:36 +00:00
var pluginPair = _contextMenuPlugins . FirstOrDefault ( o = > o . Metadata . ID = = result . PluginID ) ;
2016-03-26 01:20:42 +00:00
if ( pluginPair ! = null )
2015-02-07 15:49:46 +00:00
{
2021-01-02 14:30:56 +00:00
var plugin = ( IContextMenu ) pluginPair . Plugin ;
2016-03-26 01:20:42 +00:00
2015-02-07 15:49:46 +00:00
try
{
2021-07-01 08:03:03 +00:00
results = plugin . LoadContextMenus ( result ) ? ? results ;
2016-03-26 01:20:42 +00:00
foreach ( var r in results )
{
2020-06-25 00:40:29 +00:00
r . PluginDirectory = pluginPair . Metadata . PluginDirectory ;
r . PluginID = pluginPair . Metadata . ID ;
2016-03-26 01:20:42 +00:00
r . OriginQuery = result . OriginQuery ;
}
2015-02-07 15:49:46 +00:00
}
2015-11-09 03:20:02 +00:00
catch ( Exception e )
2015-02-07 15:49:46 +00:00
{
2021-01-02 07:52:41 +00:00
Log . Exception (
$"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{pluginPair.Metadata.Name}>" ,
e ) ;
2015-02-07 15:49:46 +00:00
}
}
2021-01-02 07:52:41 +00:00
2020-06-25 00:40:29 +00:00
return results ;
2015-02-07 15:49:46 +00:00
}
2015-11-09 03:20:02 +00:00
2016-06-20 23:14:32 +00:00
public static bool ActionKeywordRegistered ( string actionKeyword )
2015-11-09 03:20:02 +00:00
{
2021-06-05 08:44:16 +00:00
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
2020-06-25 00:40:29 +00:00
return actionKeyword ! = Query . GlobalPluginWildcardSign
2021-01-02 07:52:41 +00:00
& & NonGlobalPlugins . ContainsKey ( actionKeyword ) ;
2016-06-20 23:14:32 +00:00
}
2015-11-09 03:20:02 +00:00
2016-06-22 23:03:01 +00:00
/// <summary>
/// used to add action keyword for multiple action keyword plugin
/// e.g. web search
/// </summary>
2016-06-20 23:14:32 +00:00
public static void AddActionKeyword ( string id , string newActionKeyword )
{
var plugin = GetPluginForId ( id ) ;
if ( newActionKeyword = = Query . GlobalPluginWildcardSign )
2015-11-09 03:20:02 +00:00
{
2016-06-20 23:14:32 +00:00
GlobalPlugins . Add ( plugin ) ;
2015-11-09 03:20:02 +00:00
}
else
{
2016-06-20 23:14:32 +00:00
NonGlobalPlugins [ newActionKeyword ] = plugin ;
2015-11-09 03:20:02 +00:00
}
2021-01-02 07:52:41 +00:00
2016-06-20 23:14:32 +00:00
plugin . Metadata . ActionKeywords . Add ( newActionKeyword ) ;
2015-11-09 03:20:02 +00:00
}
2016-06-22 23:03:01 +00:00
/// <summary>
2021-05-29 12:00:10 +00:00
/// used to remove action keyword for multiple action keyword plugin
2016-06-22 23:03:01 +00:00
/// e.g. web search
/// </summary>
2016-06-20 23:14:32 +00:00
public static void RemoveActionKeyword ( string id , string oldActionkeyword )
{
var plugin = GetPluginForId ( id ) ;
2019-08-04 11:44:56 +00:00
if ( oldActionkeyword = = Query . GlobalPluginWildcardSign
& & // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
plugin . Metadata . ActionKeywords
2021-05-28 09:06:58 +00:00
. Count ( x = > x = = Query . GlobalPluginWildcardSign ) = = 1 )
2016-06-20 23:14:32 +00:00
{
GlobalPlugins . Remove ( plugin ) ;
}
2021-01-02 14:30:56 +00:00
2020-06-25 00:40:29 +00:00
if ( oldActionkeyword ! = Query . GlobalPluginWildcardSign )
2016-06-20 23:14:32 +00:00
NonGlobalPlugins . Remove ( oldActionkeyword ) ;
2021-01-02 14:30:56 +00:00
2019-08-04 11:44:56 +00:00
2016-06-20 23:14:32 +00:00
plugin . Metadata . ActionKeywords . Remove ( oldActionkeyword ) ;
}
public static void ReplaceActionKeyword ( string id , string oldActionKeyword , string newActionKeyword )
{
if ( oldActionKeyword ! = newActionKeyword )
{
AddActionKeyword ( id , newActionKeyword ) ;
RemoveActionKeyword ( id , oldActionKeyword ) ;
}
}
2023-11-10 16:40:27 +00:00
private static string GetContainingFolderPathAfterUnzip ( string unzippedParentFolderPath )
{
var unzippedFolderCount = Directory . GetDirectories ( unzippedParentFolderPath ) . Length ;
var unzippedFilesCount = Directory . GetFiles ( unzippedParentFolderPath ) . Length ;
// adjust path depending on how the plugin is zipped up
// the recommended should be to zip up the folder not the contents
if ( unzippedFolderCount = = 1 & & unzippedFilesCount = = 0 )
// folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
return Directory . GetDirectories ( unzippedParentFolderPath ) [ 0 ] ;
if ( unzippedFilesCount > 1 )
// content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
return unzippedParentFolderPath ;
return string . Empty ;
}
private static bool SameOrLesserPluginVersionExists ( string metadataPath )
{
var newMetadata = JsonSerializer . Deserialize < PluginMetadata > ( File . ReadAllText ( metadataPath ) ) ;
return AllPlugins . Any ( x = > x . Metadata . ID = = newMetadata . ID
& & newMetadata . Version . CompareTo ( x . Metadata . Version ) < = 0 ) ;
}
2023-11-11 07:05:24 +00:00
#region Public functions
public static bool PluginModified ( string uuid )
{
return _modifiedPlugins . Contains ( uuid ) ;
}
2023-11-11 08:11:21 +00:00
/// <summary>
2024-05-16 11:22:08 +00:00
/// Update a plugin to new version, from a zip file. By default will remove the zip file if update is via url,
/// unless it's a local path installation
2023-11-11 08:11:21 +00:00
/// </summary>
public static void UpdatePlugin ( PluginMetadata existingVersion , UserPlugin newVersion , string zipFilePath )
2023-11-11 07:05:24 +00:00
{
2023-11-11 08:11:21 +00:00
InstallPlugin ( newVersion , zipFilePath , checkModified : false ) ;
2025-02-06 01:35:21 +00:00
UninstallPlugin ( existingVersion , removePluginFromSettings : false , removePluginSettings : false , checkModified : false ) ;
2023-11-11 07:05:24 +00:00
_modifiedPlugins . Add ( existingVersion . ID ) ;
}
2023-11-11 08:11:21 +00:00
/// <summary>
2024-05-16 11:22:08 +00:00
/// Install a plugin. By default will remove the zip file if installation is from url, unless it's a local path installation
2023-11-11 08:11:21 +00:00
/// </summary>
public static void InstallPlugin ( UserPlugin plugin , string zipFilePath )
2023-11-11 07:05:24 +00:00
{
2024-05-16 11:22:08 +00:00
InstallPlugin ( plugin , zipFilePath , checkModified : true ) ;
2023-11-11 07:05:24 +00:00
}
2023-11-11 08:11:21 +00:00
/// <summary>
/// Uninstall a plugin.
/// </summary>
2025-02-06 01:35:21 +00:00
public static void UninstallPlugin ( PluginMetadata plugin , bool removePluginFromSettings = true , bool removePluginSettings = false )
2023-11-11 07:05:24 +00:00
{
2025-02-06 01:35:21 +00:00
UninstallPlugin ( plugin , removePluginFromSettings , removePluginSettings , true ) ;
2023-11-11 07:05:24 +00:00
}
#endregion
#region Internal functions
2023-11-11 08:11:21 +00:00
internal static void InstallPlugin ( UserPlugin plugin , string zipFilePath , bool checkModified )
2023-11-10 16:40:27 +00:00
{
2023-11-11 07:05:24 +00:00
if ( checkModified & & PluginModified ( plugin . ID ) )
{
// Distinguish exception from installing same or less version
throw new ArgumentException ( $"Plugin {plugin.Name} {plugin.ID} has been modified." , nameof ( plugin ) ) ;
}
2023-11-11 08:11:21 +00:00
// Unzip plugin files to temp folder
var tempFolderPluginPath = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
2023-11-10 16:40:27 +00:00
System . IO . Compression . ZipFile . ExtractToDirectory ( zipFilePath , tempFolderPluginPath ) ;
2024-06-27 05:10:50 +00:00
2024-05-16 11:22:08 +00:00
if ( ! plugin . IsFromLocalInstallPath )
File . Delete ( zipFilePath ) ;
2023-11-10 16:40:27 +00:00
var pluginFolderPath = GetContainingFolderPathAfterUnzip ( tempFolderPluginPath ) ;
var metadataJsonFilePath = string . Empty ;
if ( File . Exists ( Path . Combine ( pluginFolderPath , Constant . PluginMetadataFileName ) ) )
metadataJsonFilePath = Path . Combine ( pluginFolderPath , Constant . PluginMetadataFileName ) ;
if ( string . IsNullOrEmpty ( metadataJsonFilePath ) | | string . IsNullOrEmpty ( pluginFolderPath ) )
{
throw new FileNotFoundException ( $"Unable to find plugin.json from the extracted zip file, or this path {pluginFolderPath} does not exist" ) ;
}
if ( SameOrLesserPluginVersionExists ( metadataJsonFilePath ) )
{
throw new InvalidOperationException ( $"A plugin with the same ID and version already exists, or the version is greater than this downloaded plugin {plugin.Name}" ) ;
}
var folderName = string . IsNullOrEmpty ( plugin . Version ) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}" ;
var defaultPluginIDs = new List < string >
{
"0ECADE17459B49F587BF81DC3A125110" , // BrowserBookmark
"CEA0FDFC6D3B4085823D60DC76F28855" , // Calculator
"572be03c74c642baae319fc283e561a8" , // Explorer
"6A122269676E40EB86EB543B945932B9" , // PluginIndicator
"9f8f9b14-2518-4907-b211-35ab6290dee7" , // PluginsManager
"b64d0a79-329a-48b0-b53f-d658318a1bf6" , // ProcessKiller
"791FC278BA414111B8D1886DFE447410" , // Program
"D409510CD0D2481F853690A07E6DC426" , // Shell
"CEA08895D2544B019B2E9C5009600DF4" , // Sys
"0308FD86DE0A4DEE8D62B9B535370992" , // URL
"565B73353DBF4806919830B9202EE3BF" , // WebSearch
"5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
} ;
// Treat default plugin differently, it needs to be removable along with each flow release
var installDirectory = ! defaultPluginIDs . Any ( x = > x = = plugin . ID )
? DataLocation . PluginsDirectory
: Constant . PreinstalledDirectory ;
var newPluginPath = Path . Combine ( installDirectory , folderName ) ;
2024-11-25 02:38:43 +00:00
FilesFolders . CopyAll ( pluginFolderPath , newPluginPath , MessageBoxEx . Show ) ;
2023-11-10 16:40:27 +00:00
2023-11-11 08:11:21 +00:00
Directory . Delete ( tempFolderPluginPath , true ) ;
2023-11-11 07:05:24 +00:00
if ( checkModified )
{
_modifiedPlugins . Add ( plugin . ID ) ;
}
2023-11-10 16:40:27 +00:00
}
2025-02-06 01:35:21 +00:00
internal static void UninstallPlugin ( PluginMetadata plugin , bool removePluginFromSettings , bool removePluginSettings , bool checkModified )
2023-11-10 16:40:27 +00:00
{
2023-11-11 07:05:24 +00:00
if ( checkModified & & PluginModified ( plugin . ID ) )
{
throw new ArgumentException ( $"Plugin {plugin.Name} has been modified" ) ;
}
2025-02-06 01:35:21 +00:00
if ( removePluginFromSettings )
2023-11-10 16:40:27 +00:00
{
Settings . Plugins . Remove ( plugin . ID ) ;
AllPlugins . RemoveAll ( p = > p . Metadata . ID = = plugin . ID ) ;
}
2025-02-01 05:10:32 +00:00
if ( removePluginSettings )
{
var assemblyLoader = new PluginAssemblyLoader ( plugin . ExecuteFilePath ) ;
var assembly = assemblyLoader . LoadAssemblyAndDependencies ( ) ;
var assemblyName = assembly . GetName ( ) . Name ;
2025-02-08 11:12:53 +00:00
// if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin
// so we need to remove it from the api instance
var method = API . GetType ( ) . GetMethod ( "RemovePluginSettings" ) ;
var pluginJsonStorage = method ? . Invoke ( API , new object [ ] { assemblyName } ) ;
// if there exists a json storage for current plugin, we need to delete the directory path
if ( pluginJsonStorage ! = null )
2025-02-01 05:10:32 +00:00
{
2025-02-08 11:12:53 +00:00
var deleteMethod = pluginJsonStorage . GetType ( ) . GetMethod ( "DeleteDirectory" ) ;
deleteMethod ? . Invoke ( pluginJsonStorage , null ) ;
2025-02-01 05:10:32 +00:00
}
}
2023-11-10 16:40:27 +00:00
// Marked for deletion. Will be deleted on next start up
using var _ = File . CreateText ( Path . Combine ( plugin . PluginDirectory , "NeedDelete.txt" ) ) ;
2023-11-11 07:05:24 +00:00
if ( checkModified )
{
_modifiedPlugins . Add ( plugin . ID ) ;
}
2023-11-10 16:40:27 +00:00
}
2023-11-11 07:05:24 +00:00
#endregion
2014-12-26 11:36:43 +00:00
}
2022-08-09 00:35:38 +00:00
}