2025-04-23 14:07:22 +02:00
using System.Text ;
2026-09-13 14:17:25 +02:00
using AIStudio.Models.Registry ;
2026-07-05 15:20:29 +02:00
using AIStudio.Settings ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem.Assistants ;
2025-04-23 14:07:22 +02: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 20:40:52 +02:00
private static async Task < List < PluginConfigurationObject >> RestartAllPlugins ( CancellationToken cancellationToken = default )
2025-04-23 14:07:22 +02:00
{
LOG . LogInformation ( "Try to start or restart all plugins." );
2025-08-18 20:40:52 +02:00
var configObjects = new List < PluginConfigurationObject >();
2026-08-23 21:15:37 +02:00
//
// Dropping the plugins is not enough: each one owns a Lua runtime, which we have to release
// ourselves. Otherwise, every restart — above all every hot reload during development —
// leaves another set of runtimes behind:
//
foreach ( var runningPlugin in RUNNING_PLUGINS )
runningPlugin . Dispose ();
2025-04-23 14:07:22 +02: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 21:14:21 +02: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 20:43:47 +01:00
BaseLanguage = languagePlugin ;
2025-06-01 21:14:21 +02: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 20:43:47 +01:00
BaseLanguage = NoPluginLanguage . INSTANCE ;
2025-06-01 21:14:21 +02:00
}
2025-04-23 14:07:22 +02:00
}
2025-06-01 21:14:21 +02:00
2025-04-23 14:07:22 +02:00
//
2026-08-08 18:35:46 +02: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 14:07:22 +02:00
//
2026-08-08 18:35:46 +02: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 14:07:22 +02:00
{
if ( cancellationToken . IsCancellationRequested )
2025-06-01 21:14:21 +02:00
{
LOG . LogWarning ( "Cancellation requested while starting plugins. Stopping the plugin startup process. Probably due to a timeout." );
2025-04-23 14:07:22 +02:00
break ;
2025-06-01 21:14:21 +02:00
}
2025-04-23 14:07:22 +02:00
if ( availablePlugin . Id == baseLanguagePluginId )
continue ;
2025-06-01 21:14:21 +02:00
try
{
2026-09-13 14:17:25 +02:00
//
// A model plugin runs like a configuration plugin, without anybody switching it on:
// it describes models an organization deployed it to describe, and a description
// somebody has to enable first would leave half the installations answering
// differently from the other half for no reason anyone could see.
//
if ( availablePlugin . IsInternal || SettingsManagerAccess . IsPluginEnabled ( availablePlugin ) || availablePlugin . Type is PluginType . CONFIGURATION or PluginType . ASSISTANT or PluginType . MODEL )
2025-06-01 21:14:21 +02:00
if ( await Start ( availablePlugin , cancellationToken ) is { IsValid : true } plugin )
2025-08-18 20:40:52 +02:00
{
if ( plugin is PluginConfiguration configPlugin )
configObjects . AddRange ( configPlugin . ConfigObjects );
2025-06-01 21:14:21 +02:00
RUNNING_PLUGINS . Add ( plugin );
2025-08-18 20:40:52 +02:00
}
2025-06-01 21:14:21 +02: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 14:07:22 +02:00
}
2026-07-05 15:20:29 +02:00
LogAssistantPluginStartupState ();
2026-09-13 14:17:25 +02:00
//
// Hand what the model plugins declare to the registry before anything is told that the
// plugins are up. Whoever reacts to that message may ask about a model right away, and the
// registry keeps the answers it gives: an answer handed out before the declarations arrived
// would be the answer everybody gets until the next reload.
//
ModelRegistry . Shared . Declare ( GetModelDeclarations ());
2025-06-01 21:14:21 +02:00
// Inform all components that the plugins have been reloaded or started:
await MessageBus . INSTANCE . SendMessage < bool >( null , Event . PLUGINS_RELOADED );
2025-08-18 20:40:52 +02:00
return configObjects ;
2025-04-23 14:07:22 +02:00
}
2026-07-05 15:20:29 +02:00
2026-08-08 18:35:46 +02: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 19:01:58 +02: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 18:35:46 +02: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 19:01:58 +02:00
PluginType . CONFIGURATION when IsEnterpriseTestConfigurationPath ( plugin . LocalPath ) => 1 ,
PluginType . CONFIGURATION => 2 ,
2026-08-08 18:35:46 +02:00
2026-08-09 19:01:58 +02:00
_ => 3 ,
2026-08-08 18:35:46 +02:00
};
2026-07-05 15:20:29 +02: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 18:35:46 +02: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 15:20:29 +02: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 14:07:22 +02: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 );
2026-09-04 15:48:07 +02:00
var plugin = await Load ( meta . LocalPath , code , cancellationToken : cancellationToken );
2026-04-09 10:01:24 +02:00
plugin . PluginPath = meta . LocalPath ;
2025-04-23 14:07:22 +02: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 20:43:47 +01:00
if ( plugin is PluginLanguage languagePlugin && BaseLanguage != NoPluginLanguage . INSTANCE )
languagePlugin . SetBaseLanguage ( BaseLanguage );
2025-04-23 14:07:22 +02:00
2025-06-01 21:14:21 +02:00
if ( plugin is PluginConfiguration configPlugin )
2025-08-09 19:29:43 +02:00
await configPlugin . InitializeAsync ( false );
2025-06-01 21:14:21 +02:00
2025-04-23 14:07:22 +02: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 10:01:24 +02:00
}