Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
Expand Down Expand Up @@ -79,6 +80,198 @@ public void StartProcessLocked_WhenReplacingCachedProcess_ShouldDisposePreviousH
}
}

/// <summary>
/// Verifies a broken graceful shutdown channel still falls back to forced termination.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenGracefulRequestThrowsIOException_ShouldStillForceKill()
{
IOException shutdownFailure = new("worker input closed");
Exception loggedFailure = null;
int forceKillCallCount = 0;
int disposeCallCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () => false,
requestGracefulShutdown: () => throw shutdownFailure,
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex => loggedFailure = ex);

Assert.That(loggedFailure, Is.SameAs(shutdownFailure));
Assert.That(forceKillCallCount, Is.EqualTo(1));
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies a disposed graceful shutdown channel still falls back to forced termination.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenGracefulRequestThrowsObjectDisposedException_ShouldStillForceKill()
{
ObjectDisposedException shutdownFailure = new("worker input");
Exception loggedFailure = null;
int forceKillCallCount = 0;
int disposeCallCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () => false,
requestGracefulShutdown: () => throw shutdownFailure,
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex => loggedFailure = ex);

Assert.That(loggedFailure, Is.SameAs(shutdownFailure));
Assert.That(forceKillCallCount, Is.EqualTo(1));
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies an operating-system kill failure is logged without escaping process disposal.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenForceKillThrowsWin32Exception_ShouldLogAndDispose()
{
Win32Exception shutdownFailure = new(5, "worker kill denied");
Exception loggedFailure = null;
int disposeCallCount = 0;

Assert.DoesNotThrow(() => SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () => false,
requestGracefulShutdown: () => { },
forceKill: () => throw shutdownFailure,
dispose: () => disposeCallCount++,
logFailure: ex => loggedFailure = ex));

Assert.That(loggedFailure, Is.SameAs(shutdownFailure));
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies an unknown exit state still attempts forced termination after logging the query failure.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenForceExitQueryThrowsWin32Exception_ShouldStillForceKill()
{
Win32Exception queryFailure = new(5, "worker exit code unavailable");
Exception loggedFailure = null;
int hasExitedCallCount = 0;
int forceKillCallCount = 0;
int disposeCallCount = 0;
int failureLogCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () =>
{
hasExitedCallCount++;
if (hasExitedCallCount == 2)
{
throw queryFailure;
}

return false;
},
requestGracefulShutdown: () => { },
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex =>
{
failureLogCount++;
loggedFailure = ex;
});

Assert.That(loggedFailure, Is.SameAs(queryFailure));
Assert.That(failureLogCount, Is.EqualTo(1));
Assert.That(forceKillCallCount, Is.EqualTo(1));
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies a missing associated process skips forced termination after logging the query failure.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenForceExitQueryThrowsInvalidOperationException_ShouldSkipForceKill()
{
InvalidOperationException queryFailure = new("worker process unavailable");
Exception loggedFailure = null;
int hasExitedCallCount = 0;
int forceKillCallCount = 0;
int disposeCallCount = 0;
int failureLogCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () =>
{
hasExitedCallCount++;
if (hasExitedCallCount == 2)
{
throw queryFailure;
}

return false;
},
requestGracefulShutdown: () => { },
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex =>
{
failureLogCount++;
loggedFailure = ex;
});

Assert.That(loggedFailure, Is.SameAs(queryFailure));
Assert.That(failureLogCount, Is.EqualTo(1));
Assert.That(forceKillCallCount, Is.Zero);
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies successful graceful shutdown skips forced termination and disposes once.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenGracefulRequestExitsProcess_ShouldSkipForceKill()
{
bool processExited = false;
int forceKillCallCount = 0;
int disposeCallCount = 0;
int failureLogCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () => processExited,
requestGracefulShutdown: () => processExited = true,
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex => failureLogCount++);

