传递文件或标准输入以模糊地提升进程子进程

Passing file or stdin to boost process child ambiguously

我正在尝试编写一个基于 boost::process 的程序,该程序能够根据是否定义了将它们重定向到的文件来模糊地重定向输入、输出和错误流,但我正在努力弄清楚如何处理它:

#include <string>
#include <boost/process.hpp>

int main(){
  std::string in;
  std::string out("out.txt");

  namespace bp = boost::process;

  bp::child c(bp::exe("test.exe"), bp::std_in < (in.empty() ? stdin : in),  bp::std_out > (out.empty() ? stdout : out)); // error

  return 0;
}

三元运算符无法工作,因为类型不兼容,但我不知道如何实现。我探索了 boost::variant 和 boost::any 但无济于事。

Boost 选择了一种对熟悉命令行重定向的人来说很直观的表示法。但是,请记住这是 C++。 <> 符号仍然是运算符;它们没有成为命令行参数。代码 bp::std_out > stdout 是一个表达式。它计算出某种记录标准输出应该去向的对象。


适应不同类型的一个技巧是调整条件的产生点。不是有条件地 select 将参数 operator<operator>,而是有条件地 select bp::child 构造函数的参数:

bp::child c(bp::exe("test.exe"),
            in.empty() ? (bp::std_in  < stdin) : (bp::std_in  < in),
            out.empty() ? (bp::std_out > stdout) : (bp::std_out > out));

如果不是运算符重载,这会更明显。尽管上面的两个 < 符号看起来一样,但它们命名不同的运算符,因为它们的操作数类型不同。您的情况本质上是需要调用 f(bp::std_in, stdin)g(bp::std_in, in),并且无法将这些调用与三元运算符合并。之所以令人困惑,是因为运算符名称不是 fg,而是 <<.

您可能希望使用辅助变量来使代码更易于阅读:

auto in_stream  =  in.empty() ? (bp::std_in  < stdin)  : (bp::std_in  < in);
auto out_stream = out.empty() ? (bp::std_out > stdout) : (bp::std_out > out);

bp::child c(bp::exe("test.exe"), in_stream, out_stream);

静态类型和可变参数接口存在问题。[¹]

一般来说,不能保证各种关键​​字参数的类型兼容,因此像 cond? keyword1 : keyword2 这样的三元组会由于类型不匹配而很快崩溃。

此外,有时您会希望动态 add/remove 选项。

在我自己的生产代码中,我经常选择传统的控制流和一些代码重复,所以像

if (condition)
    child = bp::child(/* params 1st style */);
else
    child = bp::child(/* params 2nd style */);

当然,对于许多选项,这会导致难以维护的组合爆炸。您可以使用更复杂的可变 lambda 系统来组合关键字参数来欺骗它。

boost::asio::io_service ios;
boost::asio::deadline_timer deadline(ios);

bp::group process_group;

bp::async_pipe input(ios);
std::future<std::string> output, error;

if (_working_directory.empty())
    _working_directory = ".";

auto on_exit = [this, &deadline](int exit, std::error_code ec) {
    if (!_command_timed_out) {
        _exit_code = exit;
    }
    deadline.cancel();
    if (ec) log(LOG_WARNING) << "Child process returned " << ec.message();
    else    log(LOG_DEBUG)   << "Child process returned";
};

auto launcher = [](auto&&... args) { return bp::child(std::forward<decltype(args)>(args)..., bp::posix::fd.restrict_inherit()); };

auto redirect_out = [&](auto f) {
    return [&](auto&&... args) {
        if (_discard_output) {
            if (_redirected_output_fd != -1 && !_redirected_output_fname.empty()) {
                log(LOG_ERR) << "Ignoring output redirection with set_discard_output. This is a bug.";
            }
            return f(std::forward<decltype(args)>(args)..., bp::std_out > bp::null, bp::std_err > bp::null);
        }

        if (_redirected_output_fd != -1 && !_redirected_output_fname.empty()) {
            log(LOG_WARNING) << "Conflicting output redirection, ignoring filename with descriptor";
        }

        if (_redirected_output_fd != -1) {
            return f(std::forward<decltype(args)>(args)..., bp::posix::fd.bind(1, _redirected_output_fd), bp::std_err > error);
        }

        return _redirected_output_fname.empty()
            ? f(std::forward<decltype(args)>(args)..., bp::std_out > output,                   bp::std_err > error)
            : f(std::forward<decltype(args)>(args)..., bp::std_out > _redirected_output_fname, bp::std_err > error);
    };
};

bp::environment bp_env = boost::this_process::environment();
for (auto& p : _env)
    bp_env[p.first] = p.second;

auto c = redirect_out(launcher)(_command_path, _args,
        process_group,
        bp::std_in < input,
        bp::start_dir(_working_directory),
        bp_env,
        ios, bp::on_exit(on_exit)
    );

if (_time_constraint) {
    deadline.expires_from_now(*_time_constraint);
    deadline.async_wait([&](boost::system::error_code ec) {
        if (ec != boost::asio::error::operation_aborted) {
            if (ec) {
                log(LOG_WARNING) << "Unexpected condition in CommandRunner deadline: " << ec.message();
            }

            _command_timed_out = true;
            _exit_code = 1;
            ::killpg(process_group.native_handle(), SIGTERM);

            deadline.expires_from_now(3s); // grace time
            deadline.async_wait([&](boost::system::error_code ec) { if (!ec) process_group.terminate(); }); // timed out
        }
    });
}

boost::asio::async_write(input, bp::buffer(_stdin_data), [&input](auto ec, auto bytes_written){
    if (ec) {
        log(LOG_WARNING) << "Standard input rejected: " << ec.message() << " after " << bytes_written << " bytes written";
    }
    may_fail([&] { input.close(); });
});

ios.run();

if (output.valid()) _stdout_str = output.get();
if (error.valid())  _stderr_str = error.get();

// make sure no grand children survive
if (process_group && process_group.joinable() && !process_group.wait_for(1s))
    process_group.terminate();

当然你应该添加异常处理并适应你的需要。


[¹] 别让我开始。对我来说,这是过度设计的错误。在存在 process-exec 开销的情况下,没有性能参数可以弥补增加的复杂性和无数错误 [多年来我已经看到了一些]。我不知道如何使用 Boost Process 来做一些重要的事情。

当然,图书馆仍然有用,所以我会限制我的咆哮:)我觉得有一些 karma to burn today