Skip to content

Unix process termination #49374

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

Draft
wants to merge 4 commits into
base: main
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
dotnet-watch may inject this assembly to .NET 6.0+ app, so we can't target a newer version.
At the same time source build requires us to not target 6.0, so we fall back to netstandard.
-->
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFrameworks>netstandard2.1;net10.0</TargetFrameworks>
<StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId>

<!-- NuGet -->
Expand Down
37 changes: 36 additions & 1 deletion src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

/// <summary>
Expand All @@ -13,7 +14,7 @@ internal sealed class StartupHook
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_namedPipeName = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName);

/// <summary>
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
Expand All @@ -28,6 +29,12 @@ public static void Initialize()

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

if (s_namedPipeName == null)
{
Log($"Environment variable {AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName} has no value");
return;
}

// Connect to the pipe synchronously.
//
// If a debugger is attached and there is a breakpoint in the startup code connecting asynchronously would
Expand All @@ -48,22 +55,50 @@ public static void Initialize()
return;
}

RegisterPosixSignalHandlers();

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

#pragma warning disable CA2025 // Ensure tasks using 'IDisposable' instances complete before the instances are disposed
// fire and forget:
_ = ReceiveAndApplyUpdatesAsync(pipeClient, agent, initialUpdates: false, CancellationToken.None);
#pragma warning restore
}
catch (Exception ex)
{
Log(ex.Message);
pipeClient.Dispose();
agent.Dispose();
}
}

private static void RegisterPosixSignalHandlers()
{
#if NET10_0_OR_GREATER
// Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
// See https://github.com/dotnet/docs/issues/46226.

// Note: registered handlers are executed in reverse order of their registration.
// Since the startup hook is executed before any code of the application, it is the first handler registered and thus the last to run.

_ = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
{
Log($"SIGTERM received. Cancel={context.Cancel}");

if (!context.Cancel)
{
Environment.Exit(0);
}
});

Log("Posix signal handlers registered.");
#endif
}

private static async ValueTask InitializeAsync(NamedPipeClientStream pipeClient, HotReloadAgent agent, CancellationToken cancellationToken)
{
agent.Reporter.Report("Writing capabilities: " + agent.Capabilities, AgentMessageSeverity.Verbose);
Expand Down
2 changes: 1 addition & 1 deletion src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
};

var browserRefreshServer = (projectRootNode != null)
? await browserConnector.GetOrCreateBrowserRefreshServerAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, DefaultAppModel.Instance, shutdownCancellationToken)
? await browserConnector.GetOrCreateBrowserRefreshServerAsync(projectRootNode, processSpec, environmentBuilder, Context.RootProjectOptions, new DefaultAppModel(projectRootNode), shutdownCancellationToken)
: null;

