22
33#include < windows.h>
44
5+ #include < array>
56#include < chrono>
67#include < future>
78#include < thread>
@@ -13,10 +14,12 @@ namespace et {
1314
1415class OutputPressureTerminal : public PseudoUserTerminal {
1516 public:
16- size_t queuedOutputSize () {
17+ std::string queuedOutput () {
1718 std::lock_guard<std::mutex> guard (pendingMutex);
18- return pendingOutput. size () ;
19+ return pendingOutput;
1920 }
21+
22+ static constexpr size_t outputLimit () { return MAX_PENDING_OUTPUT ; }
2023};
2124
2225TEST_CASE (" PseudoUserTerminal cleanup falls back without a job" ,
@@ -53,35 +56,47 @@ TEST_CASE("PseudoUserTerminal cleanup falls back without a job",
5356// parked waiting for drainOutput() to free up its pending-output cap, and
5457// nobody calls drainOutput() again during shutdown), the ConPTY host can
5558// block writing to a full pipe and ClosePseudoConsole waits for that host
56- // to exit forever. Heap-allocate the terminal and leak it (rather than
57- // join/delete) if cleanup() does not return in time, since a hung cleanup()
58- // thread could otherwise touch freed memory.
59+ // to exit forever. Teardown must continue draining without discarding output
60+ // that was already queued for the router. Heap-allocate the terminal and leak
61+ // it (rather than join/delete) if cleanup() does not return in time, since a
62+ // hung cleanup() thread could otherwise touch freed memory.
5963TEST_CASE (
6064 " PseudoUserTerminal cleanup does not hang under queued, undrained output" ,
6165 " [PseudoUserTerminal][windows]" ) {
6266 const char * previousShell = ::getenv (" SHELL" );
6367 const bool hadShell = previousShell != nullptr ;
6468 const std::string savedShell = hadShell ? previousShell : " " ;
65- REQUIRE (_putenv_s (" SHELL" , " cmd.exe" ) == 0 );
69+ std::array<wchar_t , 32768 > testExecutable;
70+ const DWORD testExecutableLength =
71+ GetModuleFileNameW (nullptr , testExecutable.data (),
72+ static_cast <DWORD >(testExecutable.size ()));
73+ REQUIRE (testExecutableLength > 0 );
74+ REQUIRE (testExecutableLength < testExecutable.size ());
75+ const fs::path helperPath =
76+ fs::path (std::wstring (testExecutable.data (), testExecutableLength))
77+ .parent_path () /
78+ " et-conpty-output-helper.exe" ;
79+ REQUIRE (fs::is_regular_file (helperPath));
80+ REQUIRE (fs::is_directory (helperPath.parent_path ()));
81+ REQUIRE (_putenv_s (" SHELL" , helperPath.string ().c_str ()) == 0 );
6682 auto * term = new OutputPressureTerminal ();
6783 const int setupResult = term->setup (-1 );
6884 REQUIRE (_putenv_s (" SHELL" , savedShell.c_str ()) == 0 );
6985 REQUIRE (setupResult == 0 );
7086
71- // Flood stdout continuously and never call drainOutput(), modeling a
72- // stalled router consumer. This drives the drain thread past its
73- // pending-output cap and fills the underlying pipe before teardown.
74- term->writeInput (
75- " for /L %i in (1,1,1000000) do @echo "
76- " AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
77- " \r\n " );
87+ // The helper writes a finite 1 MiB directly to its ConPTY stdout. Never
88+ // call drainOutput() before cleanup, modeling a stalled router consumer
89+ // without relying on shell startup, command parsing, or loop throughput.
7890 const auto pressureDeadline =
79- std::chrono::steady_clock::now () + std::chrono::seconds (5 );
80- while (term->queuedOutputSize () < 256 * 1024 &&
91+ std::chrono::steady_clock::now () + std::chrono::seconds (15 );
92+ string queuedBeforeCleanup;
93+ while (queuedBeforeCleanup.size () < OutputPressureTerminal::outputLimit () &&
8194 std::chrono::steady_clock::now () < pressureDeadline) {
8295 std::this_thread::sleep_for (std::chrono::milliseconds (20 ));
96+ queuedBeforeCleanup = term->queuedOutput ();
8397 }
84- const bool capReached = term->queuedOutputSize () >= 256 * 1024 ;
98+ const bool capReached =
99+ queuedBeforeCleanup.size () >= OutputPressureTerminal::outputLimit ();
85100
86101 auto done = std::make_shared<std::promise<void >>();
87102 std::future<void > doneFuture = done->get_future ();
@@ -91,14 +106,22 @@ TEST_CASE(
91106 });
92107
93108 const auto status = doneFuture.wait_for (std::chrono::seconds (15 ));
109+ bool queuedOutputPreserved = false ;
94110 if (status == std::future_status::ready) {
95111 cleanupThread.join ();
112+ const string queuedAfterCleanup = term->drainOutput ();
113+ queuedOutputPreserved =
114+ queuedAfterCleanup.size () >= queuedBeforeCleanup.size () &&
115+ std::equal (queuedBeforeCleanup.begin (), queuedBeforeCleanup.end (),
116+ queuedAfterCleanup.begin ());
96117 delete term;
97118 } else {
98119 cleanupThread.detach ();
99120 }
121+ INFO (" queued before cleanup: " << queuedBeforeCleanup.size ());
100122 REQUIRE (capReached);
101123 REQUIRE (status == std::future_status::ready);
124+ REQUIRE (queuedOutputPreserved);
102125}
103126
104127} // namespace et
0 commit comments