1

I have a parent GUI app built with GTKmm, and I need to spawn a child process (another GUI app) and communicate with it. I use boost::process to do that. I know that I should do it asynchronously, so that the parent UI wouldn't be blocked.

So the questions:

  • How can I asynchronously listen to any output from the child app and process it?
  • How can I know when the child app/process has been closed/terminated?

here is how I currently do it (which is blocking the UI):

#include <iostream>
#include <boost/process.hpp>
#include <gtkmm.h>

using namespace std;
using namespace boost::process;

class MyWindow : public Gtk::Window
{
public:
MyWindow();

private:
Gtk::Button *start_btn;

void Start();
};

void MyWindow::Start() {
// The target app is built from .NET 5.0 to run on RPi (linux-arm)

ipstream pipe_stream;
// change to your own target process
child c("/usr/bin/dotnet", "/home/pi/updater/Updater.dll", std_out > pipe_stream);
std::string line;
bool upToDate;
while (pipe_stream && std::getline(pipe_stream, line) && !line.empty()) {
  std::cout << line << std::endl;
  try {
    upToDate = line == "True" || line == "true" || line == "1";
    if (upToDate) {
      std::cout << "up-to-date" << std::endl;
      break;
    }
    else {
      std::cout << "update available!" << std::endl;
      break;
    }
  }
  catch(exception& e) {
    std::cerr << e.what() << std::endl;
  }

}


c.wait();
}

MyWindow::MyWindow()
{
set_title("Basic application");
set_default_size(200, 200);
start_btn = Gtk::make_managed<Gtk::Button>("Start process");

start_btn->signal_clicked().connect(sigc::mem_fun(*this, &MyWindow::Start));

this->add(*start_btn);
this->show_all();
}

int main(int argc, char* argv[])
{
auto app = Gtk::Application::create("org.gtkmm.examples.base");

MyWindow win;

return app->run(win);
}

This code use GTKmm 3.0 lib

sehe
  • 374,641
  • 47
  • 450
  • 633
