On May 9, 10:00 pm, Nobody <
nob...@nowhere.com> wrote:
> On Tue, 08 May 2012 15:24:46 -0700, Joshua Maurice wrote:
> > I'm just trying to write a general purpose process creation API, and
> > I'm wondering what I should put as the Process object's destructor, or
> > equivalently what function(s) should I provide for error cleanup? I'd
> > like to allow the user of the library to make the choice whether the
> > child process needs to die, whether we need to wait on it, or whether
> > we should "detach" and forget about it. I still guess those are all
> > valid strategies - I think - and I'd like to offer them all.
>
> You can't package something as complex as the Unix process API inside a
> simple interface without making a lot of trade-offs.
While you cannot make a general purpose simple abstraction that covers
all of the potential "power", I find my abstraction quite useful. I
have a full ProcessBuilder that is basically Java's ProcessBuilder
made in C++. It's so useful. Writing ~3 lines of code to redirect the
stdout and stderr separately or together, to a file or to a pipe, and
wrap that pipe in a std::iostream, change the directory, change env
vars of the child, and so on, it's just too useful - all in ~3 lines
of code. Example:
auto_ptr<Process> process(ProcessBuilder()
.cmd(StrList()("g++")("-c")(cppPath)("-o")(objPath))
.dir(outputDir).redirectErrToOut().pipeOut().spawn());
JPipeIstream childsOut(process->releaseStdOut()); //is a
std::istream
for (string str; getline(childsOut, str); ) //example processing
cout << str << '\n'; //example processing
process->join();
int const exitcode = process->exitcode();
If I didn't have the utility, that would take me 100+ lines of code,
easy, just for the basic functionality. Another 100+ lines for the
various system specific hacks to ensure all file handles are closed
between the fork and exec (read from "/proc/self" on linux,
closefrom() on one system, another hack on another system, etc.).
Hell, I haven't even started trying to figure out what other resources
can be leaked across a fork+exec - I wonder how annoying it'll be to
close those. Another 50-100 lines for the background thread to allow
for a convenient detach without having extra stub processes. (I give
such a low estimate only because I also happen to have access to a
good utility to spawn threads which is similar to the new C++11 API.)
This process spawn code isn't particularly easy either. The
documentation is meh. You have to be exceptionally careful in the code
between fork and exec - async-signal safe code only. And sometimes we
all get lazy when checking for error codes so doing it once properly
is great too.
All in all, I think that's just silly to write out by hand every time.
It ought to be abstracted away.
Once I add in another little blurb so the user can choose the action
taken in the dtor (SIGKILL asap, SIGTERM followed by SIGKILL with
configurable sleep time, SIGTERM followed by waitpid without timeout,
detach, join), it will handle 95% of all cases where I might want to
spawn a child process. When I get little more time, I'll give
ProcessBuilder the functions so you can provide it a file descriptor
as the input or output to the stdin, stdout, and/or stderr of the
child. That would probably make it cover 99% of all the times I would
ever need to spawn a child process.
I wrote this when I was doing a project where I had the need of
spawning a bunch of processes all over the place. It is very very
useful to do the simple case in 3 lines of code:
/* Actual (slightly shortened for demonstration purposes) C++ code
from a little project of mine: */
const JSyncExecResult x = jSyncExec(StrList()("g++")("-c")
(cppPath));
if (x.exitcode != 0)
throw std::runtime_error(string() + "g++ failed. exit code " +
toString(x.exitcode) + ". Output: " + x.stdout);
It's like system(), except portable and not broken.
(The project is a make substitute, because all build systems that I
know of suck [e.g. not out-of-the-box incrementally correct], are not
portable, don't do Java and C++ well, and/or all of the above. It's a
little hobby of mine that I've been working on for way too long.)
This isn't academic either. I recall a particularly nasty bug that was
in my company's code because of popen(). In short, it was being done
concurrently, and one child process inherited a file descriptor that
it should not, which caused fun errors when later the parent tried to
manipulate that file.
> As for clean-up, I'd suggest doing what Python's subprocess module does:
> it maintains a list of "orphan" processes. The destructor for the
> Python object checks whether the process is still alive (waitpid),
> and if so the PID is added to the list. Every time a new process is
> created, the _cleanup() function is run to prune any zombies from the list.
Alleviates the need of a background thread in a sleep busy-wait loop.
I like the idea. However, I just whipped up (possibly contrived) usage
scenarios where it may be bad, but thanks for the good idea. It can be
combined with the background thread idea to make it even better.