/// <summary> /// Returns the <see cref="AutoResetEvent"/> that signals when the process exits. /// </summary> private static AutoResetEvent WaitForExit(Process process) { Covenant.Requires <ArgumentNullException>(process != null, nameof(process)); return(new AutoResetEvent(false) { SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, ownsHandle: false) }); }
/// <summary> /// Starts a remote process by executing a command line and then wiring up a pseudo /// TTY that forwards keystrokes to the remote process and also receives VTx formatted /// output from the process and handle rendering on the local <see cref="Console"/>. /// </summary> /// <param name="command"> /// <para> /// Specifies the local command to execute as the remote process. /// </para> /// <note> /// You must take care to quote the command executable path or any arguments that /// include spaces. /// </note> /// </param> /// <param name="keyMap"> /// Optionally specifies the map to be used for translating keystrokes into /// <a href="https://www.ecma-international.org/publications-and-standards/standards/ecma-48/">ECMA-48</a> /// (or other) control sequences. This defaults to <see cref="DefaultKeyMap"/> but you /// may pass a custom map when required. /// </param> /// <remarks> /// <para> /// If the command path specifies an absolute or relative directory then the command /// will be execute from there, otherwise the method will first attempt executing the /// command from the current directory before searching the PATH for the command. /// </para> /// <para> /// You may omit the command file extension and the method will try <b>.exe</b>, /// <b>.cmd</b>, and <b>.bat</b> extensions in that order. /// </para> /// </remarks> public void Run(string command, IDictionary <ConsoleKeyInfo, string> keyMap = null) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(command), nameof(command)); command = NormalizeCommand(command); using (var inputPipe = new PseudoConsolePipe()) using (var outputPipe = new PseudoConsolePipe()) using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, (short)Console.WindowWidth, (short)Console.WindowHeight)) using (var process = Process.Start(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle)) { // Copy all output from the remote process to the console. Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide)); // Process user key presses as required and forward them to the remote process. Task.Run(() => CopyInputToPipe(inputPipe.WriteSide, keyMap ?? DefaultKeyMap)); // Free resources in case the console is ungracefully closed (e.g. by the 'X' in the window titlebar). OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe)); // We need to detect when the console is resized by the user and // then resize the pseudo TTY to match. var processExitEvent = WaitForExit(process); var consoleWidth = Console.WindowWidth; var consoleHeight = Console.WindowHeight; while (!processExitEvent.WaitOne(500)) { var newWidth = Console.WindowWidth; var newHeight = Console.WindowHeight; if (consoleHeight != newHeight || consoleWidth != newWidth) { pseudoConsole.Resize((short)newWidth, (short)newHeight); consoleWidth = newWidth; consoleHeight = newHeight; } } } }