Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent fixes #45369

Draft
wants to merge 7 commits into
base: release/9.0.2xx
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
<PackageVersion Include="runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
<PackageVersion Include="runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
<PackageVersion Include="StyleCop.Analyzers" Version="$(StyleCopAnalyzersPackageVersion)" />
<PackageVersion Include="System.Buffers" Version="$(SystemBuffersVersion)" />
<PackageVersion Include="System.Memory" Version="$(SystemMemoryVersion)" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
<PackageVersion Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
<PackageVersion Include="System.CommandLine.Rendering" Version="$(SystemCommandLineRenderingVersion)" />
Expand Down
14 changes: 14 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,20 @@
<MicrosoftSourceLinkGitLabVersion>9.0.0-beta.24617.1</MicrosoftSourceLinkGitLabVersion>
<MicrosoftSourceLinkBitbucketGitVersion>9.0.0-beta.24617.1</MicrosoftSourceLinkBitbucketGitVersion>
</PropertyGroup>
<!--
Dependencies to support netstandard2.0 targets.
Versions need to be conditionally selected: due to https://github.com/dotnet/sdk/issues/45155
-->
<PropertyGroup Condition="'$(DotNetBuildSourceOnly)' == 'true'">
<SystemBuffersVersion>4.6.0</SystemBuffersVersion>
<SystemMemoryVersion>4.6.0</SystemMemoryVersion>
<SystemThreadingTasksExtensionsVersion>4.6.0</SystemThreadingTasksExtensionsVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(DotNetBuildSourceOnly)' != 'true'">
<SystemBuffersVersion>4.5.1</SystemBuffersVersion>
<SystemMemoryVersion>4.5.5</SystemMemoryVersion>
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>
</PropertyGroup>
<!-- Get .NET Framework reference assemblies from NuGet packages -->
<PropertyGroup>
<UsingToolNetFrameworkReferenceAssemblies>true</UsingToolNetFrameworkReferenceAssemblies>
Expand Down
13 changes: 13 additions & 0 deletions sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,10 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.DotNet.HotReload.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.HotReload.Agent.Package", "src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.Package.csproj", "{2FF79F82-60C1-349A-4726-7783D5A6D5DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.HotReload.Agent.PipeRpc.Package", "src\BuiltInTools\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.Package.csproj", "{692B71D8-9C31-D1EE-6C1B-570A12B18E39}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.DotNet.HotReload.Agent.PipeRpc", "src\BuiltInTools\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.shproj", "{FA3C7F91-42A2-45AD-897C-F646B081016C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -982,6 +986,10 @@ Global
{2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FF79F82-60C1-349A-4726-7783D5A6D5DF}.Release|Any CPU.Build.0 = Release|Any CPU
{692B71D8-9C31-D1EE-6C1B-570A12B18E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{692B71D8-9C31-D1EE-6C1B-570A12B18E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{692B71D8-9C31-D1EE-6C1B-570A12B18E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{692B71D8-9C31-D1EE-6C1B-570A12B18E39}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1163,19 +1171,24 @@ Global
{1F0B4B3C-DC88-4740-B04F-1707102E9930} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
{418B10BD-CA42-49F3-8F4A-D8CC90C8A17D} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{2FF79F82-60C1-349A-4726-7783D5A6D5DF} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{692B71D8-9C31-D1EE-6C1B-570A12B18E39} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{FA3C7F91-42A2-45AD-897C-F646B081016C} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{03c5a84a-982b-4f38-ac73-ab832c645c4a}*SharedItemsImports = 5
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{0a3c9afd-f6e6-4a5d-83fb-93bf66732696}*SharedItemsImports = 5
src\BuiltInTools\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.projitems*{1bbfa19c-03f0-4d27-9d0d-0f8172642107}*SharedItemsImports = 5
src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.projitems*{1bbfa19c-03f0-4d27-9d0d-0f8172642107}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{1f0b4b3c-dc88-4740-b04f-1707102e9930}*SharedItemsImports = 5
src\BuiltInTools\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.projitems*{418b10bd-ca42-49f3-8f4a-d8cc90c8a17d}*SharedItemsImports = 13
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*SharedItemsImports = 5
src\BuiltInTools\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.projitems*{445efbd5-6730-4f09-943d-278e77501ffd}*SharedItemsImports = 5
src\BuiltInTools\AspireService\Microsoft.WebTools.AspireService.projitems*{94c8526e-dcc2-442f-9868-3dd0ba2688be}*SharedItemsImports = 13
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{9d36039f-d0a1-462f-85b4-81763c6b02cb}*SharedItemsImports = 13
src\Compatibility\ApiCompat\Microsoft.DotNet.ApiCompat.Shared\Microsoft.DotNet.ApiCompat.Shared.projitems*{a9103b98-d888-4260-8a05-fa36f640698a}*SharedItemsImports = 5
src\BuiltInTools\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.projitems*{fa3c7f91-42a2-45ad-897c-f646b081016c}*SharedItemsImports = 13
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\HotReloadAgent\Microsoft.DotNet.HotReload.Agent.projitems" Label="Shared" />
<Import Project="..\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.projitems" Label="Shared" />
<PropertyGroup>
<!--
dotnet-watch may inject this assembly to .NET 6.0+ app, so we can't target a newer version.
Expand All @@ -13,12 +14,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="..\dotnet-watch\EnvironmentVariables_StartupHook.cs" Link="EnvironmentVariables_StartupHook.cs" />
<Compile Include="..\dotnet-watch\HotReload\NamedPipeContract.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Extensions.DotNetDeltaApplier.Tests"/>
<InternalsVisibleTo Include="Microsoft.Extensions.DotNetDeltaApplier.Tests" />
</ItemGroup>

</Project>
199 changes: 133 additions & 66 deletions src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Pipes;
using Microsoft.DotNet.Watch;
using Microsoft.DotNet.HotReload;
using System.Diagnostics;

/// <summary>
/// The runtime startup hook looks for top-level type named "StartupHook".
/// </summary>
internal sealed class StartupHook
{
private static readonly bool s_logToStandardOutput = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages) == "1";
private static readonly string s_namedPipeName = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName);
private static readonly string s_targetProcessPath = Environment.GetEnvironmentVariable(EnvironmentVariables.Names.DotnetWatchHotReloadTargetProcessPath);
private const int ConnectionTimeoutMS = 5000;

private static readonly bool s_logToStandardOutput = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.HotReloadDeltaClientLogMessages) == "1";
private static readonly string s_namedPipeName = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName);
private static readonly string s_targetProcessPath = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetWatchHotReloadTargetProcessPath);

/// <summary>
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
Expand All @@ -25,63 +27,154 @@ public static void Initialize()
// When launching the application process dotnet-watch sets Hot Reload environment variables via CLI environment directives (dotnet [env:X=Y] run).
// Currently, the CLI parser sets the env variables to the dotnet.exe process itself, rather then to the target process.
// This may cause the dotnet.exe process to connect to the named pipe and break it for the target process.
if (!IsMatchingProcess(processPath, s_targetProcessPath))
//
// Only needed when the agent is injected to the process by dotnet-watch, by the IDE.
if (s_targetProcessPath != null && !IsMatchingProcess(processPath, s_targetProcessPath))
{
Log($"Ignoring process '{processPath}', expecting '{s_targetProcessPath}'");
return;
}

Log($"Loaded into process: {processPath}");
Log($"Process: '{processPath}'");

HotReloadAgent.ClearHotReloadEnvironmentVariables(typeof(StartupHook));

Log($"Connecting to hot-reload server");

// Connect to the pipe synchronously.
//
// If a debugger is attached and there is a breakpoint in the startup code connecting asynchronously would
// set up a race between this code connecting to the server, and the breakpoint being hit. If the breakpoint
// hits first, applying changes will throw an error that the client is not connected.
//
// Updates made before the process is launched need to be applied before loading the affected modules.

var pipeClient = new NamedPipeClientStream(".", s_namedPipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
try
{
pipeClient.Connect(ConnectionTimeoutMS);
Log("Connected.");
}
catch (TimeoutException)
{
Log($"Failed to connect in {ConnectionTimeoutMS}ms.");
return;
}

ClearHotReloadEnvironmentVariables();
using var agent = new HotReloadAgent();
try
{
// block until initialization completes:
InitializeAsync(pipeClient, agent, CancellationToken.None).GetAwaiter().GetResult();

_ = Task.Run(async () =>
// fire and forget:
_ = ReceiveAndApplyUpdatesAsync(pipeClient, agent, initialUpdates: false, CancellationToken.None);
}
catch (Exception ex)
{
Log($"Connecting to hot-reload server");
Log(ex.Message);
pipeClient.Dispose();
}
}

const int TimeOutMS = 5000;

using var pipeClient = new NamedPipeClientStream(".", s_namedPipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);
try
private static async ValueTask InitializeAsync(NamedPipeClientStream pipeClient, HotReloadAgent agent, CancellationToken cancellationToken)
{
agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);

var initPayload = new ClientInitializationResponse(agent.Capabilities);
await initPayload.WriteAsync(pipeClient, cancellationToken);

// Apply updates made before this process was launched to avoid executing unupdated versions of the affected modules.
await ReceiveAndApplyUpdatesAsync(pipeClient, agent, initialUpdates: true, cancellationToken);
}

private static async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipeClient, HotReloadAgent agent, bool initialUpdates, CancellationToken cancellationToken)
{
try
{
while (pipeClient.IsConnected)
{
await pipeClient.ConnectAsync(TimeOutMS);
Log("Connected.");
var payloadType = (RequestType)await pipeClient.ReadByteAsync(cancellationToken);
switch (payloadType)
{
case RequestType.ManagedCodeUpdate:
// Shouldn't get initial managed code updates when the debugger is attached.
// The debugger itself applies these updates when launching process with the debugger attached.
Debug.Assert(!Debugger.IsAttached);
await ReadAndApplyManagedCodeUpdateAsync(pipeClient, agent, cancellationToken);
break;

case RequestType.StaticAssetUpdate:
await ReadAndApplyStaticAssetUpdateAsync(pipeClient, agent, cancellationToken);
break;

case RequestType.InitialUpdatesCompleted when initialUpdates:
return;

default:
// can't continue, the pipe content is in an unknown state
Log($"Unexpected payload type: {payloadType}. Terminating agent.");
return;
}
}
catch (TimeoutException)
}
catch (Exception ex)
{
Log(ex.Message);
}
finally
{
if (!pipeClient.IsConnected)
{
Log($"Failed to connect in {TimeOutMS}ms.");
return;
await pipeClient.DisposeAsync();
}
}
}

