2025-04-23 12:07:22 +00:00
using System.Text ;
2026-07-05 13:20:29 +00:00
using AIStudio.Settings ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem.Assistants ;
2025-04-23 12:07:22 +00:00
namespace AIStudio.Tools.PluginSystem ;
public static partial class PluginFactory
{
private static readonly List < PluginBase > RUNNING_PLUGINS = [ ] ;
/// <summary>
/// A list of all running plugins.
/// </summary>
public static IReadOnlyCollection < PluginBase > RunningPlugins = > RUNNING_PLUGINS ;
2025-08-18 18:40:52 +00:00
private static async Task < List < PluginConfigurationObject > > RestartAllPlugins ( CancellationToken cancellationToken = default )
2025-04-23 12:07:22 +00:00
{
LOG . LogInformation ( "Try to start or restart all plugins." ) ;
2025-08-18 18:40:52 +00:00
var configObjects = new List < PluginConfigurationObject > ( ) ;
2025-04-23 12:07:22 +00:00
RUNNING_PLUGINS . Clear ( ) ;
//
// Get the base language plugin. This is the plugin that will be used to fill in missing keys.
//
var baseLanguagePluginId = InternalPlugin . LANGUAGE_EN_US . MetaData ( ) . Id ;
var baseLanguagePluginMetaData = AVAILABLE_PLUGINS . FirstOrDefault ( p = > p . Id = = baseLanguagePluginId ) ;
if ( baseLanguagePluginMetaData is null )
LOG . LogError ( $"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation." ) ;
else
{
2025-06-01 19:14:21 +00:00
try
{
var startedBasePlugin = await Start ( baseLanguagePluginMetaData , cancellationToken ) ;
if ( startedBasePlugin is NoPlugin noPlugin )
LOG . LogError ( $"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}" ) ;
if ( startedBasePlugin is PluginLanguage languagePlugin )
{
2026-02-19 19:43:47 +00:00
BaseLanguage = languagePlugin ;
2025-06-01 19:14:21 +00:00
RUNNING_PLUGINS . Add ( languagePlugin ) ;
LOG . LogInformation ( $"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'" ) ;
}
else
LOG . LogError ( $"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join(" ; ", startedBasePlugin.Issues)}" ) ;
}
catch ( Exception e )
{
LOG . LogError ( e , $"An error occurred while starting the base language plugin: Id='{baseLanguagePluginId}'." ) ;
2026-02-19 19:43:47 +00:00
BaseLanguage = NoPluginLanguage . INSTANCE ;
2025-06-01 19:14:21 +00:00
}
2025-04-23 12:07:22 +00:00
}
2025-06-01 19:14:21 +00:00
2025-04-23 12:07:22 +00:00
//
2026-08-08 16:35:46 +00:00
// Iterate over all available plugins and try to start them. We do that in a deterministic
// order, starting with the configuration plugins of the organization. Three reasons:
2025-04-23 12:07:22 +00:00
//
2026-08-08 16:35:46 +00:00
// - Configuration plugins write settings and configuration objects. Whoever writes one
// first owns it, so the organization has to come first: its configuration is the baseline
// every other plugin has to respect.
//
// - Within one origin, the declared priority decides. An organization can deploy a base
// configuration for everybody and refine it, e.g. per department: the higher priority is
// applied later and therefore wins.
//
// - Without an explicit order, the sequence is the one Directory.EnumerateFiles produced in
// LoadAll. That order is not guaranteed, so the same installation could behave
// differently on two machines. The plugin directory breaks any remaining tie.
//
foreach ( var availablePlugin in AVAILABLE_PLUGINS
. OrderBy ( GetStartupRank )
. ThenBy ( plugin = > plugin . ConfigurationPriority )
. ThenBy ( plugin = > plugin . LocalPath , StringComparer . OrdinalIgnoreCase ) )
2025-04-23 12:07:22 +00:00
{
if ( cancellationToken . IsCancellationRequested )
2025-06-01 19:14:21 +00:00
{
LOG . LogWarning ( "Cancellation requested while starting plugins. Stopping the plugin startup process. Probably due to a timeout." ) ;
2025-04-23 12:07:22 +00:00
break ;
2025-06-01 19:14:21 +00:00
}
2025-04-23 12:07:22 +00:00
if ( availablePlugin . Id = = baseLanguagePluginId )
continue ;
2025-06-01 19:14:21 +00:00
try
{
2026-06-10 19:01:27 +00:00
if ( availablePlugin . IsInternal | | SettingsManagerAccess . IsPluginEnabled ( availablePlugin ) | | availablePlugin . Type = = PluginType . CONFIGURATION | | availablePlugin . Type = = PluginType . ASSISTANT )
2025-06-01 19:14:21 +00:00
if ( await Start ( availablePlugin , cancellationToken ) is { IsValid : true } plugin )
2025-08-18 18:40:52 +00:00
{
if ( plugin is PluginConfiguration configPlugin )
configObjects . AddRange ( configPlugin . ConfigObjects ) ;
2025-06-01 19:14:21 +00:00
RUNNING_PLUGINS . Add ( plugin ) ;
2025-08-18 18:40:52 +00:00
}
2025-06-01 19:14:21 +00:00
}
catch ( Exception e )
{
LOG . LogError ( e , $"An error occurred while starting the plugin: Id='{availablePlugin.Id}', Type='{availablePlugin.Type}', Name='{availablePlugin.Name}', Version='{availablePlugin.Version}'." ) ;
}
2025-04-23 12:07:22 +00:00
}
2026-07-05 13:20:29 +00:00
LogAssistantPluginStartupState ( ) ;
2025-06-01 19:14:21 +00:00
// Inform all components that the plugins have been reloaded or started:
await MessageBus . INSTANCE . SendMessage < bool > ( null , Event . PLUGINS_RELOADED ) ;
2025-08-18 18:40:52 +00:00
return configObjects ;
2025-04-23 12:07:22 +00:00
}
2026-07-05 13:20:29 +00:00
2026-08-08 16:35:46 +00:00
/// <summary>
/// Determines the position of a plugin in the startup sequence. Plugins with a lower rank start earlier.
/// </summary>
/// <remarks>
/// The configuration plugins an organization deployed go first: they are the baseline for
2026-08-09 16:36:55 +00:00
/// everything else. A test configuration follows, so that an administrator sees their draft take
/// effect over the deployed baseline. Local configuration plugins come last, so they can add to
/// that baseline instead of replacing parts of it. All remaining plugin types write no settings at
/// all, so their rank is irrelevant for the outcome.<br/><br/>
2026-08-08 16:35:46 +00:00
/// The rank comes before the declared priority on purpose: a local configuration plugin must not
/// be able to jump ahead of an organization by declaring a high priority.
/// </remarks>
/// <param name="plugin">The plugin about to be started.</param>
/// <returns>The startup rank of the plugin.</returns>
private static int GetStartupRank ( IAvailablePlugin plugin ) = > plugin . Type switch
{
PluginType . CONFIGURATION when IsEnterpriseConfigurationPath ( plugin . LocalPath ) = > 0 ,
2026-08-09 16:36:55 +00:00
PluginType . CONFIGURATION when IsEnterpriseTestConfigurationPath ( plugin . LocalPath ) = > 1 ,
PluginType . CONFIGURATION = > 2 ,
2026-08-08 16:35:46 +00:00
2026-08-09 16:36:55 +00:00
_ = > 3 ,
2026-08-08 16:35:46 +00:00
} ;
2026-07-05 13:20:29 +00:00
private static void LogAssistantPluginStartupState ( )
{
ManagedConfiguration . TryGet ( x = > x . AssistantPluginAudit , x = > x . EnterpriseApprovedPlugins , out ConfigMeta < DataAssistantPluginAudit , IList < DataAssistantPluginEnterpriseApproval > > configMeta ) ;
foreach ( var assistantPlugin in RUNNING_PLUGINS . OfType < PluginAssistants > ( ) )
{
var securityState = PluginAssistantSecurityResolver . Resolve ( SettingsManagerAccess , assistantPlugin ) ;
if ( securityState . IsEnterpriseApproved )
{
2026-08-08 16:35:46 +00:00
//
// Several configuration plugins may approve assistant plugins. We look up the one
// which approved this particular plugin instead of naming an arbitrary contributor:
//
var approvedByConfigPluginId = configMeta . PluginContributions
. Where ( contribution = > contribution . Value . Any ( approval = > string . Equals ( approval . PluginHash , securityState . CurrentHash , StringComparison . Ordinal ) ) )
. Select ( contribution = > contribution . Key )
. FirstOrDefault ( ) ;
var approvedByConfigPluginName = approvedByConfigPluginId = = Guid . Empty
? string . Empty
: AVAILABLE_PLUGINS . FirstOrDefault ( x = > x . Id = = approvedByConfigPluginId ) ? . Name ? ? string . Empty ;
2026-07-05 13:20:29 +00:00
LOG . LogInformation (
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}', SecuritySource='EnterpriseApproval', ApprovedByConfigPluginId='{approvedByConfigPluginId}', ApprovedByConfigPluginName='{approvedByConfigPluginName}'" ) ;
continue ;
}
LOG . LogInformation (
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}'" ) ;
}
}
2025-04-23 12:07:22 +00:00
private static async Task < PluginBase > Start ( IAvailablePlugin meta , CancellationToken cancellationToken = default )
{
var pluginMainFile = Path . Join ( meta . LocalPath , "plugin.lua" ) ;
if ( ! File . Exists ( pluginMainFile ) )
{
LOG . LogError ( $"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: The plugin file does not exist." ) ;
return new NoPlugin ( $"The plugin file does not exist: {pluginMainFile}" ) ;
}
var code = await File . ReadAllTextAsync ( pluginMainFile , Encoding . UTF8 , cancellationToken ) ;
var plugin = await Load ( meta . LocalPath , code , cancellationToken ) ;
2026-04-09 08:01:24 +00:00
plugin . PluginPath = meta . LocalPath ;
2025-04-23 12:07:22 +00:00
if ( plugin is NoPlugin noPlugin )
{
LOG . LogError ( $"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {noPlugin.Issues.First()}" ) ;
return noPlugin ;
}
if ( plugin . IsValid )
{
//
// When this is a language plugin, we need to set the base language plugin.
//
2026-02-19 19:43:47 +00:00
if ( plugin is PluginLanguage languagePlugin & & BaseLanguage ! = NoPluginLanguage . INSTANCE )
languagePlugin . SetBaseLanguage ( BaseLanguage ) ;
2025-04-23 12:07:22 +00:00
2025-06-01 19:14:21 +00:00
if ( plugin is PluginConfiguration configPlugin )
2025-08-09 17:29:43 +00:00
await configPlugin . InitializeAsync ( false ) ;
2025-06-01 19:14:21 +00:00
2025-04-23 12:07:22 +00:00
LOG . LogInformation ( $"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'" ) ;
return plugin ;
}
LOG . LogError ( $"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join(" ; ", plugin.Issues)}" ) ;
return new NoPlugin ( $"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join(" ; ", plugin.Issues)}" ) ;
}
2026-04-09 08:01:24 +00:00
}