Add support for start /B and FreeConsole (#14544)

2 new ConPTY APIs were added as part of this commit:
* `ClosePseudoConsoleTimeout`
  Complements `ClosePseudoConsole`, allowing users to override the `INFINITE`
  wait for OpenConsole to exit with any arbitrary timeout, including 0.
* `ConptyReleasePseudoConsole`
  This releases the `\Reference` handle held by the `HPCON`. While it makes
  launching further PTY clients via `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE`
  impossible, it does allow conhost/OpenConsole to exit naturally once all
  console clients have disconnected. This makes it unnecessary to having to
  monitor the exit of the spawned shell/application, just to then call
  `ClosePseudoConsole`, while carefully continuing to read from the output
  pipe. Instead users can now just read from the output pipe until it is
  closed, knowing for sure that no more data will come or clients connect.
  This is especially useful in combination with `ClosePseudoConsoleTimeout`
  and a timeout of 0, to allow conhost/OpenConsole to exit asynchronously.

These new APIs are used to fix an array of bugs around Windows Terminal exiting
either too early or too late. They also make usage of the ConPTY API simpler in
most situations (when spawning a single application and waiting for it to exit).

Depends on #13882, #14041, #14160, #14282

Closes #4564
Closes #14416
Closes MSFT-42244182

## Validation Steps Performed
* Calling `FreeConsole` in a handoff'd application closes the tab 
* Create a .bat file containing only `start /B cmd.exe`.
  If WT stable is the default terminal the tab closes instantly 
  With these changes included the tab stays open with a cmd.exe prompt 
* New ConPTY tests 
This commit is contained in:
Leonard Hecker 2022-12-16 23:06:30 +01:00 committed by GitHub
parent a3c7bc3349
commit 165d3edde9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 271 additions and 158 deletions

View File

@ -817,6 +817,7 @@ HORZ
hostable
hostlib
HPA
hpcon
HPCON
hpj
HPR

View File

@ -2,17 +2,17 @@
// Licensed under the MIT license.
#include "pch.h"
#include "ConptyConnection.h"
#include <conpty-static.h>
#include <winternl.h>
#include "ConptyConnection.g.cpp"
#include "CTerminalHandoff.h"
#include "../../types/inc/utils.hpp"
#include "../../types/inc/Environment.hpp"
#include "LibraryResources.h"
#include "../../types/inc/Environment.hpp"
#include "../../types/inc/utils.hpp"
#include "ConptyConnection.g.cpp"
using namespace ::Microsoft::Console;
using namespace std::string_view_literals;
@ -24,13 +24,9 @@ static constexpr auto _errorFormat = L"{0} ({0:#010x})"sv;
// There is a number of ways that the Conpty connection can be terminated (voluntarily or not):
// 1. The connection is Close()d
// 2. The pseudoconsole or process cannot be spawned during Start()
// 3. The client process exits with a code.
// (Successful (0) or any other code)
// 4. The read handle is terminated.
// (This usually happens when the pseudoconsole host crashes.)
// 3. The read handle is terminated (when OpenConsole exits)
// In each of these termination scenarios, we need to be mindful of tripping the others.
// Closing the pseudoconsole in response to the client exiting (3) can trigger (4).
// Close() (1) will cause the automatic triggering of (3) and (4).
// Close() (1) will cause the automatic triggering of (3).
// In a lot of cases, we use the connection state to stop "flapping."
//
// To figure out where we handle these, search for comments containing "EXIT POINT"
@ -380,6 +376,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
}
THROW_IF_FAILED(ConptyReleasePseudoConsole(_hPC.get()));
_startTime = std::chrono::high_resolution_clock::now();
// Create our own output handling thread
@ -404,19 +402,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
LOG_IF_FAILED(SetThreadDescription(_hOutputThread.get(), L"ConptyConnection Output Thread"));
_clientExitWait.reset(CreateThreadpoolWait(
[](PTP_CALLBACK_INSTANCE /*callbackInstance*/, PVOID context, PTP_WAIT /*wait*/, TP_WAIT_RESULT /*waitResult*/) noexcept {
const auto pInstance = static_cast<ConptyConnection*>(context);
if (pInstance)
{
pInstance->_ClientTerminated();
}
},
this,
nullptr));
SetThreadpoolWait(_clientExitWait.get(), _piClient.hProcess, nullptr);
_transitionToState(ConnectionState::Connected);
}
catch (...)
@ -466,34 +451,17 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Method Description:
// - called when the client application (not necessarily its pty) exits for any reason
void ConptyConnection::_ClientTerminated() noexcept
void ConptyConnection::_LastConPtyClientDisconnected() noexcept
try
{
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
// This termination was expected.
return;
}
// EXIT POINT
DWORD exitCode{ 0 };
GetExitCodeProcess(_piClient.hProcess, &exitCode);
// Signal the closing or failure of the process.
// Load bearing. Terminating the pseudoconsole will make the output thread exit unexpectedly,
// so we need to signal entry into the correct closing state before we do that.
_transitionToState(exitCode == 0 ? ConnectionState::Closed : ConnectionState::Failed);
// Close the pseudoconsole and wait for all output to drain.
_hPC.reset();
if (auto localOutputThreadHandle = std::move(_hOutputThread))
{
LOG_LAST_ERROR_IF(WAIT_FAILED == WaitForSingleObject(localOutputThreadHandle.get(), INFINITE));
}
// exitCode might be STILL_ACTIVE if a client has called FreeConsole() and
// thus caused the tab to close, even though the CLI app is still running.
_transitionToState(exitCode == 0 || exitCode == STILL_ACTIVE ? ConnectionState::Closed : ConnectionState::Failed);
_indicateExitWithStatus(exitCode);
_piClient.reset();
}
CATCH_LOG()
@ -564,43 +532,46 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void ConptyConnection::Close() noexcept
try
{
const bool isClosing = _transitionToState(ConnectionState::Closing);
if (isClosing || _isStateAtOrBeyond(ConnectionState::Closed))
_transitionToState(ConnectionState::Closing);
// .reset()ing either of these two will signal ConPTY to send out a CTRL_CLOSE_EVENT to all attached clients.
// FYI: The other members of this class are concurrently read by the _hOutputThread
// thread running in the background and so they're not safe to be .reset().
_hPC.reset();
_inPipe.reset();
if (_hOutputThread)
{
// EXIT POINT
// _clientExitWait holds a CreateThreadpoolWait() which holds a weak reference to "this".
// This manual reset() ensures we wait for it to be teared down via WaitForThreadpoolWaitCallbacks().
_clientExitWait.reset();
_hPC.reset(); // tear down the pseudoconsole (this is like clicking X on a console window)
// CloseHandle() on pipes blocks until any current WriteFile()/ReadFile() has returned.
// CancelSynchronousIo prevents us from deadlocking ourselves.
// At this point in Close(), _inPipe won't be used anymore since the UI parts are torn down.
// _outPipe is probably still stuck in ReadFile() and might currently be written to.
if (_hOutputThread)
// Loop around `CancelSynchronousIo()` just in case the signal to shut down was missed.
// This may happen if we called `CancelSynchronousIo()` while not being stuck
// in `ReadFile()` and if OpenConsole refuses to exit in a timely manner.
for (;;)
{
// ConptyConnection::Close() blocks the UI thread, because `_TerminalOutputHandlers` might indirectly
// reference UI objects like `ControlCore`. CancelSynchronousIo() allows us to have the background
// thread exit as fast as possible by aborting any ongoing writes coming from OpenConsole.
CancelSynchronousIo(_hOutputThread.get());
}
_inPipe.reset(); // break the pipes
_outPipe.reset();
if (_hOutputThread)
{
// Waiting for the output thread to exit ensures that all pending _TerminalOutputHandlers()
// calls have returned and won't notify our caller (ControlCore) anymore. This ensures that
// we don't call a destroyed event handler asynchronously from a background thread (GH#13880).
LOG_LAST_ERROR_IF(WAIT_FAILED == WaitForSingleObject(_hOutputThread.get(), INFINITE));
_hOutputThread.reset();
}
const auto result = WaitForSingleObject(_hOutputThread.get(), 1000);
if (result == WAIT_OBJECT_0)
{
break;
}
if (isClosing)
{
_transitionToState(ConnectionState::Closed);
LOG_LAST_ERROR();
}
}
// Now that the background thread is done, we can safely clean up the other system objects, without
// race conditions, or fear of deadlocking ourselves (e.g. by calling CloseHandle() on _outPipe).
_outPipe.reset();
_hOutputThread.reset();
_piClient.reset();
_transitionToState(ConnectionState::Closed);
}
CATCH_LOG()
@ -657,15 +628,19 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
if (readFail) // reading failed (we must check this first, because read will also be 0.)
{
// EXIT POINT
const auto lastError = GetLastError();
if (lastError != ERROR_BROKEN_PIPE)
if (lastError == ERROR_BROKEN_PIPE)
{
_LastConPtyClientDisconnected();
return S_OK;
}
else
{
// EXIT POINT
_indicateExitWithStatus(HRESULT_FROM_WIN32(lastError)); // print a message
_transitionToState(ConnectionState::Failed);
return gsl::narrow_cast<DWORD>(HRESULT_FROM_WIN32(lastError));
}
// else we call convertUTF8ChunkToUTF16 with an empty string_view to convert possible remaining partials to U+FFFD
}
const auto result{ til::u8u16(std::string_view{ _buffer.data(), read }, _u16Str, _u8State) };
@ -710,6 +685,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
winrt::event_token ConptyConnection::NewConnection(const NewConnectionHandler& handler) { return _newConnectionHandlers.add(handler); };
void ConptyConnection::NewConnection(const winrt::event_token& token) { _newConnectionHandlers.remove(token); };
void ConptyConnection::closePseudoConsoleAsync(HPCON hPC) noexcept
{
::ConptyClosePseudoConsoleTimeout(hPC, 0);
}
HRESULT ConptyConnection::NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client, TERMINAL_STARTUP_INFO startupInfo) noexcept
try
{

View File

@ -6,16 +6,8 @@
#include "ConptyConnection.g.h"
#include "ConnectionStateHolder.h"
#include <conpty-static.h>
#include "ITerminalHandoff.h"
namespace wil
{
// These belong in WIL upstream, so when we reingest the change that has them we'll get rid of ours.
using unique_static_pseudoconsole_handle = wil::unique_any<HPCON, decltype(&::ConptyClosePseudoConsole), ::ConptyClosePseudoConsoleNoWait>;
}
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
struct ConptyConnection : ConptyConnectionT<ConptyConnection>, ConnectionStateHolder<ConptyConnection>
@ -65,12 +57,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler);
private:
static void closePseudoConsoleAsync(HPCON hPC) noexcept;
static HRESULT NewHandoff(HANDLE in, HANDLE out, HANDLE signal, HANDLE ref, HANDLE server, HANDLE client, TERMINAL_STARTUP_INFO startupInfo) noexcept;
static winrt::hstring _commandlineFromProcess(HANDLE process);
HRESULT _LaunchAttachedClient() noexcept;
void _indicateExitWithStatus(unsigned int status) noexcept;
void _ClientTerminated() noexcept;
void _LastConPtyClientDisconnected() noexcept;
til::CoordType _rows{};
til::CoordType _cols{};
@ -90,8 +83,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
wil::unique_hfile _outPipe; // The pipe for reading output from
wil::unique_handle _hOutputThread;
wil::unique_process_information _piClient;
wil::unique_static_pseudoconsole_handle _hPC;
wil::unique_threadpool_wait _clientExitWait;
wil::unique_any<HPCON, decltype(closePseudoConsoleAsync), closePseudoConsoleAsync> _hPC;
til::u8state _u8State{};
std::wstring _u16Str{};

View File

@ -350,6 +350,6 @@ void PtySignalInputThread::_DoSetWindowParent(const SetParentData& data)
// - <none>
void PtySignalInputThread::_Shutdown()
{
// Trigger process shutdown.
CloseConsoleProcessState();
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.GetVtIo()->SendCloseEvent();
}

View File

@ -446,7 +446,7 @@ void VtIo::SetWindowVisibility(bool showOrHide) noexcept
void VtIo::CloseInput()
{
_pVtInputThread = nullptr;
_shutdownNow();
SendCloseEvent();
}
void VtIo::CloseOutput()
@ -455,14 +455,18 @@ void VtIo::CloseOutput()
g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(nullptr);
}
void VtIo::_shutdownNow()
void VtIo::SendCloseEvent()
{
// At this point, we no longer have a renderer or inthread. So we've
// effectively been disconnected from the terminal.
LockConsole();
const auto unlock = wil::scope_exit([] { UnlockConsole(); });
// If we have any remaining attached processes, this will prepare us to send a ctrl+close to them
// if we don't, this will cause us to rundown and exit.
CloseConsoleProcessState();
// This function is called when the ConPTY signal pipe is closed (PtySignalInputThread) and when the input
// pipe is closed (VtIo). Usually these two happen at about the same time. This if condition is a bit of
// a premature optimization and prevents us from sending out a CTRL_CLOSE_EVENT right after another.
if (!std::exchange(_closeEventSent, true))
{
CloseConsoleProcessState();
}
}
// Method Description:

