| File: Watch\DotNetWatcher.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\dotnet-watch\dotnet-watch.csproj (dotnet-watch) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; using Microsoft.Build.Graph; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; internal static class DotNetWatcher { public static async Task WatchAsync(DotNetWatchContext context, CancellationToken shutdownCancellationToken) { var cancelledTaskSource = new TaskCompletionSource(); shutdownCancellationToken.Register(state => ((TaskCompletionSource)state!).TrySetResult(), cancelledTaskSource); if (context.EnvironmentOptions.SuppressMSBuildIncrementalism) { context.Logger.LogDebug("MSBuild incremental optimizations suppressed."); } var environmentBuilder = new Dictionary<string, string>(); ChangedFile? changedFile = null; var buildEvaluator = new BuildEvaluator(context); for (var iteration = 0;;iteration++) { if (await buildEvaluator.EvaluateAsync(changedFile, shutdownCancellationToken) is not { } evaluationResult) { context.Logger.LogError("Failed to find a list of files to watch"); return; } StaticFileHandler? staticFileHandler; ProjectGraphNode? projectRootNode; if (evaluationResult.ProjectGraph != null) { projectRootNode = evaluationResult.ProjectGraph.Graph.GraphRoots.Single(); staticFileHandler = new StaticFileHandler(context.Logger, evaluationResult.ProjectGraph, context.BrowserRefreshServerFactory); } else { context.Logger.LogDebug("Unable to determine if this project is a webapp."); projectRootNode = null; staticFileHandler = null; } var processSpec = new ProcessSpec { Executable = context.EnvironmentOptions.GetMuxerPath(), WorkingDirectory = context.EnvironmentOptions.WorkingDirectory, IsUserApplication = true, Arguments = buildEvaluator.GetProcessArguments(iteration), EnvironmentVariables = { [EnvironmentVariables.Names.DotnetWatch] = "1", [EnvironmentVariables.Names.DotnetWatchIteration] = (iteration + 1).ToString(CultureInfo.InvariantCulture), } }; var browserRefreshServer = projectRootNode != null && HotReloadAppModel.InferFromProject(context, projectRootNode) is WebApplicationAppModel webAppModel ? await context.BrowserRefreshServerFactory.GetOrCreateBrowserRefreshServerAsync(projectRootNode, webAppModel, shutdownCancellationToken) : null; browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder, enableHotReload: false); Action<OutputLine>? outputObserver = null; if (projectRootNode != null) { Debug.Assert(context.MainProjectOptions != null); outputObserver = context.BrowserLauncher.TryGetBrowserLaunchOutputObserver(projectRootNode, context.MainProjectOptions, browserRefreshServer, shutdownCancellationToken); } processSpec.RedirectOutput(outputObserver, context.ProcessOutputReporter, context.EnvironmentOptions, projectRootNode?.GetDisplayName() ?? ""); foreach (var (name, value) in environmentBuilder) { processSpec.EnvironmentVariables.Add(name, value); } // Reset for next run buildEvaluator.RequiresRevaluation = false; if (shutdownCancellationToken.IsCancellationRequested) { return; } using var currentRunCancellationSource = new CancellationTokenSource(); using var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownCancellationToken, currentRunCancellationSource.Token); using var fileSetWatcher = new FileWatcher(context.Logger, context.EnvironmentOptions); fileSetWatcher.WatchContainingDirectories(evaluationResult.Files.Keys, includeSubdirectories: true); var processTask = context.ProcessRunner.RunAsync(processSpec, context.Logger, launchResult: null, combinedCancellationSource.Token); Task<ChangedFile?> fileSetTask; Task finishedTask; context.Logger.Log(MessageDescriptor.WaitingForChanges); while (true) { fileSetTask = fileSetWatcher.WaitForFileChangeAsync(evaluationResult.Files, startedWatching: null, combinedCancellationSource.Token); finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task); if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue) { if (await staticFileHandler.HandleFileChangesAsync([fileSetTask.Result.Value], combinedCancellationSource.Token)) { // We're able to handle the file change event without doing a full-rebuild. continue; } } 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 (finishedTask == cancelledTaskSource.Task || shutdownCancellationToken.IsCancellationRequested) { return; } if (finishedTask == processTask) { // Process exited. Redo evalulation buildEvaluator.RequiresRevaluation = true; // Now wait for a file to change before restarting process changedFile = await fileSetWatcher.WaitForFileChangeAsync( evaluationResult.Files, startedWatching: () => context.Logger.Log(MessageDescriptor.WaitingForFileChangeBeforeRestarting), shutdownCancellationToken); } else { Debug.Assert(finishedTask == fileSetTask); changedFile = fileSetTask.Result; Debug.Assert(changedFile != null, "ChangedFile should only be null when cancelled"); context.Logger.LogInformation("File changed: {Path}", changedFile.Value.Item.FilePath); } } } }