From ac843745fa71728f6e385eed2e27805350d90c89 Mon Sep 17 00:00:00 2001 From: Rich Turner Date: Mon, 10 Sep 2018 20:07:17 -0700 Subject: [PATCH] Add an example application that uses the pseudoconsole APIs (#247) This sample implements a simple "Echo Console" that illustrates the mechanism by which a caller can directly invoke & communicate with Command-Line applications. 1. Creates two pipes - one for output, the second for output 1. Creates a Pseudo Console attached to the other end of the pipes 1. Creates a child process (an instance of `ping.exe` in this case), attached to the Pseudo Console 1. Creates a thread that reads the input pipe, displaying received text on the screen --- samples/ConPTY/EchoCon/EchoCon.sln | 36 ++++ samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp | 189 ++++++++++++++++++ .../ConPTY/EchoCon/EchoCon/EchoCon.vcxproj | 165 +++++++++++++++ .../EchoCon/EchoCon/EchoCon.vcxproj.filters | 33 +++ samples/ConPTY/EchoCon/EchoCon/stdafx.cpp | 8 + samples/ConPTY/EchoCon/EchoCon/stdafx.h | 13 ++ samples/ConPTY/EchoCon/EchoCon/targetver.h | 8 + samples/ConPTY/EchoCon/readme.md | 34 ++++ 8 files changed, 486 insertions(+) create mode 100644 samples/ConPTY/EchoCon/EchoCon.sln create mode 100644 samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp create mode 100644 samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj create mode 100644 samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters create mode 100644 samples/ConPTY/EchoCon/EchoCon/stdafx.cpp create mode 100644 samples/ConPTY/EchoCon/EchoCon/stdafx.h create mode 100644 samples/ConPTY/EchoCon/EchoCon/targetver.h create mode 100644 samples/ConPTY/EchoCon/readme.md diff --git a/samples/ConPTY/EchoCon/EchoCon.sln b/samples/ConPTY/EchoCon/EchoCon.sln new file mode 100644 index 000000000..d19812888 --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2026 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EchoCon", "EchoCon\EchoCon.vcxproj", "{96274800-9574-423E-892A-909FBE2AC8BE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{556CAA54-33E0-4F99-95C8-0DFD6E8F6C6B}" + ProjectSection(SolutionItems) = preProject + readme.md = readme.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {96274800-9574-423E-892A-909FBE2AC8BE}.Debug|x64.ActiveCfg = Debug|x64 + {96274800-9574-423E-892A-909FBE2AC8BE}.Debug|x64.Build.0 = Debug|x64 + {96274800-9574-423E-892A-909FBE2AC8BE}.Debug|x86.ActiveCfg = Debug|Win32 + {96274800-9574-423E-892A-909FBE2AC8BE}.Debug|x86.Build.0 = Debug|Win32 + {96274800-9574-423E-892A-909FBE2AC8BE}.Release|x64.ActiveCfg = Release|x64 + {96274800-9574-423E-892A-909FBE2AC8BE}.Release|x64.Build.0 = Release|x64 + {96274800-9574-423E-892A-909FBE2AC8BE}.Release|x86.ActiveCfg = Release|Win32 + {96274800-9574-423E-892A-909FBE2AC8BE}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B27C5007-61E2-4080-965D-8C934367BA4F} + EndGlobalSection +EndGlobal diff --git a/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp b/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp new file mode 100644 index 000000000..904ea5b89 --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/EchoCon.cpp @@ -0,0 +1,189 @@ +// EchoCon.cpp : Entry point for the EchoCon Pseudo-Consle sample application. +// Copyright © 2018, Microsoft + +#include "stdafx.h" +#include +#include + +// Forward declarations +HRESULT CreatePseudoConsoleAndPipes(HPCON*, HANDLE*, HANDLE*); +HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX*, HPCON); +void __cdecl PipeListener(LPVOID); + +int main() +{ + wchar_t szCommand[]{ L"ping localhost" }; + HRESULT hr{ E_UNEXPECTED }; + HANDLE hConsole = { GetStdHandle(STD_OUTPUT_HANDLE) }; + + // Enable Console VT Processing + DWORD consoleMode{}; + GetConsoleMode(hConsole, &consoleMode); + hr = SetConsoleMode(hConsole, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + ? S_OK + : GetLastError(); + if (S_OK == hr) + { + HPCON hPC{ INVALID_HANDLE_VALUE }; + + // Create the Pseudo Console and pipes to it + HANDLE hPipeIn{ INVALID_HANDLE_VALUE }; + HANDLE hPipeOut{ INVALID_HANDLE_VALUE }; + hr = CreatePseudoConsoleAndPipes(&hPC, &hPipeIn, &hPipeOut); + if (S_OK == hr) + { + // Create & start thread to listen to the incoming pipe + // Note: Using CRT-safe _beginthread() rather than CreateThread() + HANDLE hPipeListenerThread{ reinterpret_cast(_beginthread(PipeListener, 0, hPipeIn)) }; + + // Initialize the necessary startup info struct + STARTUPINFOEX startupInfo{}; + if (S_OK == InitializeStartupInfoAttachedToPseudoConsole(&startupInfo, hPC)) + { + // Launch ping to emit some text back via the pipe + PROCESS_INFORMATION piClient{}; + hr = CreateProcess( + NULL, // No module name - use Command Line + szCommand, // Command Line + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Inherit handles + EXTENDED_STARTUPINFO_PRESENT, // Creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &startupInfo.StartupInfo, // Pointer to STARTUPINFO + &piClient) // Pointer to PROCESS_INFORMATION + ? S_OK + : GetLastError(); + + if (S_OK == hr) + { + // Wait up to 10s for ping process to complete + WaitForSingleObject(piClient.hThread, 10 * 1000); + + // Allow listening thread to catch-up with final output! + Sleep(500); + } + + // --- CLOSEDOWN --- + + // Now safe to clean-up client app's process-info & thread + CloseHandle(piClient.hThread); + CloseHandle(piClient.hProcess); + + // Cleanup attribute list + DeleteProcThreadAttributeList(startupInfo.lpAttributeList); + free(startupInfo.lpAttributeList); + } + + // Close ConPTY - this will terminate client process if running + ClosePseudoConsole(hPC); + + // Clean-up the pipes + if (INVALID_HANDLE_VALUE != hPipeOut) CloseHandle(hPipeOut); + if (INVALID_HANDLE_VALUE != hPipeIn) CloseHandle(hPipeIn); + } + } + + return S_OK == hr ? EXIT_SUCCESS : EXIT_FAILURE; +} + +HRESULT CreatePseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut) +{ + HRESULT hr{ E_UNEXPECTED }; + HANDLE hPipePTYIn{ INVALID_HANDLE_VALUE }; + HANDLE hPipePTYOut{ INVALID_HANDLE_VALUE }; + + // Create the pipes to which the ConPTY will connect + if (CreatePipe(&hPipePTYIn, phPipeOut, NULL, 0) && + CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0)) + { + // Determine required size of Pseudo Console + COORD consoleSize{}; + CONSOLE_SCREEN_BUFFER_INFO csbi{}; + HANDLE hConsole{ GetStdHandle(STD_OUTPUT_HANDLE) }; + if (GetConsoleScreenBufferInfo(hConsole, &csbi)) + { + consoleSize.X = csbi.srWindow.Right - csbi.srWindow.Left + 1; + consoleSize.Y = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; + } + + // Create the Pseudo Console of the required size, attached to the PTY-end of the pipes + hr = CreatePseudoConsole(consoleSize, hPipePTYIn, hPipePTYOut, 0, phPC); + + // Note: We can close the handles to the PTY-end of the pipes here + // because the handles are dup'ed into the ConHost and will be released + // when the ConPTY is destroyed. + if (INVALID_HANDLE_VALUE != hPipePTYOut) CloseHandle(hPipePTYOut); + if (INVALID_HANDLE_VALUE != hPipePTYIn) CloseHandle(hPipePTYIn); + } + + return hr; +} + +// Initializes the specified startup info struct with the required properties and +// updates its thread attribute list with the specified ConPTY handle +HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC) +{ + HRESULT hr{ E_UNEXPECTED }; + + if (pStartupInfo) + { + size_t attrListSize{}; + + pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX); + + // Get the size of the thread attribute list. + InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize); + + // Allocate a thread attribute list of the correct size + pStartupInfo->lpAttributeList = + reinterpret_cast(malloc(attrListSize)); + + // Initialize thread attribute list + if (pStartupInfo->lpAttributeList + && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize)) + { + // Set Pseudo Console attribute + hr = UpdateProcThreadAttribute( + pStartupInfo->lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + hPC, + sizeof(HPCON), + NULL, + NULL) + ? S_OK + : HRESULT_FROM_WIN32(GetLastError()); + } + else + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + return hr; +} + +void __cdecl PipeListener(LPVOID pipe) +{ + HANDLE hPipe{ pipe }; + HANDLE hConsole{ GetStdHandle(STD_OUTPUT_HANDLE) }; + + const DWORD BUFF_SIZE{ 512 }; + char szBuffer[BUFF_SIZE]{}; + + DWORD dwBytesWritten{}; + DWORD dwBytesRead{}; + BOOL fRead{ FALSE }; + do + { + // Read from the pipe + fRead = ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwBytesRead, NULL); + + // Write received text to the Console + // Note: Write to the Console using WriteFile(hConsole...), not printf()/puts() to + // prevent partially-read VT sequences from corrupting output + WriteFile(hConsole, szBuffer, dwBytesRead, &dwBytesWritten, NULL); + + } while (fRead && dwBytesRead >= 0); +} diff --git a/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj b/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj new file mode 100644 index 000000000..42fc73fea --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj @@ -0,0 +1,165 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {96274800-9574-423E-892A-909FBE2AC8BE} + Win32Proj + EchoCon + 10.0.17738.0 + + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + Use + Level3 + Disabled + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Use + Level3 + Disabled + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Use + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Use + Level3 + MaxSpeed + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters b/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters new file mode 100644 index 000000000..69e0ecfed --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/EchoCon.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/samples/ConPTY/EchoCon/EchoCon/stdafx.cpp b/samples/ConPTY/EchoCon/EchoCon/stdafx.cpp new file mode 100644 index 000000000..dec485bab --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/stdafx.cpp @@ -0,0 +1,8 @@ +// stdafx.cpp : source file that includes just the standard includes +// EchoCon.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file \ No newline at end of file diff --git a/samples/ConPTY/EchoCon/EchoCon/stdafx.h b/samples/ConPTY/EchoCon/EchoCon/stdafx.h new file mode 100644 index 000000000..1fa6ee20b --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/stdafx.h @@ -0,0 +1,13 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#include +#include + +// TODO: reference additional headers your program requires here diff --git a/samples/ConPTY/EchoCon/EchoCon/targetver.h b/samples/ConPTY/EchoCon/EchoCon/targetver.h new file mode 100644 index 000000000..87c0086de --- /dev/null +++ b/samples/ConPTY/EchoCon/EchoCon/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/samples/ConPTY/EchoCon/readme.md b/samples/ConPTY/EchoCon/readme.md new file mode 100644 index 000000000..7d1f3572b --- /dev/null +++ b/samples/ConPTY/EchoCon/readme.md @@ -0,0 +1,34 @@ +# "EchoCon" ConPTY Sample App +This is a very simple sample application that illustrates how to use the new Win32 Pseudo Console +(ConPTY) by: + +1. Creating an input and an output pipe +1. Calling `CreatePseudoConsole()` to create a ConPTY instance attached to the other end of the pipes +1. Spawning an instance of `ping.exe` connected to the ConPTY +1. Running a thread that listens for output from ping.exe, writing received text to the Console + +# Pre-Requirements +To build and run this sample, you must install: +* Windows 10 Insider build 17733 or later +* [Latest Windows 10 Insider SDK](https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewSDK) + +# Running the sample +Once successfully built, running EchoCon should clear the screen and display the results of the +echo command: + +``` +Pinging Rincewind [::1] with 32 bytes of data: +Reply from ::1: time<1ms +Reply from ::1: time<1ms +Reply from ::1: time<1ms +Reply from ::1: time<1ms + +Ping statistics for ::1: + Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), +Approximate round trip times in milli-seconds: + Minimum = 0ms, Maximum = 0ms, Average = 0ms +``` + +# Resources +For more information on the new Pseudo Console infrastructure and API, please review +[this blog post](https://blogs.msdn.microsoft.com/commandline/2018/08/02/windows-command-line-introducing-the-windows-pseudo-console-conpty/)