Dispatch cancellation on CTRL+C to avoid deadlocks in killing the process tree
This commit is contained in:
parent
31d7971d2e
commit
6272b3625e
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -8,14 +8,13 @@ namespace Microsoft.Extensions.Tools.Internal
|
||||||
public static class CliContext
|
public static class CliContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// dotnet --verbose subcommand
|
/// dotnet -d|--diagnostics subcommand
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static bool IsGlobalVerbose()
|
public static bool IsGlobalVerbose()
|
||||||
{
|
{
|
||||||
bool globalVerbose;
|
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out bool globalVerbose);
|
||||||
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE"), out globalVerbose);
|
|
||||||
return globalVerbose;
|
return globalVerbose;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,10 @@ namespace Microsoft.DotNet.Watcher
|
||||||
|
|
||||||
await Task.WhenAll(processTask, fileSetTask);
|
await Task.WhenAll(processTask, fileSetTask);
|
||||||
|
|
||||||
if (processTask.Result != 0 && finishedTask == processTask)
|
if (processTask.Result != 0 && finishedTask == processTask && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
// Only show this error message if the process exited non-zero due to a normal process exit.
|
||||||
|
// Don't show this if dotnet-watch killed the inner process due to file change or CTRL+C by the user
|
||||||
_reporter.Error($"Exited with error code {processTask.Result}");
|
_reporter.Error($"Exited with error code {processTask.Result}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -33,7 +33,7 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
var stopwatch = new Stopwatch();
|
var stopwatch = new Stopwatch();
|
||||||
|
|
||||||
using (var process = CreateProcess(processSpec))
|
using (var process = CreateProcess(processSpec))
|
||||||
using (var processState = new ProcessState(process))
|
using (var processState = new ProcessState(process, _reporter))
|
||||||
{
|
{
|
||||||
cancellationToken.Register(() => processState.TryKill());
|
cancellationToken.Register(() => processState.TryKill());
|
||||||
|
|
||||||
|
|
@ -97,27 +97,36 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
private class ProcessState : IDisposable
|
private class ProcessState : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly IReporter _reporter;
|
||||||
private readonly Process _process;
|
private readonly Process _process;
|
||||||
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
|
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
|
||||||
private volatile bool _disposed;
|
private volatile bool _disposed;
|
||||||
|
|
||||||
public ProcessState(Process process)
|
public ProcessState(Process process, IReporter reporter)
|
||||||
{
|
{
|
||||||
|
_reporter = reporter;
|
||||||
_process = process;
|
_process = process;
|
||||||
_process.Exited += OnExited;
|
_process.Exited += OnExited;
|
||||||
Task = _tcs.Task.ContinueWith(_ =>
|
Task = _tcs.Task.ContinueWith(_ =>
|
||||||
{
|
{
|
||||||
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
|
try
|
||||||
// this code used Process.Exited, which could result in us missing some output due to the ordering of
|
|
||||||
// events.
|
|
||||||
//
|
|
||||||
// See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
|
|
||||||
if (!process.WaitForExit(Int32.MaxValue))
|
|
||||||
{
|
{
|
||||||
throw new TimeoutException();
|
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
|
||||||
}
|
// this code used Process.Exited, which could result in us missing some output due to the ordering of
|
||||||
|
// events.
|
||||||
|
//
|
||||||
|
// See the remarks here: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit#System_Diagnostics_Process_WaitForExit_System_Int32_
|
||||||
|
if (!_process.WaitForExit(Int32.MaxValue))
|
||||||
|
{
|
||||||
|
throw new TimeoutException();
|
||||||
|
}
|
||||||
|
|
||||||
process.WaitForExit();
|
_process.WaitForExit();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// suppress if this throws if no process is associated with this object anymore.
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,15 +134,26 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
|
|
||||||
public void TryKill()
|
public void TryKill()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_process.HasExited)
|
if (!_process.HasExited)
|
||||||
{
|
{
|
||||||
|
_reporter.Verbose($"Killing process {_process.Id}");
|
||||||
_process.KillTree();
|
_process.KillTree();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{ }
|
{
|
||||||
|
_reporter.Verbose($"Error while killing process '{_process.StartInfo.FileName} {_process.StartInfo.Arguments}': {ex.Message}");
|
||||||
|
#if DEBUG
|
||||||
|
_reporter.Verbose(ex.ToString());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExited(object sender, EventArgs args)
|
private void OnExited(object sender, EventArgs args)
|
||||||
|
|
@ -143,8 +163,8 @@ namespace Microsoft.DotNet.Watcher.Internal
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
{
|
{
|
||||||
_disposed = true;
|
|
||||||
TryKill();
|
TryKill();
|
||||||
|
_disposed = true;
|
||||||
_process.Exited -= OnExited;
|
_process.Exited -= OnExited;
|
||||||
_process.Dispose();
|
_process.Dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,8 @@ namespace Microsoft.DotNet.Watcher
|
||||||
_reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
|
_reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_cts.Cancel();
|
// Invoke the cancellation on the default thread pool to workaround https://github.com/dotnet/corefx/issues/29699
|
||||||
|
ThreadPool.QueueUserWorkItem(_ => _cts.Cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> MainInternalAsync(
|
private async Task<int> MainInternalAsync(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -17,16 +17,26 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
{
|
{
|
||||||
private Process _process;
|
private Process _process;
|
||||||
private readonly ProcessSpec _spec;
|
private readonly ProcessSpec _spec;
|
||||||
|
private readonly List<string> _lines;
|
||||||
private BufferBlock<string> _source;
|
private BufferBlock<string> _source;
|
||||||
private ITestOutputHelper _logger;
|
private ITestOutputHelper _logger;
|
||||||
|
private TaskCompletionSource<int> _exited;
|
||||||
|
|
||||||
public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger)
|
public AwaitableProcess(ProcessSpec spec, ITestOutputHelper logger)
|
||||||
{
|
{
|
||||||
_spec = spec;
|
_spec = spec;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_source = new BufferBlock<string>();
|
_source = new BufferBlock<string>();
|
||||||
|
_lines = new List<string>();
|
||||||
|
_exited = new TaskCompletionSource<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> Output => _lines;
|
||||||
|
|
||||||
|
public Task Exited => _exited.Task;
|
||||||
|
|
||||||
|
public int Id => _process.Id;
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
if (_process != null)
|
if (_process != null)
|
||||||
|
|
@ -52,6 +62,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
foreach (var env in _spec.EnvironmentVariables)
|
||||||
|
{
|
||||||
|
_process.StartInfo.EnvironmentVariables[env.Key] = env.Value;
|
||||||
|
}
|
||||||
|
|
||||||
_process.OutputDataReceived += OnData;
|
_process.OutputDataReceived += OnData;
|
||||||
_process.ErrorDataReceived += OnData;
|
_process.ErrorDataReceived += OnData;
|
||||||
_process.Exited += OnExit;
|
_process.Exited += OnExit;
|
||||||
|
|
@ -65,13 +80,13 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
public async Task<string> GetOutputLineAsync(string message, TimeSpan timeout)
|
public async Task<string> GetOutputLineAsync(string message, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
_logger.WriteLine($"Waiting for output line [msg == '{message}']. Will wait for {timeout.TotalSeconds} sec.");
|
_logger.WriteLine($"Waiting for output line [msg == '{message}']. Will wait for {timeout.TotalSeconds} sec.");
|
||||||
return await GetOutputLineAsync(m => message == m).TimeoutAfter(timeout);
|
return await GetOutputLineAsync(m => string.Equals(m, message, StringComparison.Ordinal)).TimeoutAfter(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetOutputLineStartsWithAsync(string message, TimeSpan timeout)
|
public async Task<string> GetOutputLineStartsWithAsync(string message, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
_logger.WriteLine($"Waiting for output line [msg.StartsWith('{message}')]. Will wait for {timeout.TotalSeconds} sec.");
|
_logger.WriteLine($"Waiting for output line [msg.StartsWith('{message}')]. Will wait for {timeout.TotalSeconds} sec.");
|
||||||
return await GetOutputLineAsync(m => m.StartsWith(message)).TimeoutAfter(timeout);
|
return await GetOutputLineAsync(m => m != null && m.StartsWith(message, StringComparison.Ordinal)).TimeoutAfter(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetOutputLineAsync(Predicate<string> predicate)
|
private async Task<string> GetOutputLineAsync(Predicate<string> predicate)
|
||||||
|
|
@ -81,6 +96,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
while (await _source.OutputAvailableAsync())
|
while (await _source.OutputAvailableAsync())
|
||||||
{
|
{
|
||||||
var next = await _source.ReceiveAsync();
|
var next = await _source.ReceiveAsync();
|
||||||
|
_lines.Add(next);
|
||||||
_logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
|
_logger.WriteLine($"{DateTime.Now}: recv: '{next}'");
|
||||||
if (predicate(next))
|
if (predicate(next))
|
||||||
{
|
{
|
||||||
|
|
@ -119,6 +135,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
// Wait to ensure the process has exited and all output consumed
|
// Wait to ensure the process has exited and all output consumed
|
||||||
_process.WaitForExit();
|
_process.WaitForExit();
|
||||||
_source.Complete();
|
_source.Complete();
|
||||||
|
_exited.TrySetResult(_process.ExitCode);
|
||||||
|
_logger.WriteLine($"Process {_process.Id} has exited");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -135,6 +153,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
_process.ErrorDataReceived -= OnData;
|
_process.ErrorDataReceived -= OnData;
|
||||||
_process.OutputDataReceived -= OnData;
|
_process.OutputDataReceived -= OnData;
|
||||||
_process.Exited -= OnExit;
|
_process.Exited -= OnExit;
|
||||||
|
_process.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
await _app.PrepareAsync();
|
await _app.PrepareAsync();
|
||||||
_app.Start(new [] { "--list" });
|
_app.Start(new [] { "--list" });
|
||||||
var lines = await _app.Process.GetAllOutputLines();
|
var lines = await _app.Process.GetAllOutputLines();
|
||||||
|
var files = lines.Where(l => !l.StartsWith("watch :"));
|
||||||
|
|
||||||
AssertEx.EqualFileList(
|
AssertEx.EqualFileList(
|
||||||
_app.Scenario.WorkFolder,
|
_app.Scenario.WorkFolder,
|
||||||
|
|
@ -111,7 +112,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
"GlobbingApp/include/Foo.cs",
|
"GlobbingApp/include/Foo.cs",
|
||||||
"GlobbingApp/GlobbingApp.csproj",
|
"GlobbingApp/GlobbingApp.csproj",
|
||||||
},
|
},
|
||||||
lines);
|
files);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
|
@ -15,10 +17,34 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
private readonly WatchableApp _app;
|
private readonly WatchableApp _app;
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
public NoDepsAppTests(ITestOutputHelper logger)
|
public NoDepsAppTests(ITestOutputHelper logger)
|
||||||
{
|
{
|
||||||
_app = new WatchableApp("NoDepsApp", logger);
|
_app = new WatchableApp("NoDepsApp", logger);
|
||||||
|
_output = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "Testing SIGINT is specific to macOS/Linux")]
|
||||||
|
public async Task KillsProcessOnSigInt()
|
||||||
|
{
|
||||||
|
void SendSigInt(int pid)
|
||||||
|
{
|
||||||
|
_output.WriteLine($"kill -SIGINT {pid}");
|
||||||
|
Process.Start("kill", $"-SIGINT {pid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _app.StartWatcherAsync(new[] { "--no-exit" });
|
||||||
|
|
||||||
|
var childPid = await _app.GetProcessId();
|
||||||
|
|
||||||
|
SendSigInt(_app.Process.Id);
|
||||||
|
SendSigInt(childPid);
|
||||||
|
|
||||||
|
await _app.Process.Exited.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
Assert.DoesNotContain(_app.Process.Output, l => l.StartsWith("Exited with error code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -33,6 +59,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
File.WriteAllText(fileToChange, programCs);
|
File.WriteAllText(fileToChange, programCs);
|
||||||
|
|
||||||
await _app.HasRestarted();
|
await _app.HasRestarted();
|
||||||
|
Assert.DoesNotContain(_app.Process.Output, l => l.StartsWith("Exited with error code"));
|
||||||
|
|
||||||
var pid2 = await _app.GetProcessId();
|
var pid2 = await _app.GetProcessId();
|
||||||
Assert.NotEqual(pid, pid2);
|
Assert.NotEqual(pid, pid2);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -12,7 +11,6 @@ using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
using Microsoft.Extensions.Tools.Internal;
|
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,25 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
|
||||||
{
|
{
|
||||||
_tempDir
|
_tempDir
|
||||||
.WithCSharpProject("testproj")
|
.WithCSharpProject("testproj")
|
||||||
.WithTargetFrameworks("netcoreapp1.0")
|
.WithTargetFrameworks("netcoreapp2.2")
|
||||||
.Dir()
|
.Dir()
|
||||||
.WithFile("Program.cs")
|
.WithFile("Program.cs")
|
||||||
.Create();
|
.Create();
|
||||||
|
|
||||||
var stdout = new StringBuilder();
|
var output = new StringBuilder();
|
||||||
_console.Out = new StringWriter(stdout);
|
_console.Error = _console.Out = new StringWriter(output);
|
||||||
var program = new Program(_console, _tempDir.Root)
|
using (var app = new Program(_console, _tempDir.Root))
|
||||||
.RunAsync(new[] { "run" });
|
{
|
||||||
|
var run = app.RunAsync(new[] { "run" });
|
||||||
|
|
||||||
await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
|
await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
_console.ConsoleCancelKey();
|
_console.ConsoleCancelKey();
|
||||||
|
|
||||||
var exitCode = await program.TimeoutAfter(TimeSpan.FromSeconds(30));
|
var exitCode = await run.TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
Assert.Contains("Shutdown requested. Press Ctrl+C again to force exit.", stdout.ToString());
|
Assert.Contains("Shutdown requested. Press Ctrl+C again to force exit.", output.ToString());
|
||||||
Assert.Equal(0, exitCode);
|
Assert.Equal(0, exitCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue