Skip to content
Open
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 @@ -37,6 +37,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
1 change: 1 addition & 0 deletions eng/Version.Details.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ This file should be imported by eng/Versions.props
<MicrosoftCodeAnalysisCSharpCodeStylePackageVersion>5.0.0-2.25509.106</MicrosoftCodeAnalysisCSharpCodeStylePackageVersion>
<MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>5.0.0-2.25509.106</MicrosoftCodeAnalysisCSharpFeaturesPackageVersion>
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>5.0.0-2.25509.106</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
<MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion>5.0.0-2.25479.108</MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion>
<MicrosoftCodeAnalysisPublicApiAnalyzersPackageVersion>5.0.0-2.25509.106</MicrosoftCodeAnalysisPublicApiAnalyzersPackageVersion>
<MicrosoftCodeAnalysisRazorToolingInternalPackageVersion>10.0.0-preview.25509.106</MicrosoftCodeAnalysisRazorToolingInternalPackageVersion>
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>5.0.0-2.25509.106</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
Expand Down
4 changes: 4 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 Expand Up @@ -320,6 +322,8 @@
<Project Path="test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.HotReload.Test.Utilities/Microsoft.DotNet.HotReload.Test.Utilities.csproj" />
<Project Path="test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.PackageInstall.Tests/Microsoft.DotNet.PackageInstall.Tests.csproj" />
<Project Path="test/Microsoft.DotNet.TemplateLocator.Tests/Microsoft.DotNet.TemplateLocator.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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
84 changes: 84 additions & 0 deletions src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Logging;

namespace Microsoft.DotNet.Watch;

internal static class DotNetWatchLauncher
{
public static async Task<bool> RunAsync(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 = options.ProjectPath,
WorkingDirectory = workingDirectory,
TargetFramework = null,
BuildArguments = [],
NoLaunchProfile = options.NoLaunchProfile,
LaunchProfileName = null,
Command = "run",
CommandArguments = [.. commandArguments],
LaunchEnvironmentVariables = [],
};

var muxerPath = Path.GetFullPath(Path.Combine(options.SdkDirectory, "..", "..", "dotnet" + PathUtilities.ExecutableExtension));

var console = new PhysicalConsole(TestFlags.None);
var reporter = new ConsoleReporter(console, globalOptions.Verbose, globalOptions.Quiet, suppressEmojis: false);
var environmentOptions = EnvironmentOptions.FromEnvironment(muxerPath);
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;
}
}
82 changes: 82 additions & 0 deletions src/BuiltInTools/Watch.Aspire/DotNetWatchOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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 System.CommandLine;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.DotNet.Watch;

internal sealed class DotNetWatchOptions
{
/// <summary>
/// The .NET SDK directory to load msbuild from (e.g. C:\Program Files\dotnet\sdk\10.0.100).
/// Also used to locate `dotnet` executable.
/// </summary>
public required string SdkDirectory { get; init; }

public required string ProjectPath { get; init; }
public required ImmutableArray<string> ApplicationArguments { get; init; }
public bool IsVerbose { get; init; }
public bool IsQuiet { get; init; }
public bool NoLaunchProfile { get; init; }

public static bool TryParse(string[] args, [NotNullWhen(true)] out DotNetWatchOptions? options)
{
var sdkOption = new Option<string>("--sdk") { Arity = ArgumentArity.ExactlyOne, Required = true, AllowMultipleArgumentsPerToken = false };
var projectOption = new Option<string>("--project") { Arity = ArgumentArity.ExactlyOne, Required = true, AllowMultipleArgumentsPerToken = false };
var quietOption = new Option<bool>("--quiet") { Arity = ArgumentArity.Zero };
var verboseOption = new Option<bool>("--verbose") { Arity = ArgumentArity.Zero };
var noLaunchProfileOption = new Option<bool>("--no-launch-profile") { Arity = ArgumentArity.Zero };
var applicationArguments = new Argument<string[]>("arguments") { Arity = ArgumentArity.ZeroOrMore };

verboseOption.Validators.Add(v =>
{
if (v.GetValue(quietOption) && v.GetValue(verboseOption))
{
v.AddError("Cannot specify both '--quiet' and '--verbose' options.");
}
});

var rootCommand = new RootCommand()
{
Directives = { new EnvironmentVariablesDirective() },
Options =
{
sdkOption,
projectOption,
quietOption,
verboseOption,
noLaunchProfileOption
},
Arguments =
{
applicationArguments
}
};

var parseResult = rootCommand.Parse(args);
if (parseResult.Errors.Count > 0)
{
foreach (var error in parseResult.Errors)
{
Console.Error.WriteLine(error);
}

options = null;
return false;
}

options = new DotNetWatchOptions()
{
SdkDirectory = parseResult.GetRequiredValue(sdkOption),
ProjectPath = parseResult.GetRequiredValue(projectOption),
IsQuiet = parseResult.GetValue(quietOption),
IsVerbose = parseResult.GetValue(verboseOption),
ApplicationArguments = [.. parseResult.GetValue(applicationArguments) ?? []],
NoLaunchProfile = parseResult.GetValue(noLaunchProfileOption),
};

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

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

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

<ItemGroup>
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Build.Locator" />
</ItemGroup>

<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>

<Import Project="..\Watch\RuntimeDependencies.props" />
</Project>

12 changes: 12 additions & 0 deletions src/BuiltInTools/Watch.Aspire/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Build.Locator;
using Microsoft.DotNet.Watch;

if (!DotNetWatchOptions.TryParse(args, out var options))
{
return -1;
}

MSBuildLocator.RegisterMSBuildPath(options.SdkDirectory);

var workingDirectory = Directory.GetCurrentDirectory();
return await DotNetWatchLauncher.RunAsync(workingDirectory, options) ? 0 : 1;
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;

[assembly: InternalsVisibleTo("Microsoft.DotNet.HotReload.Watch.Aspire.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
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 @@ -108,7 +108,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 @@ -87,7 +87,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 @@ -46,10 +46,10 @@ internal sealed record EnvironmentOptions(
TestFlags TestFlags = TestFlags.None,
string TestOutput = "")
{
public static EnvironmentOptions FromEnvironment() => new
public static EnvironmentOptions FromEnvironment(string muxerPath) => new
(
WorkingDirectory: Directory.GetCurrentDirectory(),
MuxerPath: GetMuxerPathFromEnvironment(),
MuxerPath: ValidateMuxerPath(muxerPath),
ProcessCleanupTimeout: EnvironmentVariables.ProcessCleanupTimeout,
IsPollingEnabled: EnvironmentVariables.IsPollingEnabled,
SuppressHandlingStaticContentFiles: EnvironmentVariables.SuppressHandlingStaticContentFiles,
Expand All @@ -74,12 +74,10 @@ public TimeSpan GetProcessCleanupTimeout(bool isHotReloadEnabled)

public bool RunningAsTest { get => (TestFlags & TestFlags.RunningAsTest) != TestFlags.None; }

private static string GetMuxerPathFromEnvironment()
private static string ValidateMuxerPath(string path)
{
var muxerPath = Environment.ProcessPath;
Debug.Assert(muxerPath != null);
Debug.Assert(Path.GetFileNameWithoutExtension(muxerPath) == "dotnet", $"Invalid muxer path {muxerPath}");
return muxerPath;
Debug.Assert(Path.GetFileNameWithoutExtension(path) == "dotnet");
return path;
}

public string? GetBinLogPath(string projectPath, string operationName, GlobalOptions options)
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";
}
Loading
Loading