mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
add c# files
- Move from rather ad-hoc, error-prone resource management to IDisposable, which should give us a bit more enforcement. - Optimistically remove "buggy" from readme because the known bugs are now fixed! The main source of bugs was the incorrect InitializeProcThreadAttributeList usage. - Handle ctrl-c by forwarding it to the PseudoConsole - Handle terminal close when the window close button is used - Use .NET's CopyTo in the CopyPipeToOutput, it's much simpler code and seems more robust than the ReadFile/WriteFile approach - Minor refactor to split native APIs to multiple files
This commit is contained in:
parent
e09359138e
commit
637c57473e
133
samples/ConPTY/MiniTerm/MiniTerm/Process.cs
Normal file
133
samples/ConPTY/MiniTerm/MiniTerm/Process.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using static MiniTerm.Native.ProcessApi;
|
||||
|
||||
namespace MiniTerm
|
||||
{
|
||||
/// <summary>
|
||||
/// Support for starting and configuring processes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Possible to replace with managed code? The key is being able to provide the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute
|
||||
/// </remarks>
|
||||
static class Process
|
||||
{
|
||||
/// <summary>
|
||||
/// Start and configure a process. The return value should be considered opaque, and eventually disposed.
|
||||
/// </summary>
|
||||
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<STARTUPINFOEX>();
|
||||
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<SECURITY_ATTRIBUTES>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
samples/ConPTY/MiniTerm/MiniTerm/Program.cs
Normal file
35
samples/ConPTY/MiniTerm/MiniTerm/Program.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace MiniTerm
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
samples/ConPTY/MiniTerm/MiniTerm/PseudoConsole.cs
Normal file
39
samples/ConPTY/MiniTerm/MiniTerm/PseudoConsole.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using static MiniTerm.Native.PseudoConsoleApi;
|
||||
|
||||
namespace MiniTerm
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility functions around the new Pseudo Console APIs
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
samples/ConPTY/MiniTerm/MiniTerm/PseudoConsolePipe.cs
Normal file
33
samples/ConPTY/MiniTerm/MiniTerm/PseudoConsolePipe.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using static MiniTerm.Native.PseudoConsoleApi;
|
||||
|
||||
namespace MiniTerm
|
||||
{
|
||||
/// <summary>
|
||||
/// A pipe used to talk to the pseudoconsole, as described in:
|
||||
/// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We'll have two instances of this class, one for input and one for output.
|
||||
/// </remarks>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
150
samples/ConPTY/MiniTerm/MiniTerm/Terminal.cs
Normal file
150
samples/ConPTY/MiniTerm/MiniTerm/Terminal.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class Terminal
|
||||
{
|
||||
private const string ExitCommand = "exit\r";
|
||||
private const string CtrlC_Command = "\x3";
|
||||
|
||||
public Terminal()
|
||||
{
|
||||
EnableVirtualTerminalSequenceProcessing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Newer versions of the windows console support interpreting virtual terminal sequences, we just have to opt-in
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="command">the command to run, e.g. cmd.exe</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads terminal input and copies it to the PseudoConsole
|
||||
/// </summary>
|
||||
/// <param name="inputWriteSide">the "write" side of the pseudo console input pipe</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Don't let ctrl-c kill the terminal, it should be sent to the process in the terminal.
|
||||
/// </summary>
|
||||
private static void ForwardCtrlC(StreamWriter writer)
|
||||
{
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
writer.Write(CtrlC_Command);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads PseudoConsole output and copies it to the terminal's standard out.
|
||||
/// </summary>
|
||||
/// <param name="outputReadSide">the "read" side of the pseudo console output pipe</param>
|
||||
private static void CopyPipeToOutput(SafeFileHandle outputReadSide)
|
||||
{
|
||||
using (var terminalOutput = Console.OpenStandardOutput())
|
||||
using (var pseudoConsoleOutput = new FileStream(outputReadSide, FileAccess.Read))
|
||||
{
|
||||
pseudoConsoleOutput.CopyTo(terminalOutput);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button).
|
||||
/// Intended for resource cleanup logic.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user