Console.Write() will hang in WPF, but works in Console application
Basic Explanation: It hangs because the buffer Console.Write
writes to before the text is displayed is getting full and is not draining out for WPF applications when passing in null characters (\0
) for reasons beyond my knowledge.
Detailed Explanation: When you call Console.Write
it creates a Handle to output it's data to and eventually calls WriteFile
on that handle. The other end of the handle needs to process the data that was written to it then return control to the caller. There are two major differences between WPF and a console application I could find:
First, if you inspect the handle type with a console application you get a handle of type FILE_TYPE_CHAR
, from WPF you get FILE_TYPE_PIPE
.
Console.Write(msgStr);var cOut = Console.OpenStandardOutput();var handle = cOut.GetType().GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cOut);var method = Type.GetType("Microsoft.Win32.Win32Native").GetMethod("GetFileType", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);var type = method.Invoke(null, new object[] { handle });Debugger.Break();
Second, how the handle is processed on the receiving end is different. In a console application the handle is read in by conhost.exe
, in WPF it is read in by visual studio.
The hang itself comes from the fact that there is limited space in the buffer and only so much text can be queued up before the handle has to block new incoming requests so the existing information can drain out. It appears that the handle for the console application can process large numbers of \0
characters but the handle that WPF generates can not. If that difference is from it being a different kind of handle or from the processor on the other side of the handle reading in data differently I don't know.
Hopefully someone who has more experience than me with the Windows API call WriteFile
that can explain the differences between the two handle types and will give us a better idea if this is because of the handle type or because of the receiving program.
This works on my machine without issues (Winodws 8.1 VS2013). The problem has nothing to do with the console application. As Scott explained there is some blocking going on. It works when not run under a debugger
- As WPF Application
- As Console Application
It will hang when you are trying to debug the application. The deeper reason is that you are sending \0 over the pipe. A pipe has a send buffer (ca. 4 KB) before it blocks further writes to it. This is what you are seeing as a hanging WriteFile call in kernel32.dll. To be able to block there must be someone who wants to read from your pipe. In that case it is VS trying to get your stdout to the debugger output window. When no one is listening the pipe acts as null device which will never block.
Now back to the question why it does work with all strings except \0? This has something to do how to stop reading from a a pipe. The receiver can stop reading from the pipe when it receives \0 as the only message. This is the signal that the process has exited and no further data will be written to it. With your \0 messages you violate that implicit contract and send further data over the pipe but your client (VS) has stopped listening to you. That is not actually an API but seems to be a common agreement. In .NET yout get for async pipe reads e.g. null as last message back. Other applications (e.g. VS) seem to handle \0 messages in a similar way and assume the writer has exited.
It is legal to simply close the pipe handle in that case ReadFile returns false. Our you can also write a message of 0 bytes length to the pipe. Or you can write a 1024 KB null array to the pipe. It is up the reader of your messages to decide if this is the signal to stop reading from your pipe.
Update 1Since at least one commenter was thinking that logic is not enough here is the result of debugging VS. VS does read from the pipe via ReadFile
vsdebug!CReader::ReadPipe
There is a check if the first byte is 0 which then leads to thread termination. If the first byte is not 0 it is treated as unicode string and copied to a string buffer which is displayed in the debugger output window. You can verify this easily by sending instead e.g. 1000 c chars which will show up in the buffer. Then you can follow the control flow where it differs from 1000 0 bytes. It turns out that the relevant piece is:
0:048> db ebp-4201973f794 00 00 00 00 38 63 71 10-fe 03 00 00 92 82 b5 45 ....8cq........E1973f7a4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f7b4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f7c4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f7d4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f7e4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f7f4 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc1973f804 63 63 63 63 63 63 63 63-63 63 63 63 63 63 63 63 cccccccccccccccc
Buffer contains data a ebp-410 where are our c characters. Before that there is the buffer size and file handle stored.
0:048> u 5667dd48 L50 vsdebug!ReaderThreadStart+0x72: **5667dd48 80bdf0fbffff00 cmp byte ptr [ebp-410h],0** check if first byte is 0 5667dd4f 7446 je vsdebug!ReaderThreadStart+0xc1 (5667dd97) 5667dd51 899decfbffff mov dword ptr [ebp-414h],ebx 5667dd57 897dfc mov dword ptr [ebp-4],edi 5667dd5a 8d85f0fbffff lea eax,[ebp-410h] 5667dd60 53 push ebx 5667dd61 53 push ebx 5667dd62 50 push eax 5667dd63 8d8decfbffff lea ecx,[ebp-414h] 5667dd69 e8f5960200 call vsdebug!CVSUnicodeString::CopyString (566a7463) 5667dd6e c745fc02000000 mov dword ptr [ebp-4],2 5667dd75 8b4e30 mov ecx,dword ptr [esi+30h] 5667dd78 6aff push 0FFFFFFFFh 5667dd7a ffb5ecfbffff push dword ptr [ebp-414h] 5667dd80 e84453f6ff call vsdebug!CMinimalStreamEx::AddStringW (565e30c9) 5667dd85 834dfcff or dword ptr [ebp-4],0FFFFFFFFh 5667dd89 8d8decfbffff lea ecx,[ebp-414h] 5667dd8f 53 push ebx 5667dd90 e86339f5ff call vsdebug!CVSVoidPointer::Assign (565d16f8) 5667dd95 eb03 jmp vsdebug!ReaderThreadStart+0xc4 (5667dd9a) ** 5667dd97 897e28 mov dword ptr [esi+28h],edi ** When 0 go here and sleep 200ms 5667dd9a 68c8000000 push 0C8h 5667dd9f ff151c228056 call dword ptr [vsdebug!_imp__Sleep (5680221c)] 5667dda5 e978caf9ff jmp vsdebug!ReaderThreadStart+0xd4 (5661a822) 5667ddaa e87ffc0d00 call vsdebug!__report_rangecheckfailure (5675da2e) 5667ddaf cc int 3 5667ddb0 b9fe030000 mov ecx,3FEh 5667ddb5 899de4fbffff mov dword ptr [ebp-41Ch],ebx 5667ddbb 3bc1 cmp eax,ecx 5667ddbd 7702 ja vsdebug!CReader::Stop+0x84 (5667ddc1) 5667ddbf 8bc8 mov ecx,eax 5667ddc1 53 push ebx 5667ddc2 8d85e4fbffff lea eax,[ebp-41Ch] 5667ddc8 50 push eax 5667ddc9 51 push ecx 5667ddca 8d85f0fbffff lea eax,[ebp-410h] 5667ddd0 50 push eax 5667ddd1 ff762c push dword ptr [esi+2Ch] 5667ddd4 ff1590218056 call dword ptr [vsdebug!_imp__ReadFile (56802190)] 5667ddda 85c0 test eax,eax 5667dddc 0f845a80f9ff je vsdebug!CReader::Stop+0x117 (56615e3c) 5667dde2 8b85e4fbffff mov eax,dword ptr [ebp-41Ch] 5667dde8 85c0 test eax,eax 5667ddea 0f844c80f9ff je vsdebug!CReader::Stop+0x117 (56615e3c) 5667ddf0 b900040000 mov ecx,400h 5667ddf5 3bc1 cmp eax,ecx 5667ddf7 736c jae vsdebug!CReader::Stop+0x125 (5667de65) 5667ddf9 889c05f0fbffff mov byte ptr [ebp+eax-410h],bl 5667de00 40 inc eax 5667de01 3bc1 cmp eax,ecx 5667de03 7360 jae vsdebug!CReader::Stop+0x125 (5667de65) 5667de05 889c05f0fbffff mov byte ptr [ebp+eax-410h],bl 5667de0c 389df0fbffff cmp byte ptr [ebp-410h],bl 5667de12 0f842480f9ff je vsdebug!CReader::Stop+0x117 (56615e3c) 5667de18 899decfbffff mov dword ptr [ebp-414h],ebx 5667de1e c745fc01000000 mov dword ptr [ebp-4],1 5667de25 8d85f0fbffff lea eax,[ebp-410h] 5667de2b 53 push ebx 5667de2c 53 push ebx 5667de2d 50 push eax 5667de2e 8d8decfbffff lea ecx,[ebp-414h] 5667de34 e82a960200 call vsdebug!CVSUnicodeString::CopyString (566a7463) 5667de39 c745fc02000000 mov dword ptr [ebp-4],2 5667de40 8b4e30 mov ecx,dword ptr [esi+30h] 5667de43 6aff push 0FFFFFFFFh 5667de45 ffb5ecfbffff push dword ptr [ebp-414h] 5667de4b e87952f6ff call vsdebug!CMinimalStreamEx::AddStringW (565e30c9) 5667de50 834dfcff or dword ptr [ebp-4],0FFFFFFFFh 5667de54 8d8decfbffff lea ecx,[ebp-414h] 5667de5a 53 push ebx 5667de5b e89838f5ff call vsdebug!CVSVoidPointer::Assign (565d16f8) 5667de60 e9d77ff9ff jmp vsdebug!CReader::Stop+0x117 (56615e3c) 5667de65 e8c4fb0d00 call vsdebug!__report_rangecheckfailure (5675da2e) 5667de6a cc int 3 5667de6b b81a7e5d56 mov eax,offset vsdebug!ATL::CAtlMap<unsigned long,CScriptNode *,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<CScriptNode *> >::~CAtlMap<unsigned long,CScriptNode *,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<CScriptNode *> >+0x15 (565d7e1a) 5667de70 c3 ret 5667de71 b84c8e5d56 mov eax,offset vsdebug!ATL::CAtlMap<unsigned long,ATL::CComPtr<IVsHierarchyEvents>,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<ATL::CComPtr<IVsHierarchyEvents> > >::~CAtlMap<unsigned long,ATL::CComPtr<IVsHierarchyEvents>,ATL::CElementTraits<unsigned long>,ATL::CElementTraits<ATL::CComPtr<IVsHierarchyEvents> > >+0x15 (565d8e4c) 5667de76 c3 ret 5667de77 6857000780 push 80070057h 5667de7c e8df0af6ff call vsdebug!treegrid::IGridView::CleanupItems (565de960)** 0:048> u 5661a822 ** Jump here vsdebug!ReaderThreadStart+0xd4: 5661a822 395e28 cmp dword ptr [esi+28h],ebx 5661a825 74d0 je vsdebug!ReaderThreadStart+0x26 (5661a7f7) 5661a827 ff7624 push dword ptr [esi+24h] 5661a82a ff157c228056 call dword ptr [vsdebug!_imp__SetEvent (5680227c)] 5661a830 53 push ebx** 5661a831 ff1508218056 call dword ptr [vsdebug!_imp__ExitThread (56802108)] ** Stop reading
That is the whole magic around that. The reader simply stops reading and you your application will block when the sender buffer is full. No magic involved. It is all depends on the behaviour of the reader.
Because WPF has no unamanged handle to a Console window. I am unable to see the implementation of the Write method, but you can see on most public properties of static Console class return an IO Error with "Message = "The handle is invalid.\r\n".
If you want to display a console window, in a WPF application you need to execute code in the kernel32.dll unmanaged library.