Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions UniversalDeviceToolkit.Lib.Plugins/IPluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,21 @@ public interface IPluginManager : IDisposable
/// </summary>
void InstallPlugin(string pluginId);

/// <summary>
/// Install plugin asynchronously
/// </summary>
Task InstallPluginAsync(string pluginId);

/// <summary>
/// Uninstall plugin
/// </summary>
bool UninstallPlugin(string pluginId);

/// <summary>
/// Uninstall plugin asynchronously
/// </summary>
Task<bool> UninstallPluginAsync(string pluginId);

/// <summary>
/// Get all installed plugin IDs
/// </summary>
Expand All @@ -66,6 +76,11 @@ public interface IPluginManager : IDisposable
/// </summary>
bool PermanentlyDeletePlugin(string pluginId);

/// <summary>
/// Permanently delete plugin files from disk asynchronously
/// </summary>
Task<bool> PermanentlyDeletePluginAsync(string pluginId);

/// <summary>
/// Unload all plugins and release references (useful before plugin updates)
/// </summary>
Expand All @@ -88,6 +103,36 @@ public interface IPluginManager : IDisposable
/// <param name="plugin">The plugin instance if found</param>
/// <returns>True if the plugin was found</returns>
bool TryGetPlugin(string pluginId, out IPlugin? plugin);

/// <summary>
/// Perform pending plugin deletions asynchronously
/// </summary>
Task PerformPendingDeletionsAsync();

/// <summary>
/// Check if all plugin dependencies are satisfied
/// </summary>
bool CheckDependencies(string pluginId, out List<string> missingDependencies);

/// <summary>
/// Get plugins that depend on the given plugin
/// </summary>
IEnumerable<string> GetDependentPlugins(string pluginId);

/// <summary>
/// Validate plugin health
/// </summary>
PluginHealthStatus CheckPluginHealth(string pluginId);

/// <summary>
/// Check for plugin updates (returns a dictionary of pluginId -> availableVersion)
/// </summary>
Task<Dictionary<string, string>> CheckForUpdatesAsync();

/// <summary>
/// Check if a specific plugin has an update available
/// </summary>
Task<bool> HasUpdateAsync(string pluginId);
}

