Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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 @@ -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
}
}
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
}

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,23 +51,21 @@ 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;

Expand Down Expand Up @@ -95,11 +94,11 @@ private async Task ExecuteShutdownTest(string testName, string shutdownMechanic)
}
};

await started.Task.WaitAsync(TimeSpan.FromSeconds(60));
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