Skip to content
This repository has been archived by the owner on Apr 20, 2023. It is now read-only.

Improve Ctrl-C experience for dotnet run. #10544

Merged
merged 1 commit into from
Jan 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions TestAssets/TestProjects/TestAppThatWaits/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Diagnostics;
using System.Threading;

namespace TestAppThatWaits
{
class Program
{
static void Main(string[] args)
{
Console.CancelKeyPress += HandleCancelKeyPress;
Console.WriteLine(Process.GetCurrentProcess().Id);
Console.Out.Flush();
Console.Read();
Thread.Sleep(10000);
}

static void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
Console.WriteLine("Interrupted!");
Environment.Exit(42);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>

</Project>
12 changes: 11 additions & 1 deletion src/Microsoft.DotNet.Cli.Utils/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
Expand All @@ -28,7 +29,6 @@ public Command(Process process)

public CommandResult Execute()
{

Reporter.Verbose.WriteLine(string.Format(
LocalizableStrings.RunningFileNameArguments,
_process.StartInfo.FileName,
Expand All @@ -40,6 +40,8 @@ public CommandResult Execute()

_process.EnableRaisingEvents = true;

Console.CancelKeyPress += HandleCancelKeyPress;

#if DEBUG
var sw = Stopwatch.StartNew();

Expand All @@ -61,6 +63,8 @@ public CommandResult Execute()
taskErr?.Wait();
}

Console.CancelKeyPress -= HandleCancelKeyPress;

var exitCode = _process.ExitCode;

#if DEBUG
Expand Down Expand Up @@ -211,5 +215,11 @@ private void ThrowIfRunning([CallerMemberName] string memberName = null)
memberName));
}
}

private void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
// Ignore SIGINT/SIGQUIT so that the child can process the signal
e.Cancel = true;
}
}
}
60 changes: 60 additions & 0 deletions test/dotnet-run.Tests/GivenDotnetRunIsInterrupted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using FluentAssertions;
using Microsoft.DotNet.Tools.Test.Utilities;

namespace Microsoft.DotNet.Cli.Run.Tests
{
public class GivenDotnetRunIsInterrupted : TestBase
{
// This test is Unix only for the same reason that CoreFX does not test Console.CancelKeyPress on Windows
// See https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.Console/tests/CancelKeyPress.Unix.cs#L63-L67
[UnixOnlyFact]
public void ItIgnoresSIGINT()
{
var asset = TestAssets.Get("TestAppThatWaits")
.CreateInstance()
.WithSourceFiles();

var command = new RunCommand()
.WithWorkingDirectory(asset.Root.FullName);

bool killed = false;
command.OutputDataReceived += (s, e) =>
{
if (killed)
{
return;
}

// Simulate a SIGINT sent to a process group (i.e. both `dotnet run` and `TestAppThatWaits`).
// Ideally we would send SIGINT to an actual process group, but the new child process (i.e. `dotnet run`)
// will inherit the current process group from the `dotnet test` process that is running this test.
// We would need to fork(), setpgid(), and then execve() to break out of the current group and that is
// too complex for a simple unit test.
kill(command.CurrentProcess.Id, SIGINT).Should().Be(0); // dotnet run
kill(Convert.ToInt32(e.Data), SIGINT).Should().Be(0); // TestAppThatWaits

killed = true;
};

command
.ExecuteWithCapturedOutput()
.Should()
.ExitWith(42)
.And
.HaveStdOutContaining("Interrupted!");

killed.Should().BeTrue();
}

[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);

private const int SIGINT = 2;
}
}