-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New sample for MiniTerm #17462
New sample for MiniTerm #17462
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<!--Public AOT--> | ||
<PublishAot>true</PublishAot> | ||
Check failure on line 9 in samples/ConPTY/MiniTerm/MiniTermCore/MiniTermCore.csproj
|
||
<InvariantGlobalization>true</InvariantGlobalization> | ||
<StripSymbols>true</StripSymbols> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
CreatePseudoConsole | ||
CreatePipe | ||
GetStdHandle | ||
SetConsoleMode | ||
GetConsoleMode | ||
SetConsoleCtrlHandler |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using MiniTermCore; | ||
|
||
/// <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> | ||
try | ||
{ | ||
Terminal.Run("cmd.exe"); | ||
} | ||
catch (InvalidOperationException e) | ||
{ | ||
Console.Error.WriteLine(e.Message); | ||
throw; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using Microsoft.Win32.SafeHandles; | ||
using Windows.Win32; | ||
using Windows.Win32.System.Console; | ||
|
||
namespace MiniTermCore | ||
{ | ||
/// <summary> | ||
/// Utility functions around the new Pseudo Console APIs | ||
/// </summary> | ||
internal sealed class PseudoConsole : IDisposable | ||
{ | ||
public ClosePseudoConsoleSafeHandle Handle { get; } | ||
|
||
private PseudoConsole(ClosePseudoConsoleSafeHandle handle) | ||
{ | ||
Handle = handle; | ||
} | ||
|
||
internal static PseudoConsole Create(SafeFileHandle inputReadSide, SafeFileHandle outputWriteSide, int width, int height) | ||
{ | ||
var createResult = PInvoke.CreatePseudoConsole( | ||
new COORD { X = (short)width, Y = (short)height }, | ||
inputReadSide, outputWriteSide, | ||
0, out ClosePseudoConsoleSafeHandle hPC); | ||
|
||
if (createResult != 0) | ||
{ | ||
throw new InvalidOperationException("Could not create pseudo console. Error Code " + createResult); | ||
} | ||
|
||
return new PseudoConsole(hPC); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
Handle.Close(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using Microsoft.Win32.SafeHandles; | ||
using Windows.Win32; | ||
using Windows.Win32.Security; | ||
|
||
namespace MiniTermCore | ||
{ | ||
/// <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 readonly SafeFileHandle ReadSide; | ||
public readonly SafeFileHandle WriteSide; | ||
|
||
public PseudoConsolePipe() | ||
{ | ||
if (!OperatingSystem.IsWindows()) | ||
{ | ||
throw new PlatformNotSupportedException("OperatingSystem is not support"); | ||
} | ||
|
||
#pragma warning disable CA1416 | ||
if (!PInvoke.CreatePipe(out ReadSide, out WriteSide, new SECURITY_ATTRIBUTES(), 0)) | ||
{ | ||
throw new InvalidOperationException("failed to create pipe"); | ||
} | ||
#pragma warning restore CA1416 | ||
} | ||
|
||
void Dispose(bool disposing) | ||
{ | ||
if (disposing) | ||
{ | ||
Comment on lines
+36
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd understand if you used a |
||
ReadSide?.Dispose(); | ||
WriteSide?.Dispose(); | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
Dispose(true); | ||
GC.SuppressFinalize(this); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
using Microsoft.Win32.SafeHandles; | ||
using System.Text; | ||
using Windows.Win32; | ||
|
||
namespace MiniTermCore | ||
{ | ||
/// <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 static class Terminal | ||
{ | ||
private const string CtrlC_Command = "\x3"; | ||
|
||
internal enum CtrlTypes : uint | ||
{ | ||
CTRL_C_EVENT = 0, | ||
CTRL_BREAK_EVENT, | ||
CTRL_CLOSE_EVENT, | ||
CTRL_LOGOFF_EVENT = 5, | ||
CTRL_SHUTDOWN_EVENT | ||
} | ||
|
||
/// <summary> | ||
/// 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 | ||
/// </summary> | ||
/// <param name="command">the command to run, e.g. cmd.exe</param> | ||
public static void Run(string command) | ||
{ | ||
using var inputPipe = new PseudoConsolePipe(); | ||
using var outputPipe = new PseudoConsolePipe(); | ||
using var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, (short)Console.WindowWidth, (short)Console.WindowHeight); | ||
using var process = System.Diagnostics.Process.Start(command) ?? throw new Exception(); | ||
|
||
// copy all pseudoconsole output to stdout | ||
Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide)); | ||
// prompt for stdin input and send the result to the pseudoconsole | ||
Task.Run(() => CopyInputToPipe(inputPipe.WriteSide)); | ||
// free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar) | ||
OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe)); | ||
|
||
process.WaitForExit(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Windows 11 26100 includes a new API to make process shutdown safer ( But given how novel the API is, I'm not sure if it's time for that yet. |
||
} | ||
|
||
/// <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 stream = new FileStream(inputWriteSide, FileAccess.Write); | ||
ForwardCtrlC(stream); | ||
|
||
while (true) | ||
{ | ||
if (!Console.KeyAvailable) continue; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't that a busy loop when there aren't any keys available? |
||
|
||
// send input character-by-character to the pipe | ||
char key = Console.ReadKey(intercept: true).KeyChar; | ||
stream.WriteByte((byte)key); | ||
stream.Flush(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Don't let ctrl-c kill the terminal, it should be sent to the process in the terminal. | ||
/// </summary> | ||
private static void ForwardCtrlC(FileStream stream) | ||
{ | ||
Console.CancelKeyPress += (sender, e) => | ||
{ | ||
e.Cancel = true; | ||
stream.Write(Encoding.UTF8.GetBytes(CtrlC_Command)); | ||
stream.Flush(); | ||
}; | ||
} | ||
|
||
/// <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) | ||
{ | ||
PInvoke.SetConsoleCtrlHandler(eventType => | ||
{ | ||
if (eventType == (uint)CtrlTypes.CTRL_CLOSE_EVENT) | ||
{ | ||
handler(); | ||
} | ||
return false; | ||
}, true); | ||
} | ||
|
||
private static void DisposeResources(params IDisposable[] disposables) | ||
{ | ||
foreach (var disposable in disposables) | ||
{ | ||
disposable.Dispose(); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can pass
null
instead ofnew SECURITY_ATTRIBUTES()