Skip to content

Commit a5ac77a

Browse files
committed
Send Ctrl+C to launched process before killing it on Windows
1 parent ea7c246 commit a5ac77a

File tree

7 files changed

+30
-37
lines changed

7 files changed

+30
-37
lines changed

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken)
130130
};
131131

132132
var launchResult = new ProcessLaunchResult();
133-
var runningProcess = _processRunner.RunAsync(processSpec, processReporter, isUserApplication: true, launchResult, processTerminationSource.Token);
133+
var runningProcess = _processRunner.RunAsync(processSpec, processReporter, launchResult, processTerminationSource.Token);
134134
if (launchResult.ProcessId == null)
135135
{
136136
// error already reported

src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ await FileWatcher.WaitForFileChangeAsync(
796796
{
797797
Executable = _context.EnvironmentOptions.MuxerPath,
798798
WorkingDirectory = Path.GetDirectoryName(projectPath)!,
799+
IsUserApplication = false,
799800
OnOutput = line =>
800801
{
801802
lock (buildOutput)
@@ -804,12 +805,12 @@ await FileWatcher.WaitForFileChangeAsync(
804805
}
805806
},
806807
// pass user-specified build arguments last to override defaults:
807-
Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. binLogArguments, .. buildArguments]
808+
Arguments = ["build", projectPath, "-consoleLoggerParameters:NoSummary;Verbosity=minimal", .. binLogArguments, .. buildArguments],
808809
};
809810

810811
_context.Reporter.Output($"Building {projectPath} ...");
811812

812-
var exitCode = await _context.ProcessRunner.RunAsync(processSpec, _context.Reporter, isUserApplication: false, launchResult: null, cancellationToken);
813+
var exitCode = await _context.ProcessRunner.RunAsync(processSpec, _context.Reporter, launchResult: null, cancellationToken);
813814
return (exitCode == 0, buildOutput.ToImmutableArray(), projectPath);
814815
}
815816

