Boost C++ Libraries

PrevUpHomeNext

4.Usage

4.1. Quick tutorials
4.2. The command line
4.3. Interprocess communication
4.4. Environment variables
4.5. The startup work directory
4.6. Single process startup and termination
4.7. Pipeline startup and termination

This chapter describes all the portable features in Boost.Process that will let you achieve the most common tasks. The chapter starts presenting a couple of quick tutorials for the lazy reader, outlining the general structure of a program using the library. It later dives into details concerning each part of the tutorial.

The single process tutorial outlines all the steps necessary to prepare, launch, manage and terminate a single child process. It also illustrates interprocess communication by capturing the child's output messages and printing them in a different format. Each step is accompanied with a snippet to aid in the explanations which, if joined together, produce a complete example application. Let's get started.

As happens with any C++ library, the very first step to use Boost.Process is to include some header file that pulls in the necessary class and function definitions. To make things simple, we include the boost/process.hpp all-in-one header file and define an alias for its main name space. However, a bigger application will want to include the more finer grained headers to avoid unnecessary rebuilds shall the library's code change.

#include <boost/process.hpp>
namespace bp = ::boost::process;

Getting into the child process startup area, the first thing to do is to tell the library which application has to be started and which arguments shall be passed to it. In an attempt to choose a sample child program that works on all supported platforms, we use the Subversion command line client whose binary is named svn (svn.exe under Windows, but this difference is not important).

Our sample command line uses the provided command_line class, tells it to locate the binary in the path and asks the program to update the current directory. We could specify additional arguments if we wanted to.

bp::command_line cl("svn");
cl.argument("update");

Following the definition of the command line, we define the execution context of the new process; this is done by creating a launcher object. As Subversion update command updates the "current directory", we will want the child process to start in another location (that specified in the dir variable) so that it updates the desired directory. We do this by asking the launcher to use a different work directory than the current one. Furthermore, as outlined above, we also want to capture the standard output and error channels for later processing, so we end up with the following:

bp::launcher l;
l.set_stdout_behavior(bp::redirect_stream);
l.set_merge_out_err(true);
l.set_work_directory(dir);

Once we have the command line and the launcher configured, we start the new child process by mixing them together. (Yes, we could reuse the same launcher to start different command lines if we wanted to.)

bp::child c = l.start(cl);

At this point the cchild instance represents a running process. We can use this object to interact with the child, which in our example means reading its output messages. We do this by obtaining a C++ input stream that is connected to the child's standard output and error channels. From there on, it is a simple matter of reading data and processing them as we want:

bp::pistream& is = c.get_stdout();
std::string line;
while (std::getline(is, line))
    std::cout << "Got line from child: " << line << std::endl;

After parsing the output generated by the child process, we wait until it terminates execution to retrieve its exit status, which is later stored in the sstatus object for further treatment. (Strictly speaking we get to this call because the process has already terminated; otherwise the previous loop could not have finished.)

bp::status s = c.wait();

At last we parse the process' output status stored in s to see if everything was successful:

if (s.exited() && s.exit_status() == EXIT_SUCCESS)
    std::cout << "Directory updated successfully." << std::endl;
else
    std::cout << "Update failed." << std::endl;
std::cout << std::endl;

To sum it up, the complete example code is:

#include <cstdlib>
#include <iostream>
#include <string>

// quickbook:begin(include-headers)
#include <boost/process.hpp>
namespace bp = ::boost::process;
// quickbook:end(include-headers)