using var agent = new HotReloadAgent();
try
{
agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);
private static async ValueTask ReadAndApplyManagedCodeUpdateAsync(
NamedPipeClientStream pipeClient,
HotReloadAgent agent,
CancellationToken cancellationToken)
{
var request = await ManagedCodeUpdateRequest.ReadAsync(pipeClient, cancellationToken);

var initPayload = new ClientInitializationPayload(agent.Capabilities);
await initPayload.WriteAsync(pipeClient, CancellationToken.None);
bool success;
try
{
agent.ApplyDeltas(request.Deltas);
success = true;
}
catch (Exception e)
{
agent.Reporter.Report($"The runtime failed to applying the change: {e.Message}", AgentMessageSeverity.Error);
agent.Reporter.Report("Further changes won't be applied to this process.", AgentMessageSeverity.Warning);
success = false;
}

while (pipeClient.IsConnected)
{
var update = await UpdatePayload.ReadAsync(pipeClient, CancellationToken.None);
var logEntries = agent.GetAndClearLogEntries(request.ResponseLoggingLevel);

Log($"ResponseLoggingLevel = {update.ResponseLoggingLevel}");
var response = new UpdateResponse(logEntries, success);
await response.WriteAsync(pipeClient, cancellationToken);
}

agent.ApplyDeltas(update.Deltas);
var logEntries = agent.GetAndClearLogEntries(update.ResponseLoggingLevel);
private static async ValueTask ReadAndApplyStaticAssetUpdateAsync(
NamedPipeClientStream pipeClient,
HotReloadAgent agent,
CancellationToken cancellationToken)
{
var request = await StaticAssetUpdateRequest.ReadAsync(pipeClient, cancellationToken);

// response:
await pipeClient.WriteAsync((byte)UpdatePayload.ApplySuccessValue, CancellationToken.None);
await UpdatePayload.WriteLogAsync(pipeClient, logEntries, CancellationToken.None);
}
}
catch (Exception ex)
{
Log(ex.Message);
}
agent.ApplyStaticAssetUpdate(new StaticAssetUpdate(request.AssemblyName, request.RelativePath, request.Contents, request.IsApplicationProject));

