Skip to content

Commit f8e1fc8

Browse files
authored
Merge pull request #601 from adamralph/process-tree
cancel child processes by default
2 parents fa7efc4 + 90fae8b commit f8e1fc8

File tree

4 files changed

+61
-19
lines changed

4 files changed

+61
-19
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ bool createNoWindow = false,
6464
Encoding? encoding = null,
6565
Func<int, bool>? handleExitCode = null,
6666
string? standardInput = null,
67+
bool cancellationIgnoresProcessTree = false,
6768
CancellationToken cancellationToken = default,
6869
```
6970

SimpleExec/Command.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public static class Command
3535
/// returns <see langword="true"/> when it has handled the exit code and default exit code handling should be suppressed, and
3636
/// returns <see langword="false"/> otherwise.
3737
/// </param>
38+
/// <param name="cancellationIgnoresProcessTree">
39+
/// Whether to ignore the process tree when cancelling the command.
40+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
41+
/// are left running after the command is cancelled.
42+
/// </param>
3843
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
3944
/// <exception cref="ExitCodeException">The command exited with non-zero exit code.</exception>
4045
/// <remarks>
@@ -50,6 +55,7 @@ public static void Run(
5055
Action<IDictionary<string, string?>>? configureEnvironment = null,
5156
bool createNoWindow = false,
5257
Func<int, bool>? handleExitCode = null,
58+
bool cancellationIgnoresProcessTree = false,
5359
CancellationToken cancellationToken = default) =>
5460
ProcessStartInfo
5561
.Create(
@@ -60,7 +66,7 @@ public static void Run(
6066
false,
6167
configureEnvironment ?? defaultAction,
6268
createNoWindow)
63-
.Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationToken);
69+
.Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
6470

6571
/// <summary>
6672
/// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin).
@@ -81,6 +87,11 @@ public static void Run(
8187
/// returns <see langword="true"/> when it has handled the exit code and default exit code handling should be suppressed, and
8288
/// returns <see langword="false"/> otherwise.
8389
/// </param>
90+
/// <param name="cancellationIgnoresProcessTree">
91+
/// Whether to ignore the process tree when cancelling the command.
92+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
93+
/// are left running after the command is cancelled.
94+
/// </param>
8495
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
8596
/// <exception cref="ExitCodeException">The command exited with non-zero exit code.</exception>
8697
public static void Run(
@@ -92,6 +103,7 @@ public static void Run(
92103
Action<IDictionary<string, string?>>? configureEnvironment = null,
93104
bool createNoWindow = false,
94105
Func<int, bool>? handleExitCode = null,
106+
bool cancellationIgnoresProcessTree = false,
95107
CancellationToken cancellationToken = default) =>
96108
ProcessStartInfo
97109
.Create(
@@ -102,19 +114,20 @@ public static void Run(
102114
false,
103115
configureEnvironment ?? defaultAction,
104116
createNoWindow)
105-
.Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationToken);
117+
.Run(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken);
106118

107119
private static void Run(
108120
this System.Diagnostics.ProcessStartInfo startInfo,
109121
bool noEcho,
110122
string echoPrefix,
111123
Func<int, bool>? handleExitCode,
124+
bool cancellationIgnoresProcessTree,
112125
CancellationToken cancellationToken)
113126
{
114127
using var process = new Process();
115128
process.StartInfo = startInfo;
116129

117-
process.Run(noEcho, echoPrefix, cancellationToken);
130+
process.Run(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken);
118131

119132
if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
120133
{
@@ -138,6 +151,11 @@ private static void Run(
138151
/// returns <see langword="true"/> when it has handled the exit code and default exit code handling should be suppressed, and
139152
/// returns <see langword="false"/> otherwise.
140153
/// </param>
154+
/// <param name="cancellationIgnoresProcessTree">
155+
/// Whether to ignore the process tree when cancelling the command.
156+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
157+
/// are left running after the command is cancelled.
158+
/// </param>
141159
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
142160
/// <returns>A <see cref="Task"/> that represents the asynchronous running of the command.</returns>
143161
/// <exception cref="ExitCodeReadException">The command exited with non-zero exit code.</exception>
@@ -154,6 +172,7 @@ public static async Task RunAsync(
154172
Action<IDictionary<string, string?>>? configureEnvironment = null,
155173
bool createNoWindow = false,
156174
Func<int, bool>? handleExitCode = null,
175+
bool cancellationIgnoresProcessTree = false,
157176
CancellationToken cancellationToken = default) =>
158177
await ProcessStartInfo
159178
.Create(
@@ -164,7 +183,7 @@ await ProcessStartInfo
164183
false,
165184
configureEnvironment ?? defaultAction,
166185
createNoWindow)
167-
.RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationToken)
186+
.RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken)
168187
.ConfigureAwait(false);
169188

170189
/// <summary>
@@ -186,6 +205,11 @@ await ProcessStartInfo
186205
/// returns <see langword="true"/> when it has handled the exit code and default exit code handling should be suppressed, and
187206
/// returns <see langword="false"/> otherwise.
188207
/// </param>
208+
/// <param name="cancellationIgnoresProcessTree">
209+
/// Whether to ignore the process tree when cancelling the command.
210+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
211+
/// are left running after the command is cancelled.
212+
/// </param>
189213
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
190214
/// <returns>A <see cref="Task"/> that represents the asynchronous running of the command.</returns>
191215
/// <exception cref="ExitCodeReadException">The command exited with non-zero exit code.</exception>
@@ -198,6 +222,7 @@ public static async Task RunAsync(
198222
Action<IDictionary<string, string?>>? configureEnvironment = null,
199223
bool createNoWindow = false,
200224
Func<int, bool>? handleExitCode = null,
225+
bool cancellationIgnoresProcessTree = false,
201226
CancellationToken cancellationToken = default) =>
202227
await ProcessStartInfo
203228
.Create(
@@ -208,20 +233,21 @@ await ProcessStartInfo
208233
false,
209234
configureEnvironment ?? defaultAction,
210235
createNoWindow)
211-
.RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationToken)
236+
.RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, handleExitCode, cancellationIgnoresProcessTree, cancellationToken)
212237
.ConfigureAwait(false);
213238

214239
private static async Task RunAsync(
215240
this System.Diagnostics.ProcessStartInfo startInfo,
216241
bool noEcho,
217242
string echoPrefix,
218243
Func<int, bool>? handleExitCode,
244+
bool cancellationIgnoresProcessTree,
219245
CancellationToken cancellationToken)
220246
{
221247
using var process = new Process();
222248
process.StartInfo = startInfo;
223249

224-
await process.RunAsync(noEcho, echoPrefix, cancellationToken).ConfigureAwait(false);
250+
await process.RunAsync(noEcho, echoPrefix, cancellationIgnoresProcessTree, cancellationToken).ConfigureAwait(false);
225251

226252
if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
227253
{
@@ -243,6 +269,11 @@ private static async Task RunAsync(
243269
/// returns <see langword="false"/> otherwise.
244270
/// </param>
245271
/// <param name="standardInput">The contents of standard input (stdin).</param>
272+
/// <param name="cancellationIgnoresProcessTree">
273+
/// Whether to ignore the process tree when cancelling the command.
274+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
275+
/// are left running after the command is cancelled.
276+
/// </param>
246277
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
247278
/// <returns>
248279
/// A <see cref="Task{TResult}"/> representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr).
@@ -259,6 +290,7 @@ private static async Task RunAsync(
259290
Encoding? encoding = null,
260291
Func<int, bool>? handleExitCode = null,
261292
string? standardInput = null,
293+
bool cancellationIgnoresProcessTree = false,
262294
CancellationToken cancellationToken = default) =>
263295
await ProcessStartInfo
264296
.Create(
@@ -273,6 +305,7 @@ await ProcessStartInfo
273305
.ReadAsync(
274306
handleExitCode,
275307
standardInput,
308+
cancellationIgnoresProcessTree,
276309
cancellationToken)
277310
.ConfigureAwait(false);
278311

@@ -293,6 +326,11 @@ await ProcessStartInfo
293326
/// returns <see langword="false"/> otherwise.
294327
/// </param>
295328
/// <param name="standardInput">The contents of standard input (stdin).</param>
329+
/// <param name="cancellationIgnoresProcessTree">
330+
/// Whether to ignore the process tree when cancelling the command.
331+
/// If set to <c>true</c>, when the command is cancelled, any child processes created by the command
332+
/// are left running after the command is cancelled.
333+
/// </param>
296334
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the command to exit.</param>
297335
/// <returns>
298336
/// A <see cref="Task{TResult}"/> representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr).
@@ -309,6 +347,7 @@ await ProcessStartInfo
309347
Encoding? encoding = null,
310348
Func<int, bool>? handleExitCode = null,
311349
string? standardInput = null,
350+
bool cancellationIgnoresProcessTree = false,
312351
CancellationToken cancellationToken = default) =>
313352
await ProcessStartInfo
314353
.Create(
@@ -323,19 +362,21 @@ await ProcessStartInfo
323362
.ReadAsync(
324363
handleExitCode,
325364
standardInput,
365+
cancellationIgnoresProcessTree,
326366
cancellationToken)
327367
.ConfigureAwait(false);
328368

329369
private static async Task<(string StandardOutput, string StandardError)> ReadAsync(
330370
this System.Diagnostics.ProcessStartInfo startInfo,
331371
Func<int, bool>? handleExitCode,
332372
string? standardInput,
373+
bool cancellationIgnoresProcessTree,
333374
CancellationToken cancellationToken)
334375
{
335376
using var process = new Process();
336377
process.StartInfo = startInfo;
337378

338-
var runProcess = process.RunAsync(true, "", cancellationToken);
379+
var runProcess = process.RunAsync(true, "", cancellationIgnoresProcessTree, cancellationToken);
339380

340381
Task<string> readOutput;
341382
Task<string> readError;

SimpleExec/ProcessExtensions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace SimpleExec
99
{
1010
internal static class ProcessExtensions
1111
{
12-
public static void Run(this Process process, bool noEcho, string echoPrefix, CancellationToken cancellationToken)
12+
public static void Run(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
1313
{
1414
var cancelled = 0L;
1515

@@ -23,7 +23,7 @@ public static void Run(this Process process, bool noEcho, string echoPrefix, Can
2323
using var register = cancellationToken.Register(
2424
() =>
2525
{
26-
if (process.TryKill())
26+
if (process.TryKill(cancellationIgnoresProcessTree))
2727
{
2828
_ = Interlocked.Increment(ref cancelled);
2929
}
@@ -38,7 +38,7 @@ public static void Run(this Process process, bool noEcho, string echoPrefix, Can
3838
}
3939
}
4040

41-
public static async Task RunAsync(this Process process, bool noEcho, string echoPrefix, CancellationToken cancellationToken)
41+
public static async Task RunAsync(this Process process, bool noEcho, string echoPrefix, bool cancellationIgnoresProcessTree, CancellationToken cancellationToken)
4242
{
4343
using var sync = new SemaphoreSlim(1, 1);
4444
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -58,7 +58,7 @@ public static async Task RunAsync(this Process process, bool noEcho, string echo
5858
() => tcs.Task.Status != TaskStatus.RanToCompletion,
5959
() =>
6060
{
61-
if (process.TryKill())
61+
if (process.TryKill(cancellationIgnoresProcessTree))
6262
{
6363
_ = tcs.TrySetCanceled(cancellationToken);
6464
}
@@ -94,14 +94,14 @@ private static string GetEchoLines(this System.Diagnostics.ProcessStartInfo info
9494
return builder.ToString();
9595
}
9696

97-
private static bool TryKill(this Process process)
97+
private static bool TryKill(this Process process, bool ignoreProcessTree)
9898
{
9999
// exceptions may be thrown for all kinds of reasons
100100
// and the _same exception_ may be thrown for all kinds of reasons
101101
// System.Diagnostics.Process is "fine"
102102
try
103103
{
104-
process.Kill();
104+
process.Kill(!ignoreProcessTree);
105105
}
106106
#pragma warning disable CA1031 // Do not catch general exception types
107107
catch (Exception)

0 commit comments

Comments
 (0)