Fix race conditions between CTRL+C and RunAsync in dotnet-watch

Increase timeouts in testing dotnet-run and improve verbosity of test logging
This commit is contained in:
Nate McMaster 2017-04-27 17:01:55 -07:00
parent 4569b0e4de
commit 88f43ffc1a
11 changed files with 143 additions and 114 deletions

View File

@ -38,6 +38,9 @@ Environment variables:
changes. This is required for some file systems, such as network shares, changes. This is required for some file systems, such as network shares,
Docker mounted volumes, and other virtual file systems. Docker mounted volumes, and other virtual file systems.
DOTNET_WATCH
dotnet-watch sets this variable to '1' on all child processes launched.
Remarks: Remarks:
The special option '--' is used to delimit the end of the options and The special option '--' is used to delimit the end of the options and
the beginning of arguments that will be passed to the child dotnet process. the beginning of arguments that will be passed to the child dotnet process.

View File

@ -12,10 +12,12 @@ using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher namespace Microsoft.DotNet.Watcher
{ {
public class Program public class Program : IDisposable
{ {
private readonly IConsole _console; private readonly IConsole _console;
private readonly string _workingDir; private readonly string _workingDir;
private readonly CancellationTokenSource _cts;
private IReporter _reporter;
public Program(IConsole console, string workingDir) public Program(IConsole console, string workingDir)
{ {
@ -24,15 +26,21 @@ namespace Microsoft.DotNet.Watcher
_console = console; _console = console;
_workingDir = workingDir; _workingDir = workingDir;
_cts = new CancellationTokenSource();
_console.CancelKeyPress += OnCancelKeyPress;
_reporter = CreateReporter(verbose: true, quiet: false, console: _console);
} }
public static int Main(string[] args) public static int Main(string[] args)
{ {
DebugHelper.HandleDebugSwitch(ref args); DebugHelper.HandleDebugSwitch(ref args);
return new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()) using (var program = new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()))
.RunAsync(args) {
.GetAwaiter() return program
.GetResult(); .RunAsync(args)
.GetAwaiter()
.GetResult();
}
} }
public async Task<int> RunAsync(string[] args) public async Task<int> RunAsync(string[] args)
@ -44,8 +52,7 @@ namespace Microsoft.DotNet.Watcher
} }
catch (CommandParsingException ex) catch (CommandParsingException ex)
{ {
CreateReporter(verbose: true, quiet: false, console: _console) _reporter.Error(ex.Message);
.Error(ex.Message);
return 1; return 1;
} }
@ -60,54 +67,55 @@ namespace Microsoft.DotNet.Watcher
return 2; return 2;
} }
var reporter = CreateReporter(options.IsVerbose, options.IsQuiet, _console); // update reporter as configured by options
_reporter = CreateReporter(options.IsVerbose, options.IsQuiet, _console);
using (CancellationTokenSource ctrlCTokenSource = new CancellationTokenSource()) try
{ {
_console.CancelKeyPress += (sender, ev) => if (_cts.IsCancellationRequested)
{ {
if (!ctrlCTokenSource.IsCancellationRequested)
{
reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
ev.Cancel = true;
}
else
{
ev.Cancel = false;
}
ctrlCTokenSource.Cancel();
};
try
{
if (options.ListFiles)
{
return await ListFilesAsync(reporter,
options.Project,
ctrlCTokenSource.Token);
}
else
{
return await MainInternalAsync(reporter,
options.Project,
options.RemainingArguments,
ctrlCTokenSource.Token);
}
}
catch (Exception ex)
{
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// swallow when only exception is the CTRL+C forced an exit
return 0;
}
reporter.Error(ex.ToString());
reporter.Error("An unexpected error occurred");
return 1; return 1;
} }
if (options.ListFiles)
{
return await ListFilesAsync(_reporter,
options.Project,
_cts.Token);
}
else
{
return await MainInternalAsync(_reporter,
options.Project,
options.RemainingArguments,
_cts.Token);
}
} }
catch (Exception ex)
{
if (ex is TaskCanceledException || ex is OperationCanceledException)
{
// swallow when only exception is the CTRL+C forced an exit
return 0;
}
_reporter.Error(ex.ToString());
_reporter.Error("An unexpected error occurred");
return 1;
}
}
private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
{
// suppress CTRL+C on the first press
args.Cancel = !_cts.IsCancellationRequested;
if (args.Cancel)
{
_reporter.Output("Shutdown requested. Press Ctrl+C again to force exit.");
}
_cts.Cancel();
} }
private async Task<int> MainInternalAsync( private async Task<int> MainInternalAsync(
@ -181,5 +189,11 @@ namespace Microsoft.DotNet.Watcher
private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console) private static IReporter CreateReporter(bool verbose, bool quiet, IConsole console)
=> new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet); => new PrefixConsoleReporter(console, verbose || CliContext.IsGlobalVerbose(), quiet);
public void Dispose()
{
_console.CancelKeyPress -= OnCancelKeyPress;
_cts.Dispose();
}
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -22,13 +23,13 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact] [Fact]
public async Task ChangeFileInDependency() public async Task ChangeFileInDependency()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var fileToChange = Path.Combine(_app.DependencyFolder, "Foo.cs"); var fileToChange = Path.Combine(_app.DependencyFolder, "Foo.cs");
var programCs = File.ReadAllText(fileToChange); var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs); File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
} }
public void Dispose() public void Dispose()

