From 71b7bb50b28409614d730c7965ff7035534b1974 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 5 Sep 2018 17:21:59 -0700 Subject: [PATCH] Ensure external processes are killed when test process exits (#1371) - Prevents locked files when stop debugging unit test - Addresses #1279 --- .../IISDeployer.cs | 5 + .../IISExpressDeployer.cs | 6 + .../ProcessTracker.cs | 128 ++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs index 0e6b1cc932..b1394bccb5 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs @@ -254,6 +254,11 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS } HostProcess = Process.GetProcessById(workerProcess.ProcessId); + + // Ensure w3wp.exe is killed if test process termination is non-graceful. + // Prevents locked files when stop debugging unit test. + ProcessTracker.Add(HostProcess); + // cache the process start time for verifying log file name. var _ = HostProcess.StartTime; diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs index 4604fb5652..56d4afb46b 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs @@ -249,8 +249,14 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS else { _hostProcess = process; + + // Ensure iisexpress.exe is killed if test process termination is non-graceful. + // Prevents locked files when stop debugging unit test. + ProcessTracker.Add(_hostProcess); + // cache the process start time for verifying log file name. var _ = _hostProcess.StartTime; + Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); return (url: url, hostExitToken: hostExitTokenSource.Token); } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs new file mode 100644 index 0000000000..be2e6cc62b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/ProcessTracker.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS +{ + // Uses Windows Job Objects to ensure external processes are killed if the current process is terminated non-gracefully. + internal static class ProcessTracker + { + private static readonly IntPtr _jobHandle; + + static ProcessTracker() + { + // Requires Win8 or later + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || Environment.OSVersion.Version < new Version(6, 2)) + { + return; + } + + // Job name is optional but helps with diagnostics. Job name must be unique if non-null. + _jobHandle = CreateJobObject(IntPtr.Zero, name: $"ProcessTracker{Process.GetCurrentProcess().Id}"); + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + } + }; + + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + var extendedInfoPtr = Marshal.AllocHGlobal(length); + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(_jobHandle, JobObjectInfoType.ExtendedLimitInformation, + extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + public static void Add(Process process) + { + if (_jobHandle != IntPtr.Zero) + { + var success = AssignProcessToJobObject(_jobHandle, process.Handle); + if (!success && !process.HasExited) + { + throw new Win32Exception(); + } + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, + IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + private enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public JOBOBJECTLIMIT LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public Int64 Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; + } + + [Flags] + private enum JOBOBJECTLIMIT : uint + { + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 + } + + [StructLayout(LayoutKind.Sequential)] + private struct IO_COUNTERS + { + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + } +}