From 8080b4c167af3a46db4092eac4a71e1b3545a969 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 10:44:14 +0200 Subject: [PATCH 1/5] Solving memory leak by reusing BuildManager and ProjectRoolElementCache --- src/Build/Definition/ProjectCollection.cs | 43 ++++++++++++++++++- .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + .../netstandard/PublicAPI.Unshipped.txt | 1 + src/MSBuild/XMake.cs | 12 +++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 485b905abe0..096fcc073a4 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -145,6 +145,8 @@ public void Dispose() /// private static string s_assemblyDisplayVersion; + private static ProjectRootElementCacheBase s_projectRootElementCache = null; + /// /// The projects loaded into this collection. /// @@ -302,6 +304,26 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab /// If set to true, only critical events will be logged. /// If set to true, load all projects as read-only. public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly) + : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, reuseProjectRootElementCache: false) + { + } + + /// + /// Instantiates a project collection with specified global properties and loggers and using the + /// specified toolset locations, node count, and setting of onlyLogCriticalEvents. + /// Global properties and loggers may be null. + /// Throws InvalidProjectFileException if any of the global properties are reserved. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + /// The loggers to register. May be null and specified to any build instead. + /// Any remote loggers to register. May be null and specified to any build instead. + /// The locations from which to load toolsets. + /// The maximum number of nodes to use for building. + /// If set to true, only critical events will be logged. + /// If set to true, load all projects as read-only. + /// If set to true, it will try to reuse singleton. + public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) { _loadedProjects = new LoadedProjectCollection(); ToolsetLocations = toolsetDefinitionLocations; @@ -311,10 +333,23 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab { ProjectRootElementCache = new SimpleProjectRootElementCache(); } + else if (reuseProjectRootElementCache && s_projectRootElementCache != null) + { + ProjectRootElementCache = s_projectRootElementCache; + } else { - ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk: false, loadProjectsReadOnly); + // When we are reusing ProjectRootElementCache we need to reload XMLs if it has changed between MSBuild Server sessions/builds. + // If we are not reusing, cache will be released at end of build and as we do not support project files will changes during build + // we do not need to auto reload. + bool autoReloadFromDisk = reuseProjectRootElementCache; + ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly); + if (reuseProjectRootElementCache && s_projectRootElementCache == null) + { + s_projectRootElementCache = ProjectRootElementCache; + } } + OnlyLogCriticalEvents = onlyLogCriticalEvents; try @@ -1603,6 +1638,12 @@ protected virtual void Dispose(bool disposing) if (disposing) { ShutDownLoggingService(); + if (ProjectRootElementCache != null) + { + ProjectRootElementCache.ProjectRootElementAddedHandler -= ProjectRootElementCache_ProjectRootElementAddedHandler; + ProjectRootElementCache.ProjectRootElementDirtied -= ProjectRootElementCache_ProjectRootElementDirtiedHandler; + ProjectRootElementCache.ProjectDirtied -= ProjectRootElementCache_ProjectDirtiedHandler; + } Tracing.Dump(); } } diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 349a8e57aac..ee20877adfb 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 39c901f1b5c..44179d2f0e1 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index b7d98c179d5..37434c6fc65 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1082,7 +1082,8 @@ string[] commandLine toolsetDefinitionLocations, cpuCount, onlyLogCriticalEvents, - loadProjectsReadOnly: !preprocessOnly + loadProjectsReadOnly: !preprocessOnly, + reuseProjectRootElementCache: s_isServerNode ); if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) @@ -1315,7 +1316,14 @@ string[] commandLine FileUtilities.ClearCacheDirectory(); projectCollection?.Dispose(); - BuildManager.DefaultBuildManager.Dispose(); + // Build manager shall be reused for all build sessions. + // If, for one reason or another, this behavior needs to change in future + // please be aware that current code creates and keep running InProcNode even + // when its owning default build manager is disposed resulting in leek of memory and threads. + if (!s_isServerNode) + { + BuildManager.DefaultBuildManager.Dispose(); + } } return success; From e41cf8a6ff737fe197b89d7c856444a9f39b89b6 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 12:13:16 +0200 Subject: [PATCH 2/5] Do not clear project root element cache if in auto reload. --- src/Build/Evaluation/ProjectRootElementCache.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 17ecae43227..208a43ed668 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -415,6 +415,12 @@ internal override void Clear() /// internal override void DiscardImplicitReferences() { + if (_autoReloadFromDisk) + { + // no need to clear it, as auto reload properly invalidates caches if changed. + return; + } + lock (_locker) { // Make a new Weak cache only with items that have been explicitly loaded, this will be a small number, there will most likely From 447225c121b96cdadf7bec6ca0e8d2ffb15900e2 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 22:02:32 +0200 Subject: [PATCH 3/5] Reduce if Co-authored-by: Forgind --- src/Build/Definition/ProjectCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 096fcc073a4..f5d45290cd2 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -344,7 +344,7 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab // we do not need to auto reload. bool autoReloadFromDisk = reuseProjectRootElementCache; ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly); - if (reuseProjectRootElementCache && s_projectRootElementCache == null) + if (reuseProjectRootElementCache) { s_projectRootElementCache = ProjectRootElementCache; } From fba463075aa3095ecd3088abf078e5e5b436c72d Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 14 Jun 2022 17:22:34 +0200 Subject: [PATCH 4/5] Handle race condition --- .../Communications/NodeEndpointInProc.cs | 6 ++++ src/Build/BackEnd/Node/OutOfProcServerNode.cs | 11 ++++--- src/Shared/INodeEndpoint.cs | 6 ++++ src/Shared/NodeEndpointOutOfProcBase.cs | 31 +++++++++++++++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index fe81fa4298d..35dcda21565 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -221,6 +221,12 @@ public void SendData(INodePacket packet) EnqueuePacket(packet); } } + + public void ClientWillDisconnect() + { + // We do not need to do anything here for InProc node. + } + #endregion #region Internal Methods diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index f795a3eceae..19342b4ef57 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -219,13 +219,13 @@ private NodeEngineShutdownReason HandleShutdown(out Exception? exception) { CommunicationsUtilities.Trace("Shutting down with reason: {0}, and exception: {1}.", _shutdownReason, _shutdownException); - exception = _shutdownException; + // On Windows, a process holds a handle to the current directory, + // so reset it away from a user-requested folder that may get deleted. + NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory); - if (_nodeEndpoint.LinkStatus == LinkStatus.Active) - { - _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; - } + exception = _shutdownException; + _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; _nodeEndpoint.Disconnect(); CommunicationsUtilities.Trace("Shut down complete."); @@ -348,6 +348,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) // so reset it away from a user-requested folder that may get deleted. NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory); + _nodeEndpoint.ClientWillDisconnect(); var response = new ServerNodeBuildResult(buildResult.exitCode, buildResult.exitType); SendPacket(response); diff --git a/src/Shared/INodeEndpoint.cs b/src/Shared/INodeEndpoint.cs index cb8ce4a4c0a..ef2f319f023 100644 --- a/src/Shared/INodeEndpoint.cs +++ b/src/Shared/INodeEndpoint.cs @@ -103,5 +103,11 @@ LinkStatus LinkStatus /// The packet to be sent. void SendData(INodePacket packet); #endregion + + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// This is needed to handle race condition when both client and server is gracefully about to close connection. + /// + void ClientWillDisconnect(); } } diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 6477869dc05..0be21ce32c0 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -73,6 +73,13 @@ internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint /// private AutoResetEvent _terminatePacketPump; + /// + /// True if this side is gracefully disconnecting. + /// In such case we have sent last packet to client side and we expect + /// client will soon broke pipe connection - unless server do it first. + /// + private bool _isClientDisconnecting; + /// /// The thread which runs the asynchronous packet pump /// @@ -179,6 +186,14 @@ public void SendData(INodePacket packet) } } + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// + public void ClientWillDisconnect() + { + _isClientDisconnecting = true; + } + #endregion #region Construction @@ -312,6 +327,7 @@ private void InitializeAsyncPacketThread() { lock (_asyncDataMonitor) { + _isClientDisconnecting = false; _packetPump = new Thread(PacketPumpProc); _packetPump.IsBackground = true; _packetPump.Name = "OutOfProc Endpoint Packet Pump"; @@ -548,14 +564,25 @@ private void RunReadLoop(Stream localReadPipe, Stream localWritePipe, // Incomplete read. Abort. if (bytesRead == 0) { - CommunicationsUtilities.Trace("Parent disconnected abruptly"); + if (_isClientDisconnecting) + { + CommunicationsUtilities.Trace("Parent disconnected gracefully."); + // Do not change link status to failed as this could make node think connection has failed + // and recycle node, while this is perfectly expected and handled race condition + // (both client and node is about to close pipe and client can be faster). + } + else + { + CommunicationsUtilities.Trace("Parent disconnected abruptly."); + ChangeLinkStatus(LinkStatus.Failed); + } } else { CommunicationsUtilities.Trace("Incomplete header read from server. {0} of {1} bytes read", bytesRead, headerByte.Length); + ChangeLinkStatus(LinkStatus.Failed); } - ChangeLinkStatus(LinkStatus.Failed); exitLoop = true; break; } From 563dc2166b0bd0b7a877dbf30d79f9bb3d2833a3 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 15 Jun 2022 01:17:06 +0200 Subject: [PATCH 5/5] Clean running server nodes. --- ...Microsoft.Build.Framework.UnitTests.csproj | 1 + src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 77 ++++++++++--------- src/Shared/UnitTests/TestEnvironment.cs | 28 +++++++ 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj index b66f3a66b80..e3b953a332a 100644 --- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj +++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj @@ -34,6 +34,7 @@ + diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index a7ecdde6176..28b9ae44e3e 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -109,7 +109,7 @@ public void MSBuildServerTest() Thread.Sleep(1000); // Kill the server - ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); + Process.GetProcessById(pidOfServerProcess).KillTree(1000); }); // Start long-lived task execution @@ -120,9 +120,12 @@ public void MSBuildServerTest() // Ensure that a new build can still succeed and that its server node is different. output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); newPidOfInitialProcess = ParseNumber(output, "Process ID is "); int newServerProcessId = ParseNumber(output, "Server ID is "); + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(newServerProcessId); newPidOfInitialProcess.ShouldNotBe(pidOfInitialProcess, "Process started by two MSBuild executions should be different."); newPidOfInitialProcess.ShouldNotBe(newServerProcessId, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); pidOfServerProcess.ShouldNotBe(newServerProcessId, "Node used by both the first and second build should not be the same."); @@ -138,6 +141,8 @@ public void VerifyMixedLegacyBehavior() success.ShouldBeTrue(); int pidOfInitialProcess = ParseNumber(output, "Process ID is "); int pidOfServerProcess = ParseNumber(output, "Server ID is "); + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(pidOfServerProcess); pidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); Environment.SetEnvironmentVariable("MSBUILDUSESERVER", ""); @@ -154,6 +159,12 @@ public void VerifyMixedLegacyBehavior() pidOfNewserverProcess = ParseNumber(output, "Server ID is "); pidOfInitialProcess.ShouldNotBe(pidOfNewserverProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); pidOfServerProcess.ShouldBe(pidOfNewserverProcess, "Server node should be the same as from earlier."); + + if (pidOfServerProcess != pidOfNewserverProcess) + { + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(pidOfNewserverProcess); + } } [Fact] @@ -163,48 +174,40 @@ public void BuildsWhileBuildIsRunningOnServer() TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); TransientTestFile sleepProject = _env.CreateFile("napProject.proj", sleepingTaskContents); - int pidOfServerProcess = -1; - Task? t = null; - try - { - // Start a server node and find its PID. - string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); - pidOfServerProcess = ParseNumber(output, "Server ID is "); + int pidOfServerProcess; + Task t; + // Start a server node and find its PID. + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + pidOfServerProcess = ParseNumber(output, "Server ID is "); + _env.WithTransientProcess(pidOfServerProcess); - t = Task.Run(() => - { - RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _, false, _output); - }); + t = Task.Run(() => + { + RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _, false, _output); + }); - // The server will soon be in use; make sure we don't try to use it before that happens. - Thread.Sleep(1000); + // The server will soon be in use; make sure we don't try to use it before that happens. + Thread.Sleep(1000); - Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "0"); + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "0"); - output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); - success.ShouldBeTrue(); - ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); - Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); - output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); - success.ShouldBeTrue(); - pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Server ID is "), "The server should be otherwise occupied."); - pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); - ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "Process ID and Server ID should coincide."); - } - finally - { - if (pidOfServerProcess > -1) - { - ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); - } - - if (t is not null) - { - t.Wait(); - } - } + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Server ID is "), "The server should be otherwise occupied."); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "Process ID and Server ID should coincide."); + + // Clean up process and tasks + // 1st kill registered processes + _env.Dispose(); + // 2nd wait for sleep task which will ends as soon as the process is killed above. + t.Wait(); } private int ParseNumber(string searchString, string toFind) diff --git a/src/Shared/UnitTests/TestEnvironment.cs b/src/Shared/UnitTests/TestEnvironment.cs index 6ede3f2d7fb..2db94fa9e83 100644 --- a/src/Shared/UnitTests/TestEnvironment.cs +++ b/src/Shared/UnitTests/TestEnvironment.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; @@ -328,6 +329,15 @@ public TransientTestState SetCurrentDirectory(string newWorkingDirectory) return WithTransientTestState(new TransientWorkingDirectory(newWorkingDirectory)); } + /// + /// Register process ID to be finished/killed after tests ends. + /// + public TransientTestProcess WithTransientProcess(int processId) + { + TransientTestProcess transientTestProcess = new(processId); + return WithTransientTestState(transientTestProcess); + } + #endregion private class DefaultOutput : ITestOutputHelper @@ -560,6 +570,24 @@ public override void Revert() } } + public class TransientTestProcess : TransientTestState + { + private readonly int _processId; + + public TransientTestProcess(int processId) + { + _processId = processId; + } + + public override void Revert() + { + if (_processId > -1) + { + Process.GetProcessById(_processId).KillTree(1000); + } + } + } + public class TransientTestFile : TransientTestState {