diff --git a/samples/drop-ins/SampleDropIn/DropIn.cs b/samples/drop-ins/SampleDropIn/DropIn.cs index 173cb0493..03feb2134 100644 --- a/samples/drop-ins/SampleDropIn/DropIn.cs +++ b/samples/drop-ins/SampleDropIn/DropIn.cs @@ -1,9 +1,11 @@ using Elsa.DropIns.Core; using Elsa.Extensions; using Elsa.Features.Services; +using Elsa.Workflows; using Elsa.Workflows.Contracts; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using SampleDropIn.Activities; namespace SampleDropIn; @@ -21,4 +23,10 @@ public class DropIn : IDropIn var activityRegistry = serviceProvider.GetRequiredService(); await activityRegistry.RegisterAsync(cancellationToken: cancellationToken); } + + public void Unconfigure(IServiceProvider serviceProvider) + { + var activityRegistry = serviceProvider.GetRequiredService(); + activityRegistry.Remove(typeof(ActivityRegistry), activityRegistry.Find()!); + } } \ No newline at end of file diff --git a/src/common/Elsa.DropIns.Core/IDropIn.cs b/src/common/Elsa.DropIns.Core/IDropIn.cs index a5fd12371..ef2ca2d47 100644 --- a/src/common/Elsa.DropIns.Core/IDropIn.cs +++ b/src/common/Elsa.DropIns.Core/IDropIn.cs @@ -17,4 +17,9 @@ public interface IDropIn /// Called when the drop-in is being configured. /// ValueTask ConfigureAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken); + + /// + /// Unconfigure the drop-in when it's deleted. + /// + void Unconfigure(IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs b/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs index febba4f8a..8c2d1264e 100644 --- a/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs +++ b/src/common/Elsa.DropIns/Contexts/NuGetPackageAssemblyLoadContext.cs @@ -22,6 +22,8 @@ internal sealed class NuGetPackageAssemblyLoadContext : AssemblyLoadContext _loadedAssemblies[assembly.FullName!] = assembly; } + //Closes the package reader, closing the .nupkg file + packageReader.Dispose(); } protected override Assembly? Load(AssemblyName assemblyName) diff --git a/src/common/Elsa.DropIns/Elsa.DropIns.csproj b/src/common/Elsa.DropIns/Elsa.DropIns.csproj index f26afbffa..ea1b7958c 100644 --- a/src/common/Elsa.DropIns/Elsa.DropIns.csproj +++ b/src/common/Elsa.DropIns/Elsa.DropIns.csproj @@ -10,13 +10,15 @@ + - + @@ -24,4 +26,4 @@ - + \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs b/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs index d1dfca0db..64b779970 100644 --- a/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs +++ b/src/common/Elsa.DropIns/Helpers/AssemblyLoader.cs @@ -10,7 +10,14 @@ public static class AssemblyLoader { var loadContext = new DirectoryAssemblyLoadContext(path); var assemblyName = AssemblyLoadContext.GetAssemblyName(path); - var assembly = loadContext.LoadFromAssemblyName(assemblyName); + + // Copy drop in to memory stream to avoid file locking + using var fileStream = File.OpenRead(path); + using var memoryStream = new MemoryStream(); + fileStream.CopyTo(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + fileStream.Close(); + var assembly = loadContext.LoadFromStream(memoryStream); return assembly; } diff --git a/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs b/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs index c33161313..7fed31ed2 100644 --- a/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs +++ b/src/common/Elsa.DropIns/HostedServices/DropInDirectoryMonitorHostedService.cs @@ -2,6 +2,7 @@ using Elsa.DropIns.Catalogs; using Elsa.DropIns.Core; using Elsa.DropIns.Options; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ThrottleDebounce; @@ -13,16 +14,29 @@ namespace Elsa.DropIns.HostedServices; public class DropInDirectoryMonitorHostedService : BackgroundService { private readonly IOptions _options; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly RateLimitedFunc _debouncedLoader; + private readonly RateLimitedFunc _debouncedUnloader; private readonly FileSystemWatcher _watcher; + private readonly Dictionary> _installedDropIns; + + /// - public DropInDirectoryMonitorHostedService(IOptions options, IServiceProvider serviceProvider) + public DropInDirectoryMonitorHostedService( + IOptions options, + IServiceProvider serviceProvider, + ILogger logger + ) { _options = options; + _logger = logger; _serviceProvider = serviceProvider; _debouncedLoader = Debouncer.Debounce(LoadDropInAssemblyAsync, TimeSpan.FromSeconds(2)); + _debouncedUnloader = Debouncer.Debounce(UnloadDropInAssemblyAsync, TimeSpan.FromSeconds(2)); + + _installedDropIns = []; var rootDirectoryPath = _options.Value.DropInRootDirectory; @@ -38,10 +52,12 @@ public class DropInDirectoryMonitorHostedService : BackgroundService } /// - protected override Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + await LoadDropInAssemblyAsync(_options.Value.DropInRootDirectory); + _watcher.Changed += OnChanged; - return Task.CompletedTask; + _watcher.Deleted += OnDeleted; } private async void OnChanged(object sender, FileSystemEventArgs e) @@ -54,6 +70,16 @@ public class DropInDirectoryMonitorHostedService : BackgroundService await task; } + private async void OnDeleted(object sender, FileSystemEventArgs e) + { + var task = _debouncedUnloader.Invoke(e.FullPath); + + if (task == null) + return; + + await task; + } + private async Task LoadDropInAssemblyAsync(string fullPath) { var directory = Path.GetDirectoryName(fullPath)!; @@ -63,7 +89,35 @@ public class DropInDirectoryMonitorHostedService : BackgroundService foreach (var dropInDescriptor in dropInDescriptors) { var dropIn = (IDropIn)Activator.CreateInstance(dropInDescriptor.Type)!; + if (_installedDropIns.TryGetValue(fullPath, out var installedDropIns)) + { + installedDropIns.Add(dropIn); + } + else + { + _installedDropIns[fullPath] = [dropIn]; + } await dropIn.ConfigureAsync(_serviceProvider, CancellationToken.None); } } + + private Task UnloadDropInAssemblyAsync(string fullPath) + { + if (_installedDropIns.TryGetValue(fullPath, out var installedDropIn)) + { + installedDropIn.ForEach(dropIn => + { + try + { + dropIn.Unconfigure(_serviceProvider); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error unconfiguring drop-in {DropIn}", dropIn.GetType().Name); + } + }); + _installedDropIns.Remove(fullPath); + } + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/common/Elsa.DropIns/Models/DropInDescriptor.cs b/src/common/Elsa.DropIns/Models/DropInDescriptor.cs index 3d2a29996..8b036263c 100644 --- a/src/common/Elsa.DropIns/Models/DropInDescriptor.cs +++ b/src/common/Elsa.DropIns/Models/DropInDescriptor.cs @@ -1,3 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + namespace Elsa.DropIns.Models; -public record DropInDescriptor(Type Type); \ No newline at end of file +public class DropInDescriptor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type) +{ + /// + /// Gets or sets the type of the drop-in. + /// The DynamicallyAccessedMembers attribute ensures that the IL2072 warning is suppressed. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + public Type Type { get; set; } = type; +} \ No newline at end of file