View File

@ -32,11 +32,10 @@ namespace Microsoft::Console::VirtualTerminal
[[nodiscard]] HRESULT StartIfNeeded();
[[nodiscard]] static HRESULT ParseIoMode(const std::wstring& VtMode, _Out_ VtIoMode& ioMode);
[[nodiscard]] HRESULT SuppressResizeRepaint();
[[nodiscard]] HRESULT SetCursorPosition(const til::point coordCursor);
[[nodiscard]] HRESULT SwitchScreenBuffer(const bool useAltBuffer);
void SendCloseEvent();
void CloseInput();
void CloseOutput();
@ -71,6 +70,7 @@ namespace Microsoft::Console::VirtualTerminal
bool _resizeQuirk{ false };
bool _win32InputMode{ false };
bool _passthroughMode{ false };
bool _closeEventSent{ false };
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
std::unique_ptr<Microsoft::Console::VtInputThread> _pVtInputThread;
@ -78,8 +78,6 @@ namespace Microsoft::Console::VirtualTerminal
[[nodiscard]] HRESULT _Initialize(const HANDLE InHandle, const HANDLE OutHandle, const std::wstring& VtMode, _In_opt_ const HANDLE SignalHandle);
void _shutdownNow();
#ifdef UNIT_TESTING
friend class VtIoTests;
#endif

