public async Task WatchAsync(DotNetWatchContext context, CancellationToken cancellationToken) { var processSpec = context.ProcessSpec; if (context.SuppressMSBuildIncrementalism) { _reporter.Verbose("MSBuild incremental optimizations suppressed."); } _reporter.Output("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload. " + "Press \"Ctrl + R\" to restart."); while (true) { context.Iteration++; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } // Reset for next run context.RequiresMSBuildRevaluation = false; processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (!fileSet.Project.IsNetCoreApp60OrNewer()) { _reporter.Error($"Hot reload based watching is only supported in .NET 6.0 or newer apps. Update the project's launchSettings.json to disable this feature."); return; } if (cancellationToken.IsCancellationRequested) { return; } if (context.Iteration == 0) { ConfigureExecutable(context, processSpec); } using var currentRunCancellationSource = new CancellationTokenSource(); var forceReload = _console.ListenForForceReloadRequest(); using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token, forceReload); using var fileSetWatcher = new FileSetWatcher(fileSet, _reporter); try { using var hotReload = new HotReload(_processRunner, _reporter); await hotReload.InitializeAsync(context, cancellationToken); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = string.Join(" ", processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started"); Task <FileItem?> fileSetTask; Task finishedTask; while (true) { fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask).WaitAsync(combinedCancellationSource.Token); if (finishedTask != fileSetTask || fileSetTask.Result is not FileItem fileItem) { // The app exited. break; } else { _reporter.Output($"File changed: {fileItem.FilePath}."); var start = Stopwatch.GetTimestamp(); if (await hotReload.TryHandleFileChange(context, fileItem, combinedCancellationSource.Token)) { var totalTime = TimeSpan.FromTicks(Stopwatch.GetTimestamp() - start); _reporter.Output($"Hot reload of changes succeeded."); _reporter.Verbose($"Hot reload applied in {totalTime.TotalMilliseconds}ms."); } else { _reporter.Verbose($"Unable to handle changes to {fileItem.FilePath}. Rebuilding the app."); break; } } } // Regardless of the which task finished first, make sure everything is cancelled // and wait for dotnet to exit. We don't want orphan processes currentRunCancellationSource.Cancel(); await Task.WhenAll(processTask, fileSetTask); if (processTask.Result != 0 && finishedTask == processTask && !cancellationToken.IsCancellationRequested) { // Only show this error message if the process exited non-zero due to a normal process exit. // Don't show this if dotnet-watch killed the inner process due to file change or CTRL+C by the user _reporter.Error($"Exited with error code {processTask.Result}"); } else { _reporter.Output("Exited"); } if (finishedTask == processTask) { // Process exited. Redo evaludation context.RequiresMSBuildRevaluation = true; // Now wait for a file to change before restarting process context.ChangedFile = await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet...")); } else { Debug.Assert(finishedTask == fileSetTask); var changedFile = fileSetTask.Result; context.ChangedFile = changedFile; } }
private async Task <int> MainInternalAsync( IReporter reporter, string project, IReadOnlyList <string> args, CancellationToken cancellationToken) { // TODO multiple projects should be easy enough to add here string projectFile; try { projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, project); } catch (FileNotFoundException ex) { reporter.Error(ex.Message); return(1); } var isDefaultRunCommand = false; if (args.Count == 1 && args[0] == "run") { isDefaultRunCommand = true; } else if (args.Count == 0) { isDefaultRunCommand = true; args = new[] { "run" }; } var watchOptions = DotNetWatchOptions.Default; var fileSetFactory = new MsBuildFileSetFactory(reporter, watchOptions, projectFile, waitOnError: true, trace: false); var processInfo = new ProcessSpec { Executable = DotnetMuxer.MuxerPath, WorkingDirectory = Path.GetDirectoryName(projectFile), Arguments = args, EnvironmentVariables = { ["DOTNET_WATCH"] = "1" }, }; if (CommandLineOptions.IsPollingEnabled) { _reporter.Output("Polling file watcher is enabled"); } var defaultProfile = LaunchSettingsProfile.ReadDefaultProfile(_workingDirectory, reporter); var context = new DotNetWatchContext { ProcessSpec = processInfo, Reporter = _reporter, SuppressMSBuildIncrementalism = watchOptions.SuppressMSBuildIncrementalism, DefaultLaunchSettingsProfile = defaultProfile, }; if (isDefaultRunCommand && !string.IsNullOrEmpty(defaultProfile?.HotReloadProfile)) { _reporter.Verbose($"Found HotReloadProfile={defaultProfile.HotReloadProfile}. Watching with hot-reload"); // We'll sue hot-reload based watching if // a) watch was invoked with no args or with exactly one arg - the run command e.g. `dotnet watch` or `dotnet watch run` // b) The launch profile supports hot-reload based watching. // The watcher will complain if users configure this for runtimes that would not support it. await using var watcher = new HotReloadDotNetWatcher(reporter, fileSetFactory, watchOptions, _console); await watcher.WatchAsync(context, cancellationToken); } else { _reporter.Verbose("Did not find a HotReloadProfile or running a non-default command. Watching with legacy behavior."); // We'll use the presence of a profile to decide if we're going to use the hot-reload based watching. // The watcher will complain if users configure this for runtimes that would not support it. await using var watcher = new DotNetWatcher(reporter, fileSetFactory, watchOptions); await watcher.WatchAsync(context, cancellationToken); } return(0); }
public async Task WatchAsync(DotNetWatchContext context, CancellationToken cancellationToken) { var cancelledTaskSource = new TaskCompletionSource(); cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(), cancelledTaskSource); var processSpec = context.ProcessSpec; var initialArguments = processSpec.Arguments.ToArray(); if (context.SuppressMSBuildIncrementalism) { _reporter.Verbose("MSBuild incremental optimizations suppressed."); } while (true) { context.Iteration++; // Reset arguments processSpec.Arguments = initialArguments; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } // Reset for next run context.RequiresMSBuildRevaluation = false; processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (cancellationToken.IsCancellationRequested) { return; } using (var currentRunCancellationSource = new CancellationTokenSource()) using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token)) using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter)) { var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = string.Join(" ", processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started"); Task <FileItem?> fileSetTask; Task finishedTask; while (true) { fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); if (finishedTask == fileSetTask && fileSetTask.Result is FileItem fileItem && await _staticFileHandler.TryHandleFileChange(context, fileItem, combinedCancellationSource.Token)) { // We're able to handle the file change event without doing a full-rebuild. }
public async Task WatchAsync(DotNetWatchContext context, CancellationToken cancellationToken) { var processSpec = context.ProcessSpec; _reporter.Output("Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload. " + "Press \"Ctrl + R\" to restart."); var forceReload = new CancellationTokenSource(); _console.KeyPressed += (key) => { var modifiers = ConsoleModifiers.Control; if ((key.Modifiers & modifiers) == modifiers && key.Key == ConsoleKey.R) { var cancellationTokenSource = Interlocked.Exchange(ref forceReload, new CancellationTokenSource()); cancellationTokenSource.Cancel(); } }; while (true) { context.Iteration++; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (!fileSet.Project.IsNetCoreApp60OrNewer()) { _reporter.Error($"Hot reload based watching is only supported in .NET 6.0 or newer apps. Update the project's launchSettings.json to disable this feature."); return; } if (cancellationToken.IsCancellationRequested) { return; } if (context.Iteration == 0) { ConfigureExecutable(context, processSpec); } using var currentRunCancellationSource = new CancellationTokenSource(); using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token, forceReload.Token); using var fileSetWatcher = new HotReloadFileSetWatcher(fileSet, _reporter); try { using var hotReload = new HotReload(_processRunner, _reporter); await hotReload.InitializeAsync(context, cancellationToken); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = string.Join(" ", processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started"); Task <FileItem[]> fileSetTask; Task finishedTask; while (true) { fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask).WaitAsync(combinedCancellationSource.Token); if (finishedTask != fileSetTask || fileSetTask.Result is not FileItem[] fileItems) { // The app exited. break; } else { if (MayRequireRecompilation(context, fileItems) is { } newFile) { _reporter.Output($"New file: {newFile.FilePath}. Rebuilding the application."); break; }
public async Task WatchAsync(ProcessSpec processSpec, CancellationToken cancellationToken) { Ensure.NotNull(processSpec, nameof(processSpec)); var cancelledTaskSource = new TaskCompletionSource(); cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(), cancelledTaskSource); var initialArguments = processSpec.Arguments.ToArray(); var context = new DotNetWatchContext { Iteration = -1, ProcessSpec = processSpec, Reporter = _reporter, SuppressMSBuildIncrementalism = _dotnetWatchOptions.SuppressMSBuildIncrementalism, }; if (context.SuppressMSBuildIncrementalism) { _reporter.Verbose("MSBuild incremental optimizations suppressed."); } while (true) { context.Iteration++; // Reset arguments processSpec.Arguments = initialArguments; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } // Reset for next run context.RequiresMSBuildRevaluation = false; processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (cancellationToken.IsCancellationRequested) { return; } using (var currentRunCancellationSource = new CancellationTokenSource()) using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token)) using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter)) { var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started"); Task <FileItem?> fileSetTask; Task finishedTask; while (true) { fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); if (context.BrowserRefreshServer is not null && finishedTask == fileSetTask && fileSetTask.Result is FileItem { FileKind : FileKind.StaticFile } file) { _reporter.Verbose($"Handling file change event for static content {file.FilePath}."); // If we can handle the file change without a browser refresh, do it. await StaticContentHandler.TryHandleFileAction(context.BrowserRefreshServer, file, combinedCancellationSource.Token); }
private async Task <int> MainInternalAsync(CommandLineOptions options, CancellationToken cancellationToken) { // TODO multiple projects should be easy enough to add here string projectFile; try { projectFile = MsBuildProjectFinder.FindMsBuildProject(_workingDirectory, options.Project); } catch (FileNotFoundException ex) { _reporter.Error(ex.Message); return(1); } var args = options.RemainingArguments; var isDefaultRunCommand = false; if (args.Count == 1 && args[0] == "run") { isDefaultRunCommand = true; } else if (args.Count == 0) { isDefaultRunCommand = true; args = new[] { "run" }; } var watchOptions = DotNetWatchOptions.Default; watchOptions.NonInteractive = options.NonInteractive; var fileSetFactory = new MsBuildFileSetFactory(_reporter, watchOptions, projectFile, waitOnError: true, trace: false); var processInfo = new ProcessSpec { Executable = DotnetMuxer.MuxerPath, WorkingDirectory = Path.GetDirectoryName(projectFile), Arguments = args, EnvironmentVariables = { ["DOTNET_WATCH"] = "1" }, }; if (CommandLineOptions.IsPollingEnabled) { _reporter.Output("Polling file watcher is enabled"); } var defaultProfile = LaunchSettingsProfile.ReadDefaultProfile(processInfo.WorkingDirectory, _reporter) ?? new(); var context = new DotNetWatchContext { ProcessSpec = processInfo, Reporter = _reporter, SuppressMSBuildIncrementalism = watchOptions.SuppressMSBuildIncrementalism, DefaultLaunchSettingsProfile = defaultProfile, }; context.ProjectGraph = TryReadProject(projectFile); if (!options.NoHotReload && isDefaultRunCommand && context.ProjectGraph is not null && IsHotReloadSupported(context.ProjectGraph)) { _reporter.Verbose($"Project supports hot reload and was configured to run with the default run-command. Watching with hot-reload"); // Use hot-reload based watching if // a) watch was invoked with no args or with exactly one arg - the run command e.g. `dotnet watch` or `dotnet watch run` // b) The launch profile supports hot-reload based watching. // The watcher will complain if users configure this for runtimes that would not support it. await using var watcher = new HotReloadDotNetWatcher(_reporter, _requester, fileSetFactory, watchOptions, _console, _workingDirectory); await watcher.WatchAsync(context, cancellationToken); }
public async Task WatchAsync(ProcessSpec processSpec, CancellationToken cancellationToken) { Ensure.NotNull(processSpec, nameof(processSpec)); var cancelledTaskSource = new TaskCompletionSource(); cancellationToken.Register(state => ((TaskCompletionSource)state).TrySetResult(), cancelledTaskSource); var initialArguments = processSpec.Arguments.ToArray(); var suppressMSBuildIncrementalism = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM"); var context = new DotNetWatchContext { Iteration = -1, ProcessSpec = processSpec, Reporter = _reporter, SuppressMSBuildIncrementalism = suppressMSBuildIncrementalism == "1" || suppressMSBuildIncrementalism == "true", }; if (context.SuppressMSBuildIncrementalism) { _reporter.Verbose("MSBuild incremental optimizations suppressed."); } while (true) { context.Iteration++; // Reset arguments processSpec.Arguments = initialArguments; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } // Reset for next run context.RequiresMSBuildRevaluation = false; processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (cancellationToken.IsCancellationRequested) { return; } using (var currentRunCancellationSource = new CancellationTokenSource()) using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token)) using (var fileSetWatcher = new FileSetWatcher(fileSet, _reporter)) { var fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started"); var finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); // Regardless of the which task finished first, make sure everything is cancelled // and wait for dotnet to exit. We don't want orphan processes currentRunCancellationSource.Cancel(); await Task.WhenAll(processTask, fileSetTask); if (processTask.Result != 0 && finishedTask == processTask && !cancellationToken.IsCancellationRequested) { // Only show this error message if the process exited non-zero due to a normal process exit. // Don't show this if dotnet-watch killed the inner process due to file change or CTRL+C by the user _reporter.Error($"Exited with error code {processTask.Result}"); } else { _reporter.Output("Exited"); } if (finishedTask == cancelledTaskSource.Task || cancellationToken.IsCancellationRequested) { return; } context.ChangedFile = fileSetTask.Result; if (finishedTask == processTask) { // Process exited. Redo evaludation context.RequiresMSBuildRevaluation = true; // Now wait for a file to change before restarting process context.ChangedFile = await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _reporter.Warn("Waiting for a file to change before restarting dotnet...")); } if (!string.IsNullOrEmpty(fileSetTask.Result)) { _reporter.Output($"File changed: {fileSetTask.Result}"); } } } }
public async Task WatchAsync(DotNetWatchContext context, CancellationToken cancellationToken) { var processSpec = context.ProcessSpec; var forceReload = new CancellationTokenSource(); var hotReloadEnabledMessage = "Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload."; if (!_dotNetWatchOptions.NonInteractive) { _reporter.Output($"{hotReloadEnabledMessage}{Environment.NewLine} {(_dotNetWatchOptions.SuppressEmojis ? string.Empty : "💡")} Press \"Ctrl + R\" to restart.", emoji: "🔥"); _console.KeyPressed += (key) => { var modifiers = ConsoleModifiers.Control; if ((key.Modifiers & modifiers) == modifiers && key.Key == ConsoleKey.R) { var cancellationTokenSource = Interlocked.Exchange(ref forceReload, new CancellationTokenSource()); cancellationTokenSource.Cancel(); } }; } else { _reporter.Output(hotReloadEnabledMessage, emoji: "🔥"); } while (true) { context.Iteration++; for (var i = 0; i < _filters.Length; i++) { await _filters[i].ProcessAsync(context, cancellationToken); } processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = (context.Iteration + 1).ToString(CultureInfo.InvariantCulture); var fileSet = context.FileSet; if (fileSet == null) { _reporter.Error("Failed to find a list of files to watch"); return; } if (!fileSet.Project.IsNetCoreApp60OrNewer()) { _reporter.Error($"Hot reload based watching is only supported in .NET 6.0 or newer apps. Update the project's launchSettings.json to disable this feature."); return; } if (cancellationToken.IsCancellationRequested) { return; } if (context.Iteration == 0) { ConfigureExecutable(context, processSpec); } using var currentRunCancellationSource = new CancellationTokenSource(); using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, currentRunCancellationSource.Token, forceReload.Token); using var fileSetWatcher = new HotReloadFileSetWatcher(fileSet, _reporter); try { using var hotReload = new HotReload(_processRunner, _reporter); await hotReload.InitializeAsync(context, cancellationToken); var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token); var args = string.Join(" ", processSpec.Arguments); _reporter.Verbose($"Running {processSpec.ShortDisplayName()} with the following arguments: {args}"); _reporter.Output("Started", emoji: "🚀"); Task <FileItem[]?> fileSetTask; Task finishedTask; while (true) { fileSetTask = fileSetWatcher.GetChangedFileAsync(combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask).WaitAsync(combinedCancellationSource.Token); if (finishedTask != fileSetTask || fileSetTask.Result is not FileItem[] fileItems) { if (processTask.IsFaulted && finishedTask == processTask && !cancellationToken.IsCancellationRequested) { // Only show this error message if the process exited non-zero due to a normal process exit. // Don't show this if dotnet-watch killed the inner process due to file change or CTRL+C by the user _reporter.Error($"Application failed to start: {processTask.Exception?.InnerException?.Message}"); } break; } else { if (MayRequireRecompilation(context, fileItems) is { } newFile) { _reporter.Output($"New file: {GetRelativeFilePath(newFile.FilePath)}. Rebuilding the application."); break; }