Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.ExternalAccess.HotReload" Version="$(MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion)" />

<!-- roslyn-sdk dependencies-->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
Expand Down
2 changes: 2 additions & 0 deletions sdk.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
<Project Path="src/BuiltInTools/HotReloadAgent/Microsoft.DotNet.HotReload.Agent.shproj" />
<Project Path="src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.Package.csproj" />
<Project Path="src/BuiltInTools/HotReloadClient/Microsoft.DotNet.HotReload.Client.shproj" Id="a78ff92a-d715-4249-9e3d-40d9997a098f" />
<Project Path="src/BuiltInTools/Watch.Aspire/Microsoft.DotNet.HotReload.Watch.Aspire.csproj" />
<Project Path="src/BuiltInTools/Watch/Microsoft.DotNet.HotReload.Watch.csproj" />
</Folder>
<Folder Name="/src/Cli/">
<Project Path="src/Cli/dotnet/dotnet.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<FrameworkReference Update="Microsoft.NETCore.App" TargetingPackVersion="6.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\dotnet-watch\Utilities\ProcessUtilities.cs" Link="ProcessUtilities.cs" />
<Compile Include="..\Watch\Utilities\ProcessUtilities.cs" Link="ProcessUtilities.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
94 changes: 94 additions & 0 deletions src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.Extensions.Logging;

namespace Microsoft.DotNet.Watch;

internal sealed class DotNetWatchOptions
{
public bool IsVerbose { get; init; }
public bool IsQuiet { get; init; }
public bool NoLaunchProfile { get; init; }
public ImmutableArray<string> ApplicationArguments { get; init; }
}