View File

@ -499,9 +499,6 @@ void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo)
// TODO: MSFT 9450717 This should join the ProcessList class when CtrlEvents become moved into the server. https://osgvsowi/9450717
void CloseConsoleProcessState()
{
LockConsole();
const auto unlock = wil::scope_exit([] { UnlockConsole(); });
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// If there are no connected processes, sending control events is pointless as there's no one do send them to. In

View File

@ -28,19 +28,15 @@
#define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u)
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC);
CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show);
CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC);
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC);
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleNoWait(HPCON hPC);
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleTimeout(HPCON hPC, DWORD dwMilliseconds);
CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC);

View File

@ -4,10 +4,11 @@ EXPORTS
ConptyCreatePseudoConsoleAsUser
ConptyResizePseudoConsole
ConptyClosePseudoConsole
ConptyClosePseudoConsoleNoWait
ConptyClosePseudoConsoleTimeout
ConptyClearPseudoConsole
ConptyShowHidePseudoConsole
ConptyReparentPseudoConsole
ConptyReleasePseudoConsole
ConptyPackPseudoConsole
; Compatibility aliases for P/Invoke; only required for compatibility

View File

@ -2,12 +2,91 @@
// Licensed under the MIT license.
#include "precomp.h"
#include <conpty-static.h>
#include "../winconpty.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using unique_hpcon = wil::unique_any<HPCON, decltype(&::ClosePseudoConsole), ::ClosePseudoConsole>;
struct InOut
{
wil::unique_handle in;
wil::unique_handle out;
};
struct Pipes
{
InOut our;
InOut conpty;
};
static Pipes createPipes()
{
Pipes p;
SECURITY_ATTRIBUTES sa{
.nLength = sizeof(sa),
.bInheritHandle = TRUE,
};
VERIFY_IS_TRUE(CreatePipe(p.conpty.in.addressof(), p.our.in.addressof(), &sa, 0));
VERIFY_IS_TRUE(CreatePipe(p.our.out.addressof(), p.conpty.out.addressof(), &sa, 0));
VERIFY_IS_TRUE(SetHandleInformation(p.our.in.get(), HANDLE_FLAG_INHERIT, 0));
VERIFY_IS_TRUE(SetHandleInformation(p.our.out.get(), HANDLE_FLAG_INHERIT, 0));
return p;
}
struct PTY
{
unique_hpcon hpcon;
InOut pipes;
};
static PTY createPseudoConsole()
{
auto pipes = createPipes();
unique_hpcon hpcon;
VERIFY_SUCCEEDED(ConptyCreatePseudoConsole({ 80, 30 }, pipes.conpty.in.get(), pipes.conpty.out.get(), 0, hpcon.addressof()));
return {
.hpcon = std::move(hpcon),
.pipes = std::move(pipes.our),
};
}
static std::string readOutputToEOF(const InOut& io)
{
std::string accumulator;
char buffer[1024];
for (;;)
{
DWORD read;
const auto ok = ReadFile(io.out.get(), &buffer[0], sizeof(buffer), &read, nullptr);
if (!ok)
{
const auto lastError = GetLastError();
if (lastError == ERROR_BROKEN_PIPE)
{
break;
}
else
{
VERIFY_WIN32_BOOL_SUCCEEDED(false);
}
}
accumulator.append(&buffer[0], read);
}
return accumulator;
}
class ConPtyTests
{
BEGIN_TEST_CLASS(ConPtyTests)
@ -21,6 +100,7 @@ class ConPtyTests
TEST_METHOD(GoodCreateMultiple);
TEST_METHOD(SurvivesOnBreakOutput);
TEST_METHOD(DiesOnClose);
TEST_METHOD(ReleasePseudoConsole);
};
static HRESULT _CreatePseudoConsole(const COORD size,
@ -32,7 +112,7 @@ static HRESULT _CreatePseudoConsole(const COORD size,
return _CreatePseudoConsole(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, pPty);
}
static HRESULT AttachPseudoConsole(HPCON hPC, std::wstring& command, PROCESS_INFORMATION* ppi)
static HRESULT AttachPseudoConsole(HPCON hPC, std::wstring command, PROCESS_INFORMATION* ppi)
{
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
@ -85,10 +165,10 @@ void ConPtyTests::CreateConPtyNoPipes()
VERIFY_FAILED(_CreatePseudoConsole(defaultSize, nullptr, nullptr, 0, &pcon));
VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, nullptr, goodOut, 0, &pcon));
_ClosePseudoConsoleMembers(&pcon, TRUE);
_ClosePseudoConsoleMembers(&pcon, INFINITE);
VERIFY_SUCCEEDED(_CreatePseudoConsole(defaultSize, goodIn, nullptr, 0, &pcon));
_ClosePseudoConsoleMembers(&pcon, TRUE);
_ClosePseudoConsoleMembers(&pcon, INFINITE);
}
void ConPtyTests::CreateConPtyBadSize()
@ -132,7 +212,7 @@ void ConPtyTests::GoodCreate()
&pcon));
auto closePty = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon, TRUE);
_ClosePseudoConsoleMembers(&pcon, INFINITE);
});
}
@ -161,7 +241,7 @@ void ConPtyTests::GoodCreateMultiple()
0,
&pcon1));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon1, TRUE);
_ClosePseudoConsoleMembers(&pcon1, INFINITE);
});
VERIFY_SUCCEEDED(
@ -171,7 +251,7 @@ void ConPtyTests::GoodCreateMultiple()
0,
&pcon2));
auto closePty2 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pcon2, TRUE);
_ClosePseudoConsoleMembers(&pcon2, INFINITE);
});
}
@ -198,7 +278,7 @@ void ConPtyTests::SurvivesOnBreakOutput()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty, TRUE);
_ClosePseudoConsoleMembers(&pty, INFINITE);
});
DWORD dwExit;
@ -257,7 +337,7 @@ void ConPtyTests::DiesOnClose()
0,
&pty));
auto closePty1 = wil::scope_exit([&] {
_ClosePseudoConsoleMembers(&pty, TRUE);
_ClosePseudoConsoleMembers(&pty, INFINITE);
});
DWORD dwExit;
@ -281,8 +361,22 @@ void ConPtyTests::DiesOnClose()
Log::Comment(NoThrowString().Format(L"Sleep a bit to let the process attach"));
Sleep(100);
_ClosePseudoConsoleMembers(&pty, TRUE);
_ClosePseudoConsoleMembers(&pty, INFINITE);
GetExitCodeProcess(hConPtyProcess.get(), &dwExit);
VERIFY_ARE_NOT_EQUAL(dwExit, (DWORD)STILL_ACTIVE);
}
// Issues with ConptyReleasePseudoConsole functionality might present itself as sporadic/flaky test failures,
// which should not ever happen (otherwise something is broken). This is because "start /b" runs concurrently
// with the initially spawned "cmd.exe" exiting and so this test involves sort of a race condition.
void ConPtyTests::ReleasePseudoConsole()
{
const auto pty = createPseudoConsole();
wil::unique_process_information pi;
VERIFY_SUCCEEDED(AttachPseudoConsole(pty.hpcon.get(), L"cmd.exe /c start /b cmd.exe /c echo foobar", pi.addressof()));
VERIFY_SUCCEEDED(ConptyReleasePseudoConsole(pty.hpcon.get()));
const auto output = readOutputToEOF(pty.pipes);
VERIFY_ARE_NOT_EQUAL(std::string::npos, output.find("foobar"));
}

