Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.6k
Re-enable Microsoft.Extensions.Hosting shutdown functional coverage in CI and harden shutdown startup/Helix/NET481 compatibility#128252
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
Changes from all commits
a12d2b96bc1499b9cd702609f41535998106f852bfa12c0f645143a98c11db288150102252aace8c2e66ebd1e8f306e3f5637c6bd5138a422cfc44b01891b0f9172b45986a6516bc033bd3dc0c14a6f4cfd5da0dd537e79f61eae8bd5b0d14835942631cf1e67badf894ade959ed8db119f58bf353194bf9bb9ef7300b76f8e37ba50fd0a9b1a81c04f3c4f3fb36eb2c865020ebbdf1ab5ea333File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,6 +3,7 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Runtime.InteropServices; | ||
| @@ -11,9 +12,38 @@ namespace Microsoft.Extensions.Internal | ||
| { | ||
| internal static class ProcessExtensions | ||
| { | ||
| private const int ESRCH = 3; | ||
| #if NET | ||
| private static readonly int s_sigint = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 2 : GetPlatformSignalNumber(PosixSignal.SIGINT); | ||
| private static readonly int s_sigterm = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 15 : GetPlatformSignalNumber(PosixSignal.SIGTERM); | ||
| #endif | ||
| private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); | ||
| private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); | ||
| internal static int SigIntSignalNumber | ||
| { | ||
| get | ||
| { | ||
| #if NET | ||
| return s_sigint; | ||
| #else | ||
| return 2; | ||
| #endif | ||
| } | ||
| } | ||
| internal static int SigTermSignalNumber | ||
| { | ||
| get | ||
| { | ||
| #if NET | ||
| return s_sigterm; | ||
| #else | ||
| return 15; | ||
| #endif | ||
| } | ||
| } | ||
| public static void KillTree(this Process process) => process.KillTree(_defaultTimeout); | ||
| public static void KillTree(this Process process, TimeSpan timeout) | ||
| @@ -41,11 +71,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; | ||
| } | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (!string.IsNullOrEmpty(stdout)) | ||
| { | ||
| @@ -72,11 +110,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, SigTermSignalNumber) != 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); | ||
| } | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| 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. | ||
| } | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| private static void RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) | ||
| @@ -102,5 +191,25 @@ private static void RunProcessAndWaitForExit(string fileName, string arguments, | ||
| process.Kill(); | ||
| } | ||
| } | ||
| internal static void SendSignal(int pid, int signal) | ||
| { | ||
| if (_isWindows) | ||
| { | ||
| throw new PlatformNotSupportedException("Sending POSIX signals is only supported on Unix-like platforms."); | ||
| } | ||
Copilot marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (Kill(pid, signal) != 0) | ||
| { | ||
| throw new Win32Exception(Marshal.GetLastWin32Error()); | ||
| } | ||
| } | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| [DllImport("libc", EntryPoint = "kill", SetLastError = true)] | ||
| private static extern int Kill(int pid, int sig); | ||
| #if NET | ||
| [DllImport("libSystem.Native", EntryPoint = "SystemNative_GetPlatformSignalNumber")] | ||
| private static extern int GetPlatformSignalNumber(PosixSignal signal); | ||
| #endif | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,8 +3,6 @@ | ||
| <PropertyGroup> | ||
| <TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks> | ||
| <EnableDefaultItems>true</EnableDefaultItems> | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| <!-- ActiveIssue in AssemblyInfo.cs --> | ||
| <IgnoreForCI>true</IgnoreForCI> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,10 +3,10 @@ | ||
| using System; | ||
| using System.Diagnostics; | ||
| using System.IO; | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.Hosting.IntegrationTesting; | ||
| using Microsoft.Extensions.Internal; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Test; | ||
| using Xunit; | ||
| @@ -21,6 +21,7 @@ public class ShutdownTests | ||
| "Stopping end\n" + | ||
| "Stopped firing\n" + | ||
| "Stopped end"; | ||
| private static readonly TimeSpan s_shutdownExitTimeout = TimeSpan.FromSeconds(30); | ||
| private readonly ITestOutputHelper _output; | ||
| public ShutdownTests(ITestOutputHelper output) | ||
| @@ -50,36 +51,32 @@ 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); | ||
rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. rosebyte marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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)) | ||
| if (!string.IsNullOrEmpty(args.Data) && args.Data.StartsWith(StartedMessage, StringComparison.Ordinal)) | ||
| { | ||
| output += args.Data.Substring(StartedMessage.Length) + '\n'; | ||
| started.TrySetResult(0); | ||
| @@ -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); | ||
| @@ -132,27 +131,48 @@ private void SendShutdownSignal(Process hostProcess) | ||
| private static void SendSIGINT(int processId) | ||
| { | ||
| var startInfo = new ProcessStartInfo | ||
| { | ||
| FileName = "kill", | ||
| Arguments = processId.ToString(), | ||
| RedirectStandardOutput = true, | ||
| UseShellExecute = false | ||
| }; | ||
| var process = Process.Start(startInfo); | ||
| WaitForExitOrKill(process); | ||
| ProcessExtensions.SendSignal(processId, ProcessExtensions.SigIntSignalNumber); | ||
| } | ||
| private static void WaitForExitOrKill(Process process) | ||
| { | ||
| process.WaitForExit(1000); | ||
| if (!process.HasExited) | ||
| bool exited = process.WaitForExit((int)s_shutdownExitTimeout.TotalMilliseconds); | ||
| if (!exited) | ||
| { | ||
| process.Kill(); | ||
| try | ||
| { | ||
| process.Kill(); | ||
| } | ||
| catch (InvalidOperationException) { } // Process may have exited between WaitForExit and Kill | ||
| // Wait for the process to actually exit after Kill() before accessing ExitCode | ||
| if (!process.WaitForExit(5000)) | ||
| { | ||
| 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() { } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.