c++boostboost-asiogtkmmboost-process

How to communicate with child process asynchronously?


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:

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


Solution

  • 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