View File

@ -23,7 +23,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
Assert.True(string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_WATCH")), "DOTNET_WATCH cannot be set already when this test is running"); Assert.True(string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_WATCH")), "DOTNET_WATCH cannot be set already when this test is running");
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
const string messagePrefix = "DOTNET_WATCH = "; const string messagePrefix = "DOTNET_WATCH = ";
var message = await _app.Process.GetOutputLineAsync(m => m.StartsWith(messagePrefix)); var message = await _app.Process.GetOutputLineAsync(m => m.StartsWith(messagePrefix));
var envValue = message.Substring(messagePrefix.Length); var envValue = message.Substring(messagePrefix.Length);

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Microsoft.DotNet.Watcher.Tools.Tests; using Microsoft.DotNet.Watcher.Tools.Tests;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -13,6 +14,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
public class GlobbingAppTests : IDisposable public class GlobbingAppTests : IDisposable
{ {
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
private GlobbingApp _app; private GlobbingApp _app;
public GlobbingAppTests(ITestOutputHelper logger) public GlobbingAppTests(ITestOutputHelper logger)
{ {
@ -25,84 +28,84 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[InlineData(false)] [InlineData(false)]
public async Task ChangeCompiledFile(bool usePollingWatcher) public async Task ChangeCompiledFile(bool usePollingWatcher)
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var programCs = File.ReadAllText(fileToChange); var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs); File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
} }
[Fact(Skip = "Broken. See https://github.com/aspnet/DotNetTools/issues/212")] [Fact(Skip = "Broken. See https://github.com/aspnet/DotNetTools/issues/212")]
public async Task AddCompiledFile() public async Task AddCompiledFile()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Bar.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Bar.cs");
File.WriteAllText(fileToChange, "public class Bar {}"); File.WriteAllText(fileToChange, "public class Bar {}");
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(3, types); Assert.Equal(3, types);
} }
[Fact] [Fact]
public async Task DeleteCompiledFile() public async Task DeleteCompiledFile()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
File.Delete(fileToChange); File.Delete(fileToChange);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(1, types); Assert.Equal(1, types);
} }
[Fact] [Fact]
public async Task DeleteSourceFolder() public async Task DeleteSourceFolder()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); var types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(2, types); Assert.Equal(2, types);
var folderToDelete = Path.Combine(_app.SourceDirectory, "include"); var folderToDelete = Path.Combine(_app.SourceDirectory, "include");
Directory.Delete(folderToDelete, recursive: true); Directory.Delete(folderToDelete, recursive: true);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
types = await _app.GetCompiledAppDefinedTypes().OrTimeout(); types = await _app.GetCompiledAppDefinedTypes().TimeoutAfter(DefaultTimeout);
Assert.Equal(1, types); Assert.Equal(1, types);
} }
[Fact] [Fact]
public async Task RenameCompiledFile() public async Task RenameCompiledFile()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs"); var oldFile = Path.Combine(_app.SourceDirectory, "include", "Foo.cs");
var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs"); var newFile = Path.Combine(_app.SourceDirectory, "include", "Foo_new.cs");
File.Move(oldFile, newFile); File.Move(oldFile, newFile);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
} }
[Fact] [Fact]
public async Task ChangeExcludedFile() public async Task ChangeExcludedFile()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs"); var changedFile = Path.Combine(_app.SourceDirectory, "exclude", "Baz.cs");
File.WriteAllText(changedFile, ""); File.WriteAllText(changedFile, "");

