Reputation: 33
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
Upvotes: 1
Views: 682
Reputation: 393487
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!
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:
#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);
}
Upvotes: 1