Reading from a pipe never returns

Armin Zingler 21 Reputation points
2024-01-03T01:33:03.6066667+00:00

Hi,

happy new year.

In a .Net Console application (Framework 4.8.1, Windows 10), I'm starting a new process using the Process class. Unfortunatelly, this class doesn't offer a way to have the child process open it's own console window. Using the CreateProcess API function, this is done by passing CREATE_NEW_CONSOLE as a creation flag. Consequently, I've changed my code to call CreateProcess directly. Now, the child process opens it's own console window. Problem fixed.

However, there is one more task that I have to migrate to the native approach: Redirecting standard output. With the pure .Net approach, this was simply done by setting .RedirectStandardOutput to true and calling p.StandardOutput.ReadToEnd(). With the native approach: First, I've created a native pipe before discovering the PipeStream classes, so the current code is:

      Dim SI As STARTUPINFO = Nothing
      Dim PI As PROCESS_INFORMATION
      Dim exePath = IO.Path.Combine(FFProbeFolder, "ffprobe.exe")
      Dim args = String.Format(argsTemplate, Path)
      Dim commandLine = exePath & " " & args

      SI.size = CUInt(Marshal.SizeOf(GetType(STARTUPINFO)))
      SI.Flags = STARTF_USESTDHANDLES

      Using pipeServer As New AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable),
          pipeClient As New AnonymousPipeClientStream(PipeDirection.In, pipeServer.ClientSafePipeHandle)

         SI.hStdOutput = pipeServer.SafePipeHandle

         If CreateProcess(
             Nothing, commandLine, IntPtr.Zero, IntPtr.Zero, True,
             CREATE_NEW_CONSOLE, IntPtr.Zero, Nothing, SI, PI) Then

             Using reader As New StreamReader(pipeClient)
                result = reader.ReadToEnd()
             End Using


My problem is that reader.ReadToEnd never returns. Why? Is this approach wrong?

I've looked at the reference source how the reader is created there:

https://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,2159

Compared to my code: The code creates a native pipe, not a manage pipe. It creates a FileStream passing the pipe's read handle, then passes that FileStream to the StreamReader. This is a different approach that obviously works, though I'd like to know why my one does not work. And, that code uses a SafeFileHandle, not a SafePipeHandle like I do, so I can't pass it (directly) to the ctor of the FileStream w/o any "dangerous" operations. ...... After changing my code using a FilesStream too, the ReadToEnd call still never returns. A call to reader.Peek() also never returns. So what is wrong?

Thanks