View File

@ -5,6 +5,7 @@ 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 Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -12,6 +13,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
public class NoDepsAppTests : IDisposable public class NoDepsAppTests : IDisposable
{ {
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
private readonly WatchableApp _app; private readonly WatchableApp _app;
public NoDepsAppTests(ITestOutputHelper logger) public NoDepsAppTests(ITestOutputHelper logger)
@ -23,16 +26,16 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact] [Fact]
public async Task RestartProcessOnFileChange() public async Task RestartProcessOnFileChange()
{ {
await _app.StartWatcherAsync(new[] { "--no-exit" }).OrTimeout(); await _app.StartWatcherAsync(new[] { "--no-exit" });
var pid = await _app.GetProcessId().OrTimeout(); var pid = await _app.GetProcessId().TimeoutAfter(DefaultTimeout);
// Then wait for it to restart when we change a file // Then wait for it to restart when we change a file
var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs");
var programCs = File.ReadAllText(fileToChange); var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs); File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
var pid2 = await _app.GetProcessId().OrTimeout(); var pid2 = await _app.GetProcessId().TimeoutAfter(DefaultTimeout);
Assert.NotEqual(pid, pid2); Assert.NotEqual(pid, pid2);
// first app should have shut down // first app should have shut down
@ -42,18 +45,18 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
[Fact] [Fact]
public async Task RestartProcessThatTerminatesAfterFileChange() public async Task RestartProcessThatTerminatesAfterFileChange()
{ {
await _app.StartWatcherAsync().OrTimeout(); await _app.StartWatcherAsync();
var pid = await _app.GetProcessId().OrTimeout(); var pid = await _app.GetProcessId().TimeoutAfter(DefaultTimeout);
await _app.HasExited().OrTimeout(); // process should exit after run await _app.HasExited().TimeoutAfter(DefaultTimeout); // process should exit after run
var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs"); var fileToChange = Path.Combine(_app.SourceDirectory, "Program.cs");
var programCs = File.ReadAllText(fileToChange); var programCs = File.ReadAllText(fileToChange);
File.WriteAllText(fileToChange, programCs); File.WriteAllText(fileToChange, programCs);
await _app.HasRestarted().OrTimeout(); await _app.HasRestarted();
var pid2 = await _app.GetProcessId().OrTimeout(); var pid2 = await _app.GetProcessId().TimeoutAfter(DefaultTimeout);
Assert.NotEqual(pid, pid2); Assert.NotEqual(pid, pid2);
await _app.HasExited().OrTimeout(); // process should exit after run await _app.HasExited().TimeoutAfter(DefaultTimeout); // process should exit after run
} }
public void Dispose() public void Dispose()

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -38,7 +39,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public string SourceDirectory { get; } public string SourceDirectory { get; }
public Task HasRestarted() public Task HasRestarted()
=> Process.GetOutputLineAsync(StartedMessage); => Process.GetOutputLineAsync(StartedMessage).TimeoutAfter(TimeSpan.FromMinutes(2));
public Task HasExited() public Task HasExited()
=> Process.GetOutputLineAsync(ExitingMessage); => Process.GetOutputLineAsync(ExitingMessage);
@ -74,7 +75,11 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
Executable = DotNetMuxer.MuxerPathOrDefault(), Executable = DotNetMuxer.MuxerPathOrDefault(),
Arguments = args, Arguments = args,
WorkingDirectory = SourceDirectory WorkingDirectory = SourceDirectory,
EnvironmentVariables =
{
["DOTNET_CLI_CONTEXT_VERBOSE"] = bool.TrueString
},
}; };
Process = new AwaitableProcess(spec, _logger); Process = new AwaitableProcess(spec, _logger);
@ -88,11 +93,15 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{ {
var args = new[] { "run", "--" }.Concat(arguments); var args = new[] { "run", "--" }.Concat(arguments);
Start(args, name); Start(args, name);
await Process.GetOutputLineAsync(StartedMessage);
// Make this timeout long because it depends much on the MSBuild compilation speed.
// Slow machines may take a bit to compile and boot test apps
await Process.GetOutputLineAsync(StartedMessage).TimeoutAfter(TimeSpan.FromMinutes(2));
} }
public virtual void Dispose() public virtual void Dispose()
{ {
_logger?.WriteLine("Disposing WatchableApp");
Process?.Dispose(); Process?.Dispose();
Scenario.Dispose(); Scenario.Dispose();
} }

