sources/Google.Solutions.Platform/Dispatch/Win32PseudoConsole.cs (206 lines of code) (raw):

// // Copyright 2024 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Runtime; using Google.Solutions.Platform.Interop; using Google.Solutions.Platform.IO; using Microsoft.Win32.SafeHandles; using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; #pragma warning disable VSTHRD004 // Return task namespace Google.Solutions.Platform.Dispatch { /// <summary> /// A Win32 pseudo-console for interacting with a process. /// /// Note: Pseudo consoles don't work properly in NUnit tests! /// </summary> public class Win32PseudoConsole : DisposableBase, IPseudoTerminal { /// <summary> /// Encoding used by terminal, which is always UTF-8 (not UCS-2!) /// without BOM. /// </summary> internal static Encoding Encoding = new UTF8Encoding(false); private readonly Task pumpOutputTask; private readonly TextReader outputReader; private readonly TextWriter inputWriter; internal AnonymousPipe InputPipe { get; } internal AnonymousPipe OutputPipe { get; } internal PseudoConsoleHandle Handle { get; } public Win32PseudoConsole(PseudoTerminalSize size) { var stdin = new AnonymousPipe(); var stdout = new AnonymousPipe(); try { try { var hresult = NativeMethods.CreatePseudoConsole( new NativeMethods.COORD { X = (short)size.Width, Y = (short)size.Height }, stdin.ReadSideHandle, stdout.WriteSideHandle, 0, out var handle); if (hresult.Failed()) { throw PseudoTerminalException.FromHresult( hresult, "Failed to create pseudo console"); } stdout.CloseWriteSide(); stdin.CloseReadSide(); this.Handle = handle; this.InputPipe = stdin; this.OutputPipe = stdout; this.inputWriter = new StreamWriter(stdin.WriteSide, Encoding) { AutoFlush = true }; this.outputReader = new StreamReader(stdout.ReadSide, Encoding); this.pumpOutputTask = PumpEventsAsync(); } catch (EntryPointNotFoundException) { throw new PseudoTerminalException( "This feature requires Windows 10 version 1809 or newer"); } } catch { stdin.Dispose(); stdout.Dispose(); throw; } } private async Task PumpEventsAsync() { var buffer = new char[1024]; while (!this.IsDisposed) { try { var charsRead = await this.outputReader .ReadAsync(buffer, 0, buffer.Length) .ConfigureAwait(false); if (charsRead == 0) { // // EOF reached. // this.Disconnected?.Invoke(this, EventArgs.Empty); return; } else { this.OutputAvailable?.Invoke( this, new PseudoTerminalDataEventArgs( new string(buffer, 0, charsRead))); } } catch (Exception) when (this.IsDisposed) { // // The pseudo console was closed or disposed while the // async read was pending. // } catch (Exception e) { this.FatalError?.Invoke( this, new PseudoTerminalErrorEventArgs(e)); return; } } } private void ExpectNotClosed() { if (this.IsClosed) { throw new InvalidOperationException("Pseudo-console is closed"); } } //--------------------------------------------------------------------- // IPseudoTerminal. //--------------------------------------------------------------------- public event EventHandler<PseudoTerminalDataEventArgs>? OutputAvailable; public event EventHandler<PseudoTerminalErrorEventArgs>? FatalError; public event EventHandler<EventArgs>? Disconnected; public bool IsClosed { get; private set; } public Task ResizeAsync( PseudoTerminalSize size, CancellationToken cancellationToken) { ExpectNotClosed(); return Task.Run(() => { var hresult = NativeMethods.ResizePseudoConsole( this.Handle, new NativeMethods.COORD() { X = (short)size.Width, Y = (short)size.Height }); if (hresult.Failed()) { throw PseudoTerminalException.FromHresult( hresult, "Failed to resize pseudo console"); } }, cancellationToken); } public Task WriteAsync( string data, CancellationToken cancellationToken) { ExpectNotClosed(); return this.inputWriter.WriteAsync(data); } public Task DrainAsync() { return this.pumpOutputTask; } public async Task CloseAsync() { this.Handle.Close(); this.IsClosed = true; // // Drain all pending output. // await DrainAsync().ConfigureAwait(false); } //--------------------------------------------------------------------- // IDisposable. //--------------------------------------------------------------------- protected override void Dispose(bool disposing) { base.Dispose(disposing); this.inputWriter.Dispose(); this.InputPipe.Dispose(); this.outputReader.Dispose(); this.OutputPipe.Dispose(); if (!this.IsClosed) { this.Handle.Close(); this.IsClosed = true; } } //--------------------------------------------------------------------- // P/Invoke. //--------------------------------------------------------------------- internal class PseudoConsoleHandle : SafeHandleZeroOrMinusOneIsInvalid { private PseudoConsoleHandle() : base(true) { } protected override bool ReleaseHandle() { // // NB. This not only ends the pseudo console session, // but also terminates the attached process. // NativeMethods.ClosePseudoConsole(this.handle); return true; } } private static class NativeMethods { internal struct COORD { public short X; public short Y; } [DllImport("kernel32.dll", SetLastError = false)] internal static extern HRESULT CreatePseudoConsole( COORD size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out PseudoConsoleHandle handle); [DllImport("kernel32.dll", SetLastError = false)] internal static extern HRESULT ResizePseudoConsole( PseudoConsoleHandle handle, COORD size); [DllImport("kernel32.dll", SetLastError = false)] internal static extern void ClosePseudoConsole(IntPtr hPC); } } }