Fork a child process and read its CASA log output.

    function
      A function to run in the child process
    filter
      The CASA log level filter to apply in the child process: less urgent
      messages will not be shown. Valid values are strings: "DEBUG1", "INFO5",
      ... "INFO1", "INFO", "WARN", "SEVERE".
    debug
      If true, the standard output and error of the child process are *not*
      redirected to /dev/null.

    Some CASA tools produce important results that are *only* provided via log
    messages. This is a problem for automation, since there’s no way for
    Python code to intercept those log messages and extract the results of
    interest. This function provides a framework for working around this
    limitation: by forking a child process and sending its log output to a
    pipe, the parent process can capture the log messages.

    This function is a generator. It yields lines from the child process’ CASA
    log output.

    Because the child process is a fork of the parent, it inherits a complete
    clone of the parent’s state at the time of forking. That means that the
    *function* argument you pass it can do just about anything you’d do in a
    regular program.

    The child process’ standard output and error streams are redirected to
    ``/dev/null`` unless the *debug* argument is true. Note that the CASA log
    output is redirected to a pipe that is neither of these streams. So, if
    the function raises an unhandled Python exception, the Python traceback
    will not pollute the CASA log output. But, by the same token, the calling
    program will not be able to detect that the exception occurred except by
    its impact on the expected log output.

def forkandlog(function, filter='INFO5', debug=False):
    """Fork a child process and read its CASA log output.

    function
      A function to run in the child process
    filter
      The CASA log level filter to apply in the child process: less urgent
      messages will not be shown. Valid values are strings: "DEBUG1", "INFO5",
      ... "INFO1", "INFO", "WARN", "SEVERE".
    debug
      If true, the standard output and error of the child process are *not*
      redirected to /dev/null.

    Some CASA tools produce important results that are *only* provided via log
    messages. This is a problem for automation, since there’s no way for
    Python code to intercept those log messages and extract the results of
    interest. This function provides a framework for working around this
    limitation: by forking a child process and sending its log output to a
    pipe, the parent process can capture the log messages.

    This function is a generator. It yields lines from the child process’ CASA
    log output.

    Because the child process is a fork of the parent, it inherits a complete
    clone of the parent’s state at the time of forking. That means that the
    *function* argument you pass it can do just about anything you’d do in a
    regular program.

    The child process’ standard output and error streams are redirected to
    ``/dev/null`` unless the *debug* argument is true. Note that the CASA log
    output is redirected to a pipe that is neither of these streams. So, if
    the function raises an unhandled Python exception, the Python traceback
    will not pollute the CASA log output. But, by the same token, the calling
    program will not be able to detect that the exception occurred except by
    its impact on the expected log output.

    """
    import sys, os

    readfd, writefd = os.pipe()
    pid = os.fork()

    if pid == 0:
        # Child process. We never leave this branch.
        #
        # Log messages of priority >WARN are sent to stderr regardless of the
        # status of log.showconsole(). The idea is for this subprocess to be
        # something super lightweight and constrained, so it seems best to
        # nullify stderr, and stdout, to not pollute the output of the calling
        # process.
        #
        # I thought of using the default logger() setup and dup2'ing stderr to
        # the pipe fd, but then if anything else gets printed to stderr (e.g.
        # Python exception info), it'll get sent along the pipe too. The
        # caller would have to be much more complex to be able to detect and
        # handle such output.

        os.close(readfd)

        if not debug:
            f = open(os.devnull, 'w')
            os.dup2(f.fileno(), 1)
            os.dup2(f.fileno(), 2)

        sink = logger(filter=filter)
        sink.setlogfile(b'/dev/fd/%d' % writefd)
        function(sink)
        sys.exit(0)

    # Original process.

    os.close(writefd)

    with os.fdopen(readfd) as readhandle:
        for line in readhandle:
            yield line

    info = os.waitpid(pid, 0)

    if info[1]:
        # Because we're a generator, this is the only way for us to signal if
        # the process died. We could be rewritten as a context manager.
        e = RuntimeError('logging child process PID %d exited '
                         'with error code %d' % tuple(info))
        e.pid, e.exitcode = info
        raise e