diff --git a/samples/ConPTY/MiniTerm/MiniTerm/Process.cs b/samples/ConPTY/MiniTerm/MiniTerm/Process.cs
new file mode 100644
index 0000000000..be79b5fb3e
--- /dev/null
+++ b/samples/ConPTY/MiniTerm/MiniTerm/Process.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Runtime.InteropServices;
+using static MiniTerm.Native.ProcessApi;
+
+namespace MiniTerm
+{
+ ///
+ /// Support for starting and configuring processes.
+ ///
+ ///
+ /// Possible to replace with managed code? The key is being able to provide the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute
+ ///
+ static class Process
+ {
+ ///
+ /// Start and configure a process. The return value should be considered opaque, and eventually disposed.
+ ///
+ internal static ProcessResources Start(IntPtr hPC, string command, IntPtr attributes)
+ {
+ var startupInfo = ConfigureProcessThread(hPC, attributes);
+ var processInfo = RunProcess(ref startupInfo, "cmd.exe");
+ return new ProcessResources(startupInfo, processInfo);
+ }
+
+ private static STARTUPINFOEX ConfigureProcessThread(IntPtr hPC, IntPtr attributes)
+ {
+ var lpSize = IntPtr.Zero;
+ var success = InitializeProcThreadAttributeList(
+ lpAttributeList: IntPtr.Zero,
+ dwAttributeCount: 1,
+ dwFlags: 0,
+ lpSize: ref lpSize
+ );
+ if (success || lpSize == IntPtr.Zero) // we're not expecting `success` here, we just want to get the calculated lpSize
+ {
+ throw new InvalidOperationException("Could not calculate the number of bytes for the attribute list. " + Marshal.GetLastWin32Error());
+ }
+
+ var startupInfo = new STARTUPINFOEX();
+ startupInfo.StartupInfo.cb = Marshal.SizeOf();
+ startupInfo.lpAttributeList = Marshal.AllocHGlobal(lpSize);
+
+ success = InitializeProcThreadAttributeList(
+ lpAttributeList: startupInfo.lpAttributeList,
+ dwAttributeCount: 1,
+ dwFlags: 0,
+ lpSize: ref lpSize
+ );
+ if (!success)
+ {
+ throw new InvalidOperationException("Could not set up attribute list. " + Marshal.GetLastWin32Error());
+ }
+
+ success = UpdateProcThreadAttribute(
+ lpAttributeList: startupInfo.lpAttributeList,
+ dwFlags: 0,
+ attribute: attributes,
+ lpValue: hPC,
+ cbSize: (IntPtr)IntPtr.Size,
+ lpPreviousValue: IntPtr.Zero,
+ lpReturnSize: IntPtr.Zero
+ );
+ if (!success)
+ {
+ throw new InvalidOperationException("Could not set pseudoconsole thread attribute. " + Marshal.GetLastWin32Error());
+ }
+
+ return startupInfo;
+ }
+
+ private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine)
+ {
+ int securityAttributeSize = Marshal.SizeOf();
+ var pSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
+ var tSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
+ var success = CreateProcess(
+ lpApplicationName: null,
+ lpCommandLine: commandLine,
+ lpProcessAttributes: ref pSec,
+ lpThreadAttributes: ref tSec,
+ bInheritHandles: false,
+ dwCreationFlags: EXTENDED_STARTUPINFO_PRESENT,
+ lpEnvironment: IntPtr.Zero,
+ lpCurrentDirectory: null,
+ lpStartupInfo: ref sInfoEx,
+ lpProcessInformation: out PROCESS_INFORMATION pInfo
+ );
+ if (!success)
+ {
+ throw new InvalidOperationException("Could not create process. " + Marshal.GetLastWin32Error());
+ }
+
+ return pInfo;
+ }
+
+ private static void CleanUp(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo)
+ {
+ // Free the attribute list
+ if (startupInfo.lpAttributeList != IntPtr.Zero)
+ {
+ DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
+ Marshal.FreeHGlobal(startupInfo.lpAttributeList);
+ }
+
+ // Close process and thread handles
+ if (processInfo.hProcess != IntPtr.Zero)
+ {
+ CloseHandle(processInfo.hProcess);
+ }
+ if (processInfo.hThread != IntPtr.Zero)
+ {
+ CloseHandle(processInfo.hThread);
+ }
+ }
+
+ internal sealed class ProcessResources : IDisposable
+ {
+ public ProcessResources(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo)
+ {
+ StartupInfo = startupInfo;
+ ProcessInfo = processInfo;
+ }
+
+ STARTUPINFOEX StartupInfo { get; }
+ PROCESS_INFORMATION ProcessInfo { get; }
+
+ public void Dispose()
+ {
+ CleanUp(StartupInfo, ProcessInfo);
+ }
+ }
+ }
+}
diff --git a/samples/ConPTY/MiniTerm/MiniTerm/Program.cs b/samples/ConPTY/MiniTerm/MiniTerm/Program.cs
new file mode 100644
index 0000000000..6af24169f3
--- /dev/null
+++ b/samples/ConPTY/MiniTerm/MiniTerm/Program.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace MiniTerm
+{
+ ///
+ /// C# version of:
+ /// https://blogs.msdn.microsoft.com/commandline/2018/08/02/windows-command-line-introducing-the-windows-pseudo-console-conpty/
+ /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
+ ///
+ /// System Requirements:
+ /// As of September 2018, requires Windows 10 with the "Windows Insider Program" installed for Redstone 5.
+ /// Also requires the Windows Insider Preview SDK: https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewSDK
+ ///
+ ///
+ /// Basic design is:
+ /// Terminal UI starts the PseudoConsole, and controls it using a pair of PseudoConsolePipes
+ /// Terminal UI will run the Process (cmd.exe) and associate it with the PseudoConsole.
+ ///
+ static class Program
+ {
+ static void Main(string[] args)
+ {
+ try
+ {
+ var terminal = new Terminal();
+ terminal.Run("cmd.exe");
+ }
+ catch (InvalidOperationException e)
+ {
+ Console.Error.WriteLine(e.Message);
+ throw;
+ }
+ }
+ }
+}
diff --git a/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsole.cs b/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsole.cs
new file mode 100644
index 0000000000..b0072033d4
--- /dev/null
+++ b/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsole.cs
@@ -0,0 +1,39 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using static MiniTerm.Native.PseudoConsoleApi;
+
+namespace MiniTerm
+{
+ ///
+ /// Utility functions around the new Pseudo Console APIs
+ ///
+ internal sealed class PseudoConsole : IDisposable
+ {
+ public static readonly IntPtr PseudoConsoleThreadAttribute = (IntPtr)PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE;
+
+ public IntPtr Handle { get; }
+
+ private PseudoConsole(IntPtr handle)
+ {
+ this.Handle = handle;
+ }
+
+ internal static PseudoConsole Create(SafeFileHandle inputReadSide, SafeFileHandle outputWriteSide, int width, int height)
+ {
+ var createResult = CreatePseudoConsole(
+ new COORD { X = (short)width, Y = (short)height },
+ inputReadSide, outputWriteSide,
+ 0, out IntPtr hPC);
+ if(createResult != 0)
+ {
+ throw new InvalidOperationException("Could not create psuedo console. Error Code " + createResult);
+ }
+ return new PseudoConsole(hPC);
+ }
+
+ public void Dispose()
+ {
+ ClosePseudoConsole(Handle);
+ }
+ }
+}
diff --git a/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsolePipe.cs b/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsolePipe.cs
new file mode 100644
index 0000000000..d2e36d6f0c
--- /dev/null
+++ b/samples/ConPTY/MiniTerm/MiniTerm/PseudoConsolePipe.cs
@@ -0,0 +1,33 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using static MiniTerm.Native.PseudoConsoleApi;
+
+namespace MiniTerm
+{
+ ///
+ /// A pipe used to talk to the pseudoconsole, as described in:
+ /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
+ ///
+ ///
+ /// We'll have two instances of this class, one for input and one for output.
+ ///
+ internal sealed class PseudoConsolePipe : IDisposable
+ {
+ public SafeFileHandle ReadSide;
+ public SafeFileHandle WriteSide;
+
+ public PseudoConsolePipe()
+ {
+ if (!CreatePipe(out ReadSide, out WriteSide, IntPtr.Zero, 0))
+ {
+ throw new InvalidOperationException("failed to create pipe");
+ }
+ }
+
+ public void Dispose()
+ {
+ ReadSide.Dispose();
+ WriteSide.Dispose();
+ }
+ }
+}
diff --git a/samples/ConPTY/MiniTerm/MiniTerm/Terminal.cs b/samples/ConPTY/MiniTerm/MiniTerm/Terminal.cs
new file mode 100644
index 0000000000..546355c542
--- /dev/null
+++ b/samples/ConPTY/MiniTerm/MiniTerm/Terminal.cs
@@ -0,0 +1,150 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using static MiniTerm.Native.ConsoleApi;
+
+namespace MiniTerm
+{
+ ///
+ /// The UI of the terminal. It's just a normal console window, but we're managing the input/output.
+ /// In a "real" project this could be some other UI.
+ ///
+ internal sealed class Terminal
+ {
+ private const string ExitCommand = "exit\r";
+ private const string CtrlC_Command = "\x3";
+
+ public Terminal()
+ {
+ EnableVirtualTerminalSequenceProcessing();
+ }
+
+ ///
+ /// Newer versions of the windows console support interpreting virtual terminal sequences, we just have to opt-in
+ ///
+ private static void EnableVirtualTerminalSequenceProcessing()
+ {
+ var iStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (!GetConsoleMode(iStdOut, out uint outConsoleMode))
+ {
+ throw new InvalidOperationException("Could not get console mode");
+ }
+
+ outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
+ if (!SetConsoleMode(iStdOut, outConsoleMode))
+ {
+ throw new InvalidOperationException("Could not enable virtual terminal processing");
+ }
+ }
+
+ ///
+ /// Start the psuedoconsole 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
+ public void Run(string command)
+ {
+ using (var inputPipe = new PseudoConsolePipe())
+ using (var outputPipe = new PseudoConsolePipe())
+ using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, Console.WindowWidth, Console.WindowHeight))
+ using (var process = Process.Start(pseudoConsole.Handle, command, PseudoConsole.PseudoConsoleThreadAttribute))
+ {
+ // set up a background task to copy all pseudoconsole output to stdout
+ Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide));
+
+ // free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar)
+ OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe));
+
+ // prompt for stdin input and send the result to the pipe.
+ // blocks until the user types "exit"
+ CopyInputToPipe(inputPipe.WriteSide);
+ }
+ }
+
+ ///
+ /// Reads terminal input and copies it to the PseudoConsole
+ ///
+ /// the "write" side of the pseudo console input pipe
+ private static void CopyInputToPipe(SafeFileHandle inputWriteSide)
+ {
+ using (var writer = new StreamWriter(new FileStream(inputWriteSide, FileAccess.Write)))
+ {
+ ForwardCtrlC(writer);
+ writer.AutoFlush = true;
+ writer.WriteLine(@"cd \");
+
+ StringBuilder buffer = new StringBuilder();
+ while (true)
+ {
+ // send input character-by-character to the pipe
+ char key = Console.ReadKey(intercept: true).KeyChar;
+ writer.Write(key);
+
+ // stop the input loop if 'exit' was sent
+ // TODO: if we have nested cmd.exe process, this will kill all of them which is wrong.
+ // could we somehow detect when the top-level cmd.exe process has ended and remove this logic?
+ buffer.Append(key);
+ if (key == '\r')
+ {
+ if (buffer.ToString() == ExitCommand)
+ {
+ break;
+ }
+ buffer.Clear();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Don't let ctrl-c kill the terminal, it should be sent to the process in the terminal.
+ ///
+ private static void ForwardCtrlC(StreamWriter writer)
+ {
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ e.Cancel = true;
+ writer.Write(CtrlC_Command);
+ };
+ }
+
+ ///
+ /// Reads PseudoConsole output and copies it to the terminal's standard out.
+ ///
+ /// the "read" side of the pseudo console output pipe
+ private static void CopyPipeToOutput(SafeFileHandle outputReadSide)
+ {
+ using (var terminalOutput = Console.OpenStandardOutput())
+ using (var pseudoConsoleOutput = new FileStream(outputReadSide, FileAccess.Read))
+ {
+ pseudoConsoleOutput.CopyTo(terminalOutput);
+ }
+ }
+
+ ///
+ /// 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();
+ }
+ }
+ }
+}