Skip to content

Commit ce1400c

Browse files
Read std err before waiting to avoid deadlock (#4875)
* Read std err before waiting to avoid deadlock * Document that we will wait forever
1 parent f90154a commit ce1400c

File tree

3 files changed

+14
-29
lines changed

3 files changed

+14
-29
lines changed

src/ServiceControlInstaller.Engine.UnitTests/Setup/SetupInstanceTests.cs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,22 @@
99
public class SetupInstanceTests
1010
{
1111
[Test]
12-
public void Should_not_throw_on_0_exit_code() => Assert.DoesNotThrow(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "", Timeout.Infinite));
12+
public void Should_not_throw_on_0_exit_code() => Assert.DoesNotThrow(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", ""));
1313

1414
[Test]
1515
public void Should_capture_and_rethrow_failures()
1616
{
17-
var ex = Assert.Throws<Exception>(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "fail", Timeout.Infinite));
17+
var ex = Assert.Throws<Exception>(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "fail"));
1818

1919
Assert.That(ex.Message, Does.Contain("Fake exception"));
2020
}
2121

2222
[Test]
2323
public void Should_capture_and_rethrow_non_zero_exit_codes()
2424
{
25-
var ex = Assert.Throws<Exception>(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "non-zero-exit-code", Timeout.Infinite));
25+
var ex = Assert.Throws<Exception>(() => InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "non-zero-exit-code"));
2626

2727
Assert.That(ex.Message, Does.Contain("returned a non-zero exit code: 3"));
2828
Assert.That(ex.Message, Does.Contain("Fake non zero exit code message"));
2929
}
30-
31-
[Test]
32-
public void Should_not_kill_the_process_if_wait_time_is_execeeded()
33-
{
34-
var process = InstanceSetup.Run(TestContext.CurrentContext.WorkDirectory, "SetupProcessFake.exe", "test", "delay", 10);
35-
36-
Assert.That(process.HasExited, Is.False);
37-
38-
process.Kill();
39-
process.WaitForExit();
40-
}
4130
}

src/ServiceControlInstaller.Engine/Setup/InstanceSetup.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@ static void Run(string installPath, string exeName, string instanceName, bool sk
3535
args += " --skip-queue-creation";
3636
}
3737

38-
// we will wait "forever" since that is the most safe action right not. We will leave it up to the setup code in the instances to make sure it won't run forever.
39-
// If/when provide better UI experience we might revisit this and potentially find a way for the installer to control the timeout.
40-
Run(installPath, exeName, instanceName, args, Timeout.Infinite);
38+
Run(installPath, exeName, instanceName, args);
4139
}
4240

43-
internal static Process Run(string installPath, string exeName, string instanceName, string args, int timeout)
41+
internal static void Run(string installPath, string exeName, string instanceName, string args)
4442
{
4543
var processStartupInfo = new ProcessStartInfo
4644
{
@@ -56,15 +54,20 @@ internal static Process Run(string installPath, string exeName, string instanceN
5654

5755
var p = Process.Start(processStartupInfo) ?? throw new Exception($"Attempt to launch {exeName} failed.");
5856

59-
p.WaitForExit(timeout);
57+
// Reading std err needs to happen before waiting to avoid the risk of a deadlock.
58+
// See https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?view=net-9.0&redirectedfrom=MSDN#remarks for more details.
59+
// Note that this will wait forever, should we want to avoid that the async ProcessStartInfo.ErrorDataReceived API needs to be used.
60+
var error = p.StandardError.ReadToEnd();
61+
62+
// we will wait "forever" since that is the most safe action right not. We will leave it up to the setup code in the instances to make sure it won't run forever.
63+
// If/when provide better UI experience we might revisit this and potentially find a way for the installer to control the timeout.
64+
p.WaitForExit();
6065

6166
if (!p.HasExited || p.ExitCode == 0)
6267
{
63-
return p;
68+
return;
6469
}
6570

66-
var error = p.StandardError.ReadToEnd();
67-
6871
throw new Exception($"{exeName} returned a non-zero exit code: {p.ExitCode}. This typically indicates a configuration error. The error output was:\r\n {error}");
6972
}
7073
}

src/SetupProcessFake/Program.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,4 @@
1010
return 3;
1111
}
1212

13-
if (args.Any(a => a == "delay"))
14-
{
15-
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
16-
await Task.Delay(TimeSpan.FromSeconds(5));
17-
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
18-
}
19-
2013
return 0;

0 commit comments

Comments
 (0)