environmentBuilder.SetProcessEnvironmentVariables(processSpec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,23 @@

namespace Microsoft.DotNet.Watch;

internal abstract partial class HotReloadAppModel
/// <summary>
/// Blazor client-only WebAssembly app.
/// </summary>
internal sealed class BlazorWebAssemblyAppModel(ProjectGraphNode clientProject)
// Blazor WASM does not need agent injected as all changes are applied in the browser, the process being launched is a dev server.
: HotReloadAppModel(agentInjectionProject: null)
{
/// <summary>
/// Blazor client-only WebAssembly app.
/// </summary>
internal sealed class BlazorWebAssemblyAppModel(ProjectGraphNode clientProject) : HotReloadAppModel
{
public override bool RequiresBrowserRefresh => true;

/// <summary>
/// Blazor WASM does not need dotnet applier as all changes are applied in the browser,
/// the process being launched is a dev server.
/// </summary>
public override bool InjectDeltaApplier => false;
public override bool RequiresBrowserRefresh => true;

public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
{
if (browserRefreshServer == null)
{
if (browserRefreshServer == null)
{
// error has been reported earlier
return null;
}

return new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer, clientProject);
// error has been reported earlier
return null;
}

return new BlazorWebAssemblyDeltaApplier(processReporter, browserRefreshServer, clientProject);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,24 @@

namespace Microsoft.DotNet.Watch;

internal abstract partial class HotReloadAppModel
/// <summary>
/// Blazor WebAssembly app hosted by an ASP.NET Core app.
/// App has a client and server projects and deltas are applied to both processes.
/// Agent is injected into the server process. The client process is updated via WebSocketScriptInjection.js injected into the browser.
/// </summary>
internal sealed class BlazorWebAssemblyHostedAppModel(ProjectGraphNode clientProject, ProjectGraphNode serverProject)
: HotReloadAppModel(agentInjectionProject: serverProject)
{
/// <summary>
/// Blazor WebAssembly app hosted by an ASP.NET Core app.
/// App has a client and server projects and deltas are applied to both processes.
/// </summary>
internal sealed class BlazorWebAssemblyHostedAppModel(ProjectGraphNode clientProject) : HotReloadAppModel
{
public override bool RequiresBrowserRefresh => true;
public override bool InjectDeltaApplier => true;
public override bool RequiresBrowserRefresh => true;

public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
{
if (browserRefreshServer == null)
{
if (browserRefreshServer == null)
{
// error has been reported earlier
return null;
}

return new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer, clientProject);
// error has been reported earlier
return null;
}

return new BlazorWebAssemblyHostedDeltaApplier(processReporter, browserRefreshServer, clientProject);
}
}
8 changes: 4 additions & 4 deletions src/BuiltInTools/dotnet-watch/HotReload/DefaultAppModel.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch;

/// <summary>
/// Default model.
/// </summary>
internal sealed class DefaultAppModel : HotReloadAppModel
internal sealed class DefaultAppModel(ProjectGraphNode project)
: HotReloadAppModel(agentInjectionProject: project)
{
public static readonly DefaultAppModel Instance = new();

public override bool RequiresBrowserRefresh => false;
public override bool InjectDeltaApplier => true;

public override DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter)
=> new DefaultDeltaApplier(processReporter);
Expand Down
2 changes: 0 additions & 2 deletions src/BuiltInTools/dotnet-watch/HotReload/DeltaApplier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ internal abstract class DeltaApplier(IReporter reporter) : IDisposable
{
public readonly IReporter Reporter = reporter;

public static readonly string StartupHookPath = Path.Combine(AppContext.BaseDirectory, "hotreload", "Microsoft.Extensions.DotNetDeltaApplier.dll");

public abstract void CreateConnection(string namedPipeName, CancellationToken cancellationToken);

/// <summary>
Expand Down
39 changes: 28 additions & 11 deletions src/BuiltInTools/dotnet-watch/HotReload/HotReloadAppModel.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Execution;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch;

internal abstract partial class HotReloadAppModel
internal abstract partial class HotReloadAppModel(ProjectGraphNode? agentInjectionProject)
{
public abstract bool RequiresBrowserRefresh { get; }

public abstract DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter);

/// <summary>
/// True to inject delta applier to the process.
/// Returns true and the path to the client agent implementation binary if the application needs the agent to be injected.
/// </summary>
public abstract bool InjectDeltaApplier { get; }
public bool TryGetStartupHookPath([NotNullWhen(true)] out string? path)
{
if (agentInjectionProject == null)
{
path = null;
return false;
}

public abstract DeltaApplier? CreateDeltaApplier(BrowserRefreshServer? browserRefreshServer, IReporter processReporter);
var hookTargetFramework = agentInjectionProject.GetTargetFramework() switch
{
// Note: Hot Reload is only supported on net6.0+
"net6.0" or "net7.0" or "net8.0" or "net9.0" => "netstandard2.1",
_ => "net10.0",
};

path = Path.Combine(AppContext.BaseDirectory, "hotreload", hookTargetFramework, "Microsoft.Extensions.DotNetDeltaApplier.dll");
return true;
}

public static HotReloadAppModel InferFromProject(ProjectGraphNode projectNode, IReporter reporter)
{
Expand All @@ -24,7 +41,7 @@ public static HotReloadAppModel InferFromProject(ProjectGraphNode projectNode, I
var queue = new Queue<ProjectGraphNode>();
queue.Enqueue(projectNode);

ProjectInstance? aspnetCoreProject = null;
ProjectGraphNode? aspnetCoreProject = null;

var visited = new HashSet<ProjectGraphNode>();

Expand All @@ -37,17 +54,17 @@ public static HotReloadAppModel InferFromProject(ProjectGraphNode projectNode, I
{
if (item.EvaluatedInclude == "AspNetCore")
{
aspnetCoreProject = currentNode.ProjectInstance;
aspnetCoreProject = currentNode;
break;
}

if (item.EvaluatedInclude == "WebAssembly")
{
// We saw a previous project that was AspNetCore. This must be a blazor hosted app.
if (aspnetCoreProject is not null && aspnetCoreProject != currentNode.ProjectInstance)
if (aspnetCoreProject is not null && aspnetCoreProject.ProjectInstance != currentNode.ProjectInstance)
{
reporter.Verbose($"HotReloadProfile: BlazorHosted. {aspnetCoreProject.FullPath} references BlazorWebAssembly project {currentNode.ProjectInstance.FullPath}.", emoji: "🔥");
return new BlazorWebAssemblyHostedAppModel(clientProject: currentNode);
reporter.Verbose($"HotReloadProfile: BlazorHosted. {aspnetCoreProject.ProjectInstance.FullPath} references BlazorWebAssembly project {currentNode.ProjectInstance.FullPath}.", emoji: "🔥");
return new BlazorWebAssemblyHostedAppModel(clientProject: currentNode, serverProject: aspnetCoreProject);
}

reporter.Verbose("HotReloadProfile: BlazorWebAssembly.", emoji: "🔥");
Expand All @@ -66,6 +83,6 @@ public static HotReloadAppModel InferFromProject(ProjectGraphNode projectNode, I
}

reporter.Verbose("HotReloadProfile: Default.", emoji: "🔥");
return DefaultAppModel.Instance;
return new DefaultAppModel(projectNode);
}
}
4 changes: 2 additions & 2 deletions src/BuiltInTools/dotnet-watch/HotReload/ProjectLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ public EnvironmentOptions EnvironmentOptions
// https://github.com/dotnet/runtime/blob/342936c5a88653f0f622e9d6cb727a0e59279b31/src/mono/browser/runtime/loader/config.ts#L330
environmentBuilder.SetVariable(EnvironmentVariables.Names.DotNetModifiableAssemblies, "debug");

if (appModel.InjectDeltaApplier)
if (appModel.TryGetStartupHookPath(out var startupHookPath))
{
// HotReload startup hook should be loaded before any other startup hooks:
environmentBuilder.DotNetStartupHooks.Insert(0, DeltaApplier.StartupHookPath);
environmentBuilder.DotNetStartupHooks.Insert(0, startupHookPath);

environmentBuilder.SetVariable(EnvironmentVariables.Names.DotNetWatchHotReloadNamedPipeName, namedPipeName);

Expand Down
23 changes: 21 additions & 2 deletions src/BuiltInTools/dotnet-watch/dotnet-watch.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\AspireService\Microsoft.WebTools.AspireService.projitems" Label="Shared" />
<Import Project="..\HotReloadAgent.PipeRpc\Microsoft.DotNet.HotReload.Agent.PipeRpc.projitems" Label="Shared" />
Expand Down Expand Up @@ -50,8 +50,27 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\Cli\dotnet\dotnet.csproj" />
<ProjectReference Include="..\BrowserRefresh\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework;TargetFrameworks" OutputItemType="Content" TargetPath="middleware\Microsoft.AspNetCore.Watch.BrowserRefresh.dll" CopyToOutputDirectory="PreserveNewest" />
<ProjectReference Include="..\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework;TargetFrameworks" OutputItemType="Content" TargetPath="hotreload\Microsoft.Extensions.DotNetDeltaApplier.dll" CopyToOutputDirectory="PreserveNewest" />
<ProjectReference Include="..\DotNetWatchTasks\DotNetWatchTasks.csproj" PrivateAssets="All" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework;TargetFrameworks" OutputItemType="Content" CopyToOutputDirectory="PreserveNewest" />

<ProjectReference Include="..\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj">
<PrivateAssets>all</PrivateAssets>
<OutputItemType>Content</OutputItemType>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SetTargetFramework>TargetFramework=net10.0</SetTargetFramework>
<TargetPath>hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ProjectReference>

<ProjectReference Include="..\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj">
<PrivateAssets>all</PrivateAssets>
<OutputItemType>Content</OutputItemType>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SetTargetFramework>TargetFramework=netstandard2.1</SetTargetFramework>
<TargetPath>hotreload\netstandard2.1\Microsoft.Extensions.DotNetDeltaApplier.dll</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ProjectReference>
</ItemGroup>

<!-- Publish dotnet-watch files to the redist testhost folder so that in innerloop, redist doesn't need to be built again. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public enum UpdateLocation
TopFunction,
}

[Theory(Skip = "https://github.com/dotnet/sdk/issues/49307")]
[Theory]
[CombinatorialData]
public async Task HostRestart(UpdateLocation updateLocation)
{
Expand Down
Loading