Skip to content
This repository was archived by the owner on Jun 11, 2022. It is now read-only.

Redesign 0.6 #2

@klemens-morgenstern

Description

@klemens-morgenstern

I'd really like to have the boost.process library, but since development was on hiatus for two years, I think a few small design changes would be nice - or rather big ones, as described below.
But, not everything I write here is completely tested. I'd really need some discussion about those and other ideas how such a library could look like, because I really would change the frontend.

Here's the reference of the current version.

@dlaugt @BorisSchaeling @whizmo @hotgloupi @nat-goodspeed @havoc-io @rconde01 @JeffGarland

Since we now have a variadic templates, I think the execute function can be extended and much more flexible. Please notice, that execute shall return an undefined type, which depends on the parameters. It might be converted to child, as discussed below.
#1 Defaults

1.1 Unification

First of all, I like to unify the signal handling, to provide the following:

    void on_setup(Executor&) const
    void on_error(Executor&, const std::error_code &ec) const
    void on_success(Executor&) const

This would require the posix-executor to always use the self-pipe in order to provide the on_success signal. Therefore I think it would be also possible to add a initializer which removes this, in the following way:

   using namespace boost::process::initializers;
   auto p = execute("some_prog", posix::no_success_signal);

If then a success handler is provided, it would provide an error, i.e.:

   using namespace boost::process::initializers;
   auto p = execute("some_prog", posix::no_success_signal, on_success([](auto){}));
   //yields an compiler error

Posix Structure

The way it should work on posix is this:

uml

A possible code in posix would look like this:

executor::operator()(...)
{
   pipe error_pipe = create_pipe(FD_CLOEXEC); //closes the pipe on execve call
   on_setup();
   auto pid = fork();
   if (pid == -1)
      on_fork_error();
   else if (pid == 0)
   {
      ::close(selfe_pipe.source);
      on_exec_setup();
      ::execve(exe, cmd_line, env);
      on_exec_error();
      ::write(error_pipe.sink, &errno, sizeof(int));
      ::_exit(EXIT_FAILURE);
   }
   //ok, here we are back in the father-process 
   int code, cnt;
   do 
   {
     cnt = ::read(error_pipe.source, &code, sizeof(code));
      int err = errno;
      if (res == EDABF)  //pipe is closed.
          break;
       //EAGAIN not yet forked, EINTR interrupted, i.e. try again
      else if ((err != EAGAIN) && (err != EINTR)) 
      else
          throw error();
   }
   while(cnt == -1);

   if (cnt == 4) //error.
       on_error(code);
   else if (cnt > 0) //some other strang error
       on_error(...); //don't know yet
   else
       on_success();
}

Now, on default the father process is waiting. I think to provide a ignore_error would be very usefule, since this would then ignore this.

The additional signal-handlers will be availble as part of the boost::process::posix namespace.

1.2 Throw on error

I also would make the throw_on_error the default, because it just makes the most sense, since execute constructs a process-object. I.e. not being able to construct it should result in an error.

auto p1 = execute("not_runnable"); //throws
auto p2 = execute("not_runnable", ignore_error); //no throw, no handling;
std::error_code ec;
auto p3 = execute("not_runnable", set_on_error(ec)); //no throw, takes the error
auto p3 = execute("not_runnable", ec); //no throw, takes the error, shortcut.

I would introduce posix::no_success_signal and ignore_error because the posix implementation now waits for the child-process to start. A user might consider that unneeded and could hence wish to skip that.
#2. Simplified syntax

2.1. Idea

I think it would be possible to make the syntax much simpler, as in the following examples (corresponding to the current). All are equal.

auto p1 = execute(exe("cmd"), args("x", "y")); //shortened version of the current syntax
auto p2 = execute(exe="cmd", args={"x", "y"}); //alternate syntax
auto p3 = execute(exe="cmd", args="x", args+="y"); //other syntax
auto p4 = execute("cmd", "x", "y"); //lazy syntax
auto p5 = execute(cmd("cmd x y"));
auto p6 = execute(cmd="cmd x y");
auto p7 = execute("cmd x y");

The cmd and args get parsed differently, that is:

execute(cmd="cmd x y z");//yields "cmd x y z"
execute(cmd="cmd \"x y\" z");//  yields "cmd \"x y\" z"
execute(exe="cmd", args={"x y", "z"}); //yields "cmd \"x y\" z"

2.2. Other initializers

The following signal handlers shall have syntax similar to the above.

  • inherit_env
  • env
  • start_dir

And the platform specifics.

2.3 I/O

This is the part where I am the least confident, but those are all things I consider possible.

2.3.1 Close Pipe / Redirect to null

If a pipe shall be closed, it should be possible in the following way