View File

@ -34,6 +34,10 @@
<Import Project="$(SolutionDir)src\common.build.tests.props" />
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
<ItemDefinitionGroup>
<ClCompile>
<!-- By defining this here, we ensure that we don't try to dllimport conpty -->
<PreprocessorDefinitions>CONPTY_IMPEXP=;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>$(OutDir)\conptylib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>

View File

@ -112,6 +112,14 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
wil::unique_handle serverHandle;
RETURN_IF_NTSTATUS_FAILED(CreateServerHandle(serverHandle.addressof(), TRUE));
// The hPtyReference we create here is used when the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute is processed.
// This ensures that conhost's client processes inherit the correct (= our) console handle.
wil::unique_handle referenceHandle;
RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(referenceHandle.addressof(),
serverHandle.get(),
L"\\Reference",
FALSE));
wil::unique_handle signalPipeConhostSide;
wil::unique_handle signalPipeOurSide;
@ -226,18 +234,9 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
}
}
// Move the process handle out of the PROCESS_INFORMATION into out Pseudoconsole
pPty->hConPtyProcess = pi.hProcess;
pi.hProcess = nullptr;
// The hPtyReference we create here is used when the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute is processed.
// This ensures that conhost's client processes inherit the correct (= our) console handle.
RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(&pPty->hPtyReference,
serverHandle.get(),
L"\\Reference",
FALSE));
pPty->hSignal = signalPipeOurSide.release();
pPty->hPtyReference = referenceHandle.release();
pPty->hConPtyProcess = std::exchange(pi.hProcess, nullptr);
return S_OK;
}
@ -352,7 +351,7 @@ HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const
// - wait: If true, waits for conhost/OpenConsole to exit first.
// Return Value:
// - <none>
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait)
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds)
{
if (pPty != nullptr)
{
@ -376,9 +375,9 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait)
// has yet to send before we hard kill it.
if (_HandleIsValid(pPty->hConPtyProcess))
{
if (wait)
if (dwMilliseconds)
{
WaitForSingleObject(pPty->hConPtyProcess, INFINITE);
WaitForSingleObject(pPty->hConPtyProcess, dwMilliseconds);
}
CloseHandle(pPty->hConPtyProcess);
@ -396,11 +395,11 @@ void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait)
// - wait: If true, waits for conhost/OpenConsole to exit first.
// Return Value:
// - <none>
static void _ClosePseudoConsole(_In_ PseudoConsole* pPty, BOOL wait) noexcept
static void _ClosePseudoConsole(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds) noexcept
{
if (pPty != nullptr)
{
_ClosePseudoConsoleMembers(pPty, wait);
_ClosePseudoConsoleMembers(pPty, dwMilliseconds);
HeapFree(GetProcessHeap(), 0, pPty);
}
}
@ -441,12 +440,12 @@ extern "C" HRESULT WINAPI ConptyCreatePseudoConsole(_In_ COORD size,
return ConptyCreatePseudoConsoleAsUser(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, phPC);
}
extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
_In_ COORD size,
_In_ HANDLE hInput,
_In_ HANDLE hOutput,
_In_ DWORD dwFlags,
_Out_ HPCON* phPC)
extern "C" HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
_In_ COORD size,
_In_ HANDLE hInput,
_In_ HANDLE hOutput,
_In_ DWORD dwFlags,
_Out_ HPCON* phPC)
{
if (phPC == nullptr)
{
@ -461,7 +460,7 @@ extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
auto pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole));
RETURN_IF_NULL_ALLOC(pPty);
auto cleanupPty = wil::scope_exit([&]() noexcept {
_ClosePseudoConsole(pPty, TRUE);
_ClosePseudoConsole(pPty, 0);
});
wil::unique_handle duplicatedInput;
@ -525,13 +524,28 @@ extern "C" HRESULT WINAPI ConptyShowHidePseudoConsole(_In_ HPCON hPC, bool show)
// - Used to support GH#2988
extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newParent)
{
const PseudoConsole* const pPty = (PseudoConsole*)hPC;
auto hr = pPty == nullptr ? E_INVALIDARG : S_OK;
if (SUCCEEDED(hr))
return _ReparentPseudoConsole((PseudoConsole*)hPC, newParent);
}
// The \Reference handle ensures that conhost keeps running by keeping the ConDrv server pipe open.
// After you've finished setting up your PTY via PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, this method may be called
// to release that handle, allowing conhost to shut down automatically once the last client has disconnected.
// You'll know when this happens, because a ReadFile() on the output pipe will return ERROR_BROKEN_PIPE.
extern "C" HRESULT WINAPI ConptyReleasePseudoConsole(_In_ HPCON hPC)
{
const auto pPty = (PseudoConsole*)hPC;
if (pPty == nullptr)
{
hr = _ReparentPseudoConsole(pPty, newParent);
return E_INVALIDARG;
}
return hr;
if (_HandleIsValid(pPty->hPtyReference))
{
CloseHandle(pPty->hPtyReference);
pPty->hPtyReference = nullptr;
}
return S_OK;
}
// Function Description:
@ -543,7 +557,7 @@ extern "C" HRESULT WINAPI ConptyReparentPseudoConsole(_In_ HPCON hPC, HWND newPa
// Waits for conhost/OpenConsole to exit first.
extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC)
{
_ClosePseudoConsole((PseudoConsole*)hPC, TRUE);
_ClosePseudoConsole((PseudoConsole*)hPC, INFINITE);
}
// Function Description:
@ -553,9 +567,9 @@ extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC)
// This can fail if the conhost hosting the pseudoconsole failed to be
// terminated, or if the pseudoconsole was already terminated.
// Doesn't wait for conhost/OpenConsole to exit.
extern "C" VOID WINAPI ConptyClosePseudoConsoleNoWait(_In_ HPCON hPC)
extern "C" VOID WINAPI ConptyClosePseudoConsoleTimeout(_In_ HPCON hPC, _In_ DWORD dwMilliseconds)
{
_ClosePseudoConsole((PseudoConsole*)hPC, FALSE);
_ClosePseudoConsole((PseudoConsole*)hPC, dwMilliseconds);
}
// NOTE: This one is not defined in the Windows headers but is