Assert.That(forceKillCallCount, Is.Zero);
Assert.That(failureLogCount, Is.Zero);
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies an already exited process skips both termination phases and disposes once.
/// </summary>
[Test]
public void ExecuteProcessShutdown_WhenProcessAlreadyExited_ShouldOnlyDispose()
{
int gracefulRequestCallCount = 0;
int forceKillCallCount = 0;
int disposeCallCount = 0;
int failureLogCount = 0;

SharedRoslynCompilerWorkerSession.ExecuteProcessShutdown(
hasExited: () => true,
requestGracefulShutdown: () => gracefulRequestCallCount++,
forceKill: () => forceKillCallCount++,
dispose: () => disposeCallCount++,
logFailure: ex => failureLogCount++);

Assert.That(gracefulRequestCallCount, Is.Zero);
Assert.That(forceKillCallCount, Is.Zero);
Assert.That(failureLogCount, Is.Zero);
Assert.That(disposeCallCount, Is.EqualTo(1));
}

/// <summary>
/// Verifies a pending compiler stream does not keep the bounded drain waiting.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
Expand Down Expand Up @@ -104,37 +105,117 @@ internal void ShutdownProcessLocked()
return;
}

try
{
if (!workerProcess.HasExited)
ExecuteProcessShutdown(
hasExited: () => workerProcess.HasExited,
requestGracefulShutdown: () =>
{
workerProcess.StandardInput.WriteLine(
SharedRoslynCompilerWorkerProtocol.SharedCompilerWorkerQuitCommand);
workerProcess.StandardInput.Flush();
workerProcess.WaitForExit(500);
}

if (!workerProcess.HasExited)
},
forceKill: () =>
{
workerProcess.Kill();
workerProcess.WaitForExit(500);
},
dispose: workerProcess.Dispose,
logFailure: LogWorkerShutdownFailure);
}

internal static void ExecuteProcessShutdown(
Func<bool> hasExited,
Action requestGracefulShutdown,
Action forceKill,
Action dispose,
Action<Exception> logFailure)
{
Debug.Assert(hasExited != null, "hasExited must not be null");
Debug.Assert(requestGracefulShutdown != null, "requestGracefulShutdown must not be null");
Debug.Assert(forceKill != null, "forceKill must not be null");
Debug.Assert(dispose != null, "dispose must not be null");
Debug.Assert(logFailure != null, "logFailure must not be null");

try
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{
// A failed graceful request must not prevent the forced termination phase.
TryRequestGracefulShutdown(hasExited, requestGracefulShutdown, logFailure);
TryForceKill(hasExited, forceKill, logFailure);
}
finally
{
dispose();
}
}

private static void TryRequestGracefulShutdown(
Func<bool> hasExited,
Action requestGracefulShutdown,
Action<Exception> logFailure)
{
try
{
if (!hasExited())
{
requestGracefulShutdown();
}
}
catch (IOException ex)
{
LogWorkerShutdownFailure(ex);
logFailure(ex);
}
catch (ObjectDisposedException ex)
{
LogWorkerShutdownFailure(ex);
logFailure(ex);
}
catch (InvalidOperationException ex)
{
LogWorkerShutdownFailure(ex);
logFailure(ex);
}
finally
catch (Win32Exception ex)
{
logFailure(ex);
}
}

private static void TryForceKill(
Func<bool> hasExited,
Action forceKill,
Action<Exception> logFailure)
{
bool shouldForceKill;
try
{
shouldForceKill = !hasExited();
}
catch (Win32Exception ex)
{
logFailure(ex);
// An unavailable exit code leaves process state unknown, so forced termination is safer.
shouldForceKill = true;
}
catch (InvalidOperationException ex)
{
logFailure(ex);
return;
}

if (!shouldForceKill)
{
return;
}

try
{
forceKill();
}
catch (Win32Exception ex)
{
logFailure(ex);
}
catch (InvalidOperationException ex)
{
workerProcess.Dispose();
logFailure(ex);
}
}

Expand Down
Loading