File: Watch\DotNetWatcher.cs
Web Access
Project: ..\..\..\src\BuiltInTools\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.GraphRoots.Single();
                    var projectMap = new ProjectNodeMap(evaluationResult.ProjectGraph, context.Logger);
                    staticFileHandler = new StaticFileHandler(context.Logger, projectMap, 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.MuxerPath,
                    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);
 
                foreach (var (name, value) in environmentBuilder)
                {
                    processSpec.EnvironmentVariables.Add(name, value);
                }
 
                if (projectRootNode != null)
                {
                    context.BrowserLauncher.InstallBrowserLaunchTrigger(processSpec, projectRootNode, context.RootProjectOptions, browserRefreshServer, shutdownCancellationToken);
                }
 
                // 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);
                }
            }
        }
    }
}