|
// 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.CodeAnalysis;
using System.Globalization;
using System.Runtime.Loader;
using Microsoft.Build.Locator;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watch
{
internal sealed class Program(
IConsole console,
ILoggerFactory loggerFactory,
ILogger logger,
IProcessOutputReporter processOutputReporter,
ProjectOptions rootProjectOptions,
CommandLineOptions options,
EnvironmentOptions environmentOptions)
{
public const string LogComponentName = nameof(Program);
public static async Task<int> Main(string[] args)
{
try
{
var sdkRootDirectory = EnvironmentVariables.SdkRootDirectory;
// We can register the MSBuild that is bundled with the SDK to perform MSBuild things.
// In production deployment dotnet-watch is in a nested folder of the SDK's root, we'll back up to it.
// AppContext.BaseDirectory = $sdkRoot\$sdkVersion\DotnetTools\dotnet-watch\$version\tools\net6.0\any\
// MSBuild.dll is located at $sdkRoot\$sdkVersion\MSBuild.dll
if (string.IsNullOrEmpty(sdkRootDirectory))
{
sdkRootDirectory = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..");
}
MSBuildLocator.RegisterMSBuildPath(sdkRootDirectory);
// Register listeners that load Roslyn-related assemblies from the `Roslyn/bincore` directory.
RegisterAssemblyResolutionEvents(sdkRootDirectory);
var environmentOptions = EnvironmentOptions.FromEnvironment();
var program = TryCreate(
args,
new PhysicalConsole(environmentOptions.TestFlags),
environmentOptions,
EnvironmentVariables.VerboseCliOutput,
out var exitCode);
if (program == null)
{
return exitCode;
}
return await program.RunAsync();
}
catch (Exception ex)
{
Console.Error.WriteLine("Unexpected error:");
Console.Error.WriteLine(ex.ToString());
return 1;
}
}
private static Program? TryCreate(IReadOnlyList<string> args, IConsole console, EnvironmentOptions environmentOptions, bool verbose, out int errorCode)
{
var parsingLoggerFactory = new LoggerFactory(new ConsoleReporter(console, verbose, quiet: false, environmentOptions.SuppressEmojis));
var options = CommandLineOptions.Parse(args, parsingLoggerFactory.CreateLogger(LogComponentName), console.Out, out errorCode);
if (options == null)
{
// an error reported or help printed:
return null;
}
var reporter = new ConsoleReporter(console, verbose || options.GlobalOptions.Verbose, options.GlobalOptions.Quiet, environmentOptions.SuppressEmojis);
var loggerFactory = new LoggerFactory(reporter);
return TryCreate(options, console, environmentOptions, loggerFactory, reporter, out errorCode);
}
// internal for testing
internal static Program? TryCreate(CommandLineOptions options, IConsole console, EnvironmentOptions environmentOptions, LoggerFactory loggerFactory, IProcessOutputReporter processOutputReporter, out int errorCode)
{
var logger = loggerFactory.CreateLogger(LogComponentName);
var workingDirectory = environmentOptions.WorkingDirectory;
logger.LogDebug("Working directory: '{Directory}'", workingDirectory);
if (environmentOptions.TestFlags != TestFlags.None)
{
logger.LogDebug("Test flags: {Flags}", environmentOptions.TestFlags);
}
if (!TryFindProject(workingDirectory, options, logger, out var projectPath))
{
errorCode = 1;
return null;
}
var rootProjectOptions = options.GetProjectOptions(projectPath, workingDirectory);
errorCode = 0;
return new Program(console, loggerFactory, logger, processOutputReporter, rootProjectOptions, options, environmentOptions);
}
/// <summary>
/// Finds a compatible MSBuild project.
/// <param name="searchBase">The base directory to search</param>
/// <param name="project">The filename of the project. Can be null.</param>
/// </summary>
private static bool TryFindProject(string searchBase, CommandLineOptions options, ILogger logger, [NotNullWhen(true)] out string? projectPath)
{
projectPath = options.ProjectPath ?? searchBase;
if (!Path.IsPathRooted(projectPath))
{
projectPath = Path.Combine(searchBase, projectPath);
}
if (Directory.Exists(projectPath))
{
var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly)
.Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
.ToList();
if (projects.Count > 1)
{
logger.LogError(Resources.Error_MultipleProjectsFound, projectPath);
return false;
}
if (projects.Count == 0)
{
logger.LogError(Resources.Error_NoProjectsFound, projectPath);
return false;
}
projectPath = projects[0];
return true;
}
if (!File.Exists(projectPath))
{
logger.LogError(Resources.Error_ProjectPath_NotFound, projectPath);
return false;
}
return true;
}
// internal for testing
internal async Task<int> RunAsync()
{
var shutdownCancellationSourceDisposed = false;
var shutdownCancellationSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationSource.Token;
var isHotReloadEnabled = IsHotReloadEnabled();
var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled));
console.KeyPressed += key =>
{
if (!shutdownCancellationSourceDisposed && key.Modifiers.HasFlag(ConsoleModifiers.Control) && key.Key == ConsoleKey.C)
{
// if we already canceled, we force immediate shutdown:
var forceShutdown = shutdownCancellationSource.IsCancellationRequested;
if (!forceShutdown)
{
logger.Log(MessageDescriptor.ShutdownRequested);
shutdownCancellationSource.Cancel();
}
else
{
Environment.Exit(0);
}
}
};
try
{
if (shutdownCancellationToken.IsCancellationRequested)
{
return 1;
}
if (options.List)
{
return await ListFilesAsync(processRunner, shutdownCancellationToken);
}
if (environmentOptions.IsPollingEnabled)
{
logger.LogInformation("Polling file watcher is enabled");
}
using var context = CreateContext(processRunner);
if (isHotReloadEnabled)
{
var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null);
await watcher.WatchAsync(shutdownCancellationToken);
}
else
{
await DotNetWatcher.WatchAsync(context, shutdownCancellationToken);
}
return 0;
}
catch (OperationCanceledException) when (shutdownCancellationToken.IsCancellationRequested)
{
// Ctrl+C forced an exit
return 0;
}
catch (Exception e)
{
logger.LogError("An unexpected error occurred: {Exception}", e.ToString());
return 1;
}
finally
{
shutdownCancellationSourceDisposed = true;
shutdownCancellationSource.Dispose();
}
}
// internal for testing
internal DotNetWatchContext CreateContext(ProcessRunner processRunner)
{
var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName);
return new()
{
ProcessOutputReporter = processOutputReporter,
LoggerFactory = loggerFactory,
Logger = logger,
BuildLogger = loggerFactory.CreateLogger(DotNetWatchContext.BuildLogComponentName),
ProcessRunner = processRunner,
Options = options.GlobalOptions,
EnvironmentOptions = environmentOptions,
RootProjectOptions = rootProjectOptions,
BrowserRefreshServerFactory = new BrowserRefreshServerFactory(),
BrowserLauncher = new BrowserLauncher(logger, environmentOptions),
};
}
private bool IsHotReloadEnabled()
{
if (rootProjectOptions.Command != "run")
{
logger.LogDebug("Command '{Command}' does not support Hot Reload.", rootProjectOptions.Command);
return false;
}
if (options.GlobalOptions.NoHotReload)
{
logger.LogDebug("Hot Reload disabled by command line switch.");
return false;
}
logger.Log(MessageDescriptor.WatchingWithHotReload);
return true;
}
private async Task<int> ListFilesAsync(ProcessRunner processRunner, CancellationToken cancellationToken)
{
var buildLogger = loggerFactory.CreateLogger(DotNetWatchContext.BuildLogComponentName);
var fileSetFactory = new MSBuildFileSetFactory(
rootProjectOptions.ProjectPath,
rootProjectOptions.BuildArguments,
processRunner,
new BuildReporter(buildLogger, options.GlobalOptions, environmentOptions));
if (await fileSetFactory.TryCreateAsync(requireProjectGraph: null, cancellationToken) is not { } evaluationResult)
{
return 1;
}
foreach (var (filePath, _) in evaluationResult.Files.OrderBy(e => e.Key))
{
console.Out.WriteLine(filePath);
}
return 0;
}
private static void RegisterAssemblyResolutionEvents(string sdkRootDirectory)
{
var roslynPath = Path.Combine(sdkRootDirectory, "Roslyn", "bincore");
AssemblyLoadContext.Default.Resolving += (context, assembly) =>
{
if (assembly.Name is "Microsoft.CodeAnalysis" or "Microsoft.CodeAnalysis.CSharp")
{
var loadedAssembly = context.LoadFromAssemblyPath(Path.Combine(roslynPath, assembly.Name + ".dll"));
// Avoid scenarios where the assembly in rosylnPath is older than what we expect
if (loadedAssembly.GetName().Version < assembly.Version)
{
throw new Exception($"Found a version of {assembly.Name} that was lower than the target version of {assembly.Version}");
}
return loadedAssembly;
}
return null;
};
}
}
}
|