View File

@ -10,8 +10,33 @@ extern "C" {
// This structure is part of an ABI shared with the rest of the operating system.
typedef struct _PseudoConsole
{
// hSignal is a anonymous pipe used for out of band communication with conhost.
// It's used to send the various PTY_SIGNAL_* messages.
HANDLE hSignal;
// The "server handle" in conhost represents the console IPC "pipe" over which all console
// messages, all client connect and disconnect events, API calls, text output, etc. flow.
// The full type of this handle is \Device\ConDrv\Server and is implemented in
// /minkernel/console/condrv/server.c. If you inspect conhost's handles it'll show up
// as a handle of name \Device\ConDrv, because that's the namespace of these handles.
//
// hPtyReference is derived from that handle (= a child), is named \Reference and is implemented
// in /minkernel/console/condrv/reference.c. While conhost is the sole owner and user of the
// "server handle", the "reference handle" is what console processes actually inherit in order
// to communicate with the console server (= conhost). When the reference count of the
// \Reference handle drops to 0, it'll release its reference to the server handle.
// The server handle in turn is implemented in such a way that the IPC pipe is broken
// once the reference count drops to 1, because then conhost must be the last one using it.
//
// In other words: As long as hPtyReference exists it'll keep the server handle alive
// and thus keep conhost alive. Closing this handle will make conhost exit as soon as all
// currently connected clients have disconnected and closed the reference handle as well.
//
// This benefit of this system is that it naturally works with handle inheritance in
// CreateProcess, which ensures that the reference handle is safely duplicated and
// transmitted from a parent process to a new child process, even if the parent
// process exits before the OS has even finished spawning the child process.
HANDLE hPtyReference;
// hConPtyProcess is a process handle to the conhost instance that we've spawned for ConPTY.
HANDLE hConPtyProcess;
} PseudoConsole;
@ -24,11 +49,18 @@ typedef struct _PseudoConsole
#define PTY_SIGNAL_RESIZE_WINDOW (8u)
// CreatePseudoConsole Flags
// The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo
// #define PSEUDOCONSOLE_INHERIT_CURSOR (0x1)
#ifndef PSEUDOCONSOLE_INHERIT_CURSOR
#define PSEUDOCONSOLE_INHERIT_CURSOR (0x1)
#endif
#ifndef PSEUDOCONSOLE_RESIZE_QUIRK
#define PSEUDOCONSOLE_RESIZE_QUIRK (0x2)
#endif
#ifndef PSEUDOCONSOLE_WIN32_INPUT_MODE
#define PSEUDOCONSOLE_WIN32_INPUT_MODE (0x4)
#endif
#ifndef PSEUDOCONSOLE_PASSTHROUGH_MODE
#define PSEUDOCONSOLE_PASSTHROUGH_MODE (0x8)
#endif
// Implementations of the various PseudoConsole functions.
HRESULT _CreatePseudoConsole(const HANDLE hToken,
@ -42,14 +74,14 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty);
HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show);
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, BOOL wait);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds);
HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
_In_ COORD size,
_In_ HANDLE hInput,
_In_ HANDLE hOutput,
_In_ DWORD dwFlags,
_Out_ HPCON* phPC);
HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
_In_ COORD size,
_In_ HANDLE hInput,
_In_ HANDLE hOutput,
_In_ DWORD dwFlags,
_Out_ HPCON* phPC);
#ifdef __cplusplus
}