Don't run gnuplot process connected to stdout

This change fixes incompatibility of Gnuplot.jl with Documenter.jl
versions 0.27.0 and above. Without this change, Gnuplot.jl has at
least these problems:

1. When building Gnuplot.jl documentation, the process blocks and
   never finishes.

2. When using Gnuplot.jl in docstrings in other code, running
   `doctest` blocks and never finishes.

The reason is that Documenter uses a new version of IOCapture.jl,
which contains this commit:
6cb4cdff34.

Documenter evaluates code snippets from the documentation with
`stdout` redirected to a pipe to show the command's output. The
mentioned commit changes the behavior so that now capturing waits
until the pipe is closed. The problem with Gnuplot.jl is that when the
gnuplot process is started as a part of the execution of documentation
code snippet, its `stdout` is bound to Documenter's pipe. The pipe is
not closed until the gnuplot process exits, which does not happen
unless the code snippet calls `Gnuplot.quit` explicitly. Therefore
Documenter blocks indefinitely.

This can be demonstrated by storing the following code in `test.jl`

    module GnuplotDocTest
    """
    ```jldoctest; setup = :(using Gnuplot)
    julia> @gp rand(100)

    ```
    """
    test() = nothing
    end

    using Documenter
    doctest(pwd(), [GnuplotDocTest])

and running `julia test.jl`.

To fix this problem, we run the gnuplot process with stdout redirected
to a pipe and create an asynchronous task, which reads the gnuplot's
stdout and writes it to Julia's current stdout.

Correctness of this approach can be verified by running:

    using Gnuplot
    Gnuplot.options.term = "dumb"
    @gp "plot sin(x)"

Dumb terminal prints to stdout and the above command shows the graph
on Julia's stdout too. In the next commit, we add the above code as a
doctest.
This commit is contained in:
Michal Sojka 2021-12-05 10:01:44 +01:00
parent 04484adc22
commit d02c211e99

View File

@ -520,6 +520,18 @@ function readTask(gp::GPSession)
delete!(sessions, gp.sid)
end
# Read data from `from` and forward them to `stdout`.
#
# This is similar to `Base.write(to::IO, from::IO)` when called as
# write(stdout, from), but the difference is in situation when
# `stdout` changes. This function writes data to the changed `stdout`,
# whereas the call to `Base.write` writes to the original `stdout`
# forever.
function writeToStdout(from::IO)
while !eof(from)
write(stdout, readavailable(from))
end
end
function GPSession(sid::Symbol)
session = DrySession(sid)
@ -535,13 +547,16 @@ function GPSession(sid::Symbol)
end
pin = Base.Pipe()
pout = Base.Pipe()
perr = Base.Pipe()
proc = run(pipeline(`$(options.cmd)`, stdin=pin, stdout=stdout, stderr=perr), wait=false)
proc = run(pipeline(`$(options.cmd)`, stdin=pin, stdout=pout, stderr=perr), wait=false)
chan = Channel{String}(32)
# Close unused sides of the pipes
Base.close(pout.in)
Base.close(perr.in)
Base.close(pin.out)
Base.start_reading(pout.out)
Base.start_reading(perr.out)
out = GPSession(getfield.(Ref(session), fieldnames(DrySession))...,
@ -550,6 +565,7 @@ function GPSession(sid::Symbol)
# Start reading tasks
@async readTask(out)
@async writeToStdout(pout)
# Read gnuplot default terminal
if options.term == ""