How to redirect error but not output and keep text written to console in the right order? (F#)

99 Views Asked by At

I have two programs that I am running, one of which is run as a child process by the other. When the child process writes to stderr, I want to capture that in the parent process so I can tell if there was an error, and then log the error message to the console just like the child process tried to do.

The problem is that the child process writes several lines of text to stdout immediately after writing to stderr. When the parent process intercepts stderr messages and logs them itself, they get written in parallel with the stdout messages and the text appears in the wrong order.

I have created two F# scripts as a minimal reproduction of this problem. This is the script for the child process:

open System

let logError (x : string) : unit =
    Console.ForegroundColor <- ConsoleColor.Red
    try stderr.WriteLine x
    finally Console.ResetColor ()

let logWarning (x : string) : unit =
    Console.ForegroundColor <- ConsoleColor.Yellow
    try stdout.WriteLine x
    finally Console.ResetColor ()

let logInfo (x : string) : unit =
    Console.ForegroundColor <- ConsoleColor.Green
    try stdout.WriteLine x
    finally Console.ResetColor ()

logError "The quick brown fox jumps over the lazy dog."

logWarning "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc eu est ut arcu finibus iaculis. Maecenas dapibus luctus convallis. Donec tristique accumsan ante sit amet maximus. Sed molestie eros sit amet pretium rhoncus. Sed odio lectus, vestibulum vitae consequat sit amet, eleifend ac augue. Vivamus eros quam, lobortis eget consequat in, pulvinar vel dolor. Sed efficitur fermentum purus eu imperdiet. Mauris posuere, metus nec fringilla accumsan, massa nisi egestas augue, et tristique ligula dolor sit amet nibh. Proin ultricies fermentum tellus, vitae porttitor mauris elementum id. Donec arcu dolor, posuere vel efficitur ultrices, sollicitudin sit amet mauris. Sed eu suscipit leo, in vehicula sem. Morbi congue nibh vitae orci lobortis, gravida volutpat augue imperdiet. Phasellus fringilla arcu ac tellus porttitor mattis. Donec in ante vitae sem varius pulvinar."

logInfo "Nam lorem justo, laoreet ac convallis et, semper et leo. Fusce ornare, risus ut porta tristique, purus lacus ultricies ante, ac semper metus eros quis sapien. Nunc vulputate neque ut efficitur condimentum. Quisque facilisis lacus at lorem condimentum suscipit. Aenean volutpat et dui non pharetra. Pellentesque pretium euismod sollicitudin. Phasellus ullamcorper nulla quis nibh tincidunt consectetur. Nulla gravida finibus mi, sed elementum ligula maximus sed. Ut eu dignissim ex. Nullam vestibulum accumsan ex, ut facilisis elit facilisis scelerisque. Integer pellentesque, sem a molestie porta, tortor felis consectetur lorem, ut interdum lacus mauris vel nisi. Maecenas justo nulla, pharetra at malesuada ac, sollicitudin quis tortor. Integer vehicula, mauris ac tristique vehicula, leo nibh cursus sem, sed rhoncus libero sapien ac tellus."

And this is the script for the parent process:

open System
open System.Diagnostics

let handleErr (args : DataReceivedEventArgs) : unit =
    Console.ForegroundColor <- ConsoleColor.Red
    try stderr.WriteLine args.Data
    finally Console.ResetColor ()

let p = new Process ()
p.StartInfo.FileName <- "fsi"
p.StartInfo.Arguments <- "child.fsx"
p.StartInfo.UseShellExecute <- false
p.StartInfo.RedirectStandardError <- true
p.ErrorDataReceived.Add handleErr
p.Start ()
p.BeginErrorReadLine ()
p.WaitForExit ()

The expected output of the child process would be a line in red, followed by a paragraph in yellow, and finally a paragraph in green. When I run the child process on its own that is what gets output. But when I run it through the parent process this happens: Mangled output

You can see that the error message "The quick brown fox jumped over the lazy dog." printed in the middle of the paragraph that should have printed after it, and the foreground color is incorrect. I have tried to find a solution for this and failed so far, as none of the following things will work:

  • Use locks to allow only one function to print to the console at once - This won't help because the functions are isolated across different processes, not just different threads.
  • Redirect stdout as well as stderr - This makes it impossible to preserve the color of the messages written to stdout.
  • Combine stderr with stdout - I need a way to distinguish between stderr and stdout, so that the parent process can tell if stderr has been written to.

Is there a way in the parent process that I can detect whether the child process has written to stderr, while not redirecting the stderr stream and still letting the child process handle logging to the console?

1

There are 1 best solutions below

0
On

The typical way for a parent process to know if the child had an error is with the exit code when it is different than zero. Other than that you would need some other type of inter process communication.

I know you said that locks would not work but just in case, I will provide you this possible solution that works with the specific scenario you posted:

This one involves using MailBoxProcessor to sequence the actions. First some helper functions:

module Mailbox =

    /// A simple Mailbox processor to serially process Async tasks
    /// use:
    ///      let logThisMsgA = Mailbox.iterA (printfn "%A") (fun msg -> async { printfn "Log: %s" msg } )
    ///      logThisMsgA.Post "message Async"
    ///      
    let iterA hndl f =
        MailboxProcessor.Start(fun inbox ->
            async {
                while true do
                    try       let!   msg = inbox.Receive()
                              do!  f msg
                    with e -> hndl e
            }
        )

    /// A simple Mailbox processor to serially process tasks
    /// use:
    ///      let logThisMsg = Mailbox.iter (printfn "%A") (printfn "Log: %s")
    ///      logThisMsg.Post "message"
    ///      
    let iter hndl f = iterA hndl (fun msg -> async { f msg } )

Here is the Mailbox agent and a function to invoke it:

let sequenceActions = Mailbox.iter (printfn "%A") (fun f -> f() )
let logSeq    f txt = sequenceActions.Post <| fun () -> f txt

and your process invokes the functions this way:

logSeq logError   "The quick brown fox jumps over the lazy dog."

logSeq logWarning "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc eu est ut arcu finibus iaculis. Maecenas dapibus luctus convallis. Donec tristique accumsan ante sit amet maximus. Sed molestie eros sit amet pretium rhoncus. Sed odio lectus, vestibulum vitae consequat sit amet, eleifend ac augue. Vivamus eros quam, lobortis eget consequat in, pulvinar vel dolor. Sed efficitur fermentum purus eu imperdiet. Mauris posuere, metus nec fringilla accumsan, massa nisi egestas augue, et tristique ligula dolor sit amet nibh. Proin ultricies fermentum tellus, vitae porttitor mauris elementum id. Donec arcu dolor, posuere vel efficitur ultrices, sollicitudin sit amet mauris. Sed eu suscipit leo, in vehicula sem. Morbi congue nibh vitae orci lobortis, gravida volutpat augue imperdiet. Phasellus fringilla arcu ac tellus porttitor mattis. Donec in ante vitae sem varius pulvinar."

logSeq logInfo    "Nam lorem justo, laoreet ac convallis et, semper et leo. Fusce ornare, risus ut porta tristique, purus lacus ultricies ante, ac semper metus eros quis sapien. Nunc vulputate neque ut efficitur condimentum. Quisque facilisis lacus at lorem condimentum suscipit. Aenean volutpat et dui non pharetra. Pellentesque pretium euismod sollicitudin. Phasellus ullamcorper nulla quis nibh tincidunt consectetur. Nulla gravida finibus mi, sed elementum ligula maximus sed. Ut eu dignissim ex. Nullam vestibulum accumsan ex, ut facilisis elit facilisis scelerisque. Integer pellentesque, sem a molestie porta, tortor felis consectetur lorem, ut interdum lacus mauris vel nisi. Maecenas justo nulla, pharetra at malesuada ac, sollicitudin quis tortor. Integer vehicula, mauris ac tristique vehicula, leo nibh cursus sem, sed rhoncus libero sapien ac tellus."

I hope this helps.