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);
}
