File: Process\ProjectLauncher.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.Globalization;
using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.DotNet.Watch;
 
internal delegate ValueTask ProcessExitAction(int processId, int? exitCode);
 
internal sealed class ProjectLauncher(
    DotNetWatchContext context,
    ProjectNodeMap projectMap,
    CompilationHandler compilationHandler,
    int iteration)
{
    public int Iteration = iteration;
 
    public ILogger Logger
        => context.Logger;
 
    public ILoggerFactory LoggerFactory
        => context.LoggerFactory;
 
    public EnvironmentOptions EnvironmentOptions
        => context.EnvironmentOptions;
 
    public async ValueTask<RunningProject?> TryLaunchProcessAsync(
        ProjectOptions projectOptions,
        CancellationTokenSource processTerminationSource,
        Action<OutputLine>? onOutput,
        RestartOperation restartOperation,
        CancellationToken cancellationToken)
    {
        var projectNode = projectMap.TryGetProjectNode(projectOptions.ProjectPath, projectOptions.TargetFramework);
        if (projectNode == null)
        {
            // error already reported
            return null;
        }
 
        if (!projectNode.IsNetCoreApp(Versions.Version6_0))
        {
            Logger.LogError($"Hot Reload based watching is only supported in .NET 6.0 or newer apps. Use --no-hot-reload switch or update the project's launchSettings.json to disable this feature.");
            return null;
        }
 
        var appModel = HotReloadAppModel.InferFromProject(context, projectNode);
 
        // create loggers that include project name in messages:
        var projectDisplayName = projectNode.GetDisplayName();
        var clientLogger = context.LoggerFactory.CreateLogger(HotReloadDotNetWatcher.ClientLogComponentName, projectDisplayName);
        var agentLogger = context.LoggerFactory.CreateLogger(HotReloadDotNetWatcher.AgentLogComponentName, projectDisplayName);
 
        var clients = await appModel.TryCreateClientsAsync(clientLogger, agentLogger, cancellationToken);
        if (clients == null)
        {
            // error already reported
            return null;
        }
 
        var processSpec = new ProcessSpec
        {
            Executable = EnvironmentOptions.MuxerPath,
            IsUserApplication = true,
            WorkingDirectory = projectOptions.WorkingDirectory,
            OnOutput = onOutput,
        };
 
        // Stream output lines to the process output reporter.
        // The reporter synchronizes the output of the process with the logger output,
        // so that the printed lines don't interleave.
        processSpec.OnOutput += line =>
        {
            context.ProcessOutputReporter.ReportOutput(context.ProcessOutputReporter.PrefixProcessOutput ? line with { Content = $"[{projectDisplayName}] {line.Content}" } : line);
        };
 
        var environmentBuilder = new Dictionary<string, string>();
 
        // initialize with project settings:
        foreach (var (name, value) in projectOptions.LaunchEnvironmentVariables)
        {
            environmentBuilder[name] = value;
        }
 
        // override any project settings:
        environmentBuilder[EnvironmentVariables.Names.DotnetWatch] = "1";
        environmentBuilder[EnvironmentVariables.Names.DotnetWatchIteration] = (Iteration + 1).ToString(CultureInfo.InvariantCulture);
 
        if (Logger.IsEnabled(LogLevel.Debug))
        {
            environmentBuilder[EnvironmentVariables.Names.HotReloadDeltaClientLogMessages] =
                (EnvironmentOptions.SuppressEmojis ? Emoji.Default : Emoji.Agent).GetLogMessagePrefix() + $"[{projectDisplayName}]";
        }
 
        clients.ConfigureLaunchEnvironment(environmentBuilder);
 
        processSpec.Arguments = GetProcessArguments(projectOptions, environmentBuilder);
 
        // Attach trigger to the process that detects when the web server reports to the output that it's listening.
        // Launches browser on the URL found in the process output for root projects.
        context.BrowserLauncher.InstallBrowserLaunchTrigger(processSpec, projectNode, projectOptions, clients.BrowserRefreshServer, cancellationToken);
 
        return await compilationHandler.TrackRunningProjectAsync(
            projectNode,
            projectOptions,
            clients,
            processSpec,
            restartOperation,
            processTerminationSource,
            cancellationToken);
    }
 
    private static IReadOnlyList<string> GetProcessArguments(ProjectOptions projectOptions, IDictionary<string, string> environmentBuilder)
    {
        var arguments = new List<string>()
        {
            projectOptions.Command,
            "--no-build"
        };
 
        foreach (var (name, value) in environmentBuilder)
        {
            arguments.Add("-e");
            arguments.Add($"{name}={value}");
        }
 
        arguments.AddRange(projectOptions.CommandArguments);
        return arguments;
    }
 
    public ValueTask<int> TerminateProcessAsync(RunningProject project, CancellationToken cancellationToken)
        => compilationHandler.TerminateNonRootProcessAsync(project, cancellationToken);
}