using AIStudio.Settings; using AIStudio.Settings.DataModel; namespace AIStudio.Tools.PluginSystem; public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); private static string DATA_DIR = string.Empty; private static string PLUGINS_ROOT = string.Empty; private static string INTERNAL_PLUGINS_ROOT = string.Empty; /// /// The directory the config server downloads the configuration plugins of an organization into. /// /// /// This is not the home of configuration plugins in general: a local configuration plugin can /// live in any directory below the plugins root. Only the IT department of an organization /// deploys plugins here, each in a directory named after its configuration ID. /// private static string ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = string.Empty; private static string HOT_RELOAD_LOCK_FILE = string.Empty; private static FileSystemWatcher HOT_RELOAD_WATCHER = null!; public static ILanguagePlugin BaseLanguage { get; private set; } = NoPluginLanguage.INSTANCE; public static bool IsInitialized { get; private set; } /// /// Gets the enterprise encryption instance for decrypting API keys in configuration plugins. /// public static EnterpriseEncryption? EnterpriseEncryption { get; private set; } /// /// Initializes the enterprise encryption service by reading the encryption secret /// from the effective enterprise source. /// /// The Rust service to use for reading the encryption secret. public static async Task InitializeEnterpriseEncryption(Services.RustService rustService) { var encryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret(); InitializeEnterpriseEncryption(encryptionSecret); } /// /// Initializes the enterprise encryption service using a prefetched secret value. /// /// The base64-encoded enterprise encryption secret. public static void InitializeEnterpriseEncryption(string? encryptionSecret) { LOG.LogInformation("Initializing enterprise encryption service..."); var enterpriseEncryptionLogger = Program.LOGGER_FACTORY.CreateLogger(); EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret); if (EnterpriseEncryption.IsAvailable) LOG.LogInformation("Enterprise encryption service is available."); else LOG.LogWarning("Enterprise encryption service is not available (no secret configured)."); } /// /// Set up the plugin factory. We will read the data directory from the settings manager. /// Afterward, we will create the plugins directory and the internal plugin directory. /// public static bool Setup() { if(IsInitialized) return false; LOG.LogInformation("Initializing plugin factory..."); DATA_DIR = SettingsManager.DataDirectory!; PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins"); HOT_RELOAD_LOCK_FILE = Path.Join(PLUGINS_ROOT, ".lock"); INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal"); ENTERPRISE_CONFIGURATION_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".config"); if (!Directory.Exists(PLUGINS_ROOT)) Directory.CreateDirectory(PLUGINS_ROOT); HOT_RELOAD_WATCHER = new(PLUGINS_ROOT); IsInitialized = true; LOG.LogInformation("Plugin factory initialized successfully."); return true; } /// /// Checks whether a plugin directory belongs to the enterprise configuration area. /// /// /// Only the IT department of an organization deploys plugins there: the config server downloads /// them into a directory named after their configuration ID. We decide by path on purpose. The /// Lua field DEPLOYED_USING_CONFIG_SERVER is self-declared, so any plugin could claim to be /// deployed by an organization. /// /// The directory of the plugin. /// True when the directory is nested in the enterprise configuration directory. public static bool IsEnterpriseConfigurationPath(string? pluginPath) { if (string.IsNullOrWhiteSpace(pluginPath) || string.IsNullOrWhiteSpace(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT)) return false; try { var configurationRoot = Path.GetFullPath(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; var pluginDirectory = Path.GetFullPath(pluginPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; return pluginDirectory.StartsWith(configurationRoot, StringComparison.OrdinalIgnoreCase); } catch (Exception e) { LOG.LogWarning(e, $"Was not able to check whether the plugin directory '{pluginPath}' belongs to the enterprise configuration directory. Treating it as a local plugin."); return false; } } /// /// Checks whether a configuration plugin was deployed by the IT department of an organization. /// /// /// A plugin which is deployed but could not be loaded still counts: it might be broken, e.g. due /// to invalid Lua code or an incomplete download, but it was not removed. Everything it manages /// stays under the control of the organization until the plugin is gone for good. /// /// The ID of the configuration plugin. /// True when the plugin belongs to an organization, false when it is local or unknown. public static bool IsEnterpriseConfigurationPlugin(Guid configPluginId) { if (configPluginId == Guid.Empty || !IsInitialized) return false; if (AVAILABLE_PLUGINS.Any(plugin => plugin.Id == configPluginId && plugin.Type is PluginType.CONFIGURATION && IsEnterpriseConfigurationPath(plugin.LocalPath))) return true; return Directory.Exists(Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPluginId.ToString())); } private static async Task LockHotReloadAsync() { if (!IsInitialized) { LOG.LogError("PluginFactory is not initialized."); return; } try { if (File.Exists(HOT_RELOAD_LOCK_FILE)) { LOG.LogWarning("Hot reload lock file already exists."); return; } await File.WriteAllTextAsync(HOT_RELOAD_LOCK_FILE, DateTime.UtcNow.ToString("o")); } catch (Exception e) { LOG.LogError(e, "An error occurred while trying to lock hot reloading."); } } private static void UnlockHotReload() { if (!IsInitialized) { LOG.LogError("PluginFactory is not initialized."); return; } try { if(File.Exists(HOT_RELOAD_LOCK_FILE)) File.Delete(HOT_RELOAD_LOCK_FILE); else LOG.LogWarning("Hot reload lock file does not exist. Nothing to unlock."); } catch (Exception e) { LOG.LogError(e, "An error occurred while trying to unlock hot reloading."); } } public static void Dispose() { if(!IsInitialized) return; HOT_RELOAD_WATCHER.Dispose(); } public static IReadOnlyList GetMandatoryInfos() { return RUNNING_PLUGINS .OfType() .SelectMany(plugin => plugin.MandatoryInfos) .ToList(); } public static IReadOnlyList GetIntroductions() { return RUNNING_PLUGINS .OfType() .SelectMany(plugin => plugin.Introductions) .OrderBy(introduction => introduction.Index) .ToList(); } }