Skip to content
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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion samples/ConPTY/MiniTerm/MiniTerm.sln
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2035
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniTerm", "MiniTerm\MiniTerm.csproj", "{121D4818-BD57-433B-8AD5-C4E1ACE7E7C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniTermCore", "MiniTermCore\MiniTermCore.csproj", "{CE497A8A-80D8-4A6D-BD93-80BD233FC9BF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{121D4818-BD57-433B-8AD5-C4E1ACE7E7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{121D4818-BD57-433B-8AD5-C4E1ACE7E7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{121D4818-BD57-433B-8AD5-C4E1ACE7E7C0}.Release|Any CPU.Build.0 = Release|Any CPU
{CE497A8A-80D8-4A6D-BD93-80BD233FC9BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE497A8A-80D8-4A6D-BD93-80BD233FC9BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE497A8A-80D8-4A6D-BD93-80BD233FC9BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE497A8A-80D8-4A6D-BD93-80BD233FC9BF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
21 changes: 21 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/MiniTermCore.csproj
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

View workflow job for this annotation

GitHub Actions / Spell checking

`Aot` is not a recognized word. (unrecognized-spelling)

Check failure on line 9 in samples/ConPTY/MiniTerm/MiniTermCore/MiniTermCore.csproj

View workflow job for this annotation

GitHub Actions / Spell checking

`Aot` is not a recognized word. (unrecognized-spelling)
<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>
6 changes: 6 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CreatePseudoConsole
CreatePipe
GetStdHandle
SetConsoleMode
GetConsoleMode
SetConsoleCtrlHandler
25 changes: 25 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/Program.cs
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;
}
39 changes: 39 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/PseudoConsole.cs
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();
}
}
}
49 changes: 49 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/PseudoConsolePipe.cs
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))
Copy link
Member

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 of new SECURITY_ATTRIBUTES()

{
throw new InvalidOperationException("failed to create pipe");
}
#pragma warning restore CA1416
}

void Dispose(bool disposing)
{
if (disposing)
{
Comment on lines +36 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd understand if you used a bool disposed member (to avoid a double-dispose), but I'm not sure I understand what disposing does here.

ReadSide?.Dispose();
WriteSide?.Dispose();
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
114 changes: 114 additions & 0 deletions samples/ConPTY/MiniTerm/MiniTermCore/Terminal.cs
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows 11 26100 includes a new API to make process shutdown safer (ReleasePseudoConsole). The API closes all references to the PTY and then you can simply read from the output pipe until the pipe gets closed. That way you don't need to wait for the process exit here.

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;
Copy link
Member

Choose a reason for hiding this comment

The 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();
}
}
}
}
Loading