int
main(int argc, char* argv[])
{

if (argc < 2) {
    std::cerr << "Please specify a directory." << std::endl;
    return EXIT_FAILURE;
}
std::string dir = argv[1];

// quickbook:begin(command-line)
bp::command_line cl("svn");
cl.argument("update");
// quickbook:end(command-line)

// quickbook:begin(launcher)
bp::launcher l;
l.set_stdout_behavior(bp::redirect_stream);
l.set_merge_out_err(true);
l.set_work_directory(dir);
// quickbook:end(launcher)

// quickbook:begin(child-start)
bp::child c = l.start(cl);
// quickbook:end(child-start)

// quickbook:begin(get-output)
bp::pistream& is = c.get_stdout();
std::string line;
while (std::getline(is, line))
    std::cout << "Got line from child: " << line << std::endl;
// quickbook:end(get-output)

// quickbook:begin(wait)
bp::status s = c.wait();
// quickbook:end(wait)

// quickbook:begin(parse-status)
if (s.exited() && s.exit_status() == EXIT_SUCCESS)
    std::cout << "Directory updated successfully." << std::endl;
else
    std::cout << "Update failed." << std::endl;
std::cout << std::endl;
// quickbook:end(parse-status)

return EXIT_SUCCESS;

}

Boost.Process integrates mechanisms to spawn and control process groups using a construction known as a pipeline. This tutorial outlines the procedure to prepare, spawn, control and terminate several commands that are interconnected in a unidimensional chain. It is recommended to first read the single process tutorial because it illustrates simpler concepts than this one; you should spot the minor differences to really understand how pipeline management works.

First of all, a simple program that manages pipelines includes the general Boost.Process header. Of course, we could include the more fine grained headers, but for our demonstration purposes we need not to:

#include <boost/process.hpp>
namespace bp = ::boost::process;

As described earlier, processes are started using a launcher. When it comes to pipelines, the concept is the same but differs slightly in its usage: the pipeline specialized launcher is the single process' launcher homogeneous class but it differs in that it spawns and interconnects multiple processes. Both launchers share most of their public interface, so the code below will look very familiar.

