SiegeLord / RustGnuplot

A Rust library for drawing plots, powered by Gnuplot.
GNU Lesser General Public License v3.0
406 stars 46 forks source link

Wait for gnuplot to create the output file before continuing #32

Open jonasbb opened 5 years ago

jonasbb commented 5 years ago

I want to create a plot using gnuplot and directly afterwards read the file and process it further. Something like:

let fpath = Path::new("/tmp/plot.png");
let mut fg = Figure::new();
// Plotting code
fg.set_terminal("pngcairo", fpath.to_string_lossy());
fg.show();

// Now read the file again
let res = fs::read(fpath).unwrap();

However, reading the file fails with "No such file or directory". Putting a sleep before the read "solves" the problem. As such, the problem seems to be, that show() returns, before the file is actually shown/created.

Is there a way to wait for the gnuplot process to finish creating the output file, before show() returns? The only thing I can imagine right now, is spawning a new gnuplot process for each image and using the termination of the process as a signal that the output file was created.

SiegeLord commented 5 years ago

As a workaround, as of version 0.0.27 you can do fg.set_post_commands("unset output").show() and that should finish writing the file.

jonasbb commented 5 years ago

Thanks for the feedback. I will try this with the new version.

jonasbb commented 5 years ago

@SiegeLord Unfortunately, specifying a post command still leaves the race condition.

This is the exact code I used. Make sure to remove the /tmp/plot.png file before executing it, in case it still exists from a previous run.

use gnuplot::Figure;
use std::{fs, path::Path, thread};

fn main() {
    let fpath = Path::new("/tmp/plot.png");
    let mut fg = Figure::new();
    fg.axes2d().boxes(0..5, 0..5, &[]);
    fg.set_terminal("pngcairo", &*fpath.to_string_lossy());
    fg.set_post_commands("unset output").show();

    // Now read the file again
    // thread::sleep_ms(1000); // <== If this line is commented out, the next unwrap fails. Otherwise it works.
    let res = fs::read(fpath).unwrap();
    println!("{:?}", res);
}

The problem is that in the code which sends the print commands to gnuplot, there is no way to wait for gnuplot to finish writing the file.

https://github.com/SiegeLord/RustGnuplot/blob/abb243ba6838d391f16df449df1548e67dc83144/src/figure.rs#L148-L150

This code writes the commands to stdin of gnuplot. This might be buffered. The code continues as soon as writing to stdin succeeded. Now, it will take some time for gnuplot to process the commands and write the final output file. During the time gnuplot draws, the Rust code continues and tries to read the file, which was not yet written. The gnuplot field is private to the Figure instance, so there is no way for outside code to block until gnuplot finished writing the file. This leaves the unavoidable race condition for any code using the show function

My workaround is this:

    // Start gnuplot process
    let mut child = Command::new("gnuplot-nox")
        .stdin(Stdio::piped())
        .spawn()
        .unwrap();
    fg.echo(
        child
            .stdin
            .as_mut()
            .expect("Stdin exists, because we configured it"),
    );
child.wait().unwrap(); // <== this line waits untils gnuplot has finished executing. At that point the output file must exist

I spawn a new gnuplot process for each image I want to draw, without the --persist flag. This way I can wait on the gnuplot process to finish before executing more Rust code. After the gnuplot process finished, the image file should have been written.

SiegeLord commented 5 years ago

Ok, I see. What did is that as of version 0.0.29 Figure now has a close method which closes the gnuplot process, hopefully accomplishing the same thing as your code. Here's an excerpt from the test I added:

use std::fs;
use tempfile::TempDir;

let tmp_path = TempDir::new().unwrap().into_path();
let file_path = tmp_path.join("plot.png");
let mut fg = Figure::new();
fg.axes2d().boxes(0..5, 0..5, &[]);
fg.set_terminal("pngcairo", &*file_path.to_string_lossy());
fg.show().close();
fs::read(file_path).unwrap();
fs::remove_dir_all(&tmp_path);

I.e. add a close call after show, no more set_post_commands necessary.

I still think there's some way to get it working without shutting down the entire process, but that'll have to wait for the future.