/// <summary>
Expand Down
66 changes: 45 additions & 21 deletions UniversalDeviceToolkit.Lib.Plugins/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public class PluginLoader : IPluginLoader
/// </summary>
public async Task<IPlugin?> LoadFromFileAsync(string dllPath, IPluginSignatureValidator signatureValidator)
{
if (string.IsNullOrWhiteSpace(dllPath))
{
Log.Instance.Warning("LoadFromFileAsync: DLL path is empty");
return null;
}

if (!File.Exists(dllPath))
{
Log.Instance.Warning($"LoadFromFileAsync: DLL file not found: {dllPath}");
return null;
}

RegisteredPluginDependencyResolutionContext? registeredDependencyContext = null;
var keepDependencyContext = false;

Expand All @@ -69,8 +81,6 @@ public class PluginLoader : IPluginLoader

// Register AssemblyResolve handler early to handle dependencies that may be loaded
// during signature validation or assembly loading.
// Note: X509Certificate.CreateFromSignedFile uses native APIs and typically doesn't
// trigger managed assembly resolution, but we register early for defense in depth.
if (!string.IsNullOrWhiteSpace(pluginDirectory))
{
registeredDependencyContext = RegisterPluginDependencyResolutionContext(normalizedDllPath, pluginDirectory, signatureValidator);
Expand All @@ -81,26 +91,24 @@ public class PluginLoader : IPluginLoader
if (!signatureResult.IsValid)
{
Log.Instance.Warning($"Plugin signature validation failed for {dllPath}. Status: {signatureResult.Status}, Error: {signatureResult.ErrorMessage}");

return null;
}

// Load the main assembly from bytes to avoid file locking, but resolve plugin-local
// dependencies through a dedicated AssemblyLoadContext so online plugins can keep
// their own UI/runtime dependency graph isolated from the host.
// dependencies through a dedicated AssemblyLoadContext.
Assembly? assembly = null;
PluginAssemblyLoadContext? pluginLoadContext = null;
try
{
var assemblyBytes = File.ReadAllBytes(normalizedDllPath);
var assemblyBytes = await File.ReadAllBytesAsync(normalizedDllPath);
pluginLoadContext = new PluginAssemblyLoadContext(normalizedDllPath, pluginDirectory ?? string.Empty, signatureValidator);
assembly = pluginLoadContext.LoadFromStream(new MemoryStream(assemblyBytes));
registeredDependencyContext?.Context.SetPluginMainAssembly(assembly);
}
catch (Exception ex)
{
if (Log.Instance.IsTraceEnabled)
Log.Instance.Trace($"Failed to load assembly from {dllPath}: {ex.Message}", ex);
Log.Instance.Error($"Failed to load assembly from {dllPath}", ex);
pluginLoadContext?.Unload();
return null;
}

Expand All @@ -112,20 +120,23 @@ public class PluginLoader : IPluginLoader
}
catch (ReflectionTypeLoadException ex)
{
if (Log.Instance.IsTraceEnabled)
Log.Instance.Warning($"Failed to get types from assembly {dllPath}. Loader exceptions:");
if (ex.LoaderExceptions != null)
{
Log.Instance.Trace($"Failed to get types from assembly {dllPath}. Loader exceptions:", ex);
if (ex.LoaderExceptions != null)
foreach (var loaderEx in ex.LoaderExceptions)
{
foreach (var loaderEx in ex.LoaderExceptions)
{
Log.Instance.Trace($" Loader exception: {loaderEx?.Message}", loaderEx);
}
Log.Instance.Warning($" - {loaderEx?.Message}", loaderEx);
}
}
// Try to continue with successfully loaded types
pluginTypes = ex.Types.Where(t => t != null).OfType<Type>().ToArray();
}
catch (Exception ex)
{
Log.Instance.Error($"Error getting types from assembly {dllPath}", ex);
pluginLoadContext?.Unload();
return null;
}

var validPluginTypes = pluginTypes
.Where(t => t != null
Expand All @@ -135,6 +146,13 @@ public class PluginLoader : IPluginLoader
&& t.GetConstructor(Type.EmptyTypes) != null)
.ToList();

if (validPluginTypes.Count == 0)
{
Log.Instance.Warning($"No valid plugin types found in {dllPath}");
pluginLoadContext?.Unload();
return null;
}

if (Log.Instance.IsTraceEnabled)
Log.Instance.Trace($"Found {validPluginTypes.Count} plugin type(s) in {dllPath}");

Expand All @@ -146,18 +164,25 @@ public class PluginLoader : IPluginLoader
var plugin = CreatePluginInstance(pluginType, dllPath);
if (plugin != null)
{
if (!string.IsNullOrWhiteSpace(plugin.Id) && pluginLoadContext is not null)
if (string.IsNullOrWhiteSpace(plugin.Id))
{
Log.Instance.Warning($"Plugin from {dllPath} has empty ID, skipping");
continue;
}

if (pluginLoadContext is not null)
PluginLoadContexts[plugin.Id] = pluginLoadContext;
if (!string.IsNullOrWhiteSpace(plugin.Id) && registeredDependencyContext is not null)
if (registeredDependencyContext is not null)
PluginDependencyContexts[plugin.Id] = registeredDependencyContext.Context;
keepDependencyContext = true;

Log.Instance.Info($"Successfully created plugin instance: {plugin.Id} ({plugin.Name}) from {dllPath}");
return plugin;
}
}
catch (Exception ex)
{
if (Log.Instance.IsTraceEnabled)
Log.Instance.Trace($"Failed to create instance of plugin type {pluginType.Name}: {ex.Message}", ex);
Log.Instance.Error($"Failed to create instance of plugin type {pluginType.Name} from {dllPath}", ex);
}
}

Expand All @@ -166,8 +191,7 @@ public class PluginLoader : IPluginLoader
}
catch (Exception ex)
{
if (Log.Instance.IsTraceEnabled)
Log.Instance.Trace($"Failed to load plugin assembly from {dllPath}: {ex.Message}", ex);
Log.Instance.Error($"Failed to load plugin assembly from {dllPath}", ex);
return null;
}
finally
Expand Down
Loading
Loading