To demonstrate the launcher's construction, we capture the pipeline's standard input (which is the same as the first process' standard input); this will allow us to feed it some data for further processing. On the other hand we tell the launcher to inherit the parent's standard output so that the results are printed to the expected stream (typically the screen):

bp::pipeline p;
p.set_stdin_behavior(bp::redirect_stream);
p.set_stdout_behavior(bp::inherit_stream);

Next, we tell the library which programs form the pipeline. Each component is described by a regular command line, much like we did before in the simple tutorial. However this example uses the shell constructor to simplify argument passing (and to illustrate a different way of doing things).

Please be aware that the utilities used in these command lines are specific to Unix systems. This does not mean in any way that pipelines are restricted to the POSIX platform; the code could use equivalent programs under Windows and behave equally well:

bp::command_line cl1 = bp::command_line::shell("cut -d ' ' -f 2-5");
bp::command_line cl2 = bp::command_line::shell("sed 's,^,line: >>>,'");
bp::command_line cl3 = bp::command_line::shell("sed 's,$,<<<,'");

Once all the pipeline components are defined, we add them to the launcher, really constructing the pipeline. The order in which we add them is important:

p.add(cl1).add(cl2).add(cl3);

Just after that we can spawn the whole process group, which is not any different than starting a single process. Note that this time we get a children object instead of a regular child because we need to keep track of multiple processes. We could be interested in directly interacting with each child and not with the pipeline black box concept:

bp::children cs = p.start();

At this point the pipeline is running. The children class offers us an interface that hides the fact that there are multiple processes running; we can simply treat it as if it were an instance of child. So, similar to what we did in the previous tutorial, we obtain the pipeline's standard input and feed it some data:

bp::postream& os = cs.get_stdin();
std::string line;
while (std::getline(file, line))
    os << line << std::endl;
os.close();

Once done, we wait until all processes stop execution and collect their exit status:

bp::status s = cs.wait();

At last we process the returned status in s to determine if everything went correctly:

return (s.exited() && s.exit_status() == EXIT_SUCCESS) ?
    EXIT_SUCCESS : EXIT_FAILURE;

To sum it up, the complete example code is:

#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>

// quickbook:begin(include-headers)
#include <boost/process.hpp>
namespace bp = ::boost::process;
// quickbook:end(include-headers)

int
main(int argc, char* argv[])
{

if (argc < 2) {
    std::cerr << "Please specify a file name." << std::endl;
    return EXIT_FAILURE;
}

std::ifstream file(argv[1]);
if (!file) {
    std::cerr << "Cannot open file." << std::endl;
    return EXIT_FAILURE;
}

// quickbook:begin(pipeline)
bp::pipeline p;
p.set_stdin_behavior(bp::redirect_stream);
p.set_stdout_behavior(bp::inherit_stream);
// quickbook:end(pipeline)

// quickbook:begin(command-lines)
bp::command_line cl1 = bp::command_line::shell("cut -d ' ' -f 2-5");
bp::command_line cl2 = bp::command_line::shell("sed 's,^,line: >>>,'");
bp::command_line cl3 = bp::command_line::shell("sed 's,$,<<<,'");
// quickbook:end(command-lines)

// quickbook:begin(addition)
p.add(cl1).add(cl2).add(cl3);
// quickbook:end(addition)

// quickbook:begin(children)
bp::children cs = p.start();
// quickbook:end(children)

// quickbook:begin(feed-stdin)
bp::postream& os = cs.get_stdin();
std::string line;
while (std::getline(file, line))
    os << line << std::endl;
os.close();
// quickbook:end(feed-stdin)

// quickbook:begin(wait)
bp::status s = cs.wait();
// quickbook:end(wait)

// quickbook:begin(parse-status)
return (s.exited() && s.exit_status() == EXIT_SUCCESS) ?
    EXIT_SUCCESS : EXIT_FAILURE;
// quickbook:end(parse-status)

}

The command line is an entity that describes a call to an executable program. It is composed of the following components:

The executable
This is the path name to the file on disk that will be executed. This can be a full path name, a relative path name or a base name without any component.
The program name
The program name is a string passed as the executable's first argument (e.g. argv[0]). This typically matches the executable's base name but can be changed at will in case the program behaves differently depending on the value passed in that parameter.
The arguments list
Aside from the first argument carrying the program's name, an executable may receive a set of extra arguments, similar to function parameters. These are stored in the arguments list.

All the functions or classes in the library that use a command line are parametrized on the Command_Line concept. This allows the developer to provide a custom implementation, shall we need to.

For convenience, the library includes a reference implementation class named command_line. It should be enough in most cases. The instructions below may only apply to this implementation.

There are two main ways to construct a command line as we shall see below.

The traditional entry point of a C or C++ program looks like:

int main(int argc, char* argv[]);

Programs receive an array of strings in argv that contains all the arguments passed to them. The reference command line implementation provides an interface to construct calls to applications based on the exact contents of the argv array. If at all possible, the new process will receive a verbatim copy of the arguments provided by the caller.

Executing programs using this syntax is safe to quotation issues because the library ensures that the values provided by the user end up in the child process without modifications. Furthermore, no other process — not even the shell — is involved in the execution procedure.

Constructing command lines on a parameter basis is the recommended usage, specially if user input is involved in the construction. However it might not be appropriate in all scenarios, as we will see later on.

In order to construct a command line parameter by parameter, we first tell it which is the executable to launch and, optionally, its program name. If the latter is not provided, it is automatically guessed:

command_line cl("/path/to/executable", "program_name");

The class' constructor takes two parameters: the path to the executable and its program name. If the path does not contain any component path (no slashes nor backslashes), the executable will be searched in the system's path; could the application not be found, a not_found_error< std::string > exception is raised as shown below:

try {
    command_line cl("unknownbinary");
    ...
} catch (const not_found_error< std::string >& e) {
    std::cerr << e.get_value() << " could not be found" << std::endl;
}

Once the command line is constructed, additional arguments can be appended to it by using the command_line::argument method. This function is parametrized on the argument type to allow dealing with any data type (provided it can be inserted in an output stream). Its usage is as follows:

cl.argument("first-parameter").argument("second-parameter");
cl.argument("...").argument("nth-parameter");

Another way to construct a command line is to define it in terms of the shell using the command_line::shell static method. You create a single string that represents the whole command line, which is then passed to the default system shell for later processing.

As the shell is involved, the command line is affected by its parsing rules such as variable and wildcard expansion, construction of pipelines, redirections of streams or even execution of built-in commands.

The general syntax is as follows:

command_line cl = command_line::shell
    ("the-program first-parameter ... nth-parameter");

4.2.3.Examples

The following example program shows multiple command lines from their construction until their execution:

#include <cstdlib>
#include <iostream>

#include <boost/process.hpp>

namespace bp = ::boost::process;

//
// Helper function to print a command line's contents on a stream.
// We can see here the interface exported by the command_line class and
// how the user can use it to inspect a command_line's contents.
//
static
std::ostream&
operator<<(std::ostream& os, const bp::command_line& cl)
{
    os << "===> Binary:       " << cl.get_executable() << std::endl;

    os << "===> Command line: ";
    const bp::command_line::arguments_vector& args = cl.get_arguments();
    for (bp::command_line::arguments_vector::size_type i = 0;
         i < args.size(); i++) {
        os << args[i];
        if (i < args.size() - 1)
            os << ' ';
    }
    os << std::endl;

    return os;
}

//
// Helper function that executes the given command line.
//
template< class Command_Line >
static
void
run_it(const Command_Line& cl)
{
    std::cout << cl;

    bp::launcher l;
    l.set_stdout_behavior(bp::inherit_stream);
    l.set_stderr_behavior(bp::inherit_stream);
    bp::status s = l.start(cl).wait();
    if (s.exited() && s.exit_status() == EXIT_SUCCESS)
        std::cout << "     *** SUCCESS ***" << std::endl;
    else
        std::cout << "     *** FAILURE ***" << std::endl;
    std::cout << std::endl;
}

int
main(int argc, char* argv[])
{
    //
    // Constructs a simple command line that executes the 'ls' binary to
    // list the contents of the current directory.
    //
    bp::command_line cl1("/bin/ls");
    run_it(cl1);

    //
    // Constructs a command line similar to the previous one but passes
    // an argument to the 'ls' binary telling it which directory to list.
    //
    bp::command_line cl2("/bin/ls");
    cl2.argument("/tmp");
    run_it(cl2);

    //
    // Yet another command line that calls a system binary, but this time
    // with three arguments.  Note that when calling '/bin/test' with its
    // default program name 'test', the closing square bracket is not
    // allowed.
    //
    bp::command_line cl3("/bin/test");
    cl3.argument("foo").argument("=").argument("foo");
    run_it(cl3);

    //
    // Exactly the same command as the previous one but this one changes
    // the program name at execution.  Here, '/bin/test' is executed with
    // a program name of '[' (i.e. argv[0]'s value is "[").  Hence, it
    // requires a closing square bracket as an extra argument to execute
    // properly.
    //
    bp::command_line cl4("/bin/test", "[");
    cl4.argument("foo").argument("=").argument("foo").argument("]");
    run_it(cl4);

    //
    // Constructs a command line that is executed through the system
    // shell, much like the ::system(3) function does.  As shown here,
    // these calls are much simpler to write and read because the command
    // line is provided as a single string.  However, they may be subject
    // to quoting problems because they need to be run through the shell.
    // Also note that there is no need to give the full path to the binary
    // because the shell will resolve it for us.
    //
    bp::command_line cl5 = bp::command_line::shell("ls -l /var");
    run_it(cl5);

    //
    // Another shell command line construction used to demonstrate that
    // the command is really executed through the system shell.  Otherwise
    // the stdout redirection could not work.
    //
    bp::command_line cl6 = bp::command_line::shell("echo foo > /dev/null");
    run_it(cl6);

    return EXIT_SUCCESS;
}

Every process has three standard communication channels that allow easy communication between either a parent process and a child process or among different child processes. Thanks to these standard channels, a program can easily implement a transformation of the input data without knowing where it is located and print it to an output channel whose real location is unknown; additionally it can print error messages that will end up stored where the caller decided.

There are lots of programs that process an input and produce an input, all using the standard channels previously described. These are known as filters and are very common in the Unix work. Of course there are many other programs that do not implement a data conversion, yet they use the standard channels to communicate with the user and/or with other processes.

The three standard communication channels are reviewed below. We can assume that they are supported by all platforms recognized by the library.

Standard input channel or stdin
This is used by the process to read input data, typically fed in by the user or another running process (e.g. its parent).
Standard output channel or stdout
This is used by the process to print output messages or data. The channel is typically configured to be buffered and its output is generally dumped to the console; of course it can be redirected to another process or file.
Standard error channel or stderr
This is used by the process to print error messages and sometimes to simply separate them from the output data. The channel is typically configured to be unbuffered and its output is generally dumped to the console; of course it can be redirected to another process or file.

What concerns us is the ability to interact with these standard streams from the process spawning a child — that is, the one using Boost.Process. The parent must tell the new child process how its standard channels shall behave; this is done by modifying the execution context of the new process by means of one of the multiple launcher implementations.

Each communication channel can be configured according to different predefined behaviors by using the launcher::set_stdin_behavior, launcher::set_stdout_behavior and launcher::set_stderr_behavior methods. These take a value that specifies how they shall behave, as made explicit by the stream_behavior enumeration; its documentation details all the possible values (not repeated here to avoid inconsistencies).

Boost.Process also provides a feature known as channel merging. Merging two communication channels means that the output of the child's source channel is redirected to the target channel (be it an input flow or an output one). The key idea is that this redirection is done at the lowest possible level: the child process continues to see multiple streams but as concerns the operating system, some of them point to the same internal object. This feature is used intensively in the library to allow for efficient retrieval of a process' stdoutand stderrflows; check out the launcher::set_merge_out_err method.

Given the above consider the following code snippet. It configures a launcher for a child process that will have an infinite blank input and which will send both of its stdoutand stderrto the parent's stdout:

launcher l;
l.set_stdin_behavior(silent_stream);
l.set_stdout_behavior(inherit_stream);
l.set_stderr_behavior(close_stream);
l.set_merge_out_err(true);

Setting a channel to redirect_stream has some special consequences. The channel affected by this flag will be later available to the parent process through the class representing the child process. Depending on the redirected channel, it can be retrieved using one of the child::get_stdin, child::get_stdout and child::get_stderr methods. These calls return C++ streams as described next. An input stream for a child process is seen as an output data flow by the parent; therefore it is modelled through the postream class. On the opposite side, an output stream for a child process is seen as an input data flow by the parent, hence it is represented by the pistream class. These two classes are regular C++ streams but provide an extra method (close) to allow the explicit shutdown of a communication channel. This is required, for example, to let the child know that the parent is not willing to send any more data through its stdin. (Typically the child will close output channels on exit and the parent will close input ones.)

The following example illustrates how to some data to a process:

launcher l;
l.set_stdin_behavior(redirect_stream);
... spawn the process ...
postream os = c.get_stdin();
os << "some-string" << std::endl;
os << 200 << 1024 << std::endl;
os.close();

Similary, the following shows how to retrieve the child's data, line by line:

launcher l;
l.set_stdout_behavior(redirect_stream);
... spawn the process ...
pistream is = c.get_stdout();
std::string line;
while (std::getline(is, line))
    std::cout << "Got a line: " << line << std::endl;

Every process has a map of variable/value pairs that forms part of its execution context; these variables are generally known as environment variables and their values are always represented as text strings. The process can query these variables at will to retrieve information passed in by the parent process; in fact, they can be seen as kind of interprocess communication mechanism. Therefore, it is the parent's responsibility to configure the environment variables that its child receives.

New variables can be added and existing ones can be modified using the launcher::set_environment method. Similarly, existing variables can be removed by using the launcher::unset_environment method. At last, the launcher::clear_environment method provides a way to zap all existing environment variables. Care should be taken when using this method because the new process may expect some standard variables to be defined for proper operation (e.g. PATH). After using this call you are responsible for appropriately setting up the minimum required variables.

For quick reference, consider:

launcher l;
... l now carries a snapshot of the current environment ...
l.clear_environment();
l.set_environment("variable-name", "variable-value");
l.unset_environment("variable-name");

And also check out the following example program:

#include <cstdlib>
#include <iostream>
#include <string>

#include <boost/process.hpp>

namespace bp = ::boost::process;

//
// Helper function that forks a new process that shows the contents of
// the environment configured by the given launcher.
//
static
void
run_it(const std::string& msg, const bp::launcher& l)
{
#if defined(BOOST_PROCESS_POSIX_API)
    bp::command_line cl("env");
#elif defined(BOOST_PROCESS_WIN32_API)
    bp::command_line cl = bp::command_line::shell("set");
#endif

    bp::launcher l2(l);
    l2.set_stdout_behavior(bp::inherit_stream);

    std::cout << "===> " << msg << std::endl;
    bp::status s = l2.start(cl).wait();
    if (s.exited() && s.exit_status() == EXIT_SUCCESS)
        std::cout << "     *** SUCCESS ***" << std::endl;
    else
        std::cout << "     *** FAILURE ***" << std::endl;
    std::cout << std::endl;
}

int
main(int argc, char* argv[])
{
    //
    // This first launcher does not touch the environment so the child
    // will receive a snapshot of our current table.
    //
    bp::launcher l1;
    run_it("Inherited environment", l1);

    //
    // This second example clears the child's environment prior
    // execution so it will not receive any variable.
    //
    bp::launcher l2;
    l2.clear_environment();
    run_it("Clean environment", l2);

    //
    // This example adds an extra variable to the environment.
    //
    bp::launcher l3;
    l3.set_environment("NEW_VARIABLE", "Hello, world!");
    run_it("Environment with the NEW_VARIABLE extra variable", l3);

    //
    // This example removes a standard variable from the environment.
    //
    bp::launcher l4;
    l4.unset_environment("PATH");
    run_it("Environment without the standard PATH variable", l4);


    //
    // This last example illustrates how to start up a new child process
    // with a completely controlled environment table that is not subject
    // to existing variables at all.
    //
    bp::launcher l5;
    l5.clear_environment();
    l5.set_environment("HOME", "Known value for HOME");
    l5.set_environment("PATH", "Known value for PATH");
    run_it("Completely controlled environment", l5);

    return EXIT_SUCCESS;
}

Every process has a property in its execution context that denotes the current working directory. This location is used by the operating system to resolve relative path names. Because this property is part of the execution context, it shall be configured prior process spawning by the process launcher. The value set is known as the startup work directory because it is where the application will start processing; the process is later free to change it to whichever other directory it wishes.

A child's startup work directory can be configured through the launcher by using the launcher::set_work_directory method. If not specified, the process is started in the same directory the parent was located when it created the launcher object.

For instance:

launcher l;
... l now points to the parent#s current working directory ...
l.set_work_directory(tmp_dir_location);

A new process is defined by a command line and a launcher. Once these two items are configured, the child process can be spawned by a call to the launcher::start method which receives the command line to be executed. For example:

command_line cl(...);
... add arguments to cl ...
launcher l(...);
... configure the execution environment through l ...

child c = l.start(cl);
... c now represents the newborn child process ...

If needed a launcher can be reused to spawn multiple child processes, possibly with different command lines. Similarly, a single command line can be passed to different launchers to start processes with different properties.

When the child process terminates execution, its parent collects its exit status to check whether the process ran fine or not. Furthermore, collecting this information is required in order to free the operating system data structures used to store it. The termination information includes the specific reason for this event as well as any additional details associated to it.

All this information is accessible through the status class, an instance of which is returned by the child::wait method. Through this instance, the parent inspects the reasons behind the child process finalization. For example:

status s = c.wait();
if (s.exited()) {
    // The process terminated by itself.
    std::cout << "The exit code was: " << s.exit_status() << std::endl;
} else
    std::cout << "Abnormal program termination." << std::endl;

Of special interest are the values returned by status::exit_status', a method that can only be called when status::exited is true. As the name states, this call returns the integer that the child program gave the operating system during exit. For readability and portability reasons you should check the returned value against the standard macros defined in the cstdlib header, shown in the table:

Exit status Symbolic constant Typical value
Success. EXIT_SUCCESS 0
Failure. Many applications will use multiple values to denote denote different error conditions though, so do not assume that this value alone is used to report a failed execution. EXIT_FAILURE Not 0

Aside regular exit, some platforms allow retrieving more fine-grained information on the termination cause. This information is available though non-portable classes, as is described later on.

The following complete code example illustrates how to start and stop multiple processes:

#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

#include <boost/process.hpp>

namespace bp = ::boost::process;

int
main(int argc, char* argv[])
{
    if (argc < 2) {
        std::cerr << "Please specify some directories." << std::endl;
        return EXIT_FAILURE;
    }

    //
    // Constructs a shell-based command line.
    //
    // This command line executes a CVS process passing it an update
    // command.  Note that it will be executed through the standard system
    // shell so that we do not have to know where the binary is located.
    //
    // In this case we have a command line that will work both on POSIX
    // systems and under Windows.
    //
    bp::command_line cl = bp::command_line::shell("cvs -z3 update -dP");

    for (int i = 1; i < argc; i++) {
        std::cout << "===> Updating directory " << argv[i] << std::endl;

        //
        // Constructs the launcher.
        //
        // This launcher will redirect us CVS' stdout and stderr data flows
        // through its stdout stream.  This will allow us to print the
        // output in a fancy way.
        //
        // The launcher also forces the CVS client to use the Secure Shell
        // to connect to servers and unsets the CVSROOT environment
        // variable to avoid side-effects.
        //
        bp::launcher l;
        l.set_stdout_behavior(bp::inherit_stream);
        l.set_merge_out_err(true);
        l.set_work_directory(argv[i]);
        l.set_environment("CVS_RSH", "ssh");
        l.unset_environment("CVSROOT");

        //
        // Spawns the CVS child process.
        //
        bp::child c = l.start(cl);

        //
        // Gets CVS' stdout stream.  As we asked for a merge of stdout and
        // stderr, this stream gets both outputs at once.
        //
        bp::pistream& is = c.get_stdout();

        //
        // Reads the child's output.
        //
        // As long as the child process (CVS) does not terminate, its
        // stdout will remain open and we will be able to retrieve data
        // from it.  Once it is closed, the 'is' stream will become invalid
        // and it will not return more data.
        //
        std::string line;
        while (std::getline(is, line))
            std::cout << "=> " << line << std::endl;

        //
        // Waits for the process to terminate and parses its exit status to
        // give the user feedback on the final results.
        //
        bp::status s = c.wait();
        if (s.exited() && s.exit_status() == EXIT_SUCCESS)
            std::cout << "     *** SUCCESSFUL UPDATE ***" << std::endl;
        else
            std::cout << "     *** FAILED UPDATE ***" << std::endl;
        std::cout << std::endl;
    }

    return EXIT_SUCCESS;
}

As we saw in the tutorial, managing a pipeline is very similar to controlling a single child process because the whole pipeline is treated by the library's most external API as a single unit. There are some differences though, as we shall see below:

Instead of using the regular launcher, pipelines are spawned by the specialized basic_pipeline class. Similarly, pipelines are represented by a children object instead of a regular child to illustrate the fact that they really are a group of processes. Despite these notational changes, the external interface of these pipeline-specific classes is very similar to that used in their homologous single process management objects.

The following example outlines how to prepare, start and wait for a pipeline's termination:

command_line cl1(...);
... add arguments to cl1 ...
command_line cl2(...);
... add arguments to cl2 ...

pipeline p;
... configure the execution environment through p ...
p.add(cl1).add(cl2);

children cs = p.start();
... cs now represents the newborn process group ...

status s = cs.wait();
Copyright 2006 Julio M. Merino Vidal

PrevUpHomeNext