2025-04-01 01:57:55 +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 ;
2025-04-01 01:57:55 +00:00
using System.Text.Json ;
2021-01-02 07:52:41 +00:00
using System.Threading ;
2016-05-05 20:15:13 +00:00
using System.Threading.Tasks ;
2025-04-01 01:57:55 +00:00
using CommunityToolkit.Mvvm.DependencyInjection ;
using Flow.Launcher.Core.ExternalPlugins ;
2020-04-21 09:12:17 +00:00
using Flow.Launcher.Infrastructure ;
using Flow.Launcher.Infrastructure.UserSettings ;
using Flow.Launcher.Plugin ;
2023-11-10 16:40:27 +00:00
using Flow.Launcher.Plugin.SharedCommands ;
2025-04-05 03:31:24 +00:00
using IRemovable = Flow . Launcher . Core . Storage . IRemovable ;
2025-04-01 01:57:55 +00:00
using ISavable = Flow . Launcher . Plugin . ISavable ;
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
{
2025-04-08 13:53:00 +00:00
private static readonly string ClassName = nameof ( PluginManager ) ;
2016-01-08 01:13:36 +00:00
private static IEnumerable < PluginPair > _contextMenuPlugins ;
2025-05-03 08:50:50 +00:00
private static IEnumerable < PluginPair > _homePlugins ;
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
2025-02-28 09:03:53 +00:00
// We should not initialize API in static constructor because it will create another API instance
private static IPublicAPI api = null ;
private static IPublicAPI API = > api ? ? = Ioc . Default . GetRequiredService < IPublicAPI > ( ) ;
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 ;
2025-04-11 14:00:36 +00:00
private static readonly List < string > _modifiedPlugins = new ( ) ;
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 ( )
{
2025-04-12 00:08:26 +00:00
foreach ( var pluginPair in AllPlugins )
2016-05-02 21:37:01 +00:00
{
2025-04-12 00:08:26 +00:00
var savable = pluginPair . Plugin as ISavable ;
2025-04-11 13:52:51 +00:00
try
{
savable ? . Save ( ) ;
}
catch ( Exception e )
{
2025-04-12 00:08:26 +00:00
API . LogException ( ClassName , $"Failed to save plugin {pluginPair.Metadata.Name}" , e ) ;
2025-04-11 13:52:51 +00:00
}
2016-05-02 21:37:01 +00:00
}
2021-06-21 02:34:07 +00:00
2021-05-13 12:49:41 +00:00
API . SavePluginSettings ( ) ;
2025-04-01 06:19:59 +00:00
API . SavePluginCaches ( ) ;
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
{
2025-03-06 11:43:16 +00:00
await DisposePluginAsync ( pluginPair ) ;
}
}
private static async Task DisposePluginAsync ( PluginPair pluginPair )
{
2025-04-11 14:02:03 +00:00
try
2025-03-06 11:43:16 +00:00
{
2025-04-11 14:02:03 +00:00
switch ( pluginPair . Plugin )
{
case IDisposable disposable :
disposable . Dispose ( ) ;
break ;
case IAsyncDisposable asyncDisposable :
await asyncDisposable . DisposeAsync ( ) ;
break ;
}
}
catch ( Exception e )
{
2025-04-12 00:08:26 +00:00
API . LogException ( ClassName , $"Failed to dispose plugin {pluginPair.Metadata.Name}" , e ) ;
2021-07-05 08:50:50 +00:00
}
}
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 ) ;
2025-03-23 04:44:49 +00:00
// Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
UpdatePluginDirectory ( _metadatas ) ;
2025-02-24 05:46:58 +00:00
}
2025-03-23 04:44:49 +00:00
private static void UpdatePluginDirectory ( List < PluginMetadata > metadatas )
2025-02-24 05:46:58 +00:00
{
foreach ( var metadata in metadatas )
{
if ( AllowedLanguage . IsDotNet ( metadata . Language ) )
{
metadata . PluginSettingsDirectoryPath = Path . Combine ( DataLocation . PluginSettingsDirectory , metadata . AssemblyName ) ;
metadata . PluginCacheDirectoryPath = Path . Combine ( DataLocation . PluginCacheDirectory , metadata . AssemblyName ) ;
}
2025-03-23 04:44:49 +00:00
else
{
metadata . PluginSettingsDirectoryPath = Path . Combine ( DataLocation . PluginSettingsDirectory , metadata . Name ) ;
metadata . PluginCacheDirectoryPath = Path . Combine ( DataLocation . PluginCacheDirectory , metadata . Name ) ;
}
2025-02-24 05:46:58 +00:00
}
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>
2025-01-27 01:40:24 +00:00
public static async Task InitializePluginsAsync ( )
2016-05-10 00:08:54 +00:00
{
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
{
2025-04-08 13:53:00 +00:00
var milliseconds = await API . StopwatchLogDebugAsync ( ClassName , $"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 ;
2025-04-13 09:26:21 +00:00
API . LogInfo ( ClassName ,
$"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>" ) ;
2020-02-21 21:12:58 +00:00
}
catch ( Exception e )
{
2025-04-13 09:26:21 +00:00
API . LogException ( ClassName , $"Fail to Init plugin: {pair.Metadata.Name}" , e ) ;
2021-01-02 14:30:56 +00:00
pair . Metadata . Disabled = true ;
2025-05-03 14:34:58 +00:00
pair . Metadata . HomeDisabled = 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 > ( ) ;
2025-05-03 08:50:50 +00:00
_homePlugins = GetPluginsForInterface < IAsyncHomeQuery > ( ) ;
2016-05-05 20:15:13 +00:00
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
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 (
2025-02-23 01:29:24 +00:00
API . GetTranslation ( "failedToInitializePluginsTitle" ) ,
2024-06-27 05:10:12 +00:00
string . Format (
2025-02-23 01:29:24 +00:00
API . GetTranslation ( "failedToInitializePluginsMessage" ) ,
2024-06-27 05:10:12 +00:00
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
2025-02-24 05:46:58 +00:00
if ( ! NonGlobalPlugins . TryGetValue ( query . ActionKeyword , out var plugin ) )
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
return new List < PluginPair >
{
plugin
} ;
2014-12-26 14:51:04 +00:00
}
2016-04-21 00:53:21 +00:00
2025-05-04 01:38:35 +00:00
public static ICollection < PluginPair > ValidPluginsForHomeQuery ( )
2025-05-03 08:50:50 +00:00
{
return _homePlugins . ToList ( ) ;
}
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
{
2025-04-08 13:53:00 +00:00
var milliseconds = await API . StopwatchLogDebugAsync ( ClassName , $"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" ,
2025-04-11 14:00:36 +00:00
IcoPath = 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
}
2025-05-04 02:28:20 +00:00
public static async Task < List < Result > > QueryHomeForPluginAsync ( PluginPair pair , Query query , CancellationToken token )
2025-05-03 08:50:50 +00:00
{
var results = new List < Result > ( ) ;
var metadata = pair . Metadata ;
try
{
var milliseconds = await API . StopwatchLogDebugAsync ( ClassName , $"Cost for {metadata.Name}" ,
async ( ) = > results = await ( ( IAsyncHomeQuery ) pair . Plugin ) . HomeQueryAsync ( token ) . ConfigureAwait ( false ) ) ;
token . ThrowIfCancellationRequested ( ) ;
if ( results = = null )
return null ;
2025-05-04 02:28:20 +00:00
UpdatePluginMetadata ( results , metadata , query ) ;
2025-05-03 08:50:50 +00:00
token . ThrowIfCancellationRequested ( ) ;
}
catch ( OperationCanceledException )
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null ;
}
catch ( Exception e )
{
API . LogException ( ClassName , $"Failed to query home for plugin: {metadata.Name}" , e ) ;
return null ;
}
return results ;
}
2025-02-20 10:02:59 +00:00
public static void UpdatePluginMetadata ( IReadOnlyList < Result > results , PluginMetadata metadata , Query query )
2016-05-05 15:08:44 +00:00
{
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
{
2025-04-13 09:26:21 +00:00
API . LogException ( ClassName ,
$"Can't load context menus for plugin <{pluginPair.Metadata.Name}>" ,
2021-01-02 07:52:41 +00:00
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
2025-05-03 14:34:58 +00:00
public static bool IsHomePlugin ( string id )
{
return _homePlugins . Any ( p = > p . Metadata . ID = = id ) ;
}
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
2025-04-11 14:00:36 +00:00
return actionKeyword ! = Query . GlobalPluginWildcardSign
& & 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
2025-02-24 01:53:00 +00:00
// Update action keywords and action keyword in plugin metadata
2016-06-20 23:14:32 +00:00
plugin . Metadata . ActionKeywords . Add ( newActionKeyword ) ;
2025-02-24 01:53:00 +00:00
if ( plugin . Metadata . ActionKeywords . Count > 0 )
{
plugin . Metadata . ActionKeyword = plugin . Metadata . ActionKeywords [ 0 ] ;
}
else
{
plugin . Metadata . ActionKeyword = string . Empty ;
}
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
2025-02-24 01:53:00 +00:00
// Update action keywords and action keyword in plugin metadata
2016-06-20 23:14:32 +00:00
plugin . Metadata . ActionKeywords . Remove ( oldActionkeyword ) ;
2025-02-24 01:53:00 +00:00
if ( plugin . Metadata . ActionKeywords . Count > 0 )
2016-06-20 23:14:32 +00:00
{
2025-02-24 01:53:00 +00:00
plugin . Metadata . ActionKeyword = plugin . Metadata . ActionKeywords [ 0 ] ;
2016-06-20 23:14:32 +00:00
}
2025-02-24 01:53:00 +00:00
else
2016-06-20 23:14:32 +00:00
{
2025-02-24 01:53:00 +00:00
plugin . Metadata . ActionKeyword = string . Empty ;
2016-06-20 23:14:32 +00:00
}
}
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
2025-04-04 08:33:03 +00:00
public static bool PluginModified ( string id )
2023-11-11 07:05:24 +00:00
{
2025-04-04 08:33:03 +00:00
return _modifiedPlugins . Contains ( id ) ;
2023-11-11 07:05:24 +00:00
}
2025-03-06 12:15:49 +00:00
public static async Task UpdatePluginAsync ( 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-03-06 12:15:49 +00:00
await UninstallPluginAsync ( 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
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
}
2025-03-06 12:15:49 +00:00
public static async Task UninstallPluginAsync ( PluginMetadata plugin , bool removePluginFromSettings = true , bool removePluginSettings = false )
2023-11-11 07:05:24 +00:00
{
2025-03-06 12:15:49 +00:00
await UninstallPluginAsync ( 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 >
2025-04-04 08:33:03 +00:00
{
"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
} ;
2023-11-10 16:40:27 +00:00
// 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 ) ;
2025-01-12 12:04:44 +00:00
FilesFolders . CopyAll ( pluginFolderPath , newPluginPath , ( s ) = > API . ShowMsgBox ( s ) ) ;
2023-11-10 16:40:27 +00:00
2025-02-08 14:59:36 +00:00
try
{
if ( Directory . Exists ( tempFolderPluginPath ) )
Directory . Delete ( tempFolderPluginPath , true ) ;
}
catch ( Exception e )
{
2025-04-13 09:26:21 +00:00
API . LogException ( ClassName , $"Failed to delete temp folder {tempFolderPluginPath}" , e ) ;
2025-02-08 14:59:36 +00:00
}
2023-11-11 07:05:24 +00:00
if ( checkModified )
{
_modifiedPlugins . Add ( plugin . ID ) ;
}
2023-11-10 16:40:27 +00:00
}
2025-03-06 12:15:49 +00:00
internal static async Task UninstallPluginAsync ( 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-03-06 12:20:30 +00:00
if ( removePluginSettings | | removePluginFromSettings )
2025-03-06 11:43:16 +00:00
{
// If we want to remove plugin from AllPlugins,
// we need to dispose them so that they can release file handles
// which can help FL to delete the plugin settings & cache folders successfully
var pluginPairs = AllPlugins . FindAll ( p = > p . Metadata . ID = = plugin . ID ) ;
foreach ( var pluginPair in pluginPairs )
{
await DisposePluginAsync ( pluginPair ) ;
}
}
2025-02-01 05:10:32 +00:00
if ( removePluginSettings )
{
2025-04-01 06:19:59 +00:00
// For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
2025-04-05 03:31:24 +00:00
if ( AllowedLanguage . IsDotNet ( plugin . Language ) & & API is IRemovable removable )
2025-02-09 02:58:09 +00:00
{
2025-04-05 03:31:24 +00:00
removable . RemovePluginSettings ( plugin . AssemblyName ) ;
removable . RemovePluginCaches ( plugin . PluginCacheDirectoryPath ) ;
2025-02-24 05:35:17 +00:00
}
2025-02-08 11:12:53 +00:00
2025-02-24 05:35:17 +00:00
try
{
var pluginSettingsDirectory = plugin . PluginSettingsDirectoryPath ;
if ( Directory . Exists ( pluginSettingsDirectory ) )
Directory . Delete ( pluginSettingsDirectory , true ) ;
2025-02-09 02:58:09 +00:00
}
2025-02-24 05:35:17 +00:00
catch ( Exception e )
2025-02-09 02:58:09 +00:00
{
2025-04-13 09:26:21 +00:00
API . LogException ( ClassName , $"Failed to delete plugin settings folder for {plugin.Name}" , e ) ;
2025-02-24 05:35:17 +00:00
API . ShowMsg ( API . GetTranslation ( "failedToRemovePluginSettingsTitle" ) ,
string . Format ( API . GetTranslation ( "failedToRemovePluginSettingsMessage" ) , plugin . Name ) ) ;
2025-02-01 05:10:32 +00:00
}
}
2025-02-09 02:58:09 +00:00
if ( removePluginFromSettings )
2023-11-10 16:40:27 +00:00
{
2025-02-24 05:35:17 +00:00
try
{
var pluginCacheDirectory = plugin . PluginCacheDirectoryPath ;
if ( Directory . Exists ( pluginCacheDirectory ) )
Directory . Delete ( pluginCacheDirectory , true ) ;
}
catch ( Exception e )
{
2025-04-13 09:26:21 +00:00
API . LogException ( ClassName , $"Failed to delete plugin cache folder for {plugin.Name}" , e ) ;
2025-02-24 05:35:17 +00:00
API . ShowMsg ( API . GetTranslation ( "failedToRemovePluginCacheTitle" ) ,
string . Format ( API . GetTranslation ( "failedToRemovePluginCacheMessage" ) , plugin . Name ) ) ;
}
2025-03-30 05:59:06 +00:00
Settings . RemovePluginSettings ( plugin . ID ) ;
2023-11-10 16:40:27 +00:00
AllPlugins . RemoveAll ( p = > p . Metadata . ID = = plugin . ID ) ;
}
// 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
}