如何与 child 进程异步通信?

How to communicate with child process asynchronously?

我有一个使用 GTKmm 构建的 parent GUI 应用程序,我需要生成一个 child 进程(另一个 GUI 应用程序)并与之通信.我使用 boost::process 来做到这一点。我知道我应该异步进行,这样 parent UI 就不会被阻塞。

所以问题:

这是我目前的做法(阻止 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);
}

此代码使用 GTKmm 3.0 库

如您所料,Start() 方法阻塞,因此没有其他 Gtk 代码有机会 运行。这意味着什么都没有完成,甚至没有绘制 UI.

相反,使 child 成为 class 的成员。接下来,使用 async_pipe 而不是阻塞管道流,因此您也不必阻塞即可读取。现在,set-up 一个异步读取循环来响应来自子进程标准输出的传入数据。

我创建了一个简单的 dotnet 核心控制台应用程序来测试它:

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

现在我们将默认值 Program.cs 替换为:

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

构建并 运行ning 再次打印,总时间跨度为 5 秒:

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!

做 GTK 方面

我简化了很多事情。

最棘手的部分是让 io_context 从 Gtk 事件循环中轮询。为此,我选择使用 g_add_timeout。正确 de-register 报价处理程序非常重要,因此在 MyWindow 被破坏后不会产生未定义的行为。

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

我添加了一个 Stop 按钮作为衡量标准,并确保 Start/Stop 按钮是 enabled/disabled 合适的。让我们做一些现场演示:

完整演示

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