1
\$\begingroup\$

In the following code, I am trying to implement a program that runs a process and returns its return code, stdout and stderr. It also has a concept of timeout after which the child process is killed. This class will be used as a part of a multi threaded application.

#ifndef PROCESS_H_
#define PROCESS_H_
//Header

#include <chrono>
#include <thread>
#include <iostream>
#include <boost/process.hpp>
#include <boost/asio.hpp

  namespace bp = boost::process;
  class Proces
  {
  public:
    Process(const std::string& cmd, const int timeout);
    void run();
    int getReturnStatus();   //Must be only called after run() exits
    std::string getStdOut(); //Must be only called after run() exits
    std::string getStdErr(); //Must be only called after run() exits

    bool wasKilled(); //Must be only called after run() exits

  private:
    void initLog();
    void timeoutHandler(const boost::system::error_code& ec);
    void kill();

    const std::string command;
    const int timeout;
    int returnStatus;
    std::string stdOut;
    std::string stdErr;
    bool killed;
    bool stopped;
    boost::process::group group;
    boost::asio::io_context ioc;
    boost::asio::deadline_timer deadline_timer;
  };

#endif

//Source

#include "Process.h"



Process::Process(const std::string& cmd, const int timeout):
    command(cmd),
    timeout(timeout),
    returnStatus(0),
    stdOut(""),
    stdErr(""),
    killed(false),
    stopped(false),
    ioc(),
    deadline_timer(ioc)
{

}

void Process::timeoutHandler(const boost::system::error_code& ec)
{
    if (stopped || ec == boost::asio::error::operation_aborted)
    {
        return;
    }

    std::cout << "Time Up!"<< std::endl;
    kill();
    deadline_timer.expires_at(boost::posix_time::pos_infin);
}

void Process::run()
{
    std::future<std::string> dataOut;
    std::future<std::string> dataErr;
    std::cout << "Running command: " << command << std::endl;
    bp::child c(command, bp::std_in.close(),
        bp::std_out > dataOut,
        bp::std_err > dataErr, ioc,
        group,
        bp::on_exit([=](int e, const std::error_code& ec) {
                                                              std::cout << "on_exit: " << ec.message() << " -> " << e << std::endl;
                                                              deadline_timer.cancel();
                                                              returnStatus = e;
                                                           }));

    deadline_timer.expires_from_now(boost::posix_time::seconds(timeout));
    deadline_timer.async_wait(std::bind(&Process::timeoutHandler, this, std::placeholders::_1));

    ioc.run();
    c.wait();
    stdOut = dataOut.get();
    stdErr = dataErr.get();
}

//Must be only called after run() exits
int Process::getReturnStatus()
{
    return returnStatus;
}

//Must be only called after run() exits
std::string Process::getStdOut()
{
    return stdOut;
}

//Must be only called after run() exits
std::string Process::getStdErr()
{
    return stdErr;
}

void Process::kill()
{
    std::error_code ec;
    group.terminate(ec);
    if(ec)
    {
    std::cout << "Error occurred while trying to kill the process: " << ec.message();
    throw std::runtime_error(ec.message());
    }
    std::cout << "Killed the process and all its descendants" << std::endl;
    killed = true;
    stopped = true;
}

//Must be only called after run() exits
bool Process::wasKilled()
{
    return killed;
}

Below is the code I am using to test it

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE ProcessTest

#include <boost/test/unit_test.hpp>
#include "../src/Process.h"

BOOST_AUTO_TEST_CASE( ProcessTest )
{
    const std::vector<std::string> commands = {
      "ls"
    , "pwd"
    , "uname -a"
    , "cat /proc/cpuinfo"
    , "wget https://dl.google.com/dl/earth/client/current/google-earth-pro-stable-current.x86_64.rpm"};

    for(const auto& cmd: commands)
    {
    Process p(cmd, 3600);
    p.run();
    BOOST_CHECK( p.getReturnStatus() == 0);        // #1 continues on error
    }

    const std::vector<std::string> commandsThatShouldFail = {
      "ls doesnotexit"
    , "cat /proc/doesnotexist"
    , "wget https://dl.google.com/dl/earth/client/current/doesnotxist.rpm"};

    for(const auto& cmd: commandsThatShouldFail)
    {
    Process p(cmd, 3600);
    p.run();
    BOOST_CHECK( p.getReturnStatus() != 0);        // #1 continues on error
    }
}

Please provide your valuable comments and suggestions.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! You say you're trying to implement the program. Does it work so far? Do you have any concerns about your code? It's ok if you don't, as long as the code works as intended. \$\endgroup\$ – Mast Oct 4 '18 at 8:46
  • 1
    \$\begingroup\$ @Mast The code works for the cases I have tested it for. But I am wondering if there are any race conditions and other bugs? \$\endgroup\$ – sank Oct 4 '18 at 8:52
  • \$\begingroup\$ Good! Could you include those tests? \$\endgroup\$ – Mast Oct 4 '18 at 8:54
2
\$\begingroup\$

Missing tests

I don't see any tests for commands that should time out. An obvious example could be sleep 2, if we run it with a timeout of 1. A more rigorous test would be a command that ignores all signals (of the ones that can be ignored) - we should ensure that if SIGTERM doesn't work, that it will get SIGKILL eventually.

Data-driven tests

I don't like to see loops within test cases. Does Boost Test have provision for repeating tests with different data? Most test frameworks do; I'd be surprised if that were missing from Boost.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.