// | |
// Windows versions tested | |
// | |
// Vista Enterprise SP2 32-bit | |
// - ver reports [Version 6.0.6002] | |
// - kernel32.dll product/file versions are 6.0.6002.19381 | |
// | |
// Windows 7 Ultimate SP1 32-bit | |
// - ver reports [Version 6.1.7601] | |
// - conhost.exe product/file versions are 6.1.7601.18847 | |
// - kernel32.dll product/file versions are 6.1.7601.18847 | |
// | |
// Windows Server 2008 R2 Datacenter SP1 64-bit | |
// - ver reports [Version 6.1.7601] | |
// - conhost.exe product/file versions are 6.1.7601.23153 | |
// - kernel32.dll product/file versions are 6.1.7601.23153 | |
// | |
// Windows 8 Enterprise 32-bit | |
// - ver reports [Version 6.2.9200] | |
// - conhost.exe product/file versions are 6.2.9200.16578 | |
// - kernel32.dll product/file versions are 6.2.9200.16859 | |
// | |
// | |
// Specific version details on working Server 2008 R2: | |
// | |
// dwMajorVersion = 6 | |
// dwMinorVersion = 1 | |
// dwBuildNumber = 7601 | |
// dwPlatformId = 2 | |
// szCSDVersion = Service Pack 1 | |
// wServicePackMajor = 1 | |
// wServicePackMinor = 0 | |
// wSuiteMask = 0x190 | |
// wProductType = 0x3 | |
// | |
// Specific version details on broken Win7: | |
// | |
// dwMajorVersion = 6 | |
// dwMinorVersion = 1 | |
// dwBuildNumber = 7601 | |
// dwPlatformId = 2 | |
// szCSDVersion = Service Pack 1 | |
// wServicePackMajor = 1 | |
// wServicePackMinor = 0 | |
// wSuiteMask = 0x100 | |
// wProductType = 0x1 | |
// | |
const char *g_prefix = ""; | |
static void dumpHandles() { | |
trace("%sSTDIN=0x%I64x STDOUT=0x%I64x STDERR=0x%I64x", | |
g_prefix, | |
(long long)GetStdHandle(STD_INPUT_HANDLE), | |
(long long)GetStdHandle(STD_OUTPUT_HANDLE), | |
(long long)GetStdHandle(STD_ERROR_HANDLE)); | |
} | |
static const char *successOrFail(BOOL ret) { | |
return ret ? "ok" : "FAILED"; | |
} | |
static void startChildInSameConsole(const wchar_t *args, BOOL | |
bInheritHandles=FALSE) { | |
wchar_t program[1024]; | |
wchar_t cmdline[1024]; | |
GetModuleFileNameW(NULL, program, 1024); | |
swprintf(cmdline, L"\"%ls\" %ls", program, args); | |
STARTUPINFOW sui; | |
PROCESS_INFORMATION pi; | |
memset(&sui, 0, sizeof(sui)); | |
memset(&pi, 0, sizeof(pi)); | |
sui.cb = sizeof(sui); | |
CreateProcessW(program, cmdline, | |
NULL, NULL, | |
/*bInheritHandles=*/bInheritHandles, | |
/*dwCreationFlags=*/0, | |
NULL, NULL, | |
&sui, &pi); | |
} | |
static void closeHandle(HANDLE h) { | |
trace("%sClosing handle 0x%I64x...", g_prefix, (long long)h); | |
trace("%sClosing handle 0x%I64x... %s", g_prefix, (long long)h, successOrFail(CloseHandle(h))); | |
} | |
static HANDLE createBuffer() { | |
// If sa isn't provided, the handle defaults to not-inheritable. | |
SECURITY_ATTRIBUTES sa = {0}; | |
sa.nLength = sizeof(sa); | |
sa.bInheritHandle = TRUE; | |
trace("%sCreating a new buffer...", g_prefix); | |
HANDLE conout = CreateConsoleScreenBuffer( | |
GENERIC_READ | GENERIC_WRITE, | |
FILE_SHARE_READ | FILE_SHARE_WRITE, | |
&sa, | |
CONSOLE_TEXTMODE_BUFFER, NULL); | |
trace("%sCreating a new buffer... 0x%I64x", g_prefix, (long long)conout); | |
return conout; | |
} | |
static HANDLE openConout() { | |
// If sa isn't provided, the handle defaults to not-inheritable. | |
SECURITY_ATTRIBUTES sa = {0}; | |
sa.nLength = sizeof(sa); | |
sa.bInheritHandle = TRUE; | |
trace("%sOpening CONOUT...", g_prefix); | |
HANDLE conout = CreateFileW(L"CONOUT$", | |
GENERIC_READ | GENERIC_WRITE, | |
FILE_SHARE_READ | FILE_SHARE_WRITE, | |
&sa, | |
OPEN_EXISTING, 0, NULL); | |
trace("%sOpening CONOUT... 0x%I64x", g_prefix, (long long)conout); | |
return conout; | |
} | |
static void setConsoleActiveScreenBuffer(HANDLE conout) { | |
trace("%sSetConsoleActiveScreenBuffer(0x%I64x) called...", | |
g_prefix, (long long)conout); | |
trace("%sSetConsoleActiveScreenBuffer(0x%I64x) called... %s", | |
g_prefix, (long long)conout, | |
successOrFail(SetConsoleActiveScreenBuffer(conout))); | |
} | |
static void writeTest(HANDLE conout, const char *msg) { | |
char writeData[256]; | |
sprintf(writeData, "%s%s\n", g_prefix, msg); | |
trace("%sWriting to 0x%I64x: '%s'...", | |
g_prefix, (long long)conout, msg); | |
DWORD actual = 0; | |
BOOL ret = WriteConsoleA(conout, writeData, strlen(writeData), &actual, NULL); | |
trace("%sWriting to 0x%I64x: '%s'... %s", | |
g_prefix, (long long)conout, msg, | |
successOrFail(ret && actual == strlen(writeData))); | |
} | |
static void writeTest(const char *msg) { | |
writeTest(GetStdHandle(STD_OUTPUT_HANDLE), msg); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST 1 -- create new buffer, activate it, and close the handle. The console | |
// automatically switches the screen buffer back to the original. | |
// | |
// This test passes everywhere. | |
// | |
static void test1(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "1")) { | |
startChildProcess(L"1:child"); | |
return; | |
} | |
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); | |
writeTest(origBuffer, "<-- origBuffer -->"); | |
HANDLE newBuffer = createBuffer(); | |
writeTest(newBuffer, "<-- newBuffer -->"); | |
setConsoleActiveScreenBuffer(newBuffer); | |
Sleep(2000); | |
writeTest(origBuffer, "TEST PASSED!"); | |
// Closing the handle w/o switching the active screen buffer automatically | |
// switches the console back to the original buffer. | |
closeHandle(newBuffer); | |
while (true) { | |
Sleep(1000); | |
} | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST 2 -- Test program that creates and activates newBuffer, starts a child | |
// process, then closes its newBuffer handle. newBuffer remains activated, | |
// because the child keeps it active. (Also see TEST D.) | |
// | |
static void test2(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "2")) { | |
startChildProcess(L"2:parent"); | |
return; | |
} | |
if (!strcmp(argv[1], "2:parent")) { | |
g_prefix = "parent: "; | |
dumpHandles(); | |
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); | |
writeTest(origBuffer, "<-- origBuffer -->"); | |
HANDLE newBuffer = createBuffer(); | |
writeTest(newBuffer, "<-- newBuffer -->"); | |
setConsoleActiveScreenBuffer(newBuffer); | |
Sleep(1000); | |
writeTest(newBuffer, "bInheritHandles=FALSE:"); | |
startChildInSameConsole(L"2:child", FALSE); | |
Sleep(1000); | |
writeTest(newBuffer, "bInheritHandles=TRUE:"); | |
startChildInSameConsole(L"2:child", TRUE); | |
Sleep(1000); | |
trace("parent:----"); | |
// Close the new buffer. The active screen buffer doesn't automatically | |
// switch back to origBuffer, because the child process has a handle open | |
// to the original buffer. | |
closeHandle(newBuffer); | |
Sleep(600 * 1000); | |
return; | |
} | |
if (!strcmp(argv[1], "2:child")) { | |
g_prefix = "child: "; | |
dumpHandles(); | |
// The child's output isn't visible, because it's still writing to | |
// origBuffer. | |
trace("child:----"); | |
writeTest("writing to STDOUT"); | |
// Handle inheritability is curious. The console handles this program | |
// creates are inheritable, but CreateProcess is called with both | |
// bInheritHandles=TRUE and bInheritHandles=FALSE. | |
// | |
// Vista and Windows 7: bInheritHandles has no effect. The child and | |
// parent processes have the same STDIN/STDOUT/STDERR handles: | |
// 0x3, 0x7, and 0xB. The parent has a 0xF handle for newBuffer. | |
// The child can only write to 0x7, 0xB, and 0xF. Only the writes to | |
// 0xF are visible (i.e. they touch newBuffer). | |
// | |
// Windows 8 or Windows 10 (legacy or non-legacy): the lowest 2 bits of | |
// the HANDLE to WriteConsole seem to be ignored. The new process' | |
// console handles always refer to the buffer that was active when they | |
// started, but the values of the handles depend upon bInheritHandles. | |
// With bInheritHandles=TRUE, the child has the same | |
// STDIN/STDOUT/STDERR/newBuffer handles as the parent, and the three | |
// output handles all work, though their output is all visible. With | |
// bInheritHandles=FALSE, the child has different STDIN/STDOUT/STDERR | |
// handles, and only the new STDOUT/STDERR handles work. | |
// | |
for (unsigned int i = 0x1; i <= 0xB0; ++i) { | |
char msg[256]; | |
sprintf(msg, "Write to handle 0x%x", i); | |
HANDLE h = reinterpret_cast<HANDLE>(i); | |
writeTest(h, msg); | |
} | |
Sleep(600 * 1000); | |
return; | |
} | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST A -- demonstrate an apparent Windows bug with screen buffers | |
// | |
// Steps: | |
// - The parent starts a child process. | |
// - The child process creates and activates newBuffer | |
// - The parent opens CONOUT$ and writes to it. | |
// - The parent closes CONOUT$. | |
// - At this point, broken Windows reactivates origBuffer. | |
// - The child writes to newBuffer again. | |
// - The child activates origBuffer again, then closes newBuffer. | |
// | |
// Test passes if the message "TEST PASSED!" is visible. | |
// Test commonly fails if conhost.exe crashes. | |
// | |
// Results: | |
// - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes | |
// - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS | |
// - Windows 8 Enterprise 32-bit: PASS | |
// - Windows 10 64-bit (legacy and non-legacy): PASS | |
// | |
static void testA_parentWork() { | |
// Open an extra CONOUT$ handle so that the HANDLE values in parent and | |
// child don't collide. I think it's OK if they collide, but since we're | |
// trying to track down a Windows bug, it's best to avoid unnecessary | |
// complication. | |
HANDLE dummy = openConout(); | |
Sleep(3000); | |
// Step 2: Open CONOUT$ in the parent. This opens the active buffer, which | |
// was just created in the child. It's handle 0x13. Write to it. | |
HANDLE newBuffer = openConout(); | |
writeTest(newBuffer, "step2: writing to newBuffer"); | |
Sleep(3000); | |
// Step 3: Close handle 0x13. With Windows 7, the console switches back to | |
// origBuffer, and (unless I'm missing something) it shouldn't. | |
closeHandle(newBuffer); | |
} | |
static void testA_childWork() { | |
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); | |
// | |
// Step 1: Create the new screen buffer in the child process and make it | |
// active. (Typically, it's handle 0x0F.) | |
// | |
HANDLE newBuffer = createBuffer(); | |
setConsoleActiveScreenBuffer(newBuffer); | |
writeTest(newBuffer, "<-- newBuffer -->"); | |
Sleep(9000); | |
trace("child:----"); | |
// Step 4: write to the newBuffer again. | |
writeTest(newBuffer, "TEST PASSED!"); | |
// | |
// Step 5: Switch back to the original screen buffer and close the new | |
// buffer. The switch call succeeds, but the CloseHandle call freezes for | |
// several seconds, because conhost.exe crashes. | |
// | |
Sleep(3000); | |
setConsoleActiveScreenBuffer(origBuffer); | |
writeTest(origBuffer, "writing to origBuffer"); | |
closeHandle(newBuffer); | |
// The console HWND is NULL. | |
trace("child: console HWND=0x%I64x", (long long)GetConsoleWindow()); | |
// At this point, the console window has closed, but the parent/child | |
// processes are still running. Calling AllocConsole would fail, but | |
// calling FreeConsole followed by AllocConsole would both succeed, and a | |
// new console would appear. | |
} | |
static void testA(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "A")) { | |
startChildProcess(L"A:parent"); | |
return; | |
} | |
if (!strcmp(argv[1], "A:parent")) { | |
g_prefix = "parent: "; | |
trace("parent:----"); | |
dumpHandles(); | |
writeTest("<-- origBuffer -->"); | |
startChildInSameConsole(L"A:child"); | |
testA_parentWork(); | |
Sleep(120000); | |
return; | |
} | |
if (!strcmp(argv[1], "A:child")) { | |
g_prefix = "child: "; | |
dumpHandles(); | |
testA_childWork(); | |
Sleep(120000); | |
return; | |
} | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST B -- invert TEST A -- also crashes conhost on Windows 7 | |
// | |
// Test passes if the message "TEST PASSED!" is visible. | |
// Test commonly fails if conhost.exe crashes. | |
// | |
// Results: | |
// - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes | |
// - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS | |
// - Windows 8 Enterprise 32-bit: PASS | |
// - Windows 10 64-bit (legacy and non-legacy): PASS | |
// | |
static void testB(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "B")) { | |
startChildProcess(L"B:parent"); | |
return; | |
} | |
if (!strcmp(argv[1], "B:parent")) { | |
g_prefix = "parent: "; | |
startChildInSameConsole(L"B:child"); | |
writeTest("<-- origBuffer -->"); | |
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); | |
// | |
// Step 1: Create the new buffer and make it active. | |
// | |
trace("%s----", g_prefix); | |
HANDLE newBuffer = createBuffer(); | |
setConsoleActiveScreenBuffer(newBuffer); | |
writeTest(newBuffer, "<-- newBuffer -->"); | |
// | |
// Step 4: Attempt to write again to the new buffer. | |
// | |
Sleep(9000); | |
trace("%s----", g_prefix); | |
writeTest(newBuffer, "TEST PASSED!"); | |
// | |
// Step 5: Switch back to the original buffer. | |
// | |
Sleep(3000); | |
trace("%s----", g_prefix); | |
setConsoleActiveScreenBuffer(origBuffer); | |
closeHandle(newBuffer); | |
writeTest(origBuffer, "writing to the initial buffer"); | |
Sleep(60000); | |
return; | |
} | |
if (!strcmp(argv[1], "B:child")) { | |
g_prefix = "child: "; | |
Sleep(3000); | |
trace("%s----", g_prefix); | |
// | |
// Step 2: Open the newly active buffer and write to it. | |
// | |
HANDLE newBuffer = openConout(); | |
writeTest(newBuffer, "writing to newBuffer"); | |
// | |
// Step 3: Close the newly active buffer. | |
// | |
Sleep(3000); | |
closeHandle(newBuffer); | |
Sleep(60000); | |
return; | |
} | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST C -- Interleaving open/close of console handles also seems to break on | |
// Windows 7. | |
// | |
// Test: | |
// - child creates and activates newBuf1 | |
// - parent opens newBuf1 | |
// - child creates and activates newBuf2 | |
// - parent opens newBuf2, then closes newBuf1 | |
// - child switches back to newBuf1 | |
// * At this point, the console starts malfunctioning. | |
// - parent and child close newBuf2 | |
// - child closes newBuf1 | |
// | |
// Test passes if the message "TEST PASSED!" is visible. | |
// Test commonly fails if conhost.exe crashes. | |
// | |
// Results: | |
// - Windows 7 Ultimate SP1 32-bit: conhost.exe crashes | |
// - Windows Server 2008 R2 Datacenter SP1 64-bit: PASS | |
// - Windows 8 Enterprise 32-bit: PASS | |
// - Windows 10 64-bit (legacy and non-legacy): PASS | |
// | |
static void testC(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "C")) { | |
startChildProcess(L"C:parent"); | |
return; | |
} | |
if (!strcmp(argv[1], "C:parent")) { | |
startChildInSameConsole(L"C:child"); | |
writeTest("<-- origBuffer -->"); | |
g_prefix = "parent: "; | |
// At time=4, open newBuffer1. | |
Sleep(4000); | |
trace("%s---- t=4", g_prefix); | |
const HANDLE newBuffer1 = openConout(); | |
// At time=8, open newBuffer2, and close newBuffer1. | |
Sleep(4000); | |
trace("%s---- t=8", g_prefix); | |
const HANDLE newBuffer2 = openConout(); | |
closeHandle(newBuffer1); | |
// At time=25, cleanup of newBuffer2. | |
Sleep(17000); | |
trace("%s---- t=25", g_prefix); | |
closeHandle(newBuffer2); | |
Sleep(240000); | |
return; | |
} | |
if (!strcmp(argv[1], "C:child")) { | |
g_prefix = "child: "; | |
// At time=2, create newBuffer1 and activate it. | |
Sleep(2000); | |
trace("%s---- t=2", g_prefix); | |
const HANDLE newBuffer1 = createBuffer(); | |
setConsoleActiveScreenBuffer(newBuffer1); | |
writeTest(newBuffer1, "<-- newBuffer1 -->"); | |
// At time=6, create newBuffer2 and activate it. | |
Sleep(4000); | |
trace("%s---- t=6", g_prefix); | |
const HANDLE newBuffer2 = createBuffer(); | |
setConsoleActiveScreenBuffer(newBuffer2); | |
writeTest(newBuffer2, "<-- newBuffer2 -->"); | |
// At time=10, attempt to switch back to newBuffer1. The parent process | |
// has opened and closed its handle to newBuffer1, so does it still exist? | |
Sleep(4000); | |
trace("%s---- t=10", g_prefix); | |
setConsoleActiveScreenBuffer(newBuffer1); | |
writeTest(newBuffer1, "write to newBuffer1: TEST PASSED!"); | |
// At time=25, cleanup of newBuffer2. | |
Sleep(15000); | |
trace("%s---- t=25", g_prefix); | |
closeHandle(newBuffer2); | |
// At time=35, cleanup of newBuffer1. The console should switch to the | |
// initial buffer again. | |
Sleep(10000); | |
trace("%s---- t=35", g_prefix); | |
closeHandle(newBuffer1); | |
Sleep(240000); | |
return; | |
} | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// TEST D -- parent creates a new buffer, child launches, writes, | |
// closes it output handle, then parent writes again. (Also see TEST 2.) | |
// | |
// On success, this will appear: | |
// | |
// parent: <-- newBuffer --> | |
// child: writing to newBuffer | |
// parent: TEST PASSED! | |
// | |
// If this appears, it indicates that the child's closing its output handle did | |
// not destroy newBuffer. | |
// | |
// Results: | |
// - Windows 7 Ultimate SP1 32-bit: PASS | |
// - Windows 8 Enterprise 32-bit: PASS | |
// - Windows 10 64-bit (legacy and non-legacy): PASS | |
// | |
static void testD(int argc, char *argv[]) { | |
if (!strcmp(argv[1], "D")) { | |
startChildProcess(L"D:parent"); | |
return; | |
} | |
if (!strcmp(argv[1], "D:parent")) { | |
g_prefix = "parent: "; | |
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE); | |
writeTest(origBuffer, "<-- origBuffer -->"); | |
HANDLE newBuffer = createBuffer(); | |
writeTest(newBuffer, "<-- newBuffer -->"); | |
setConsoleActiveScreenBuffer(newBuffer); | |
// At t=2, start a child process, explicitly forcing it to use | |
// newBuffer for its standard handles. These calls are apparently | |
// redundant on Windows 8 and up. | |
Sleep(2000); | |
trace("parent:----"); | |
trace("parent: starting child process"); | |
SetStdHandle(STD_OUTPUT_HANDLE, newBuffer); | |
SetStdHandle(STD_ERROR_HANDLE, newBuffer); | |
startChildInSameConsole(L"D:child"); | |
SetStdHandle(STD_OUTPUT_HANDLE, origBuffer); | |
SetStdHandle(STD_ERROR_HANDLE, origBuffer); | |
// At t=6, write again to newBuffer. | |
Sleep(4000); | |
trace("parent:----"); | |
writeTest(newBuffer, "TEST PASSED!"); | |
// At t=8, close the newBuffer. In earlier versions of windows | |
// (including Server 2008 R2), the console then switches back to | |
// origBuffer. As of Windows 8, it doesn't, because somehow the child | |
// process is keeping the console on newBuffer, even though the child | |
// process closed its STDIN/STDOUT/STDERR handles. Killing the child | |
// process by hand after the test finishes *does* force the console | |
// back to origBuffer. | |
Sleep(2000); | |
closeHandle(newBuffer); | |
Sleep(120000); | |
return; | |
} | |
if (!strcmp(argv[1], "D:child")) { | |
g_prefix = "child: "; | |
// At t=2, the child starts. | |
trace("child:----"); | |
dumpHandles(); | |
writeTest("writing to newBuffer"); | |
// At t=4, the child explicitly closes its handle. | |
Sleep(2000); | |
trace("child:----"); | |
if (GetStdHandle(STD_ERROR_HANDLE) != GetStdHandle(STD_OUTPUT_HANDLE)) { | |
closeHandle(GetStdHandle(STD_ERROR_HANDLE)); | |
} | |
closeHandle(GetStdHandle(STD_OUTPUT_HANDLE)); | |
closeHandle(GetStdHandle(STD_INPUT_HANDLE)); | |
Sleep(120000); | |
return; | |
} | |
} | |
int main(int argc, char *argv[]) { | |
if (argc == 1) { | |
printf("USAGE: %s testnum\n", argv[0]); | |
return 0; | |
} | |
if (argv[1][0] == '1') { | |
test1(argc, argv); | |
} else if (argv[1][0] == '2') { | |
test2(argc, argv); | |
} else if (argv[1][0] == 'A') { | |
testA(argc, argv); | |
} else if (argv[1][0] == 'B') { | |
testB(argc, argv); | |
} else if (argv[1][0] == 'C') { | |
testC(argc, argv); | |
} else if (argv[1][0] == 'D') { | |
testD(argc, argv); | |
} | |
return 0; | |
} | |