mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 09:13:48 +00:00
Fixed an incomplete plugin start with extensive enterprise configurations (#1033)
This commit is contained in:
1 parent
bdf828fa4f
commit
56215aa6dd
16 files changed
+493
-70
No files matched your search
@@ -12,7 +12,6 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type), ILivePluginContentSource
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration));
|
||||
private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
|
||||
|
||||
private List<PluginConfigurationObject> configObjects = [];
|
||||
@@ -83,8 +82,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
await StoreEnterpriseApiKeysAsync();
|
||||
await StoreEnterpriseSecretsAsync();
|
||||
|
||||
await SettingsManagerAccess.StoreSettings();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
||||
//
|
||||
// The settings are not stored here: PluginFactory.LoadAll does that once, after all
|
||||
// plugins have started. Storing them per configuration plugin wrote the whole settings
|
||||
// file again for each one, which made the start of every further plugin wait for it.
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,11 +143,11 @@ public sealed record PluginConfigurationObject
|
||||
|
||||
var (wasParsingSuccessful, configObject) = configObjectType switch
|
||||
{
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject),
|
||||
PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, pluginPath, dryRun, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject),
|
||||
PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => (DataDocumentAnalysisPolicy.TryProcessConfiguration(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject is DataDocumentAnalysisPolicy, configurationObject),
|
||||
|
||||
_ => (false, NoConfigurationObject.INSTANCE)
|
||||
@@ -278,7 +278,7 @@ public sealed record PluginConfigurationObject
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject))
|
||||
if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, dryRun, out var configObject))
|
||||
{
|
||||
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
||||
continue;
|
||||
@@ -372,6 +372,11 @@ public sealed record PluginConfigurationObject
|
||||
/// those which could not be loaded. Objects of a deployed plugin are never removed, because the
|
||||
/// plugin was not removed either.
|
||||
/// </param>
|
||||
/// <param name="notStartedConfigPluginIds">
|
||||
/// The IDs of the configuration plugins which were loaded, but did not start. Their objects are
|
||||
/// never removed either: such a plugin contributed nothing to the list of configuration objects,
|
||||
/// so all of its objects would look as if the plugin had dropped them.
|
||||
/// </param>
|
||||
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
||||
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
||||
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
||||
@@ -381,6 +386,7 @@ public sealed record PluginConfigurationObject
|
||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||
IList<IAvailablePlugin> availablePlugins,
|
||||
IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds,
|
||||
IReadOnlySet<Guid> notStartedConfigPluginIds,
|
||||
IList<PluginConfigurationObject> configObjectList,
|
||||
SecretStoreType? secretStoreType = null,
|
||||
bool deleteSecret = false) where TClass : IConfigurationObject
|
||||
@@ -410,6 +416,15 @@ public sealed record PluginConfigurationObject
|
||||
if(deployedEnterpriseConfigPluginIds.Contains(configObjectSourcePluginId) && availablePlugins.All(plugin => plugin.Id != configObjectSourcePluginId))
|
||||
continue;
|
||||
|
||||
//
|
||||
// Was the source plugin loaded, but did not start? Then it is not broken, it could not
|
||||
// run this time, e.g., because it ran out of time on a slow machine. It contributed no
|
||||
// objects, so every one of its objects would look removed from the plugin. They stay
|
||||
// until the plugin starts again and tells us which ones it still defines:
|
||||
//
|
||||
if(notStartedConfigPluginIds.Contains(configObjectSourcePluginId))
|
||||
continue;
|
||||
|
||||
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
||||
// over and should be removed:
|
||||
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using AIStudio.Settings;
|
||||
@@ -13,6 +14,18 @@ public static partial class PluginFactory
|
||||
private static readonly List<IAvailablePlugin> AVAILABLE_PLUGINS = [];
|
||||
private static readonly SemaphoreSlim PLUGIN_LOAD_SEMAPHORE = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// How long a single plugin may take to load, and again to start.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The limit applies to each plugin on its own, never to all plugins together. A shared budget
|
||||
/// let a slow machine spend it on the first plugins, so the ones at the end of the line never
|
||||
/// started: the language plugin among them, and configuration plugins of the organization,
|
||||
/// whose objects the clean-up then removed. The limit holds in every build, so a developer sees
|
||||
/// the same start as a user does.
|
||||
/// </remarks>
|
||||
private static readonly TimeSpan PLUGIN_TIMEOUT = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// A list of all available plugins.
|
||||
/// </summary>
|
||||
@@ -42,6 +55,8 @@ public static partial class PluginFactory
|
||||
await PLUGIN_LOAD_SEMAPHORE.WaitAsync(cancellationToken);
|
||||
|
||||
var configObjectList = new List<PluginConfigurationObject>();
|
||||
var hasStartedConfigurationPlugins = false;
|
||||
var notStartedConfigPluginIds = new HashSet<Guid>();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -65,11 +80,13 @@ public static partial class PluginFactory
|
||||
IEnumerable<string> pluginMainFiles = pluginsDirectoryExists ? Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories) : [];
|
||||
foreach (var pluginMainFile in pluginMainFiles)
|
||||
{
|
||||
using var pluginTimeout = CreatePluginTimeout(cancellationToken);
|
||||
var loadingStartedAt = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LOG.LogWarning("Was not able to load all plugins, because the operation was cancelled. It seems to be a timeout.");
|
||||
LOG.LogWarning("Was not able to load all plugins, because the operation was cancelled.");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -79,11 +96,11 @@ public static partial class PluginFactory
|
||||
await using(var fileStream = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
using var reader = new StreamReader(fileStream, Encoding.UTF8);
|
||||
code = await reader.ReadToEndAsync(cancellationToken);
|
||||
code = await reader.ReadToEndAsync(pluginTimeout.Token);
|
||||
}
|
||||
|
||||
var pluginPath = Path.GetDirectoryName(pluginMainFile)!;
|
||||
var plugin = await Load(pluginPath, code, cancellationToken: cancellationToken);
|
||||
var plugin = await Load(pluginPath, code, cancellationToken: pluginTimeout.Token);
|
||||
|
||||
switch (plugin)
|
||||
{
|
||||
@@ -189,6 +206,10 @@ public static partial class PluginFactory
|
||||
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId, configurationPriority));
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LOG.LogError($"Was not able to load plugin '{pluginMainFile}'. Reason: It did not finish within the limit of {PLUGIN_TIMEOUT.TotalSeconds:0} seconds and was stopped after {Stopwatch.GetElapsedTime(loadingStartedAt).TotalSeconds:0.0} seconds. The remaining plugins load regardless.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOG.LogError($"Was not able to load plugin '{pluginMainFile}'. Issue: {e.Message}");
|
||||
@@ -201,6 +222,25 @@ public static partial class PluginFactory
|
||||
{
|
||||
var configObjects = await RestartAllPlugins(cancellationToken);
|
||||
configObjectList.AddRange(configObjects);
|
||||
|
||||
//
|
||||
// Each configuration plugin which started has changed the settings, but none of
|
||||
// them stored the change. We store all of it once, at the end of this method:
|
||||
//
|
||||
hasStartedConfigurationPlugins = RUNNING_PLUGINS.OfType<PluginConfiguration>().Any();
|
||||
|
||||
//
|
||||
// A configuration plugin which loaded but did not start has contributed nothing to
|
||||
// the configuration objects. To the clean-up below, it would look as if the plugin
|
||||
// had dropped all of its objects, and they would be deleted, secrets included. That
|
||||
// is what a slow machine did to the configuration of an organization, once the
|
||||
// time ran out before its plugin got its turn:
|
||||
//
|
||||
foreach (var notStartedConfigPlugin in AVAILABLE_PLUGINS.Where(plugin => plugin.Type is PluginType.CONFIGURATION && RUNNING_PLUGINS.All(runningPlugin => runningPlugin.Id != plugin.Id)))
|
||||
{
|
||||
notStartedConfigPluginIds.Add(notStartedConfigPlugin.Id);
|
||||
LOG.LogWarning($"The configuration plugin '{notStartedConfigPlugin.Name}' (Id='{notStartedConfigPlugin.Id}') was loaded, but did not start. Everything it manages stays unchanged until it starts again. Please check the errors above.");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -255,35 +295,42 @@ public static partial class PluginFactory
|
||||
LOG.LogWarning($"The configuration plugin '{unloadedEnterpriseConfigPluginId}' is deployed, but was not loaded. Everything it manages stays unchanged, because the plugin was not removed. Please check the errors above and fix the plugin.");
|
||||
}
|
||||
|
||||
var wasConfigurationChanged = hasStartedConfigurationPlugins;
|
||||
|
||||
// Check LLM providers:
|
||||
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.LLM_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check transcription providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check embedding providers:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check data sources:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check chat templates:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check profiles:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check document analysis policies:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, deployedEnterpriseConfigPluginIds, notStartedConfigPluginIds, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check left-over mandatory info acceptances:
|
||||
if (SettingsManagerAccess.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
|
||||
//
|
||||
// Check left-over mandatory info acceptances. The mandatory infos come from the running
|
||||
// configuration plugins only, so we skip this while one of them did not start: the user
|
||||
// would have to accept its infos again, although nothing about them changed:
|
||||
//
|
||||
if (notStartedConfigPluginIds.Count == 0 && SettingsManagerAccess.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos()))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check all managed settings, i.e. settings which a configuration plugin can lock,
|
||||
@@ -294,11 +341,12 @@ public static partial class PluginFactory
|
||||
//
|
||||
// The enterprise approvals of all configuration plugins add up. Now that every plugin has
|
||||
// contributed and the clean-up above has dropped the removed ones, we rebuild the effective
|
||||
// list. We skip that while a configuration plugin is deployed but could not be loaded: its
|
||||
// approvals are missing from the contributions, and withdrawing them would demand a new
|
||||
// security audit for assistant plugins the organization has approved:
|
||||
// list. We skip that while a configuration plugin is deployed but could not be loaded, or
|
||||
// was loaded but did not start: its approvals are missing from the contributions, and
|
||||
// withdrawing them would demand a new security audit for assistant plugins the
|
||||
// organization has approved:
|
||||
//
|
||||
if(unloadedEnterpriseConfigPluginIds.Count == 0 && PluginConfiguration.RefreshEnterpriseApprovedAssistantPlugins())
|
||||
if(unloadedEnterpriseConfigPluginIds.Count == 0 && notStartedConfigPluginIds.Count == 0 && PluginConfiguration.RefreshEnterpriseApprovedAssistantPlugins())
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
//
|
||||
@@ -310,7 +358,7 @@ public static partial class PluginFactory
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Compatibility shim, see documentation/compatibility-shims/2026-08-orphaned-config-locks.md (remove after 2027-08-06):
|
||||
if (RepairLegacyConfigOnlySettings(unloadedEnterpriseConfigPluginIds.Count > 0))
|
||||
if (RepairLegacyConfigOnlySettings(unloadedEnterpriseConfigPluginIds.Count > 0 || notStartedConfigPluginIds.Count > 0))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
if (wasConfigurationChanged)
|
||||
@@ -354,6 +402,23 @@ public static partial class PluginFactory
|
||||
return deployedEnterpriseConfigPluginIds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the time limit for loading or starting a single plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The limit is linked to the token of the caller, so cancelling the whole operation still stops
|
||||
/// the plugin at hand. Tell the two apart by that token: when it is not cancelled, the plugin ran
|
||||
/// out of time, and only this plugin is affected.
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken">The token of the whole operation.</param>
|
||||
/// <returns>A token source that cancels after PLUGIN_TIMEOUT or together with the caller's token.</returns>
|
||||
private static CancellationTokenSource CreatePluginTimeout(CancellationToken cancellationToken)
|
||||
{
|
||||
var pluginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
pluginTimeout.CancelAfter(PLUGIN_TIMEOUT);
|
||||
return pluginTimeout;
|
||||
}
|
||||
|
||||
/// <param name="pluginPath">The directory the plugin is located in, or null when the code has no directory yet.</param>
|
||||
/// <param name="code">The Lua code of the plugin's main file.</param>
|
||||
/// <param name="allowedBaseDirectory">
|
||||
@@ -463,17 +528,17 @@ public static partial class PluginFactory
|
||||
/// This is only valid as long as none of these settings gets a user interface. When you add
|
||||
/// one, remove the setting from this method and from the shim's document.
|
||||
/// </remarks>
|
||||
/// <param name="hasUnloadedConfigPlugins" >
|
||||
/// True when at least one configuration plugin is deployed but could not be loaded. In that case,
|
||||
/// we cannot tell whether a value comes from that plugin or from a removed one, so we repair
|
||||
/// nothing at all.
|
||||
/// <param name="hasMissingConfigPlugins" >
|
||||
/// True when at least one configuration plugin is deployed but could not be loaded, or was loaded
|
||||
/// but did not start. In that case, we cannot tell whether a value comes from that plugin or from
|
||||
/// a removed one, so we repair nothing at all.
|
||||
/// </param>
|
||||
/// <returns>True when at least one setting was repaired, otherwise false.</returns>
|
||||
private static bool RepairLegacyConfigOnlySettings(bool hasUnloadedConfigPlugins)
|
||||
private static bool RepairLegacyConfigOnlySettings(bool hasMissingConfigPlugins)
|
||||
{
|
||||
if (hasUnloadedConfigPlugins)
|
||||
if (hasMissingConfigPlugins)
|
||||
{
|
||||
LOG.LogWarning("Skipping the repair of configuration-only settings: at least one configuration plugin is deployed, but could not be loaded. We try again the next time AI Studio starts.");
|
||||
LOG.LogWarning("Skipping the repair of configuration-only settings: at least one configuration plugin could not be loaded or did not start. We try again the next time AI Studio starts.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using AIStudio.Models.Registry;
|
||||
using AIStudio.Settings;
|
||||
@@ -10,6 +11,18 @@ public static partial class PluginFactory
|
||||
{
|
||||
private static readonly List<PluginBase> RUNNING_PLUGINS = [];
|
||||
|
||||
/// <summary>
|
||||
/// From this duration on, the start of a single plugin is reported as slow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The plugins start one after another, so every plugin after a slow one has to wait. A plugin
|
||||
/// usually starts within milliseconds. A configuration plugin takes longer, because it applies its
|
||||
/// configuration, stores its secrets in the OS keyring, and loads tokenizers, but seconds are
|
||||
/// still far beyond that. The time limit per plugin covers only running its Lua code, so this
|
||||
/// warning is the only sign of a plugin which spends its time elsewhere.
|
||||
/// </remarks>
|
||||
private static readonly TimeSpan SLOW_PLUGIN_START_THRESHOLD = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// A list of all running plugins.
|
||||
/// </summary>
|
||||
@@ -41,7 +54,7 @@ public static partial class PluginFactory
|
||||
{
|
||||
try
|
||||
{
|
||||
var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken);
|
||||
var startedBasePlugin = await StartWithTimeout(baseLanguagePluginMetaData, cancellationToken);
|
||||
if (startedBasePlugin is NoPlugin noPlugin)
|
||||
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}");
|
||||
|
||||
@@ -84,7 +97,7 @@ public static partial class PluginFactory
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LOG.LogWarning("Cancellation requested while starting plugins. Stopping the plugin startup process. Probably due to a timeout.");
|
||||
LOG.LogWarning("Cancellation requested while starting plugins. Stopping the plugin startup process.");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -100,7 +113,7 @@ public static partial class PluginFactory
|
||||
// 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)
|
||||
if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin)
|
||||
if(await StartWithTimeout(availablePlugin, cancellationToken) is { IsValid: true } plugin)
|
||||
{
|
||||
if (plugin is PluginConfiguration configPlugin)
|
||||
configObjects.AddRange(configPlugin.ConfigObjects);
|
||||
@@ -184,6 +197,37 @@ public static partial class PluginFactory
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a plugin within the time limit each plugin gets on its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A plugin which runs out of time is not started, and the next plugin gets the full limit
|
||||
/// again. Only the caller's token stops the start of all plugins.
|
||||
/// </remarks>
|
||||
/// <param name="meta">The plugin to start.</param>
|
||||
/// <param name="cancellationToken">The token of the whole start.</param>
|
||||
/// <returns>The started plugin, or a NoPlugin when it could not be started in time.</returns>
|
||||
private static async Task<PluginBase> StartWithTimeout(IAvailablePlugin meta, CancellationToken cancellationToken)
|
||||
{
|
||||
using var pluginTimeout = CreatePluginTimeout(cancellationToken);
|
||||
var startingStartedAt = Stopwatch.GetTimestamp();
|
||||
try
|
||||
{
|
||||
var plugin = await Start(meta, pluginTimeout.Token);
|
||||
var startingDuration = Stopwatch.GetElapsedTime(startingStartedAt);
|
||||
if (startingDuration >= SLOW_PLUGIN_START_THRESHOLD)
|
||||
LOG.LogWarning($"Starting the plugin took {startingDuration.TotalMilliseconds:F0} ms, which is unusually long: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. All plugins after it had to wait for it.");
|
||||
|
||||
return plugin;
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var reason = $"It did not finish within the limit of {PLUGIN_TIMEOUT.TotalSeconds:0} seconds and was stopped after {Stopwatch.GetElapsedTime(startingStartedAt).TotalSeconds:0.0} seconds.";
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {reason} The remaining plugins start regardless.");
|
||||
return new NoPlugin(reason);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua");
|
||||
|
||||
Reference in new issue
Block a user