Repository: microsoft/terminal (language: cpp)

Issue:
Terminal window/tab is not closed when console application calls FreeConsole
### Windows Terminal version

_No response_

### Windows build number

_No response_

### Other Software

_No response_

### Steps to reproduce

On windows 11, with the windows terminal being set as the default console application, start a program that was compiled for the console subsystem from explorer.
That program calls ```FreeConsole``` during its lifetime and then keeps running (either because it has spawned a gui or it is still has work to do).

### Expected Behavior

The console window that got spawned by that program is closed as it did with cmd on previous windows versions.

### Actual Behavior

The console window stays open until the program terminates the execution.


Discussion / hints:
I've looked at other similar issues on this repository, but all of them are related to the program manually working on the console window HANDLE that as said various times was a deprecated behaviour, for ```FreeConsole``` instead there's no such deprecation notice, and as the docs says ```A console is closed when the last process attached to it terminates or calls FreeConsole.```, which is the behaviour I expect as only my program should be attached to that console. (That is also emphasized by the fact that when the program terminates then the console closes as well, and that indicates that there are no other programs attached to it.)
We could make this work by removing ConPTY's `\Reference` handle from the `ConsoleProcessList` after starting OpenConsole, if OpenConsole was started as part of a handoff.

BTW if you're using `FreeConsole` to hide the console conditionally as part of your startup, I personally believe a better option might be to go with `AttachConsole(ATTACH_PARENT_PROCESS)` and reopen stdout/stdin. I haven't tested it, but it should look something like this:
```cpp
// Compile with /SUBSYSTEM:WINDOWS!
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
    AllocConsole();
}

FILE* stdinNew = nullptr;
freopen_s(&stdinNew, "CONIN$", "r+", stdin);
FILE* stdoutNew = nullptr;
freopen_s(&stdoutNew, "CONOUT$", "w+", stdout);
FILE* stderrNew = nullptr;
freopen_s(&stderrNew, "CONOUT$", "w+", stderr);

printf("Hello, World!\n");
```
> BTW if you're using FreeConsole to hide the console conditionally as part of your startup

Yes, I'm doing that but because before my program reads its configs from which the console can be disabled, it has already printed some debug information that would not be there if I created the console afterwards (this is where I'm doing that call, by that time the console has already debug Info from its libraries printed to the console https://github.com/edo9300/edopro/blob/bce9989dbf6f497d1b5f3c009f4b6328d80e9201/gframe/gframe.cpp#L223), so I can't create the console afterwards.

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 08b5652e1b0..807a6298765 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -817,6 +817,7 @@ HORZ
 hostable
 hostlib
 HPA
+hpcon
 HPCON
 hpj
 HPR
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp
index 659cfc4bd47..9ddc90cdf2d 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.cpp
+++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp
@@ -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))
-        {
-            // 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();
+        _transitionToState(ConnectionState::Closing);
 
-            _hPC.reset(); // tear down the pseudoconsole (this is like clicking X on a console window)
+        // .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();
 
-            // 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)
+        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
     {
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h
index 904eb50ff73..3709ccef33a 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.h
+++ b/src/cascadia/TerminalConnection/ConptyConnection.h
@@ -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{};
diff --git a/src/host/PtySignalInputThread.cpp b/src/host/PtySignalInputThread.cpp
index 12f7ec970b2..534a6d3ae87 100644
--- a/src/host/PtySignalInputThread.cpp
+++ b/src/host/PtySignalInputThread.cpp
@@ -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();
 }
diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp
index 5f8f9ca111e..660141a6d6b 100644
--- a/src/host/VtIo.cpp
+++ b/src/host/VtIo.cpp
@@ -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:
diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp
index 7265464fbe6..b3661f59d43 100644
--- a/src/host/VtIo.hpp
+++ b/src/host/VtIo.hpp
@@ -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
diff --git a/src/host/output.cpp b/src/host/output.cpp
index b85b1d5f324..bec18730d6e 100644
--- a/src/host/output.cpp
+++ b/src/host/output.cpp
@@ -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
diff --git a/src/inc/conpty-static.h b/src/inc/conpty-static.h
index 27fa1437ae6..89b20468408 100644
--- a/src/inc/conpty-static.h
+++ b/src/inc/conpty-static.h
@@ -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);
diff --git a/src/winconpty/dll/winconpty.def b/src/winconpty/dll/winconpty.def
index c5b808e3a96..ba5d52515f5 100644
--- a/src/winconpty/dll/winconpty.def
+++ b/src/winconpty/dll/winconpty.def
@@ -4,10 +4,11 @@ EXPORTS
     ConptyCreatePseudoConsoleAsUser
     ConptyResizePseudoConsole
     ConptyClosePseudoConsole
-    ConptyClosePseudoConsoleNoWait
+    ConptyClosePseudoConsoleTimeout
     ConptyClearPseudoConsole
     ConptyShowHidePseudoConsole
     ConptyReparentPseudoConsole
+    ConptyReleasePseudoConsole
     ConptyPackPseudoConsole
 
     ; Compatibility aliases for P/Invoke; only required for compatibility
diff --git a/src/winconpty/ft_pty/ConPtyTests.cpp b/src/winconpty/ft_pty/ConPtyTests.cpp
index 8289023f879..699e2c665de 100644
--- a/src/winconpty/ft_pty/ConPtyTests.cpp
+++ b/src/winconpty/ft_pty/ConPtyTests.cpp
@@ -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"));
+}
diff --git a/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj b/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj
index 9aa2c808947..eacf4d3ae9d 100644
--- a/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj
+++ b/src/winconpty/ft_pty/winconpty.FeatureTests.vcxproj
@@ -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>
diff --git a/src/winconpty/winconpty.cpp b/src/winconpty/winconpty.cpp
index ed740920b9e..7f5e89c85d6 100644
--- a/src/winconpty/winconpty.cpp
+++ b/src/winconpty/winconpty.cpp
@@ -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
diff --git a/src/winconpty/winconpty.h b/src/winconpty/winconpty.h
index 82355ccb071..744ec09916d 100644
--- a/src/winconpty/winconpty.h
+++ b/src/winconpty/winconpty.h
@@ -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);
-
-HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
-                                        _In_ COORD size,
-                                        _In_ HANDLE hInput,
-                                        _In_ HANDLE hOutput,
-                                        _In_ DWORD dwFlags,
-                                        _Out_ HPCON* phPC);
+void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty, _In_ DWORD dwMilliseconds);
+
+HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken,
+                                               _In_ COORD size,
+                                               _In_ HANDLE hInput,
+                                               _In_ HANDLE hOutput,
+                                               _In_ DWORD dwFlags,
+                                               _Out_ HPCON* phPC);
 
 #ifdef __cplusplus
 }


Test patch: