Skip to content

Commit

Permalink
prototype pty wrapper that maybe works?
Browse files Browse the repository at this point in the history
  • Loading branch information
doubleyewdee committed Sep 29, 2018
1 parent 45222be commit 56f21d7
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 15 deletions.
171 changes: 158 additions & 13 deletions ConsoleBuffer/ConsoleWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleBuffer
namespace ConsoleBuffer
{
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;

public sealed class ConsoleWrapper : IDisposable
{
public string Contents { get; private set; }

public ConsoleWrapper(string[] command)
public string Command { get; private set; }

private NativeMethods.COORD consoleSize = new NativeMethods.COORD { X = 25, Y = 80 };

/// <summary>
/// The handle from which we read data from the console.
/// </summary>
private SafeFileHandle readHandle;

/// <summary>
/// The handle into which we write data to the console.
/// </summary>
private SafeFileHandle writeHandle;

/// <summary>
/// Handle to the console PTY.
/// </summary>
private IntPtr consoleHandle;

// used for booting / running the underlying process.
private NativeMethods.STARTUPINFOEX startupInfo;
private NativeMethods.PROCESS_INFORMATION processInfo;

public ConsoleWrapper(string command)
{
this.Contents = string.Empty;

if (string.IsNullOrWhiteSpace(command))
{
throw new ArgumentException("No command specified.", nameof(command));
}

this.Command = command;

this.CreatePTY();
this.InitializeStartupInfo();
this.StartProcess();

Task.Run(() => this.ReadConsoleTask());
}

private void CreatePTY()
{
SafeFileHandle pipeTTYin, pipeTTYout;

if ( NativeMethods.CreatePipe(out pipeTTYin, out this.writeHandle, IntPtr.Zero, 0)
&& NativeMethods.CreatePipe(out this.readHandle, out pipeTTYout, IntPtr.Zero, 0))
{
ThrowForHResult(NativeMethods.CreatePseudoConsole(this.consoleSize, pipeTTYin, pipeTTYout, 0, out this.consoleHandle),
"Failed to create PTY");

// It is safe to close these as they have been duped to the child console host and will be closed on that end.
if (!pipeTTYin.IsInvalid) pipeTTYin.Dispose();
if (!pipeTTYout.IsInvalid) pipeTTYout.Dispose();

return;
}

throw new InvalidOperationException("Unable to create pipe(s) for console.");
}

private void InitializeStartupInfo()
{
IntPtr allocSize = IntPtr.Zero;
// yes this method really returns true *if it fails to get the size as requested*
// ... fuckin' Windows.
if (NativeMethods.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref allocSize) || allocSize == IntPtr.Zero)
{
throw new InvalidOperationException("Unable to get size of process startup attribute storage.");
}

this.startupInfo = new NativeMethods.STARTUPINFOEX();
this.startupInfo.StartupInfo.cb = Marshal.SizeOf<NativeMethods.STARTUPINFOEX>();
this.startupInfo.lpAttributeList = Marshal.AllocHGlobal(allocSize);
if (!NativeMethods.InitializeProcThreadAttributeList(this.startupInfo.lpAttributeList, 1, 0, ref allocSize))
{
ThrowForHResult(Marshal.GetLastWin32Error(), "Unable to initialze process startup info.");
}

if (!NativeMethods.UpdateProcThreadAttribute(this.startupInfo.lpAttributeList, 0, (IntPtr)NativeMethods.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, this.consoleHandle,
(IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero))
{
ThrowForHResult(Marshal.GetLastWin32Error(), "Unable to update process startup info.");
}
}

private void StartProcess()
{
var processSecurityAttr = new NativeMethods.SECURITY_ATTRIBUTES();
var threadSecurityAttr = new NativeMethods.SECURITY_ATTRIBUTES();
processSecurityAttr.nLength = threadSecurityAttr.nLength = Marshal.SizeOf<NativeMethods.SECURITY_ATTRIBUTES>();

if (!NativeMethods.CreateProcess(null, this.Command, ref processSecurityAttr, ref threadSecurityAttr, false,
NativeMethods.EXTENDED_STARTUPINFO_PRESENT, IntPtr.Zero, null, ref this.startupInfo, out this.processInfo))
{
ThrowForHResult(Marshal.GetLastWin32Error(), "Unable to start process.");
}
}

private void ReadConsoleTask()
{
using (var ptyOutput = new FileStream(this.readHandle, FileAccess.Read))
{
var input = new byte[2048];

while (true)
{
if (this.disposed)
{
return;
}

var read = ptyOutput.Read(input, 0, input.Length);
this.Contents += System.Text.Encoding.UTF8.GetString(input, 0, read);
}
}
}

private static void ThrowForHResult(int hr, string exceptionMessage)
{
if (hr != 0)
throw new InvalidOperationException($"{exceptionMessage}: {Marshal.GetHRForLastWin32Error()}");
}

#region IDisposable Support
Expand All @@ -22,15 +141,41 @@ void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
this.disposed = true;

// We want to clear out managed resources first to allow our reader task to die naturally from having the other side of its
// pipe closed.
if (this.processInfo.hProcess != IntPtr.Zero)
{
NativeMethods.CloseHandle(this.processInfo.hProcess);
this.processInfo.hProcess = IntPtr.Zero;
}
if (this.processInfo.hThread != IntPtr.Zero)
{
NativeMethods.CloseHandle(this.processInfo.hThread);
this.processInfo.hThread = IntPtr.Zero;
}

if (this.consoleHandle != IntPtr.Zero)
{
// TODO: dispose managed state (managed objects).
NativeMethods.ClosePseudoConsole(this.consoleHandle);
this.consoleHandle = IntPtr.Zero;
}

// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
if (this.startupInfo.lpAttributeList != IntPtr.Zero)
{
NativeMethods.DeleteProcThreadAttributeList(this.startupInfo.lpAttributeList);
Marshal.FreeHGlobal(this.startupInfo.lpAttributeList);
this.startupInfo.lpAttributeList = IntPtr.Zero;
}

this.disposed = true;
if (disposing)
{
this.readHandle?.Dispose();
this.readHandle = null;
this.writeHandle?.Dispose();
this.writeHandle = null;
}
}
}

Expand Down
93 changes: 91 additions & 2 deletions ConsoleBuffer/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,100 @@
namespace ConsoleBuffer
{
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

public static class NativeMethods
{
public struct COORD
internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
internal const uint PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;

[StructLayout(LayoutKind.Sequential)]
internal struct COORD
{
public short X;
public short Y;
}
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFO
{
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}

[StructLayout(LayoutKind.Sequential)]
internal struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue, IntPtr cbSize,
IntPtr lpPreviousValue, IntPtr lpReturnSize);

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int CreatePseudoConsole(COORD size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out IntPtr phPC);

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int ResizePseudoConsole(IntPtr hPC, COORD size);

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int ClosePseudoConsole(IntPtr hPC);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, IntPtr lpPipeAttributes, int nSize); }
}

0 comments on commit 56f21d7

Please sign in to comment.