Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a12d2b9
Initial plan
Copilot May 15, 2026
6bc1499
Re-enable Hosting functional TestApp shutdown tests
Copilot May 15, 2026
b9cd702
Fix Hosting shutdown tests on Helix without source tree
Copilot May 18, 2026
609f415
Handle missing pgrep in hosting functional test process cleanup
Copilot May 19, 2026
3599810
Avoid external kill dependency in hosting functional test cleanup
Copilot May 19, 2026
6f852bf
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 20, 2026
a12c0f6
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 20, 2026
45143a9
Fix shutdown functional test signal delivery on minimal Helix images
Copilot May 22, 2026
8c11db2
Fix net481 compile for shutdown SIGINT error handling
Copilot May 22, 2026
8815010
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 23, 2026
2252aac
fix: use BorrowedPublishedApplication with no-op Dispose to avoid del…
Copilot May 26, 2026
e8c2e66
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 26, 2026
ebd1e8f
Potential fix for pull request finding
rosebyte May 27, 2026
306e3f5
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 27, 2026
637c6bd
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 27, 2026
5138a42
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte May 28, 2026
2cfc44b
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte Jun 3, 2026
01891b0
Fix race condition in WaitForExitOrKill by waiting for process exit a…
Copilot Jun 5, 2026
f9172b4
Use kill -TERM for graceful termination and increase test timeouts to…
Copilot Jun 5, 2026
5986a65
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte Jun 5, 2026
16bc033
Use libc SIGTERM in Hosting test process cleanup
Copilot Jun 5, 2026
bd3dc0c
Merge branch 'main' into copilot/enable-functional-tests-on-hosting
rosebyte Jun 10, 2026
14a6f4c
Fix Hosting shutdown output subscription race
Copilot Jun 10, 2026
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 @@ -20,6 +20,7 @@ public class SelfHostDeployer : ApplicationDeployer
private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down.";

public Process HostProcess { get; private set; }
public event DataReceivedEventHandler OutputReceived;

public SelfHostDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
: base(deploymentParameters, loggerFactory)
Expand Down Expand Up @@ -130,6 +131,8 @@ protected async Task<CancellationToken> StartSelfHostAsync()
{
started.TrySetResult(null);
}

OutputReceived?.Invoke(sender, dataArgs);
};
var hostExitTokenSource = new CancellationTokenSource();
HostProcess.Exited += (sender, e) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
Expand All @@ -11,6 +12,8 @@ namespace Microsoft.Extensions.Internal
{
internal static class ProcessExtensions
{
private const int ESRCH = 3;
private const int SIGTERM = 15;
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);

Expand Down Expand Up @@ -41,11 +44,19 @@ public static void KillTree(this Process process, TimeSpan timeout)

private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out var stdout);
string stdout;
try
{
RunProcessAndWaitForExit(
"pgrep",
$"-P {parentId}",
timeout,
out stdout);
}
catch (Win32Exception)
{
return;
}

if (!string.IsNullOrEmpty(stdout))
{
Expand All @@ -72,11 +83,62 @@ private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpa

private static void KillProcessUnix(int processId, TimeSpan timeout)
{
RunProcessAndWaitForExit(
"kill",
$"-TERM {processId}",
timeout,
out var stdout);
try
{
if (kill(processId, SIGTERM) != 0)
{
var error = Marshal.GetLastWin32Error();
if (error != ESRCH)
{
KillProcessUnixHard(processId, timeout);
return;
}
}

using (Process process = Process.GetProcessById(processId))
{
if (!process.WaitForExit((int)timeout.TotalMilliseconds))
{
KillProcessUnixHard(processId, timeout);
}
Comment on lines +100 to +103

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, OK, do it that way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 16bc0339: KillProcessUnix no longer throws on SIGTERM timeout; it now escalates to Process.Kill() (hard kill) as best-effort cleanup instead of failing disposal.

}
}
catch (ArgumentException)
{
// Ignore if process has already exited.
}
catch (InvalidOperationException)
{
// Ignore if process has already exited.
}
catch (Win32Exception)
{
KillProcessUnixHard(processId, timeout);
}
}

private static void KillProcessUnixHard(int processId, TimeSpan timeout)
{
try
{
using (Process process = Process.GetProcessById(processId))
{
process.Kill();
process.WaitForExit((int)timeout.TotalMilliseconds);
}
Comment on lines +86 to +104

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot , use the kill -TERM command but keep the other changes wrapping the call.

}
catch (ArgumentException)
{
// Ignore if process has already exited.
}
catch (InvalidOperationException)
{
// Ignore if process has already exited.
}
catch (Win32Exception)
{
// Ignore permission or process-not-found errors.
}
Comment on lines +110 to +141

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, implement the fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 16bc0339: Unix termination now uses libc kill(pid, SIGTERM) and falls back to Process.Kill() when graceful termination fails (including Win32Exception/error paths), so orphaned processes are still terminated.

}

private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout)
Expand All @@ -102,5 +164,8 @@ private static void RunProcessAndWaitForExit(string fileName, string arguments,
process.Kill();
}
}

