using GUIConsole.ConPTY.Processes; using Microsoft.Win32.SafeHandles; using System; using System.IO; using System.Threading; using static GUIConsole.ConPTY.Native.ConsoleApi; namespace GUIConsole.ConPTY { /// /// Class for managing communication with the underlying console, and communicating with its pseudoconsole. /// public sealed class Terminal { private const string ExitCommand = "exit\r"; private const string CtrlC_Command = "\x3"; private SafeFileHandle _consoleInputPipeWriteHandle; private StreamWriter _consoleInputWriter; /// /// A stream of VT-100-enabled output from the console. /// public FileStream ConsoleOutStream { get; private set; } /// /// Fired once the console has been hooked up and is ready to receive input. /// public event EventHandler OutputReady; public Terminal() { } /// /// Start the pseudoconsole and run the process as shown in /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-pseudoconsole /// /// the command to run, e.g. cmd.exe /// The height (in characters) to start the pseudoconsole with. Defaults to 80. /// The width (in characters) to start the pseudoconsole with. Defaults to 30. public void Start(string command, int consoleWidth = 80, int consoleHeight = 30) { using (var inputPipe = new PseudoConsolePipe()) using (var outputPipe = new PseudoConsolePipe()) using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, consoleWidth, consoleHeight)) using (var process = ProcessFactory.Start(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle)) { // copy all pseudoconsole output to a FileStream and expose it to the rest of the app ConsoleOutStream = new FileStream(outputPipe.ReadSide, FileAccess.Read); OutputReady.Invoke(this, EventArgs.Empty); // Store input pipe handle, and a writer for later reuse _consoleInputPipeWriteHandle = inputPipe.WriteSide; _consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write)) { AutoFlush = true }; // free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar) OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe, _consoleInputWriter)); WaitForExit(process).WaitOne(Timeout.Infinite); } } /// /// Sends the given string to the anonymous pipe that writes to the active pseudoconsole. /// /// A string of characters to write to the console. Supports VT-100 codes. public void WriteToPseudoConsole(string input) { if (_consoleInputWriter == null) { throw new InvalidOperationException("There is no writer attached to a pseudoconsole. Have you called Start on this instance yet?"); } _consoleInputWriter.Write(input); } /// /// Get an AutoResetEvent that signals when the process exits /// private static AutoResetEvent WaitForExit(Process process) => new AutoResetEvent(false) { SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, ownsHandle: false) }; /// /// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button). /// Intended for resource cleanup logic. /// private static void OnClose(Action handler) { SetConsoleCtrlHandler(eventType => { if (eventType == CtrlTypes.CTRL_CLOSE_EVENT) { handler(); } return false; }, true); } private void DisposeResources(params IDisposable[] disposables) { foreach (var disposable in disposables) { disposable.Dispose(); } } } }