Log("Stopped received delta updates. Server is no longer connected.");
});
var logEntries = agent.GetAndClearLogEntries(request.ResponseLoggingLevel);

// Updating static asset only invokes ContentUpdate metadata update handlers.
// Failures of these handlers are reported to the log and ignored.
// Therefore, this request always succeeds.
var response = new UpdateResponse(logEntries, success: true);

await response.WriteAsync(pipeClient, cancellationToken);
}

public static bool IsMatchingProcess(string processPath, string targetProcessPath)
Expand All @@ -102,32 +195,6 @@ public static bool IsMatchingProcess(string processPath, string targetProcessPat
string.Equals(processPath[..^4], targetProcessPath[..^4], comparison);
}

internal static void ClearHotReloadEnvironmentVariables()
{
// Clear any hot-reload specific environment variables. This prevents child processes from being
// affected by the current app's hot reload settings. See https://github.com/dotnet/runtime/issues/58000

Environment.SetEnvironmentVariable(EnvironmentVariables.Names.DotnetStartupHooks,
RemoveCurrentAssembly(Environment.GetEnvironmentVariable(EnvironmentVariables.Names.DotnetStartupHooks)));

Environment.SetEnvironmentVariable(EnvironmentVariables.Names.DotnetWatchHotReloadNamedPipeName, "");
Environment.SetEnvironmentVariable(EnvironmentVariables.Names.HotReloadDeltaClientLogMessages, "");
}

internal static string RemoveCurrentAssembly(string environment)
{
if (environment is "")
{
return environment;
}

var assemblyLocation = typeof(StartupHook).Assembly.Location;
var updatedValues = environment.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Where(e => !string.Equals(e, assemblyLocation, StringComparison.OrdinalIgnoreCase));

return string.Join(Path.PathSeparator, updatedValues);
}

private static void Log(string message)
{
if (s_logToStandardOutput)
Expand Down
Loading
Loading