Windows API - Win32
Windows API - Win32
A core set of Windows application programming interfaces (APIs) for desktop and server applications. Previously known as Win32 API.
2,442 questions
.NET Runtime
.NET Runtime
.NET: Microsoft Technologies based on the .NET software framework.Runtime: An environment required to run apps that aren't compiled to machine language.
1,130 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. RLWA32 40,941 Reputation points
    2024-01-03T15:30:37.2366667+00:00

    @Armin Zingler Give the following example code a try. Of course you'll need to conform paths and so forth for your own system.

    VB Child Process -

        Sub Main()
            For i As Integer = 1 To 10
                Console.WriteLine($"Line {i}")
                Console.Error.WriteLine($"Line {i}")
            Next
    
            Console.Error.WriteLine("Hit a key to exit")
            Console.ReadLine()
        End Sub
    

    Parent Process -

    Imports System.IO
    Imports System.Runtime.InteropServices
    
    Module Module1
    
        <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
        Public Structure STARTUPINFO
            Public cb As Integer
            Public lpReserved As String
            Public lpDesktop As String
            Public lpTitle As String
            Public dwX As Integer
            Public dwY As Integer
            Public dwXSize As Integer
            Public dwYSize As Integer
            Public dwXCountChars As Integer
            Public dwYCountChars As Integer
            Public dwFillAttribute As Integer
            Public dwFlags As Integer
            Public wWhowWindow As Short
            Public cbReserved2 As Short
            Public lpReserved2 As IntPtr
            Public hStdInput As IntPtr
            Public hStdOutput As IntPtr
            Public hStdError As IntPtr
        End Structure
    
        <StructLayout(LayoutKind.Sequential)>
        Public Structure PROCESS_INFORMATION
            Public hProcess As IntPtr
            Public hThread As IntPtr
            Public dwProcessId As Integer
            Public dwThreadId As Integer
        End Structure
    
        Public Const STARTF_USESTDHANDLES As Integer = &H100
        Public Const CREATE_NEW_CONSOLE As Integer = &H10
        Public Const HANDLE_FLAG_INHERIT As UInteger = &H1
        Public Const INVALID_HANDLE_VALUE As Integer = -1
    
        <DllImport("kernel32.dll", CallingConvention:=CallingConvention.StdCall, CharSet:=CharSet.Unicode, SetLastError:=True)>
        Function CreateProcess(lpApplicationName As String, lpCommandLine As String, lpProcessAttributes As IntPtr,
                               lpThreadAttributes As IntPtr, bInheritHandles As Integer, dwCreationFlags As Integer,
                               lpEnvironment As IntPtr, lpDirectory As IntPtr, si As STARTUPINFO, pi As PROCESS_INFORMATION) As Boolean
    
        End Function
    
        <DllImport("kernel32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Function CreatePipe(ByRef hReadPipe As SafeFileHandle, ByRef hWriteByte As SafeFileHandle, lpPipeAttributes As IntPtr, nSize As UInteger) As Boolean
    
        End Function
    
        <DllImport("kernel32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Function SetHandleInformation(hObject As SafeFileHandle, mask As UInteger, dwFlags As UInteger) As Boolean
    
        End Function
    
        Sub Main()
            Dim SI As STARTUPINFO = Nothing
            Dim PI As PROCESS_INFORMATION
            Dim exePath = "C:\Users\RLWA32\source\repos\RlwA32\PipeTest\VBChildProcess\bin\Debug\VBChildProcess.exe"
            Dim hRead As New SafeFileHandle(INVALID_HANDLE_VALUE, True)
            Dim hWrite As New SafeFileHandle(INVALID_HANDLE_VALUE, True)
    
            Try
                If CreatePipe(hRead, hWrite, IntPtr.Zero, 0) Then
    
                    SetHandleInformation(hWrite, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)
    
                    SI.cb = CUInt(Marshal.SizeOf(GetType(STARTUPINFO)))
                    SI.dwFlags = STARTF_USESTDHANDLES
                    SI.hStdOutput = hWrite.DangerousGetHandle()
    
                    Dim result As String
    
                    If CreateProcess(
                    exePath, String.Empty, IntPtr.Zero, IntPtr.Zero, True,
                    CREATE_NEW_CONSOLE, IntPtr.Zero, Nothing, SI, PI) Then
    
    
                        hWrite.Close()
                        Using reader As New StreamReader(New FileStream(hRead, FileAccess.Read))
                            result = reader.ReadToEnd()
                            Console.Write(result)
                        End Using
                    Else
                        Throw New Win32Exception(Marshal.GetLastWin32Error())
                    End If
                Else
                    Throw New Win32Exception(Marshal.GetLastWin32Error())
                End If
    
            Catch ex As Exception
                Console.WriteLine($"Exception: {ex.Message}")
            Finally
                hRead.Dispose()
                hWrite.Dispose()
            End Try
    
        End Sub
    
    End Module
    

    And the result -

    VBPipeChild

    1 person found this answer helpful.

  2. Jiachen Li-MSFT 27,001 Reputation points Microsoft Vendor
    2024-01-03T06:06:14.5533333+00:00

    Hi @Armin Zingler ,

    When redirecting the standard output, the STARTUPINFO structure should be configured to use the pipe for the child process's standard output. The issue you're encountering might be related to using the PipeDirection.In for the AnonymousPipeClientStream. Try using PipeDirection.Out for the client pipe stream, as you're trying to read from the child process's output.

    Best Regards.

    Jiachen Li


    If the answer is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.