private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args) { // suppress CTRL+C on the first press args.Cancel = !_cts.IsCancellationRequested; if (args.Cancel) { _reporter.Output("Shutdown requested. Press Ctrl+C again to force exit."); } _cts.Cancel(); }
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; } }
public async Task <FileSet> CreateAsync(CancellationToken cancellationToken) { var watchList = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { var projectDir = Path.GetDirectoryName(_projectFile); while (true) { cancellationToken.ThrowIfCancellationRequested(); var capture = _outputSink.StartCapture(); var arguments = new List <string> { "msbuild", "/nologo", _projectFile, $"/p:_DotNetWatchListFile={watchList}", }; if (_dotNetWatchOptions.SuppressHandlingStaticContentFiles) { arguments.Add("/p:DotNetWatchContentFiles=false"); } arguments.AddRange(_buildFlags); var processSpec = new ProcessSpec { Executable = _muxerPath, WorkingDirectory = projectDir, Arguments = arguments, OutputCapture = capture }; _reporter.Verbose($"Running MSBuild target '{TargetName}' on '{_projectFile}'"); var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken); if (exitCode == 0 && File.Exists(watchList)) { using var watchFile = File.OpenRead(watchList); var result = await JsonSerializer.DeserializeAsync <MSBuildFileSetResult>(watchFile, cancellationToken : cancellationToken); var fileItems = new List <FileItem>(); foreach (var project in result.Projects) { var value = project.Value; var fileCount = value.Files.Count; for (var i = 0; i < fileCount; i++) { fileItems.Add(new FileItem { FilePath = value.Files[i], ProjectPath = project.Key, }); } var staticItemsCount = value.StaticFiles.Count; for (var i = 0; i < staticItemsCount; i++) { var item = value.StaticFiles[i]; fileItems.Add(new FileItem { FilePath = item.FilePath, ProjectPath = project.Key, IsStaticFile = true, StaticWebAssetPath = item.StaticWebAssetPath, }); } } _reporter.Verbose($"Watching {fileItems.Count} file(s) for changes"); #if DEBUG foreach (var file in fileItems) { _reporter.Verbose($" -> {file.FilePath} {(file.IsStaticFile ? file.StaticWebAssetPath : null)}"); } Debug.Assert(fileItems.All(f => Path.IsPathRooted(f.FilePath)), "All files should be rooted paths"); #endif // TargetFrameworkVersion appears as v6.0 in msbuild. Ignore the leading v var targetFrameworkVersion = !string.IsNullOrEmpty(result.TargetFrameworkVersion) ? Version.Parse(result.TargetFrameworkVersion.AsSpan(1)) : // Ignore leading v null; var projectInfo = new ProjectInfo( _projectFile, result.IsNetCoreApp, targetFrameworkVersion, result.RunCommand, result.RunArguments, result.RunWorkingDirectory); return(new FileSet(projectInfo, fileItems)); } _reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'"); _reporter.Output($"MSBuild output from target '{TargetName}':"); _reporter.Output(string.Empty); foreach (var line in capture.Lines) { _reporter.Output($" {line}"); } _reporter.Output(string.Empty); if (!_waitOnError) { return(null); } else { _reporter.Warn("Fix the error to continue or press Ctrl+C to exit."); var fileSet = new FileSet(null, new[] { new FileItem { FilePath = _projectFile } }); using (var watcher = new FileSetWatcher(fileSet, _reporter)) { await watcher.GetChangedFileAsync(cancellationToken); _reporter.Output($"File changed: {_projectFile}"); } } } } finally { if (File.Exists(watchList)) { File.Delete(watchList); } } }
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(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; }
public async Task <IFileSet> CreateAsync(CancellationToken cancellationToken) { var watchList = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { var projectDir = Path.GetDirectoryName(_projectFile); while (true) { cancellationToken.ThrowIfCancellationRequested(); var capture = _outputSink.StartCapture(); var processSpec = new ProcessSpec { Executable = _muxerPath, WorkingDirectory = projectDir, Arguments = new[] { "msbuild", "/nologo", _projectFile, $"/p:_DotNetWatchListFile={watchList}", }.Concat(_buildFlags), OutputCapture = capture }; _reporter.Verbose($"Running MSBuild target '{TargetName}' on '{_projectFile}'"); var exitCode = await _processRunner.RunAsync(processSpec, cancellationToken); if (exitCode == 0 && File.Exists(watchList)) { var lines = File.ReadAllLines(watchList); var isNetCoreApp31OrNewer = lines.FirstOrDefault() == "true"; var fileset = new FileSet( isNetCoreApp31OrNewer, lines.Skip(1) .Select(l => l?.Trim()) .Where(l => !string.IsNullOrEmpty(l))); _reporter.Verbose($"Watching {fileset.Count} file(s) for changes"); #if DEBUG foreach (var file in fileset) { _reporter.Verbose($" -> {file}"); } Debug.Assert(fileset.All(Path.IsPathRooted), "All files should be rooted paths"); #endif return(fileset); } _reporter.Error($"Error(s) finding watch items project file '{Path.GetFileName(_projectFile)}'"); _reporter.Output($"MSBuild output from target '{TargetName}':"); _reporter.Output(string.Empty); foreach (var line in capture.Lines) { _reporter.Output($" {line}"); } _reporter.Output(string.Empty); if (!_waitOnError) { return(null); } else { _reporter.Warn("Fix the error to continue or press Ctrl+C to exit."); var fileSet = new FileSet(false, new[] { _projectFile }); using (var watcher = new FileSetWatcher(fileSet, _reporter)) { await watcher.GetChangedFileAsync(cancellationToken); _reporter.Output($"File changed: {_projectFile}"); } } } } finally { if (File.Exists(watchList)) { File.Delete(watchList); } } }