execute("ls", std_out=null);  //redirect to /dev/null
execute("ls", std_out=close); //close the pipe
//alternate syntax
execute("ls", std_out>null);  //redirect to /dev/null
execute("ls", close(std_out)); //close the pipe

2.3.2 Sync I/O

The sync I/O can be simplified in the following way (current example). I.e. the pipe can be constructed automatically.

file_descriptor_sink sink;
execute("my_prog", std_in=sink);//alternatively '|', because pipe.
stream<file_descriptor_sink> str;
stream<file_descriptor_source> str;
execute("my_prog", std_out=str);

If the output shall be redirectet into a file, this can be done via a boost::filesystem::path:

execute("my_prog", std_out=path("./log.txt"));

It may also be following to allow a lazy redirection to files:

execute("my_prog", std_out>"./log.txt");

With this syntax it is of course also easy to pipe from one process to another (though the implementation is not yet clear, i might need to use a named pipe on windows)

process::pipe = create_pipe(); 
execute("prog1", std_out>pipe);
execute("prog2", std_in<pipe);

2.3.3 Self-Sync I/O

Removed, because that makes child struct unecessary complicated

Another possibility, that would definitly be possible would be to allow the process to adapt to a string. I don't know it this would actually be worth the effort.

auto p = execute("c++filt", std_in=self, std_out=self);
p << "_ZN9wikipedia7article8wikilinkC1ERKSs";
std::string dem;
p >> dem;
//dem  == "wikipedia::article::wikilink::wikilink(std::string const&)"

This is only convenience, it allows short-handed I/O. One could also use the stderr instead of stdout, while declaring both as self will yield an error.

2.3.4 Async I/O

This might be the most difficult part. I imagine that we can do it in someway like this:

boost::asio::io_service io_service;
stringstream ss;//threadunsafe, but get's the point.
string buf; 

execute("some_prog", std_in<ss, std_out>buffer(buf), std_err>cerr, io_service);
io_service.run();

Thereby the whole mitigate thing can be hidden and managed automatically. Addtionally, if no io_service is passed, it could be created by the execute command and run it in another thread.

The example from the current tutorial would look like this:

boost::asio::io_service io_service;
std::array<char, 4096> buffer;

auto p = execute("test.exe", std_out>process::buffer(buffer), io_service);

io_service.run();

I can determine that buffer must be read async,because it's no stream and can construct the async_pipe in place. Also I can call async_read from the executor, because that does only start on io_service.run().
Additionally, the io_service could be skipped and constructed in the executor and launched in a new thread. Don't know if that'd be smart though.

But to be honest: I am currently not nearly familiar enough with the way io_service works.

The same would apply to any std::ostream or std::istream, which is not stream<file_descriptor_sink>, so the cerr or ss in my example should work this way. Now of course, binding std_err>cerr is by far the stupidest way, because I could pipe that directly (std_err>stderr) - at least if we provide a syntax for that (i.e. also redirecting std_err>stdout).

This whole concept needs more experimentation!

2.4 Environment

In 0.5 the inherit_env was empty for windows, I don't know if that actually works. Now I would use the following syntax:

execute("thingy"); //inherit env
execute("thingy", env+={"MY_VAR=Thingy"}); //inhertit and add
execute("things", env=  {"MY_VAR=Thingy"}); //do not inherit

So the following would be awesome, though I don't know if necessary:

//inherit the environment and append "MyThing" to "MY_VAR"
execute("thingy", env["MY_VAR"]+="MyThingy"};

#3. Classes

3.1 Process

When calling execute a process-object is returned, whichs type is undefined. It holds any data needed by the initialization. It has a set of functions, which are the following

  • is_running
  • terminate
  • wait //formerly wait_for_exit
  • wait_for //wait with a timespan to wait
  • wait_until //wait until a time timepoint
  • exit_code

3.2. Child

If the process shall be stored in an undefined class, it can be converted to child. The data is then moved onto the stack and stored in a pointer inside the child. The child class will have member-functions similar to the functions for process.
A child is movable, but not copyable.
#4. Launch Mode

Removed, is done via process groups #8

In order to provide a process in an RAII way I do think that different launch modes should be available. I do not know as for now if this can be switched at runtime or has to be defined at launch time.

Attached

If a childprocess runs attached it will be terminated on destructor-call.

Detached

If a process is detached nothing will happend on destruction

Default

By default a process is launched in an attached way. This would cause this syntax to immediately terminate the application:

execute("prog");

But this behaviour could of course be specified by the following way:

execute("prog", wait); //blocking
execute("prog", detach); //not blocking.

#5 this_process

Similar to std::this_thread I would add a this_process namespace with the following synopsis.

namespace this_process 
{
    int get_id();
    /*undefined*/ native_handle();
    environment get_environment();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions