| File: UI\PhysicalConsole.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\Watch\Microsoft.DotNet.HotReload.Watch.csproj (Microsoft.DotNet.HotReload.Watch) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.Diagnostics; namespace Microsoft.DotNet.Watch; internal sealed class PhysicalConsole : IConsole { public const char CtrlC = '\x03'; public const char CtrlR = '\x12'; public event Action<ConsoleKeyInfo>? KeyPressed; public PhysicalConsole(TestFlags testFlags) { Console.OutputEncoding = Encoding.UTF8; if (testFlags.HasFlag(TestFlags.ReadKeyFromStdin)) { _ = ListenToStandardInputAsync(); } else if (!Console.IsInputRedirected) { _ = ListenToConsoleKeyPressAsync(); } } private async Task ListenToStandardInputAsync() { using var stream = Console.OpenStandardInput(); var buffer = new byte[1]; while (true) { var bytesRead = await stream.ReadAsync(buffer, CancellationToken.None); if (bytesRead != 1) { break; } var c = (char)buffer[0]; // emulate propagation of Ctrl+C/SIGTERM to child processes if (c == CtrlC) { Console.WriteLine("Received CTRL+C key"); foreach (var processId in ProcessRunner.GetRunningApplicationProcesses()) { string? error; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Console.WriteLine($"Sending Ctrl+C to {processId}"); error = ProcessUtilities.SendWindowsCtrlCEvent(processId); } else { Console.WriteLine($"Sending SIGTERM to {processId}"); error = null; try { using var process = Process.GetProcessById(processId); process.SafeHandle.Signal(PosixSignal.SIGTERM); } catch (Win32Exception ex) { // Signal returns false when the given process has already exited. // So it can throw only when we try to kill a non-child process // that we don't have permission to kill. error = ex.Message; } catch (ArgumentException) { // Process has already exited } } if (error != null) { throw new InvalidOperationException(error); } } } // handle all input keys that watcher might consume: var key = c switch { CtrlC => new ConsoleKeyInfo('C', ConsoleKey.C, shift: false, alt: false, control: true), CtrlR => new ConsoleKeyInfo('R', ConsoleKey.R, shift: false, alt: false, control: true), '\r' or '\n' => new ConsoleKeyInfo('\r', ConsoleKey.Enter, shift: false, alt: false, control: false), >= 'a' and <= 'z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'a'), shift: false, alt: false, control: false), >= 'A' and <= 'Z' => new ConsoleKeyInfo(c, ConsoleKey.A + (c - 'A'), shift: true, alt: false, control: false), >= '0' and <= '9' => new ConsoleKeyInfo(c, ConsoleKey.NumPad0 + (c - '0'), shift: false, alt: false, control: false), '.' => new ConsoleKeyInfo('.', ConsoleKey.OemPeriod, shift: false, alt: false, control: false), '-' => new ConsoleKeyInfo('-', ConsoleKey.OemMinus, shift: false, alt: false, control: false), _ => default }; if (key.Key != ConsoleKey.None) { KeyPressed?.Invoke(key); } } } private Task ListenToConsoleKeyPressAsync() { Console.CancelKeyPress += (s, e) => { e.Cancel = true; KeyPressed?.Invoke(new ConsoleKeyInfo(CtrlC, ConsoleKey.C, shift: false, alt: false, control: true)); }; return Task.Factory.StartNew(() => { while (true) { var key = Console.ReadKey(intercept: true); KeyPressed?.Invoke(key); } }, TaskCreationOptions.LongRunning); } public TextWriter Error => Console.Error; public TextWriter Out => Console.Out; public ConsoleColor ForegroundColor { get => Console.ForegroundColor; set => Console.ForegroundColor = value; } public void ResetColor() => Console.ResetColor(); public void Clear() => Console.Clear(); }