internal static class DotNetWatchLauncher
{
public static async Task<bool> RunAsync(
string projectPath,
string workingDirectory,
DotNetWatchOptions options)
{
var globalOptions = new GlobalOptions()
{
Quiet = options.IsQuiet,
Verbose = options.IsVerbose,
NoHotReload = false,
NonInteractive = true,
};

var commandArguments = new List<string>();
if (options.NoLaunchProfile)
{
commandArguments.Add("--no-launch-profile");
}

commandArguments.AddRange(options.ApplicationArguments);

var rootProjectOptions = new ProjectOptions()
{
IsRootProject = true,
ProjectPath = projectPath,
WorkingDirectory = workingDirectory,
TargetFramework = null,
BuildArguments = [],
NoLaunchProfile = options.NoLaunchProfile,
LaunchProfileName = null,
Command = "run",
CommandArguments = [.. commandArguments],
LaunchEnvironmentVariables = [],
};

var console = new PhysicalConsole(TestFlags.None);
var reporter = new ConsoleReporter(console, globalOptions.Verbose, globalOptions.Quiet, suppressEmojis: false);
var environmentOptions = EnvironmentOptions.FromEnvironment();
var processRunner = new ProcessRunner(environmentOptions.GetProcessCleanupTimeout(isHotReloadEnabled: true));
var loggerFactory = new LoggerFactory(reporter);
var logger = loggerFactory.CreateLogger(DotNetWatchContext.DefaultLogComponentName);

using var context = new DotNetWatchContext()
{
ProcessOutputReporter = reporter,
LoggerFactory = loggerFactory,
Logger = logger,
BuildLogger = loggerFactory.CreateLogger(DotNetWatchContext.BuildLogComponentName),
ProcessRunner = processRunner,
Options = globalOptions,
EnvironmentOptions = environmentOptions,
RootProjectOptions = rootProjectOptions,
BrowserRefreshServerFactory = new BrowserRefreshServerFactory(),
BrowserLauncher = new BrowserLauncher(logger, environmentOptions),
};

using var shutdownHandler = new ShutdownHandler(console, logger);

try
{
var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null);
await watcher.WatchAsync(shutdownHandler.CancellationToken);
}
catch (OperationCanceledException) when (shutdownHandler.CancellationToken.IsCancellationRequested)
{
// Ctrl+C forced an exit
}
catch (Exception e)
{
logger.LogError("An unexpected error occurred: {Exception}", e.ToString());
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(SdkTargetFramework)</TargetFramework>
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>
<RootNamespace>Microsoft.DotNet.Watch</RootNamespace>

<!-- NuGet -->
<IsPackable>true</IsPackable>
<PackageId>Microsoft.DotNet.HotReload.Watch.Aspire</PackageId>
<PackageDescription>
A supporting package for Aspire CLI:
https://github.com/dotnet/aspire
</PackageDescription>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Watch\Microsoft.DotNet.HotReload.Watch.csproj" />
</ItemGroup>

<ItemGroup>
<PublicAPI Include="PublicAPI.Shipped.txt" />
<PublicAPI Include="PublicAPI.Unshipped.txt" />
<PublicAPI Include="InternalAPI.Shipped.txt" />
<PublicAPI Include="InternalAPI.Unshipped.txt" />
</ItemGroup>

</Project>

7 changes: 7 additions & 0 deletions src/BuiltInTools/Watch.Aspire/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

// ⚠ ONLY ASSEMBLIES BUILT FROM dotnet/aspire MAY BE ADDED HERE ⚠
[assembly: InternalsVisibleTo("Aspire.Cli, PublicKey = 00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")]
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal abstract class WebApplicationAppModel(DotNetWatchContext context) : Hot
protected WebAssemblyHotReloadClient CreateWebAssemblyClient(ILogger clientLogger, ILogger agentLogger, BrowserRefreshServer browserRefreshServer, ProjectGraphNode clientProject)
{
var capabilities = clientProject.GetWebAssemblyCapabilities().ToImmutableArray();
var targetFramework = clientProject.GetTargetFrameworkVersion() ?? throw new InvalidOperationException("Project doesn't define TargetFrameworkVersion");
var targetFramework = clientProject.GetTargetFrameworkVersion() ?? throw new InvalidOperationException($"Project doesn't define {PropertyNames.TargetFrameworkMoniker}");

return new WebAssemblyHotReloadClient(clientLogger, agentLogger, browserRefreshServer, capabilities, targetFramework, context.EnvironmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private bool CanLaunchBrowser(ProjectOptions projectOptions, [NotNullWhen(true)]
return false;
}

if (!CommandLineOptions.IsCodeExecutionCommand(projectOptions.Command))
if (!projectOptions.IsCodeExecutionCommand)
{
logger.LogDebug("Command '{Command}' does not support launching browsers.", projectOptions.Command);
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ internal static class PropertyNames
{
public const string TargetFramework = nameof(TargetFramework);
public const string TargetFrameworkIdentifier = nameof(TargetFrameworkIdentifier);
public const string TargetFrameworkMoniker = nameof(TargetFrameworkMoniker);
public const string TargetPath = nameof(TargetPath);
public const string EnableDefaultItems = nameof(EnableDefaultItems);
public const string TargetFrameworks = nameof(TargetFrameworks);
Expand Down
19 changes: 19 additions & 0 deletions src/BuiltInTools/Watch/Build/BuildUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Watch;

internal static class BuildUtilities
{
// Parses name=value pairs passed to --property. Skips invalid input.
public static IEnumerable<(string key, string value)> ParseBuildProperties(IEnumerable<string> arguments)
=> from argument in arguments
let colon = argument.IndexOf(':')
where colon >= 0 && argument[0..colon] is "--property" or "-property" or "/property" or "/p" or "-p" or "--p"
let eq = argument.IndexOf('=', colon)
where eq >= 0
let name = argument[(colon + 1)..eq].Trim()
let value = argument[(eq + 1)..]
where name is not []
select (name, value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void WatchFiles(FileWatcher fileWatcher)

// See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md

var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
var globalOptions = BuildUtilities.ParseBuildProperties(buildArguments)
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
.SetItem(PropertyNames.DotNetWatchBuild, "true")
.SetItem(PropertyNames.DesignTimeBuild, "true")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Runtime.Versioning;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Cli;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

Expand Down Expand Up @@ -83,7 +83,16 @@ public static IEnumerable<string> GetTargetFrameworks(this ProjectGraphNode proj
=> projectNode.GetStringListPropertyValue(PropertyNames.TargetFrameworks);

public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
=> EnvironmentVariableNames.TryParseTargetFrameworkVersion(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkVersion));
{
try
{
return new FrameworkName(projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)).Version;
}
catch
{
return null;
}
}

public static IEnumerable<string> GetWebAssemblyCapabilities(this ProjectGraphNode projectNode)
=> projectNode.GetStringListPropertyValue(PropertyNames.WebAssemblyHotReloadCapabilities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ internal sealed record ProjectOptions
/// Additional environment variables to set to the running process.
/// </summary>
public required IReadOnlyList<(string name, string value)> LaunchEnvironmentVariables { get; init; }

/// <summary>
/// Returns true if the command executes the code of the target project.
/// </summary>
public bool IsCodeExecutionCommand
=> Command is "run" or "test";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Microsoft.Build.Graph;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
using Microsoft.DotNet.HotReload;
using Microsoft.Extensions.Logging;

Expand All @@ -17,7 +17,7 @@ internal sealed class CompilationHandler : IDisposable
public readonly IncrementalMSBuildWorkspace Workspace;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private readonly WatchHotReloadService _hotReloadService;
private readonly HotReloadService _hotReloadService;
private readonly ProcessRunner _processRunner;

/// <summary>
Expand All @@ -36,7 +36,7 @@ internal sealed class CompilationHandler : IDisposable
/// <summary>
/// All updates that were attempted. Includes updates whose application failed.
/// </summary>
private ImmutableList<WatchHotReloadService.Update> _previousUpdates = [];
private ImmutableList<HotReloadService.Update> _previousUpdates = [];

private bool _isDisposed;

Expand All @@ -46,7 +46,7 @@ public CompilationHandler(ILoggerFactory loggerFactory, ILogger logger, ProcessR
_logger = logger;
_processRunner = processRunner;
Workspace = new IncrementalMSBuildWorkspace(logger);
_hotReloadService = new WatchHotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
_hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
}

public void Dispose()
Expand Down Expand Up @@ -226,7 +226,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
}

public async ValueTask<(
ImmutableArray<WatchHotReloadService.Update> projectUpdates,
ImmutableArray<HotReloadService.Update> projectUpdates,
ImmutableArray<string> projectsToRebuild,
ImmutableArray<string> projectsToRedeploy,
ImmutableArray<RunningProject> terminatedProjects)> HandleManagedCodeChangesAsync(
Expand All @@ -242,14 +242,14 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
let runningProject = GetCorrespondingRunningProject(project, runningProjects)
where runningProject != null
let autoRestartProject = autoRestart || runningProject.ProjectNode.IsAutoRestartEnabled()
select (project.Id, info: new WatchHotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = autoRestartProject }))
select (project.Id, info: new HotReloadService.RunningProjectInfo() { RestartWhenChangesHaveNoEffect = autoRestartProject }))
.ToImmutableDictionary(e => e.Id, e => e.info);

var updates = await _hotReloadService.GetUpdatesAsync(currentSolution, runningProjectInfos, cancellationToken);

await DisplayResultsAsync(updates, runningProjectInfos, cancellationToken);

if (updates.Status is WatchHotReloadService.Status.NoChangesToApply or WatchHotReloadService.Status.Blocked)
if (updates.Status is HotReloadService.Status.NoChangesToApply or HotReloadService.Status.Blocked)
{
// If Hot Reload is blocked (due to compilation error) we ignore the current
// changes and await the next file change.
Expand Down Expand Up @@ -291,7 +291,7 @@ private static void PrepareCompilations(Solution solution, string projectPath, C
return (updates.ProjectUpdates, projectsToRebuild, projectsToRedeploy, terminatedProjects);
}

public async ValueTask ApplyUpdatesAsync(ImmutableArray<WatchHotReloadService.Update> updates, CancellationToken cancellationToken)
public async ValueTask ApplyUpdatesAsync(ImmutableArray<HotReloadService.Update> updates, CancellationToken cancellationToken)
{
Debug.Assert(!updates.IsEmpty);

Expand Down Expand Up @@ -330,7 +330,7 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
}

// msbuild workspace doesn't set TFM if the project is not multi-targeted
var tfm = WatchHotReloadService.GetTargetFramework(project);
var tfm = HotReloadService.GetTargetFramework(project);
if (tfm == null)
{
return projectsWithPath[0];
Expand All @@ -339,18 +339,18 @@ await ForEachProjectAsync(projectsToUpdate, async (runningProject, cancellationT
return projectsWithPath.SingleOrDefault(p => string.Equals(p.ProjectNode.GetTargetFramework(), tfm, StringComparison.OrdinalIgnoreCase));
}

private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updates, ImmutableDictionary<ProjectId, WatchHotReloadService.RunningProjectInfo> runningProjectInfos, CancellationToken cancellationToken)
private async ValueTask DisplayResultsAsync(HotReloadService.Updates updates, ImmutableDictionary<ProjectId, HotReloadService.RunningProjectInfo> runningProjectInfos, CancellationToken cancellationToken)
{
switch (updates.Status)
{
case WatchHotReloadService.Status.ReadyToApply:
case HotReloadService.Status.ReadyToApply:
break;

case WatchHotReloadService.Status.NoChangesToApply:
case HotReloadService.Status.NoChangesToApply:
_logger.Log(MessageDescriptor.NoCSharpChangesToApply);
break;

case WatchHotReloadService.Status.Blocked:
case HotReloadService.Status.Blocked:
_logger.Log(MessageDescriptor.UnableToApplyChanges);
break;

Expand Down Expand Up @@ -378,7 +378,7 @@ await ForEachProjectAsync(

void ReportCompilationDiagnostics(DiagnosticSeverity severity)
{
foreach (var diagnostic in updates.CompilationDiagnostics)
foreach (var diagnostic in updates.PersistentDiagnostics)
{
if (diagnostic.Id == "CS8002")
{
Expand Down Expand Up @@ -419,7 +419,7 @@ void ReportRudeEdits()
.Where(p => !updates.ProjectsToRestart.ContainsKey(p))
.ToHashSet();

foreach (var (projectId, diagnostics) in updates.RudeEdits)
foreach (var (projectId, diagnostics) in updates.TransientDiagnostics)
{
foreach (var diagnostic in diagnostics)
{
Expand Down Expand Up @@ -629,7 +629,7 @@ private async ValueTask<IReadOnlyList<int>> TerminateRunningProjects(IEnumerable
private static Task ForEachProjectAsync(ImmutableDictionary<string, ImmutableArray<RunningProject>> projects, Func<RunningProject, CancellationToken, Task> action, CancellationToken cancellationToken)
=> Task.WhenAll(projects.SelectMany(entry => entry.Value).Select(project => action(project, cancellationToken))).WaitAsync(cancellationToken);

private static ImmutableArray<HotReloadManagedCodeUpdate> ToManagedCodeUpdates(ImmutableArray<WatchHotReloadService.Update> updates)
private static ImmutableArray<HotReloadManagedCodeUpdate> ToManagedCodeUpdates(ImmutableArray<HotReloadService.Update> updates)
=> [.. updates.Select(update => new HotReloadManagedCodeUpdate(update.ModuleId, update.MetadataDelta, update.ILDelta, update.PdbDelta, update.UpdatedTypes, update.RequiredCapabilities))];
}
}
Loading
Loading