├── .gitattributes ├── .gitignore ├── HotReload.sln ├── README.md ├── SampleApplication ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleApplication.csproj ├── Startup.cs ├── appsettings.Development.json └── appsettings.json └── Watcher ├── AppLoadContext.cs ├── DotnetMuxer.cs ├── HostingServer.cs ├── NoopHostLifetime.cs ├── Program.cs ├── ProjectOptions.cs ├── Properties └── launchSettings.json ├── Watcher.csproj └── WatcherService.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | *.sh eol=lf 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *net451.csproj 17 | *k10.csproj 18 | *.psess 19 | *.vsp 20 | *.pidb 21 | *.userprefs 22 | *DS_Store 23 | *.ncrunchsolution 24 | *.*sdf 25 | *.ipch 26 | *.sln.ide 27 | project.lock.json 28 | /.vs/ 29 | .vscode/ 30 | .build/ 31 | .testPublish/ 32 | global.json 33 | -------------------------------------------------------------------------------- /HotReload.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28606.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApplication", "SampleApplication\SampleApplication.csproj", "{EF8B4E66-F1D3-4A67-B294-54F41F1E666D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Watcher", "Watcher\Watcher.csproj", "{5E648D42-1B60-4BA7-90E0-C363341C0910}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {EF8B4E66-F1D3-4A67-B294-54F41F1E666D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {EF8B4E66-F1D3-4A67-B294-54F41F1E666D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {EF8B4E66-F1D3-4A67-B294-54F41F1E666D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {EF8B4E66-F1D3-4A67-B294-54F41F1E666D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5E648D42-1B60-4BA7-90E0-C363341C0910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5E648D42-1B60-4BA7-90E0-C363341C0910}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5E648D42-1B60-4BA7-90E0-C363341C0910}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5E648D42-1B60-4BA7-90E0-C363341C0910}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {0AEF0E8C-A4EA-4AA5-A7BD-BDDDF7E6167D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HotReload 2 | 3 | This is an experiment to see what `dotnet watch` would look like if we could unload the application without stopping the process. The idea is that we'd use unloadable AssemblyLoadContext to load the application while 4 | the hosting process would be in another application context. 5 | -------------------------------------------------------------------------------- /SampleApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.Loader; 7 | using System.Threading.Tasks; 8 | using Autofac.Extensions.DependencyInjection; 9 | using Microsoft.AspNetCore; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace SampleApplication 16 | { 17 | public class Program 18 | { 19 | public static void Main(string[] args) 20 | { 21 | CreateHostBuilder(args).Build().Run(); 22 | } 23 | 24 | public static IHostBuilder CreateHostBuilder(string[] args) 25 | { 26 | return Host.CreateDefaultBuilder(args) 27 | .ConfigureWebHostDefaults(webBuilder => 28 | { 29 | webBuilder.UseStartup(); 30 | }) 31 | .UseServiceProviderFactory(new AutofacServiceProviderFactory()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SampleApplication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58482", 7 | "sslPort": 44341 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebApplication120": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /SampleApplication/SampleApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SampleApplication/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace SampleApplication 12 | { 13 | public class Startup 14 | { 15 | // This method gets called by the runtime. Use this method to add services to the container. 16 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 17 | public void ConfigureServices(IServiceCollection services) 18 | { 19 | 20 | } 21 | 22 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 24 | { 25 | if (env.IsDevelopment()) 26 | { 27 | app.UseDeveloperExceptionPage(); 28 | } 29 | 30 | app.UseRouting(); 31 | 32 | app.UseEndpoints(routes => 33 | { 34 | routes.MapGet("/", async context => 35 | { 36 | await context.Response.WriteAsync("Hello World"); 37 | }); 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SampleApplication/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SampleApplication/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Watcher/AppLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Runtime.Loader; 5 | 6 | namespace Watcher 7 | { 8 | internal class AppLoadContext : AssemblyLoadContext 9 | { 10 | private readonly AssemblyDependencyResolver _resolver; 11 | 12 | public AppLoadContext(string path) : 13 | base(isCollectible: true) 14 | { 15 | _resolver = new AssemblyDependencyResolver(path); 16 | } 17 | 18 | protected override Assembly Load(AssemblyName assemblyName) 19 | { 20 | var path = _resolver.ResolveAssemblyToPath(assemblyName); 21 | if (path != null) 22 | { 23 | // Try to load this assembly from the default context 24 | Assembly defaultAssembly = null; 25 | try 26 | { 27 | defaultAssembly = Default.LoadFromAssemblyName(assemblyName); 28 | } 29 | catch 30 | { 31 | // This sucks but it's the only "easy" way besides storing a list of things in the default context 32 | } 33 | 34 | 35 | // Nothing in the default context, use this assembly 36 | if (defaultAssembly != null) 37 | { 38 | var appAssemblyName = AssemblyName.GetAssemblyName(path); 39 | 40 | // If the local assembly overrides the one in the default load context (version is higher), then it wins 41 | if (appAssemblyName.Version <= defaultAssembly.GetName().Version) 42 | { 43 | return defaultAssembly; 44 | } 45 | } 46 | 47 | // We're loading from Stream because I can't figure out how to make loading from file work and reliably 48 | // unlock the file after unload, the alternative is to shadow copy somewhere (like temp) 49 | var assemblyStream = new MemoryStream(File.ReadAllBytes(path)); 50 | Stream assemblySymbols = null; 51 | 52 | var symbolsPath = Path.ChangeExtension(path, ".pdb"); 53 | if (File.Exists(symbolsPath)) 54 | { 55 | // Found a symbol next to the dll to load it 56 | assemblySymbols = new MemoryStream(File.ReadAllBytes(symbolsPath)); 57 | } 58 | 59 | return LoadFromStream(assemblyStream, assemblySymbols); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) 66 | { 67 | var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); 68 | if (path != null) 69 | { 70 | // REVIEW: We're going to have to shadow copy here 71 | return LoadUnmanagedDllFromPath(path); 72 | } 73 | 74 | return IntPtr.Zero; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Watcher/DotnetMuxer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Watcher 7 | { 8 | /// 9 | /// Utilities for finding the "dotnet.exe" file from the currently running .NET Core application 10 | /// 11 | internal static class DotNetMuxer 12 | { 13 | private const string MuxerName = "dotnet"; 14 | 15 | static DotNetMuxer() 16 | { 17 | MuxerPath = TryFindMuxerPath(); 18 | } 19 | 20 | /// 21 | /// The full filepath to the .NET Core muxer. 22 | /// 23 | public static string MuxerPath { get; } 24 | 25 | /// 26 | /// Finds the full filepath to the .NET Core muxer, 27 | /// or returns a string containing the default name of the .NET Core muxer ('dotnet'). 28 | /// 29 | /// The path or a string named 'dotnet'. 30 | public static string MuxerPathOrDefault() 31 | => MuxerPath ?? MuxerName; 32 | 33 | private static string TryFindMuxerPath() 34 | { 35 | var fileName = MuxerName; 36 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 37 | { 38 | fileName += ".exe"; 39 | } 40 | 41 | var mainModule = Process.GetCurrentProcess().MainModule; 42 | if (!string.IsNullOrEmpty(mainModule?.FileName) 43 | && Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase)) 44 | { 45 | return mainModule.FileName; 46 | } 47 | 48 | return null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Watcher/HostingServer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting.Server; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Features; 6 | 7 | namespace Watcher 8 | { 9 | /// 10 | /// This server is plugged into the loaded application to notify when the application is ready to handle requests 11 | /// and ready to stop handling requests 12 | /// 13 | internal class HostingServer : IServer 14 | { 15 | private TaskCompletionSource _severReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 16 | 17 | public IFeatureCollection Features { get; set; } 18 | 19 | public HostingServer(IServer server) 20 | { 21 | // Set the features from the host server 22 | Features = server.Features; 23 | } 24 | 25 | public Task WaitForApplicationAsync(CancellationToken cancellationToken = default) 26 | { 27 | return _severReadyTcs.Task; 28 | } 29 | 30 | public void Dispose() 31 | { 32 | } 33 | 34 | public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) 35 | { 36 | // REVIEW: Doing this right requires us to hook into Hosting and the HostingApplication 37 | RequestDelegate app = context => 38 | { 39 | var ctx = application.CreateContext(context.Features); 40 | 41 | context.Response.OnCompleted(() => 42 | { 43 | application.DisposeContext(ctx, null); 44 | return Task.CompletedTask; 45 | }); 46 | 47 | return application.ProcessRequestAsync(ctx); 48 | }; 49 | 50 | _severReadyTcs.TrySetResult(app); 51 | 52 | return Task.CompletedTask; 53 | } 54 | 55 | public Task StopAsync(CancellationToken cancellationToken) 56 | { 57 | _severReadyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 58 | 59 | return Task.CompletedTask; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Watcher/NoopHostLifetime.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace Watcher 6 | { 7 | internal class NoopHostLifetime : IHostLifetime 8 | { 9 | public Task StopAsync(CancellationToken cancellationToken) 10 | { 11 | return Task.CompletedTask; 12 | } 13 | 14 | public Task WaitForStartAsync(CancellationToken cancellationToken) 15 | { 16 | return Task.CompletedTask; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Watcher/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Watcher 9 | { 10 | public class Program 11 | { 12 | static readonly string ProjectName = "SampleApplication"; 13 | static readonly string ProjectPath = Path.GetFullPath(@"..\SampleApplication\"); 14 | static readonly string DllPath = Path.Combine(ProjectPath, @"bin\Debug\netcoreapp3.0\SampleApplication.dll"); 15 | 16 | public static void Main(string[] args) 17 | { 18 | new HostBuilder() 19 | .UseContentRoot(ProjectPath) 20 | .ConfigureLogging(logging => 21 | { 22 | logging.AddConsole() 23 | .AddFilter("Watcher", LogLevel.Debug) 24 | .SetMinimumLevel(LogLevel.Warning); 25 | }) 26 | .ConfigureServices(services => 27 | { 28 | services.AddHostedService(); 29 | services.AddSingleton(); 30 | 31 | services.Configure(o => 32 | { 33 | o.ProjectName = ProjectName; 34 | o.ProjectPath = ProjectPath; 35 | o.DllPath = DllPath; 36 | o.DotNetPath = DotNetMuxer.MuxerPathOrDefault(); 37 | o.Args = args; 38 | }); 39 | }) 40 | .ConfigureWebHostDefaults(webBuilder => 41 | { 42 | webBuilder.Configure(app => 43 | { 44 | app.UseDeveloperExceptionPage(); 45 | 46 | var server = app.ApplicationServices.GetRequiredService(); 47 | 48 | app.Run(async context => 49 | { 50 | var application = await server.WaitForApplicationAsync(default); 51 | 52 | await application(context); 53 | }); 54 | }); 55 | }) 56 | .Build() 57 | .Run(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Watcher/ProjectOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Watcher 7 | { 8 | public class ProjectOptions 9 | { 10 | public string DotNetPath { get; set; } 11 | public string ProjectPath { get; set; } 12 | public string[] Args { get; set; } 13 | public string DllPath { get; set; } 14 | public object ProjectName { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Watcher/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58571", 7 | "sslPort": 44370 8 | } 9 | }, 10 | "profiles": { 11 | "Watcher": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 15 | "environmentVariables": { 16 | "DOTNET_ENVIRONMENT": "Production" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Watcher/Watcher.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Watcher/WatcherService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.Loader; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Hosting.Server; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | 14 | namespace Watcher 15 | { 16 | internal class WatcherService : BackgroundService 17 | { 18 | delegate IHostBuilder CreateHostBuilderDelegate(string[] args); 19 | 20 | private readonly ProjectOptions _options; 21 | private readonly HostingServer _server; 22 | private readonly ILogger _logger; 23 | private readonly IHostApplicationLifetime _lifetime; 24 | 25 | public WatcherService(IOptions options, HostingServer server, ILogger logger, IHostApplicationLifetime lifetime) 26 | { 27 | _options = options.Value; 28 | _server = server; 29 | _logger = logger; 30 | _lifetime = lifetime; 31 | } 32 | 33 | protected override async Task ExecuteAsync(CancellationToken cancellationToken) 34 | { 35 | try 36 | { 37 | // Don't block the thread startup thread 38 | await Task.Yield(); 39 | 40 | // Make a file watcher for the project 41 | var watcher = new FileSystemWatcher(_options.ProjectPath, "*.cs") 42 | { 43 | EnableRaisingEvents = true 44 | }; 45 | 46 | var noopLifetime = new NoopHostLifetime(); 47 | 48 | // Run until the host has been shutdown 49 | while (!cancellationToken.IsCancellationRequested) 50 | { 51 | // Load the application in a new load context 52 | var loadContext = new AppLoadContext(_options.DllPath); 53 | 54 | _logger.LogDebug("Loading {projectName} into load context", _options.ProjectName); 55 | 56 | var projectAssembly = loadContext.LoadFromAssemblyName(AssemblyName.GetAssemblyName(_options.DllPath)); 57 | 58 | using var reflection = AssemblyLoadContext.EnterContextualReflection(projectAssembly); 59 | 60 | var type = projectAssembly.GetType($"{_options.ProjectName}.Program"); 61 | var createHostBuilderMethodInfo = type.GetMethod("CreateHostBuilder", BindingFlags.Static | BindingFlags.Public); 62 | 63 | var createHostBuilder = (CreateHostBuilderDelegate)Delegate.CreateDelegate(typeof(CreateHostBuilderDelegate), createHostBuilderMethodInfo); 64 | 65 | // Create a new HostBuilder based on the application 66 | var applicationHostBuilder = createHostBuilder(_options.Args); 67 | 68 | // Override the IServer so that we get Start and Stop application notificaitons 69 | applicationHostBuilder.ConfigureServices(services => 70 | { 71 | services.AddSingleton(_server); 72 | 73 | // We delegate shutdown to the host, we'll call StopAsync on the application ourselves 74 | services.AddSingleton(noopLifetime); 75 | }); 76 | 77 | // Build the host for the child application 78 | var applicationHost = applicationHostBuilder.Build(); 79 | 80 | _logger.LogDebug("Starting application"); 81 | 82 | // Start the application host 83 | await applicationHost.StartAsync(cancellationToken); 84 | 85 | // Wait for a file change in the target application 86 | await WaitForFileChangedAsync(watcher, cancellationToken); 87 | 88 | _logger.LogDebug("Stopping application"); 89 | 90 | // Shut down the application host 91 | await applicationHost.StopAsync(); 92 | 93 | _logger.LogDebug("Application stopped"); 94 | 95 | // Unload the custom load context 96 | loadContext.Unload(); 97 | 98 | _logger.LogDebug("Application context unloaded"); 99 | 100 | // For some odd reason this ends the process 101 | // GC.Collect(); 102 | 103 | // Don't rebuild if we're shuttind down gracefully 104 | if (cancellationToken.IsCancellationRequested) 105 | { 106 | break; 107 | } 108 | 109 | _logger.LogDebug("Rebuilding application"); 110 | 111 | // Rebuild the project (without restoring) 112 | var exitCode = await RunProcessAsync(new ProcessStartInfo 113 | { 114 | FileName = _options.DotNetPath, 115 | Arguments = "build --no-restore", 116 | WorkingDirectory = _options.ProjectPath, 117 | CreateNoWindow = false, 118 | }); 119 | 120 | _logger.LogDebug("Exit code was {processExitCode}", exitCode); 121 | } 122 | } 123 | catch (Exception ex) 124 | { 125 | _logger.LogCritical(ex, "Watching loop failed, stopping application."); 126 | 127 | _lifetime.StopApplication(); 128 | } 129 | } 130 | 131 | private static async Task WaitForFileChangedAsync(FileSystemWatcher watcher, CancellationToken cancellationToken) 132 | { 133 | // Wait for a file to change 134 | var fileChangedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 135 | 136 | void OnFileChanged(object sender, FileSystemEventArgs e) 137 | { 138 | fileChangedTcs.TrySetResult(null); 139 | } 140 | 141 | void OnFileRenamed(object sender, RenamedEventArgs e) 142 | { 143 | fileChangedTcs.TrySetResult(null); 144 | } 145 | 146 | var registration = cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(null), fileChangedTcs); 147 | 148 | using (registration) 149 | { 150 | watcher.Changed += OnFileChanged; 151 | 152 | watcher.Renamed += OnFileRenamed; 153 | 154 | await fileChangedTcs.Task; 155 | 156 | watcher.Changed -= OnFileChanged; 157 | 158 | watcher.Renamed -= OnFileRenamed; 159 | } 160 | } 161 | 162 | private static Task RunProcessAsync(ProcessStartInfo processStartInfo) 163 | { 164 | var process = Process.Start(processStartInfo); 165 | process.EnableRaisingEvents = true; 166 | 167 | if (process.HasExited) 168 | { 169 | return Task.FromResult(process.ExitCode); 170 | } 171 | 172 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 173 | process.Exited += (sender, e) => 174 | { 175 | tcs.TrySetResult(process.ExitCode); 176 | }; 177 | 178 | return tcs.Task; 179 | } 180 | } 181 | } --------------------------------------------------------------------------------