Wasenshi
  • 33
  • 1
  • 8
  • Once the child process have been created, it will run independently from your parent process. Without any synchronization they will default to be asynchronous. If something is blocking the parent (GUI) process then you're performing a blocking operation which you don't tell us about. So instead of describing your program or parts of your code, please create a [mre] to actually show us. – Some programmer dude Mar 28 '22 at 11:50
  • Updated with the example code – Wasenshi Mar 28 '22 at 12:02
  • The problem is the reading from the pipe. If a pipe have nothing in it, then the read operation will block. Which will block your whole program. Read about [asynchronous I/O and pipes](https://www.boost.org/doc/libs/1_78_0/doc/html/boost_process/tutorial.html#boost_process.tutorial.async_io) – Some programmer dude Mar 28 '22 at 12:07

1 Answers1

1

As you've guessed, the Start() method blocks, so no other Gtk code gets a chance to run. This means nothing gets done, not even drawing the UI.

Instead, make the child a member of the class. Next, use an async_pipe instead of the blocking pipe stream, so you don't have to block to read either. Now, set-up an async read loop to respond to incoming data from the child process'es standard output.

I've created a simple dotnet core console application to test this with:

mkdir CORE && cd CORE
dotnet build
dotnet bin/Debug/net6.0/CORE.dll 

Now we replace the default Program.cs with:

for (int i = 1; i<11; ++i)
{
    Console.WriteLine("Hello, World {0}!", i);
    System.Threading.Thread.Sleep(500);
}
Console.WriteLine("Bye, World!");
return 42;

Building and running again prints, over a total timespan of 5 seconds:

Hello, World 1!
Hello, World 2!
Hello, World 3!
Hello, World 4!
Hello, World 5!
Hello, World 6!
Hello, World 7!
Hello, World 8!
Hello, World 9!
Hello, World 10!
Bye, World!

Doing The GTK Side

I've simplified many things.

The trickiest part is to make the io_context be polled from the Gtk event loop. I opted to use g_add_timeout for the purpose. It is very important to correctly de-register the tick handler, so no undefined behavior results after MyWindow is destructed.

tick() runs every 10ms (if possible). Perhaps for your use-case you can lower the frequency.

I added a Stop button for good measure, and made sure that Start/Stop buttons are enabled/disabled as appropriate. Let's do some live demo:

Full Demo

#include <boost/process.hpp>
#include <boost/process/async.hpp>
#include <gtkmm.h>
#include <iostream>
namespace asio = boost::asio;
namespace bp   = boost::process;

class MyWindow : public Gtk::Window {
  public:
    MyWindow();
    ~MyWindow() override;

  private:
    Gtk::Box    box_{Gtk::Orientation::ORIENTATION_VERTICAL, 4};
    Gtk::Button btnStart_{"Start Updater"};
    Gtk::Button btnStop_{"Stop Updater"};
    Gtk::Label  lblOutput_{"(click the start button)"};

    void StartUpdater();
    void StopUpdater();

    guint tick_source_{0};

    using Ctx = asio::io_context;
    Ctx                        io_;
    boost::optional<Ctx::work> work_{io_};

    struct AsyncUpdater {
        AsyncUpdater(MyWindow& win) : win_(win) { read_loop(); }

        MyWindow&      win_;
        bp::async_pipe pipe_{win_.io_};
        bp::child      child_{
            bp::search_path("dotnet"),
            std::vector<std::string>{"CORE/bin/Debug/net6.0/CORE.dll"},
            bp::std_out > pipe_, //
            bp::std_err.null(),  //
            bp::std_in.null(),   //
            bp::on_exit(std::bind(&AsyncUpdater::on_exit, this,
                                  std::placeholders::_1,
                                  std::placeholders::_2)),
            win_.io_};

        ~AsyncUpdater() {
            std::error_code ec;
            if (child_.running(ec)) {
                Gdk::Display::get_default()->beep();

                child_.terminate(ec);
                std::cerr << "Terminating running child (" << ec.message() << ")" << std::endl;
            }
        }

        std::array<char, 1024> buf_;

        void read_loop() {
            pipe_.async_read_some( //
                asio::buffer(buf_),
                [this](boost::system::error_code ec, size_t n) {
                    std::cerr << "Got " << n << " bytes (" << ec.message() << ")" << std::endl;
                    if (!ec) {
                        win_.appendOutput({buf_.data(), n});
                        read_loop(); // loop
                    } else {
                        pipe_.close();
                    }
                });
        }

        void on_exit(int exitcode, std::error_code ec) {
            win_.appendOutput("(" + std::to_string(exitcode) + " " +
                              ec.message() + ")\n");
            win_.btnStart_.set_state(Gtk::StateType::STATE_NORMAL);
            win_.btnStop_.set_state(Gtk::StateType::STATE_INSENSITIVE);
        }
    };

    friend struct AsyncUpdater;
    boost::optional<AsyncUpdater> updater_;

    void appendOutput(std::string_view text) {
        auto txt = lblOutput_.get_text();
        txt.append(text.data(), text.size());
        lblOutput_.set_text(std::move(txt));
    }

    bool tick() {
        if (io_.stopped()) {
            std::cerr << "Self-deregistering tick callback" << std::endl;
            tick_source_ = 0;
            return false;
        }
        io_.poll/*_one*/(); // integrate Asio execution context event loop
        return true;
    }
};

MyWindow::MyWindow() {
    set_title("Async Child Process");
    set_default_size(600, 600);

    add(box_);
    box_.add(btnStart_);
    box_.add(lblOutput_);
    box_.add(btnStop_);

    lblOutput_.set_vexpand(true);
    btnStop_.set_state(Gtk::StateType::STATE_INSENSITIVE);

    show_all();

    btnStart_.signal_clicked().connect(sigc::mem_fun(*this, &MyWindow::StartUpdater));
    btnStop_.signal_clicked().connect(sigc::mem_fun(*this, &MyWindow::StopUpdater));

    // wrapper... C compatibility is fun
    GSourceFunc gtick = [](void* data) -> gboolean {
        return static_cast<MyWindow*>(data)->tick();
    };
    tick_source_ = ::g_timeout_add(10, gtick, this);
}

MyWindow::~MyWindow() {
    if (tick_source_) {
        ::g_source_remove(tick_source_);
    }

    updater_.reset();
    work_.reset();
    io_.run();
}

void MyWindow::StartUpdater() {
    lblOutput_.set_text("");
    btnStart_.set_state(Gtk::StateType::STATE_INSENSITIVE);
    btnStop_.set_state(Gtk::StateType::STATE_NORMAL);

    updater_.emplace(*this);
}

void MyWindow::StopUpdater() {
    updater_.reset();
}

int main() {
    auto app = Gtk::Application::create("org.gtkmm.examples.base");

    MyWindow win;

    return app->run(win);
}

enter image description here

sehe
  • 374,641
  • 47
  • 450
  • 633
  • What if I want to process the output 1 line at a time? async_read_some doesn't guarantee that, does it? Since we read the output as a chunk into buffer. How can we async read line? – Wasenshi Mar 29 '22 at 09:07
  • You can use e.g. [`read_until`](https://www.boost.org/doc/libs/1_78_0/doc/html/boost_asio/reference/async_read_until.html). That can read into [dynamic buffers](http://coliru.stacked-crooked.com/a/059d75c290592a17), including [`asio::streambuf`](http://coliru.stacked-crooked.com/a/e3428b12e2aebc2b) which you might even use with `std::istream` [like this](http://coliru.stacked-crooked.com/a/08e8a8049e991306). – sehe Mar 29 '22 at 12:21
  • Demo of all these three take using this sample https://dotnetfiddle.net/17dnJL in animation: https://imgur.com/AC0atUh – sehe Mar 29 '22 at 12:35
  • Thanks for this demo. Boost documentation, and their examples, in particular, are sometimes pretty poor. In this case the examples of asynchronous stuff don't really show how you might use this in practice, but your example helps a lot. However, why do you do `io_.run()` at the end of the `MyWindow` destructor? – cosimo193 May 18 '23 at 13:21
  • @cosimo193 It makes sure all remaining handlers run. It could be unimportant, but maybe it is for your applicaiton. – sehe May 18 '23 at 13:24
  • Thanks for the reply. That doesn't seem unreasonable. With Boost 1.71.0, and a modified version of your example (that doesn't use gtk, and runs a python script in the background), I found that the `io_.run()` at the end causes me to get this: `terminate called after throwing an instance of 'boost::wrapexcept' what(): close: Bad file descriptor` when my program exits (on Linux); it could be something unrelated though. – cosimo193 May 18 '23 at 14:45
  • FWIW `io_.reset()` lets it exit cleanly. – cosimo193 May 18 '23 at 15:00
  • Huh. `io_.reset()` should not make that difference (see [docs](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/io_context/reset.html)). Perhaps you changed the code so `io_` is not actually of type `io_context`. `Bad file descriptor` simply means you probably are using a socket after it's been closed. Which means you probably closed it :) – sehe May 18 '23 at 18:53
  • Yes; it's strange. I've not changed anything that I think is related to closing anything, but I've also been unable to capture the expection (I might try again today!). However one thing I noticed around `async_pipe` is that there was an issue on windows, apparently, where, in certain circumstances, boost tried to close something twice; I'm not using windows, but... https://github.com/klemens-morgenstern/boost-process/issues/90 – cosimo193 May 19 '23 at 11:23
  • Commenting out the `pipe_.close()` in `read_loop()` also helps. – cosimo193 May 19 '23 at 11:27
  • Yeah I prefer to have explicit graceful shutdown. Sometimes child process won't exit until (especially) stdin is closed. Regardless, if you're linking to very old issues, what version of boost _are you_ using? – sehe May 19 '23 at 11:42