[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public PublishedApplication(string path, ILogger logger)
Path = path;
}

public void Dispose()
public virtual void Dispose()
{
RetryHelper.RetryOperation(
() => Directory.Delete(Path, true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<!-- ActiveIssue in AssemblyInfo.cs -->
<IgnoreForCI>true</IgnoreForCI>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
using Xunit;

[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
[assembly: ActiveIssue("https://github.com/dotnet/runtime/issues/34090")] // Note: remove IgnoreForCI from .csproj when reenabling
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting.IntegrationTesting;
Expand All @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests
{
public class ShutdownTests
{
private const int SIGINT = 2;
private static readonly string StartedMessage = "Started";
private static readonly string CompletionMessage = "Stopping firing\n" +
"Stopping end\n" +
Expand Down Expand Up @@ -50,34 +51,30 @@ private async Task ExecuteShutdownTest(string testName, string shutdownMechanic)
builder.AddXunit(_output);
});

// TODO refactor deployers to not depend on source code
// see https://github.com/dotnet/extensions/issues/1697 and https://github.com/dotnet/aspnetcore/issues/10268
#pragma warning disable 0618
var applicationPath = string.Empty; // disabled for now
#pragma warning restore 0618
string applicationPath = AppContext.BaseDirectory;

Version version = Environment.Version;
var deploymentParameters = new DeploymentParameters(
applicationPath,
RuntimeFlavor.CoreClr,
RuntimeArchitecture.x64)
{
ApplicationName = "Microsoft.Extensions.Hosting.TestApp",
TargetFramework = $"net{version.Major}.{version.Minor}",
ApplicationType = ApplicationType.Portable,
PublishApplicationBeforeDeployment = true,
StatusMessagesEnabled = false
};
deploymentParameters.ApplicationPublisher = new ExistingOutputApplicationPublisher(applicationPath);
Comment on lines +62 to +68

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot , implement the suggestion.

Comment thread
rosebyte marked this conversation as resolved.

deploymentParameters.EnvironmentVariables["DOTNET_STARTMECHANIC"] = shutdownMechanic;

using (var deployer = new SelfHostDeployer(deploymentParameters, xunitTestLoggerFactory))
{
var result = await deployer.DeployAsync();

var started = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
var completed = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
var output = string.Empty;
deployer.HostProcess.OutputDataReceived += (sender, args) =>
deployer.OutputReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data) && args.Data.StartsWith(StartedMessage))
{
Expand All @@ -95,11 +92,13 @@ private async Task ExecuteShutdownTest(string testName, string shutdownMechanic)
}
};

await started.Task.WaitAsync(TimeSpan.FromSeconds(60));
await deployer.DeployAsync();

await started.Task.WaitAsync(TimeSpan.FromSeconds(180));

SendShutdownSignal(deployer.HostProcess);

await completed.Task.WaitAsync(TimeSpan.FromSeconds(60));
await completed.Task.WaitAsync(TimeSpan.FromSeconds(180));

WaitForExitOrKill(deployer.HostProcess);

Expand Down Expand Up @@ -132,16 +131,10 @@ private void SendShutdownSignal(Process hostProcess)

private static void SendSIGINT(int processId)
{
var startInfo = new ProcessStartInfo
if (kill(processId, SIGINT) != 0)
{
FileName = "kill",
Arguments = processId.ToString(),
RedirectStandardOutput = true,
UseShellExecute = false
};

var process = Process.Start(startInfo);
WaitForExitOrKill(process);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}

private static void WaitForExitOrKill(Process process)
Expand All @@ -150,9 +143,37 @@ private static void WaitForExitOrKill(Process process)
if (!process.HasExited)
{
process.Kill();
// Wait for the process to actually exit after Kill() before accessing ExitCode
if (!process.WaitForExit(5000))
Comment on lines 143 to +147
{
throw new InvalidOperationException($"Process {process.Id} did not exit within timeout after Kill()");
}
}

Assert.Equal(0, process.ExitCode);
}

private sealed class ExistingOutputApplicationPublisher : ApplicationPublisher
{
public ExistingOutputApplicationPublisher(string applicationPath)
: base(applicationPath)
{
}

public override Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
=> Task.FromResult<PublishedApplication>(new BorrowedPublishedApplication(ApplicationPath, logger));

// Wraps a path that is borrowed (not owned) from the test output directory.
// Dispose is intentionally a no-op to prevent deleting AppContext.BaseDirectory.
private sealed class BorrowedPublishedApplication : PublishedApplication
{
public BorrowedPublishedApplication(string path, ILogger logger) : base(path, logger) { }

public override void Dispose() { }
}
}

[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);
}
}
Loading