Running a process using boost process in async mode with timeout
Asked Answered
K

2

2

In the following code, I am trying to implement a program that runs a shell command and get the stdio, stderr and return code. I am doing it using boost process in the async mode as advised here.

namespace bp = boost::process;
class Process {

public:
    Process(std::string & cmd, const int timeout);
    void run();

private:
    void timeout_handler();

    const std::string command;
    const int timeout;

    bool killed;
    bool stopped;

    std::string stdOut;
    std::string stdErr;
    int returnStatus;

    boost::asio::io_service ios;
    boost::process::group group;
    boost::asio::deadline_timer deadline_timer;
};

Process::Process(std::string & cmd, const int timeout):
    command(cmd),
    timeout(timeout),
    deadline_timer(ios)
{}

void Process::timeout_handler()
{
    if (stopped)
    return;

    if (deadline_timer.expires_at() <= boost::asio::deadline_timer::traits_type::now())
    {
        std::cout << "Time Up!" << std::endl;
        group.terminate();
        std::cout << "Killed the process and all its decendents" << std::endl;
        killed = true;
        stopped = true;
        deadline_timer.expires_at(boost::posix_time::pos_infin);
    }
    deadline_timer.async_wait(std::bind(&Process::timeout_handler, this));
}

void Process::run()
{

    std::future<std::string> dataOut;
    std::future<std::string> dataErr;

    bp::child c(command, bp::std_in.close(), bp::std_out > dataOut, bp::std_err > dataErr, ios, group);
    deadline_timer.expires_from_now(boost::posix_time::seconds(timeout));
    deadline_timer.async_wait(std::bind(&Process::timeout_handler, this));

    ios.run();
    c.wait();

    stdOut = dataOut.get();
    stdErr = dataErr.get();
    returnStatus = c.exit_code();
}

int main(int argc, char** argv)
{
    if(argc < 2)
    {
    std::cout << "Usage: \na.out <command>" << std::endl;
    exit(1);
    }
    std::vector<std::string> arguments(argv + 1, argv + argc);

    std::string command;
    for( const auto & tok : arguments)
    {
        command += tok + " ";
    }

    std::cout << command << std::endl;
    Process p(command, 10);
    p.run();
    return 0;
}

Now, the above code returns only after deadline_timer expires. What I want is that the child process should exit if it finishes before the timer expires or it (along with all the child processes it forks), should be terminated. Please point out the mistake in my code.

Kenlay answered 3/10, 2018 at 9:34 Comment(0)
P
2

The mistake is indeed very simple: you should cancel the deadline timer!

io_service::run() will not return unless

  1. an exception emanated from a handler
  2. no more work is queued.

While the dead line timer is in progress, that means the second condition isn't met. So io_service::run() waits for it because you asked it to.

Other notes:

  • use the error code to detect timer cancellation instead of racy time comparisons
  • no need to loop-chain the timer (in fact, that's asking for bugs where the io_service never completes)
  • your code failed to initialize stopped and killed

Live On Coliru

#include <boost/process.hpp>
#include <boost/process/async.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <iostream>

namespace bp = boost::process;
class Process {

  public:
    Process(std::string &cmd, const int timeout);
    void run();

  private:
    void timeout_handler(boost::system::error_code ec);

    const std::string command;
    const int timeout;

    bool killed = false;
    bool stopped = false;

    std::string stdOut;
    std::string stdErr;
    int returnStatus = 0;

    boost::asio::io_service ios;
    boost::process::group group;
    boost::asio::deadline_timer deadline_timer;
};

Process::Process(std::string &cmd, const int timeout) : command(cmd), timeout(timeout), deadline_timer(ios) {}

void Process::timeout_handler(boost::system::error_code ec) {
    if (stopped)
        return;

    if (ec == boost::asio::error::operation_aborted)
        return;

    if (deadline_timer.expires_at() <= boost::asio::deadline_timer::traits_type::now()) {
        std::cout << "Time Up!" << std::endl;
        group.terminate(); // NOTE: anticipate errors
        std::cout << "Killed the process and all its decendants" << std::endl;
        killed = true;
        stopped = true;
        deadline_timer.expires_at(boost::posix_time::pos_infin);
    }
    //NOTE: don't make it a loop
    //deadline_timer.async_wait(boost::bind(&Process::timeout_handler, this, boost::asio::placeholders::error));
}

void Process::run() {

    std::future<std::string> dataOut;
    std::future<std::string> dataErr;

    deadline_timer.expires_from_now(boost::posix_time::seconds(timeout));
    deadline_timer.async_wait(boost::bind(&Process::timeout_handler, this, boost::asio::placeholders::error));

    bp::child c(command, bp::std_in.close(), bp::std_out > dataOut, bp::std_err > dataErr, ios, group, 
            bp::on_exit([=](int e, std::error_code ec) {
                // TODO handle errors
                std::cout << "on_exit: " << ec.message() << " -> " << e << std::endl;
                deadline_timer.cancel();
                returnStatus = e;
            }));

    ios.run();

    stdOut = dataOut.get();
    stdErr = dataErr.get();

    c.wait();

    returnStatus = c.exit_code();
}

int main(int argc, char **argv) {
    if (argc < 2) {
        std::cout << "Usage: \na.out <command>" << std::endl;
        exit(1);
    }
    std::vector<std::string> arguments(argv + 1, argv + argc);

    std::string command;
    for (const auto &tok : arguments) {
        command += tok + " ";
    }

    std::cout << command << std::endl;
    Process p(command, 2);
    p.run();
}

Prints e.g.

 $ ./sotest 'echo hello'

echo hello 
on_exit: Success -> 0

 $ ./sotest 'sleep 1'

sleep 1 
on_exit: Success -> 0

 $ ./sotest 'sleep 3'

sleep 3 
Time Up!
Killed the process and all its decendants
on_exit: Success -> 9
Petroleum answered 3/10, 2018 at 11:41 Comment(3)
I would not recommend blindly pasting unsafe tokens into a "command" (it breaks because "echo hello" will do something else than "echo" "hello"`). See also boost.org/doc/libs/1_68_0/doc/html/boost_process/…Petroleum
Also I think we don't need the deadline_timer.expires_at check as it should only be fired once.Kenlay
That's what I meant to say with the first bullet. I didn't want to change all the code aroundPetroleum
S
1

A very simple logic for how to run a process with timeout is the following.

std::string cmd = "sleep 20";
int timeout = 5;

boost::process::child c(cmd);
std::error_code ec;
if (!c.wait_for(std::chrono::seconds(timeout), ec)) {
    std::cout << "nTimeout reached. Process terminated after "
              << timeout << " seconds.\n";
      c.terminate(ec);
}      
Scutum answered 25/6, 2021 at 0:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.