View File

@ -15,6 +15,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.DotNet.Watcher.Tools\Microsoft.DotNet.Watcher.Tools.csproj" /> <ProjectReference Include="..\..\src\Microsoft.DotNet.Watcher.Tools\Microsoft.DotNet.Watcher.Tools.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" /> <PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" /> <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Tools.Internal; using Microsoft.Extensions.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -35,18 +36,20 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
var stdout = new StringBuilder(); var stdout = new StringBuilder();
_console.Out = new StringWriter(stdout); _console.Out = new StringWriter(stdout);
var program = new Program(_console, _tempDir.Root) var program = new Program(_console, _tempDir.Root)
.RunAsync(new [] { "run" }); .RunAsync(new[] { "run" });
await _console.CancelKeyPressSubscribed.TimeoutAfter(TimeSpan.FromSeconds(30));
_console.ConsoleCancelKey(); _console.ConsoleCancelKey();
var exitCode = await program.OrTimeout(); var exitCode = await program.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.", stdout.ToString());
Assert.Equal(0, exitCode); Assert.Equal(0, exitCode);
} }
public void Dispose() public void Dispose()
{ {
_tempDir.Dispose(); _tempDir.Dispose();
} }
} }
} }

View File

@ -1,25 +0,0 @@
// 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.
using System.Runtime.CompilerServices;
namespace System.Threading.Tasks
{
public static class TaskExtensions
{
public static async Task<T> OrTimeout<T>(this Task<T> task, int timeout = 30, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0)
{
await OrTimeout((Task)task, timeout, file, line);
return task.Result;
}
public static async Task OrTimeout(this Task task, int timeout = 30, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0)
{
var finished = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(timeout)));
if (!ReferenceEquals(finished, task))
{
throw new TimeoutException($"Task exceeded max running time of {timeout}s at {file}:{line}");
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using System.Text; using System.Text;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -12,6 +13,9 @@ namespace Microsoft.Extensions.Tools.Internal
{ {
public class TestConsole : IConsole public class TestConsole : IConsole
{ {
private event ConsoleCancelEventHandler _cancelKeyPress;
private readonly TaskCompletionSource<bool> _cancelKeySubscribed = new TaskCompletionSource<bool>();
public TestConsole(ITestOutputHelper output) public TestConsole(ITestOutputHelper output)
{ {
var writer = new TestOutputWriter(output); var writer = new TestOutputWriter(output);
@ -19,7 +23,17 @@ namespace Microsoft.Extensions.Tools.Internal
Out = writer; Out = writer;
} }
public event ConsoleCancelEventHandler CancelKeyPress; public event ConsoleCancelEventHandler CancelKeyPress
{
add
{
_cancelKeyPress += value;
_cancelKeySubscribed.TrySetResult(true);
}
remove => _cancelKeyPress -= value;
}
public Task CancelKeyPressSubscribed => _cancelKeySubscribed.Task;
public TextWriter Error { get; set; } public TextWriter Error { get; set; }
public TextWriter Out { get; set; } public TextWriter Out { get; set; }
@ -36,7 +50,7 @@ namespace Microsoft.Extensions.Tools.Internal
.DeclaredConstructors .DeclaredConstructors
.Single(c => c.GetParameters().First().ParameterType == typeof(ConsoleSpecialKey)); .Single(c => c.GetParameters().First().ParameterType == typeof(ConsoleSpecialKey));
var args = (ConsoleCancelEventArgs)ctor.Invoke(new object[] { ConsoleSpecialKey.ControlC }); var args = (ConsoleCancelEventArgs)ctor.Invoke(new object[] { ConsoleSpecialKey.ControlC });
CancelKeyPress?.Invoke(this, args); _cancelKeyPress.Invoke(this, args);
return args; return args;
} }