src/BuiltInTools/dotnet-watch/Process/ProcessRunner.cs

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ private sealed class ProcessState
2222
/// <summary>
2323
/// Launches a process.
2424
/// </summary>
25-
/// <param name="isUserApplication">True if the process is a user application, false if it is a helper process (e.g. msbuild).</param>
26-
public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, bool isUserApplication, ProcessLaunchResult? launchResult, CancellationToken processTerminationToken)
25+
public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, ProcessLaunchResult? launchResult, CancellationToken processTerminationToken)
2726
{
2827
var state = new ProcessState();
2928
var stopwatch = new Stopwatch();
@@ -90,12 +89,12 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
9089
// Either Ctrl+C was pressed or the process is being restarted.
9190

9291
// Non-cancellable to not leave orphaned processes around blocking resources:
93-
await TerminateProcessAsync(process, state, reporter, CancellationToken.None);
92+
await TerminateProcessAsync(process, processSpec, state, reporter, CancellationToken.None);
9493
}
9594
}
9695
catch (Exception e)
9796
{
98-
if (isUserApplication)
97+
if (processSpec.IsUserApplication)
9998
{
10099
reporter.Error($"Application failed: {e.Message}");
101100
}
@@ -117,7 +116,7 @@ public async Task<int> RunAsync(ProcessSpec processSpec, IReporter reporter, boo
117116

118117
reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms and exited with exit code {exitCode}.");
119118

120-
if (isUserApplication)
119+
if (processSpec.IsUserApplication)
121120
{
122121
if (exitCode == 0)
123122
{
@@ -154,6 +153,7 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
154153
WorkingDirectory = processSpec.WorkingDirectory,
155154
RedirectStandardOutput = onOutput != null,
156155
RedirectStandardError = onOutput != null,
156+
CreateNewProcessGroup = processSpec.IsUserApplication && RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
157157
}
158158
};
159159

@@ -210,24 +210,20 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
210210
return process;
211211
}
212212

213-
private async ValueTask TerminateProcessAsync(Process process, ProcessState state, IReporter reporter, CancellationToken cancellationToken)
213+
private async ValueTask TerminateProcessAsync(Process process, ProcessSpec processSpec, ProcessState state, IReporter reporter, CancellationToken cancellationToken)
214214
{
215215
if (!shutdownCancellationToken.IsCancellationRequested)
216216
{
217-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
217+
var forceOnly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !processSpec.IsUserApplication;
218+
219+
// Ctrl+C hasn't been sent.
220+
TerminateProcess(process, state, reporter, forceOnly);
221+
222+
if (forceOnly)
218223
{
219-
// Ctrl+C hasn't been sent, force termination.
220-
// We don't have means to terminate gracefully on Windows (https://github.com/dotnet/runtime/issues/109432)
221-
TerminateProcess(process, state, reporter, force: true);
222224
_ = await WaitForExitAsync(process, state, timeout: null, reporter, cancellationToken);
223-
224225
return;
225226
}
226-
else
227-
{
228-
// Ctrl+C hasn't been sent, send SIGTERM now:
229-
TerminateProcess(process, state, reporter, force: false);
230-
}
231227
}
232228

233229
// Ctlr+C/SIGTERM has been sent, wait for the process to exit gracefully.
@@ -327,40 +323,28 @@ private static void TerminateProcess(Process process, ProcessState state, IRepor
327323

328324
private static void TerminateWindowsProcess(Process process, ProcessState state, IReporter reporter, bool force)
329325
{
330-
// Needs API: https://github.com/dotnet/runtime/issues/109432
331-
// Code below does not work because the process creation needs CREATE_NEW_PROCESS_GROUP flag.
332-
333-
reporter.Verbose($"Terminating process {state.ProcessId}.");
326+
reporter.Verbose($"Terminating process {state.ProcessId} ({(force ? "Kill" : "Ctrl+C")}).");
334327

335328
if (force)
336329
{
337330
process.Kill();
338331
}
339-
#if TODO
340332
else
341333
{
342334
const uint CTRL_C_EVENT = 0;
343335

344336
[DllImport("kernel32.dll", SetLastError = true)]
337+
[return: MarshalAs(UnmanagedType.Bool)]
345338
static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
346339

347-
[DllImport("kernel32.dll", SetLastError = true)]
348-
static extern bool AttachConsole(uint dwProcessId);
349-
350-
[DllImport("kernel32.dll", SetLastError = true)]
351-
static extern bool FreeConsole();
352-
353-
if (AttachConsole((uint)state.ProcessId) &&
354-
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0) &&
355-
FreeConsole())
340+
if (GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0))
356341
{
357342
return;
358343
}
359344

360345
var error = Marshal.GetLastPInvokeError();
361346
reporter.Verbose($"Failed to send Ctrl+C to process {state.ProcessId}: {Marshal.GetPInvokeErrorMessage(error)} (code {error})");
362347
}
363-
#endif
364348
}
365349

366350
private static void TerminateUnixProcess(ProcessState state, IReporter reporter, bool force)

src/BuiltInTools/dotnet-watch/Process/ProcessSpec.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ internal sealed class ProcessSpec
1515
public ProcessExitAction? OnExit { get; set; }
1616
public CancellationToken CancelOutputCapture { get; set; }
1717

18+
/// <summary>
19+
/// True if the process is a user application, false if it is a helper process (e.g. dotnet build).</param>
20+
/// </summary>
21+
public bool IsUserApplication { get; set; }
22+
1823
public string? ShortDisplayName()
1924
=> Path.GetFileNameWithoutExtension(Executable);
2025

src/BuiltInTools/dotnet-watch/Process/ProjectLauncher.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ public EnvironmentOptions EnvironmentOptions
4747
var processSpec = new ProcessSpec
4848
{
4949
Executable = EnvironmentOptions.MuxerPath,
50+
IsUserApplication = true,
5051
WorkingDirectory = projectOptions.WorkingDirectory,
51-
OnOutput = onOutput
52+
OnOutput = onOutput,
5253
};
5354

5455
var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment();

src/BuiltInTools/dotnet-watch/Watch/DotNetWatcher.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke
5353
{
5454
Executable = context.EnvironmentOptions.MuxerPath,
5555
WorkingDirectory = context.EnvironmentOptions.WorkingDirectory,
56+
IsUserApplication = true,
5657
Arguments = buildEvaluator.GetProcessArguments(iteration),
5758
EnvironmentVariables =
5859
{
@@ -81,7 +82,7 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke
8182

8283
fileSetWatcher.WatchContainingDirectories(evaluationResult.Files.Keys, includeSubdirectories: true);
8384

84-
var processTask = context.ProcessRunner.RunAsync(processSpec, context.Reporter, isUserApplication: true, launchResult: null, combinedCancellationSource.Token);
85+
var processTask = context.ProcessRunner.RunAsync(processSpec, context.Reporter, launchResult: null, combinedCancellationSource.Token);
8586

8687
Task<ChangedFile?> fileSetTask;
8788
Task finishedTask;

src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> fil
4949
{
5050
Executable = EnvironmentOptions.MuxerPath,
5151
WorkingDirectory = projectDir,
52+
IsUserApplication = false,
5253
Arguments = arguments,
5354
OnOutput = line =>
5455
{
@@ -61,7 +62,7 @@ internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> fil
6162

6263
Reporter.Verbose($"Running MSBuild target '{TargetName}' on '{rootProjectFile}'");
6364

64-
var exitCode = await processRunner.RunAsync(processSpec, Reporter, isUserApplication: false, launchResult: null, cancellationToken);
65+
var exitCode = await processRunner.RunAsync(processSpec, Reporter, launchResult: null, cancellationToken);
6566

6667
var success = exitCode == 0 && File.Exists(watchList);
6768

0 commit comments

Comments
 (0)