blob: 28bfb049c2b06999edea1d8511abaac5afd1684b [file] [log] [blame]
// Written in the D programming language.
/**
Functions for starting and interacting with other processes, and for
working with the current process' execution environment.
Process_handling:
$(UL $(LI
$(LREF spawnProcess) spawns a new process, optionally assigning it an
arbitrary set of standard input, output, and error streams.
The function returns immediately, leaving the child process to execute
in parallel with its parent. All other functions in this module that
spawn processes are built around `spawnProcess`.)
$(LI
$(LREF wait) makes the parent process wait for a child process to
terminate. In general one should always do this, to avoid
child processes becoming "zombies" when the parent process exits.
Scope guards are perfect for this – see the $(LREF spawnProcess)
documentation for examples. $(LREF tryWait) is similar to `wait`,
but does not block if the process has not yet terminated.)
$(LI
$(LREF pipeProcess) also spawns a child process which runs
in parallel with its parent. However, instead of taking
arbitrary streams, it automatically creates a set of
pipes that allow the parent to communicate with the child
through the child's standard input, output, and/or error streams.
This function corresponds roughly to C's `popen` function.)
$(LI
$(LREF execute) starts a new process and waits for it
to complete before returning. Additionally, it captures
the process' standard output and error streams and returns
the output of these as a string.)
$(LI
$(LREF spawnShell), $(LREF pipeShell) and $(LREF executeShell) work like
`spawnProcess`, `pipeProcess` and `execute`, respectively,
except that they take a single command string and run it through
the current user's default command interpreter.
`executeShell` corresponds roughly to C's `system` function.)
$(LI
$(LREF kill) attempts to terminate a running process.)
)
The following table compactly summarises the different process creation
functions and how they relate to each other:
$(BOOKTABLE,
$(TR $(TH )
$(TH Runs program directly)
$(TH Runs shell command))
$(TR $(TD Low-level process creation)
$(TD $(LREF spawnProcess))
$(TD $(LREF spawnShell)))
$(TR $(TD Automatic input/output redirection using pipes)
$(TD $(LREF pipeProcess))
$(TD $(LREF pipeShell)))
$(TR $(TD Execute and wait for completion, collect output)
$(TD $(LREF execute))
$(TD $(LREF executeShell)))
)
Other_functionality:
$(UL
$(LI
$(LREF pipe) is used to create unidirectional pipes.)
$(LI
$(LREF environment) is an interface through which the current process'
environment variables can be read and manipulated.)
$(LI
$(LREF escapeShellCommand) and $(LREF escapeShellFileName) are useful
for constructing shell command lines in a portable way.)
)
Authors:
$(LINK2 https://github.com/kyllingstad, Lars Tandle Kyllingstad),
$(LINK2 https://github.com/schveiguy, Steven Schveighoffer),
$(HTTP thecybershadow.net, Vladimir Panteleev)
Copyright:
Copyright (c) 2013, the authors. All rights reserved.
License:
$(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
Source:
$(PHOBOSSRC std/process.d)
Macros:
OBJECTREF=$(REF1 $0, object)
Note:
Most of the functionality in this module is not available on iOS, tvOS
and watchOS. The only functions available on those platforms are:
$(LREF environment), $(LREF thisProcessID) and $(LREF thisThreadID).
*/
module std.process;
import core.thread : ThreadID;
version (Posix)
{
import core.sys.posix.sys.wait;
import core.sys.posix.unistd;
}
version (Windows)
{
import core.stdc.stdio;
import core.sys.windows.winbase;
import core.sys.windows.winnt;
import std.utf;
import std.windows.syserror;
}
import std.internal.cstring;
import std.range;
import std.stdio;
version (OSX)
version = Darwin;
else version (iOS)
{
version = Darwin;
version = iOSDerived;
}
else version (TVOS)
{
version = Darwin;
version = iOSDerived;
}
else version (WatchOS)
{
version = Darwin;
version = iOSDerived;
}
// When the DMC runtime is used, we have to use some custom functions
// to convert between Windows file handles and FILE*s.
version (Win32) version (CRuntime_DigitalMars) version = DMC_RUNTIME;
// Some of the following should be moved to druntime.
private
{
// Microsoft Visual C Runtime (MSVCRT) declarations.
version (Windows)
{
version (DMC_RUNTIME) { } else
{
import core.stdc.stdint;
enum
{
STDIN_FILENO = 0,
STDOUT_FILENO = 1,
STDERR_FILENO = 2,
}
}
}
// POSIX API declarations.
version (Posix)
{
version (Darwin)
{
extern(C) char*** _NSGetEnviron() nothrow;
const(char**) getEnvironPtr() @trusted
{
return *_NSGetEnviron;
}
}
else
{
// Made available by the C runtime:
extern(C) extern __gshared const char** environ;
const(char**) getEnvironPtr() @trusted
{
return environ;
}
}
@system unittest
{
import core.thread : Thread;
new Thread({assert(getEnvironPtr !is null);}).start();
}
}
} // private
// =============================================================================
// Environment variable manipulation.
// =============================================================================
/**
Manipulates _environment variables using an associative-array-like
interface.
This class contains only static methods, and cannot be instantiated.
See below for examples of use.
*/
abstract final class environment
{
static import core.sys.posix.stdlib;
import core.stdc.errno : errno, EINVAL;
static:
/**
Retrieves the value of the environment variable with the given `name`.
---
auto path = environment["PATH"];
---
Throws:
$(OBJECTREF Exception) if the environment variable does not exist,
or $(REF UTFException, std,utf) if the variable contains invalid UTF-16
characters (Windows only).
See_also:
$(LREF environment.get), which doesn't throw on failure.
*/
string opIndex(scope const(char)[] name) @safe
{
import std.exception : enforce;
return get(name, null).enforce("Environment variable not found: "~name);
}
/**
Retrieves the value of the environment variable with the given `name`,
or a default value if the variable doesn't exist.
Unlike $(LREF environment.opIndex), this function never throws on Posix.
---
auto sh = environment.get("SHELL", "/bin/sh");
---
This function is also useful in checking for the existence of an
environment variable.
---
auto myVar = environment.get("MYVAR");
if (myVar is null)
{
// Environment variable doesn't exist.
// Note that we have to use 'is' for the comparison, since
// myVar == null is also true if the variable exists but is
// empty.
}
---
Params:
name = name of the environment variable to retrieve
defaultValue = default value to return if the environment variable doesn't exist.
Returns:
the value of the environment variable if found, otherwise
`null` if the environment doesn't exist.
Throws:
$(REF UTFException, std,utf) if the variable contains invalid UTF-16
characters (Windows only).
*/
string get(scope const(char)[] name, string defaultValue = null) @safe
{
string value;
getImpl(name, (result) { value = result ? cachedToString(result) : defaultValue; });
return value;
}
/**
Assigns the given `value` to the environment variable with the given
`name`.
If `value` is null the variable is removed from environment.
If the variable does not exist, it will be created. If it already exists,
it will be overwritten.
---
environment["foo"] = "bar";
---
Throws:
$(OBJECTREF Exception) if the environment variable could not be added
(e.g. if the name is invalid).
Note:
On some platforms, modifying environment variables may not be allowed in
multi-threaded programs. See e.g.
$(LINK2 https://www.gnu.org/software/libc/manual/html_node/Environment-Access.html#Environment-Access, glibc).
*/
inout(char)[] opIndexAssign(return scope inout char[] value, scope const(char)[] name) @trusted
{
version (Posix)
{
import std.exception : enforce, errnoEnforce;
if (value is null)
{
remove(name);
return value;
}
if (core.sys.posix.stdlib.setenv(name.tempCString(), value.tempCString(), 1) != -1)
{
return value;
}
// The default errno error message is very uninformative
// in the most common case, so we handle it manually.
enforce(errno != EINVAL,
"Invalid environment variable name: '"~name~"'");
errnoEnforce(false,
"Failed to add environment variable");
assert(0);
}
else version (Windows)
{
import std.windows.syserror : wenforce;
wenforce(
SetEnvironmentVariableW(name.tempCStringW(), value.tempCStringW()),
);
return value;
}
else static assert(0);
}
/**
Removes the environment variable with the given `name`.
If the variable isn't in the environment, this function returns
successfully without doing anything.
Note:
On some platforms, modifying environment variables may not be allowed in
multi-threaded programs. See e.g.
$(LINK2 https://www.gnu.org/software/libc/manual/html_node/Environment-Access.html#Environment-Access, glibc).
*/
void remove(scope const(char)[] name) @trusted nothrow @nogc
{
version (Windows) SetEnvironmentVariableW(name.tempCStringW(), null);
else version (Posix) core.sys.posix.stdlib.unsetenv(name.tempCString());
else static assert(0);
}
/**
Identify whether a variable is defined in the environment.
Because it doesn't return the value, this function is cheaper than `get`.
However, if you do need the value as well, you should just check the
return of `get` for `null` instead of using this function first.
Example:
-------------
// good usage
if ("MY_ENV_FLAG" in environment)
doSomething();
// bad usage
if ("MY_ENV_VAR" in environment)
doSomething(environment["MY_ENV_VAR"]);
// do this instead
if (auto var = environment.get("MY_ENV_VAR"))
doSomething(var);
-------------
*/
bool opBinaryRight(string op : "in")(scope const(char)[] name) @trusted
{
version (Posix)
return core.sys.posix.stdlib.getenv(name.tempCString()) !is null;
else version (Windows)
{
SetLastError(NO_ERROR);
if (GetEnvironmentVariableW(name.tempCStringW, null, 0) > 0)
return true;
immutable err = GetLastError();
if (err == NO_ERROR)
return true; // zero-length environment variable on Wine / XP
if (err == ERROR_ENVVAR_NOT_FOUND)
return false;
// Some other Windows error, throw.
throw new WindowsException(err);
}
else static assert(0);
}
/**
Copies all environment variables into an associative array.
Windows_specific:
While Windows environment variable names are case insensitive, D's
built-in associative arrays are not. This function will store all
variable names in uppercase (e.g. `PATH`).
Throws:
$(OBJECTREF Exception) if the environment variables could not
be retrieved (Windows only).
*/
string[string] toAA() @trusted
{
import std.conv : to;
string[string] aa;
version (Posix)
{
auto environ = getEnvironPtr;
for (int i=0; environ[i] != null; ++i)
{
import std.string : indexOf;
immutable varDef = to!string(environ[i]);
immutable eq = indexOf(varDef, '=');
assert(eq >= 0);
immutable name = varDef[0 .. eq];
immutable value = varDef[eq+1 .. $];
// In POSIX, environment variables may be defined more
// than once. This is a security issue, which we avoid
// by checking whether the key already exists in the array.
// For more info:
// http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/environment-variables.html
if (name !in aa) aa[name] = value;
}
}
else version (Windows)
{
import std.exception : enforce;
import std.uni : toUpper;
auto envBlock = GetEnvironmentStringsW();
enforce(envBlock, "Failed to retrieve environment variables.");
scope(exit) FreeEnvironmentStringsW(envBlock);
for (int i=0; envBlock[i] != '\0'; ++i)
{
auto start = i;
while (envBlock[i] != '=') ++i;
immutable name = toUTF8(toUpper(envBlock[start .. i]));
start = i+1;
while (envBlock[i] != '\0') ++i;
// Ignore variables with empty names. These are used internally
// by Windows to keep track of each drive's individual current
// directory.
if (!name.length)
continue;
// Just like in POSIX systems, environment variables may be
// defined more than once in an environment block on Windows,
// and it is just as much of a security issue there. Moreso,
// in fact, due to the case insensensitivity of variable names,
// which is not handled correctly by all programs.
auto val = toUTF8(envBlock[start .. i]);
if (name !in aa) aa[name] = val is null ? "" : val;
}
}
else static assert(0);
return aa;
}
private:
version (Windows) alias OSChar = WCHAR;
else version (Posix) alias OSChar = char;
// Retrieves the environment variable. Calls `sink` with a
// temporary buffer of OS characters, or `null` if the variable
// doesn't exist.
void getImpl(scope const(char)[] name, scope void delegate(const(OSChar)[]) @safe sink) @trusted
{
version (Windows)
{
// first we ask windows how long the environment variable is,
// then we try to read it in to a buffer of that length. Lots
// of error conditions because the windows API is nasty.
import std.conv : to;
const namezTmp = name.tempCStringW();
WCHAR[] buf;
// clear error because GetEnvironmentVariable only says it sets it
// if the environment variable is missing, not on other errors.
SetLastError(NO_ERROR);
// len includes terminating null
immutable len = GetEnvironmentVariableW(namezTmp, null, 0);
if (len == 0)
{
immutable err = GetLastError();
if (err == ERROR_ENVVAR_NOT_FOUND)
return sink(null);
if (err != NO_ERROR) // Some other Windows error, throw.
throw new WindowsException(err);
}
if (len <= 1)
return sink("");
buf.length = len;
while (true)
{
// lenRead is either the number of bytes read w/o null - if buf was long enough - or
// the number of bytes necessary *including* null if buf wasn't long enough
immutable lenRead = GetEnvironmentVariableW(namezTmp, buf.ptr, to!DWORD(buf.length));
if (lenRead == 0)
{
immutable err = GetLastError();
if (err == NO_ERROR) // sucessfully read a 0-length variable
return sink("");
if (err == ERROR_ENVVAR_NOT_FOUND) // variable didn't exist
return sink(null);
// some other windows error
throw new WindowsException(err);
}
assert(lenRead != buf.length, "impossible according to msft docs");
if (lenRead < buf.length) // the buffer was long enough
return sink(buf[0 .. lenRead]);
// resize and go around again, because the environment variable grew
buf.length = lenRead;
}
}
else version (Posix)
{
import core.stdc.string : strlen;
const vz = core.sys.posix.stdlib.getenv(name.tempCString());
if (vz == null) return sink(null);
return sink(vz[0 .. strlen(vz)]);
}
else static assert(0);
}
string cachedToString(C)(scope const(C)[] v) @safe
{
import std.algorithm.comparison : equal;
// Cache the last call's result.
static string lastResult;
if (v.empty)
{
// Return non-null array for blank result to distinguish from
// not-present result.
lastResult = "";
}
else if (!v.equal(lastResult))
{
import std.conv : to;
lastResult = v.to!string;
}
return lastResult;
}
}
@safe unittest
{
import std.exception : assertThrown;
// New variable
environment["std_process"] = "foo";
assert(environment["std_process"] == "foo");
assert("std_process" in environment);
// Set variable again (also tests length 1 case)
environment["std_process"] = "b";
assert(environment["std_process"] == "b");
assert("std_process" in environment);
// Remove variable
environment.remove("std_process");
assert("std_process" !in environment);
// Remove again, should succeed
environment.remove("std_process");
assert("std_process" !in environment);
// Throw on not found.
assertThrown(environment["std_process"]);
// get() without default value
assert(environment.get("std_process") is null);
// get() with default value
assert(environment.get("std_process", "baz") == "baz");
// get() on an empty (but present) value
environment["std_process"] = "";
auto res = environment.get("std_process");
assert(res !is null);
assert(res == "");
assert("std_process" in environment);
// Important to do the following round-trip after the previous test
// because it tests toAA with an empty var
// Convert to associative array
auto aa = environment.toAA();
assert(aa.length > 0);
foreach (n, v; aa)
{
// Wine has some bugs related to environment variables:
// - Wine allows the existence of an env. variable with the name
// "\0", but GetEnvironmentVariable refuses to retrieve it.
// As of 2.067 we filter these out anyway (see comment in toAA).
assert(v == environment[n]);
}
// ... and back again.
foreach (n, v; aa)
environment[n] = v;
// Complete the roundtrip
auto aa2 = environment.toAA();
import std.conv : text;
assert(aa == aa2, text(aa, " != ", aa2));
assert("std_process" in environment);
// Setting null must have the same effect as remove
environment["std_process"] = null;
assert("std_process" !in environment);
}
// =============================================================================
// Functions and classes for process management.
// =============================================================================
/**
* Returns the process ID of the current process,
* which is guaranteed to be unique on the system.
*
* Example:
* ---
* writefln("Current process ID: %d", thisProcessID);
* ---
*/
@property int thisProcessID() @trusted nothrow //TODO: @safe
{
version (Windows) return GetCurrentProcessId();
else version (Posix) return core.sys.posix.unistd.getpid();
}
/**
* Returns the process ID of the current thread,
* which is guaranteed to be unique within the current process.
*
* Returns:
* A $(REF ThreadID, core,thread) value for the calling thread.
*
* Example:
* ---
* writefln("Current thread ID: %s", thisThreadID);
* ---
*/
@property ThreadID thisThreadID() @trusted nothrow //TODO: @safe
{
version (Windows)
return GetCurrentThreadId();
else
version (Posix)
{
import core.sys.posix.pthread : pthread_self;
return pthread_self();
}
}
@system unittest
{
int pidA, pidB;
ThreadID tidA, tidB;
pidA = thisProcessID;
tidA = thisThreadID;
import core.thread;
auto t = new Thread({
pidB = thisProcessID;
tidB = thisThreadID;
});
t.start();
t.join();
assert(pidA == pidB);
assert(tidA != tidB);
}
package(std) string uniqueTempPath() @safe
{
import std.file : tempDir;
import std.path : buildPath;
import std.uuid : randomUUID;
// Path should contain spaces to test escaping whitespace
return buildPath(tempDir(), "std.process temporary file " ~
randomUUID().toString());
}
version (iOSDerived) {}
else:
/**
Spawns a new process, optionally assigning it an arbitrary set of standard
input, output, and error streams.
The function returns immediately, leaving the child process to execute
in parallel with its parent. It is recommended to always call $(LREF wait)
on the returned $(LREF Pid) unless the process was spawned with
`Config.detached` flag, as detailed in the documentation for `wait`.
Command_line:
There are four overloads of this function. The first two take an array
of strings, `args`, which should contain the program name as the
zeroth element and any command-line arguments in subsequent elements.
The third and fourth versions are included for convenience, and may be
used when there are no command-line arguments. They take a single string,
`program`, which specifies the program name.
Unless a directory is specified in `args[0]` or `program`,
`spawnProcess` will search for the program in a platform-dependent
manner. On POSIX systems, it will look for the executable in the
directories listed in the PATH environment variable, in the order
they are listed. On Windows, it will search for the executable in
the following sequence:
$(OL
$(LI The directory from which the application loaded.)
$(LI The current directory for the parent process.)
$(LI The 32-bit Windows system directory.)
$(LI The 16-bit Windows system directory.)
$(LI The Windows directory.)
$(LI The directories listed in the PATH environment variable.)
)
---
// Run an executable called "prog" located in the current working
// directory:
auto pid = spawnProcess("./prog");
scope(exit) wait(pid);
// We can do something else while the program runs. The scope guard
// ensures that the process is waited for at the end of the scope.
...
// Run DMD on the file "myprog.d", specifying a few compiler switches:
auto dmdPid = spawnProcess(["dmd", "-O", "-release", "-inline", "myprog.d" ]);
if (wait(dmdPid) != 0)
writeln("Compilation failed!");
---
Environment_variables:
By default, the child process inherits the environment of the parent
process, along with any additional variables specified in the `env`
parameter. If the same variable exists in both the parent's environment
and in `env`, the latter takes precedence.
If the $(LREF Config.newEnv) flag is set in `config`, the child
process will $(I not) inherit the parent's environment. Its entire
environment will then be determined by `env`.
---
wait(spawnProcess("myapp", ["foo" : "bar"], Config.newEnv));
---
Standard_streams:
The optional arguments `stdin`, `stdout` and `stderr` may
be used to assign arbitrary $(REF File, std,stdio) objects as the standard
input, output and error streams, respectively, of the child process. The
former must be opened for reading, while the latter two must be opened for
writing. The default is for the child process to inherit the standard
streams of its parent.
---
// Run DMD on the file myprog.d, logging any error messages to a
// file named errors.log.
auto logFile = File("errors.log", "w");
auto pid = spawnProcess(["dmd", "myprog.d"],
std.stdio.stdin,
std.stdio.stdout,
logFile);
if (wait(pid) != 0)
writeln("Compilation failed. See errors.log for details.");
---
Note that if you pass a `File` object that is $(I not)
one of the standard input/output/error streams of the parent process,
that stream will by default be $(I closed) in the parent process when
this function returns. See the $(LREF Config) documentation below for
information about how to disable this behaviour.
Beware of buffering issues when passing `File` objects to
`spawnProcess`. The child process will inherit the low-level raw
read/write offset associated with the underlying file descriptor, but
it will not be aware of any buffered data. In cases where this matters
(e.g. when a file should be aligned before being passed on to the
child process), it may be a good idea to use unbuffered streams, or at
least ensure all relevant buffers are flushed.
Params:
args = An array which contains the program name as the zeroth element
and any command-line arguments in the following elements.
stdin = The standard input stream of the child process.
This can be any $(REF File, std,stdio) that is opened for reading.
By default the child process inherits the parent's input
stream.
stdout = The standard output stream of the child process.
This can be any $(REF File, std,stdio) that is opened for writing.
By default the child process inherits the parent's output stream.
stderr = The standard error stream of the child process.
This can be any $(REF File, std,stdio) that is opened for writing.
By default the child process inherits the parent's error stream.
env = Additional environment variables for the child process.
config = Flags that control process creation. See $(LREF Config)
for an overview of available flags.
workDir = The working directory for the new process.
By default the child process inherits the parent's working
directory.
Returns:
A $(LREF Pid) object that corresponds to the spawned process.
Throws:
$(LREF ProcessException) on failure to start the process.$(BR)
$(REF StdioException, std,stdio) on failure to pass one of the streams
to the child process (Windows only).$(BR)
$(REF RangeError, core,exception) if `args` is empty.
*/
Pid spawnProcess(scope const(char[])[] args,
File stdin = std.stdio.stdin,
File stdout = std.stdio.stdout,
File stderr = std.stdio.stderr,
const string[string] env = null,
Config config = Config.none,
scope const char[] workDir = null)
@safe
{
version (Windows)
{
const commandLine = escapeShellArguments(args);
const program = args.length ? args[0] : null;
return spawnProcessWin(commandLine, program, stdin, stdout, stderr, env, config, workDir);
}
else version (Posix)
{
return spawnProcessPosix(args, stdin, stdout, stderr, env, config, workDir);
}
else
static assert(0);
}
/// ditto
Pid spawnProcess(scope const(char[])[] args,
const string[string] env,
Config config = Config.none,
scope const(char)[] workDir = null)
@trusted // TODO: Should be @safe
{
return spawnProcess(args,
std.stdio.stdin,
std.stdio.stdout,
std.stdio.stderr,
env,
config,
workDir);
}
/// ditto
Pid spawnProcess(scope const(char)[] program,
File stdin = std.stdio.stdin,
File stdout = std.stdio.stdout,
File stderr = std.stdio.stderr,
const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null)
@trusted
{
return spawnProcess((&program)[0 .. 1],
stdin, stdout, stderr, env, config, workDir);
}
/// ditto
Pid spawnProcess(scope const(char)[] program,
const string[string] env,
Config config = Config.none,
scope const(char)[] workDir = null)
@trusted
{
return spawnProcess((&program)[0 .. 1], env, config, workDir);
}
version (Posix) private enum InternalError : ubyte
{
noerror,
exec,
chdir,
getrlimit,
doubleFork,
malloc,
preExec,
}
/*
Implementation of spawnProcess() for POSIX.
envz should be a zero-terminated array of zero-terminated strings
on the form "var=value".
*/
version (Posix)
private Pid spawnProcessPosix(scope const(char[])[] args,
File stdin,
File stdout,
File stderr,
scope const string[string] env,
Config config,
scope const(char)[] workDir)
@trusted // TODO: Should be @safe
{
import core.exception : RangeError;
import std.algorithm.searching : any;
import std.conv : text;
import std.path : isDirSeparator;
import std.string : toStringz;
if (args.empty) throw new RangeError();
const(char)[] name = args[0];
if (!any!isDirSeparator(name))
{
name = searchPathFor(name);
if (name is null)
throw new ProcessException(text("Executable file not found: ", args[0]));
}
// Convert program name and arguments to C-style strings.
auto argz = new const(char)*[args.length+1];
argz[0] = toStringz(name);
foreach (i; 1 .. args.length) argz[i] = toStringz(args[i]);
argz[$-1] = null;
// Prepare environment.
auto envz = createEnv(env, !(config.flags & Config.Flags.newEnv));
// Open the working directory.
// We use open in the parent and fchdir in the child
// so that most errors (directory doesn't exist, not a directory)
// can be propagated as exceptions before forking.
int workDirFD = -1;
scope(exit) if (workDirFD >= 0) close(workDirFD);
if (workDir.length)
{
import core.sys.posix.fcntl : open, O_RDONLY, stat_t, fstat, S_ISDIR;
workDirFD = open(workDir.tempCString(), O_RDONLY);
if (workDirFD < 0)
throw ProcessException.newFromErrno("Failed to open working directory");
stat_t s;
if (fstat(workDirFD, &s) < 0)
throw ProcessException.newFromErrno("Failed to stat working directory");
if (!S_ISDIR(s.st_mode))
throw new ProcessException("Not a directory: " ~ cast(string) workDir);
}
static int getFD(ref File f) { return core.stdc.stdio.fileno(f.getFP()); }
// Get the file descriptors of the streams.
// These could potentially be invalid, but that is OK. If so, later calls
// to dup2() and close() will just silently fail without causing any harm.
auto stdinFD = getFD(stdin);
auto stdoutFD = getFD(stdout);
auto stderrFD = getFD(stderr);
// We don't have direct access to the errors that may happen in a child process.
// So we use this pipe to deliver them.
int[2] forkPipe;
if (core.sys.posix.unistd.pipe(forkPipe) == 0)
setCLOEXEC(forkPipe[1], true);
else
throw ProcessException.newFromErrno("Could not create pipe to check startup of child");
scope(exit) close(forkPipe[0]);
/*
To create detached process, we use double fork technique
but we don't have a direct access to the second fork pid from the caller side thus use a pipe.
We also can't reuse forkPipe for that purpose
because we can't predict the order in which pid and possible error will be written
since the first and the second forks will run in parallel.
*/
int[2] pidPipe;
if (config.flags & Config.Flags.detached)
{
if (core.sys.posix.unistd.pipe(pidPipe) != 0)
throw ProcessException.newFromErrno("Could not create pipe to get process pid");
setCLOEXEC(pidPipe[1], true);
}
scope(exit) if (config.flags & Config.Flags.detached) close(pidPipe[0]);
static void abortOnError(int forkPipeOut, InternalError errorType, int error) nothrow
{
core.sys.posix.unistd.write(forkPipeOut, &errorType, errorType.sizeof);
core.sys.posix.unistd.write(forkPipeOut, &error, error.sizeof);
close(forkPipeOut);
core.sys.posix.unistd._exit(1);
assert(0);
}
void closePipeWriteEnds()
{
close(forkPipe[1]);
if (config.flags & Config.Flags.detached)
close(pidPipe[1]);
}
auto id = core.sys.posix.unistd.fork();
if (id < 0)
{
closePipeWriteEnds();
throw ProcessException.newFromErrno("Failed to spawn new process");
}
void forkChild() nothrow @nogc
{
static import core.sys.posix.stdio;
// Child process
// no need for the read end of pipe on child side
if (config.flags & Config.Flags.detached)
close(pidPipe[0]);
close(forkPipe[0]);
immutable forkPipeOut = forkPipe[1];
immutable pidPipeOut = pidPipe[1];
// Set the working directory.
if (workDirFD >= 0)
{
if (fchdir(workDirFD) < 0)
{
// Fail. It is dangerous to run a program
// in an unexpected working directory.
abortOnError(forkPipeOut, InternalError.chdir, .errno);
}
close(workDirFD);
}
void execProcess()
{
// Redirect streams and close the old file descriptors.
// In the case that stderr is redirected to stdout, we need
// to backup the file descriptor since stdout may be redirected
// as well.
if (stderrFD == STDOUT_FILENO) stderrFD = dup(stderrFD);
dup2(stdinFD, STDIN_FILENO);
dup2(stdoutFD, STDOUT_FILENO);
dup2(stderrFD, STDERR_FILENO);
// Ensure that the standard streams aren't closed on execute, and
// optionally close all other file descriptors.
setCLOEXEC(STDIN_FILENO, false);
setCLOEXEC(STDOUT_FILENO, false);
setCLOEXEC(STDERR_FILENO, false);
if (!(config.flags & Config.Flags.inheritFDs))
{
// NOTE: malloc() and getrlimit() are not on the POSIX async
// signal safe functions list, but practically this should
// not be a problem. Java VM and CPython also use malloc()
// in its own implementation via opendir().
import core.stdc.stdlib : malloc;
import core.sys.posix.poll : pollfd, poll, POLLNVAL;
import core.sys.posix.sys.resource : rlimit, getrlimit, RLIMIT_NOFILE;
// Get the maximum number of file descriptors that could be open.
rlimit r;
if (getrlimit(RLIMIT_NOFILE, &r) != 0)
{
abortOnError(forkPipeOut, InternalError.getrlimit, .errno);
}
immutable maxDescriptors = cast(int) r.rlim_cur;
// The above, less stdin, stdout, and stderr
immutable maxToClose = maxDescriptors - 3;
// Call poll() to see which ones are actually open:
auto pfds = cast(pollfd*) malloc(pollfd.sizeof * maxToClose);
if (pfds is null)
{
abortOnError(forkPipeOut, InternalError.malloc, .errno);
}
foreach (i; 0 .. maxToClose)
{
pfds[i].fd = i + 3;
pfds[i].events = 0;
pfds[i].revents = 0;
}
if (poll(pfds, maxToClose, 0) >= 0)
{
foreach (i; 0 .. maxToClose)
{
// don't close pipe write end
if (pfds[i].fd == forkPipeOut) continue;
// POLLNVAL will be set if the file descriptor is invalid.
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
}
}
else
{
// Fall back to closing everything.
foreach (i; 3 .. maxDescriptors)
{
if (i == forkPipeOut) continue;
close(i);
}
}
}
else // This is already done if we don't inherit descriptors.
{
// Close the old file descriptors, unless they are
// either of the standard streams.
if (stdinFD > STDERR_FILENO) close(stdinFD);
if (stdoutFD > STDERR_FILENO) close(stdoutFD);
if (stderrFD > STDERR_FILENO) close(stderrFD);
}
if (config.preExecFunction !is null)
{
if (config.preExecFunction() != true)
{
abortOnError(forkPipeOut, InternalError.preExec, .errno);
}
}
// Execute program.
core.sys.posix.unistd.execve(argz[0], argz.ptr, envz);
// If execution fails, exit as quickly as possible.
abortOnError(forkPipeOut, InternalError.exec, .errno);
}
if (config.flags & Config.Flags.detached)
{
auto secondFork = core.sys.posix.unistd.fork();
if (secondFork == 0)
{
close(pidPipeOut);
execProcess();
}
else if (secondFork == -1)
{
auto secondForkErrno = .errno;
close(pidPipeOut);
abortOnError(forkPipeOut, InternalError.doubleFork, secondForkErrno);
}
else
{
core.sys.posix.unistd.write(pidPipeOut, &secondFork, pid_t.sizeof);
close(pidPipeOut);
close(forkPipeOut);
_exit(0);
}
}
else
{
execProcess();
}
}
if (id == 0)
{
forkChild();
assert(0);
}
else
{
closePipeWriteEnds();
auto status = InternalError.noerror;
auto readExecResult = core.sys.posix.unistd.read(forkPipe[0], &status, status.sizeof);
// Save error number just in case if subsequent "waitpid" fails and overrides errno
immutable lastError = .errno;
if (config.flags & Config.Flags.detached)
{
// Forked child exits right after creating second fork. So it should be safe to wait here.
import core.sys.posix.sys.wait : waitpid;
int waitResult;
waitpid(id, &waitResult, 0);
}
if (readExecResult == -1)
throw ProcessException.newFromErrno(lastError, "Could not read from pipe to get child status");
bool owned = true;
if (status != InternalError.noerror)
{
int error;
readExecResult = read(forkPipe[0], &error, error.sizeof);
string errorMsg;
final switch (status)
{
case InternalError.chdir:
errorMsg = "Failed to set working directory";
break;
case InternalError.getrlimit:
errorMsg = "getrlimit failed";
break;
case InternalError.exec:
errorMsg = "Failed to execute '" ~ cast(string) name ~ "'";
break;
case InternalError.doubleFork:
// Can happen only when starting detached process
assert(config.flags & Config.Flags.detached);
errorMsg = "Failed to fork twice";
break;
case InternalError.malloc:
errorMsg = "Failed to allocate memory";
break;
case InternalError.preExec:
errorMsg = "Failed to execute preExecFunction";
break;
case InternalError.noerror:
assert(false);
}
if (readExecResult == error.sizeof)
throw ProcessException.newFromErrno(error, errorMsg);
throw new ProcessException(errorMsg);
}
else if (config.flags & Config.Flags.detached)
{
owned = false;
if (read(pidPipe[0], &id, id.sizeof) != id.sizeof)
throw ProcessException.newFromErrno("Could not read from pipe to get detached process id");
}
// Parent process: Close streams and return.
if (!(config.flags & Config.Flags.retainStdin ) && stdinFD > STDERR_FILENO
&& stdinFD != getFD(std.stdio.stdin ))
stdin.close();
if (!(config.flags & Config.Flags.retainStdout) && stdoutFD > STDERR_FILENO
&& stdoutFD != getFD(std.stdio.stdout))
stdout.close();
if (!(config.flags & Config.Flags.retainStderr) && stderrFD > STDERR_FILENO
&& stderrFD != getFD(std.stdio.stderr))
stderr.close();
return new Pid(id, owned);
}
}
version (Posix)
@system unittest
{
import std.concurrency : ownerTid, receiveTimeout, send, spawn;
import std.datetime : seconds;
sigset_t ss;
sigemptyset(&ss);
sigaddset(&ss, SIGINT);
pthread_sigmask(SIG_BLOCK, &ss, null);
Config config = {
preExecFunction: () @trusted @nogc nothrow {
// Reset signal handlers
sigset_t ss;
if (sigfillset(&ss) != 0)
{
return false;
}
if (sigprocmask(SIG_UNBLOCK, &ss, null) != 0)
{
return false;
}
return true;
},
};
auto pid = spawnProcess(["sleep", "10000"],
std.stdio.stdin,
std.stdio.stdout,
std.stdio.stderr,
null,
config,
null);
scope(failure)
{
kill(pid, SIGKILL);
wait(pid);
}
// kill the spawned process with SIGINT
// and send its return code
spawn((shared Pid pid) {
auto p = cast() pid;
kill(p, SIGINT);
auto code = wait(p);
assert(code < 0);
send(ownerTid, code);
}, cast(shared) pid);
auto received = receiveTimeout(3.seconds, (int) {});
assert(received);
}
/*
Implementation of spawnProcess() for Windows.
commandLine must contain the entire command line, properly
quoted/escaped as required by CreateProcessW().
envz must be a pointer to a block of UTF-16 characters on the form
"var1=value1\0var2=value2\0...varN=valueN\0\0".
*/
version (Windows)
private Pid spawnProcessWin(scope const(char)[] commandLine,
scope const(char)[] program,
File stdin,
File stdout,
File stderr,
scope const string[string] env,
Config config,
scope const(char)[] workDir)
@trusted
{
import core.exception : RangeError;
import std.conv : text;
if (commandLine.empty) throw new RangeError("Command line is empty");
// Prepare environment.
auto envz = createEnv(env, !(config.flags & Config.Flags.newEnv));
// Startup info for CreateProcessW().
STARTUPINFO_W startinfo;
startinfo.cb = startinfo.sizeof;
static int getFD(ref File f) { return f.isOpen ? f.fileno : -1; }
// Extract file descriptors and HANDLEs from the streams and make the
// handles inheritable.
static void prepareStream(ref File file, DWORD stdHandle, string which,
out int fileDescriptor, out HANDLE handle)
{
enum _NO_CONSOLE_FILENO = cast(HANDLE)-2;
fileDescriptor = getFD(file);
handle = null;
if (fileDescriptor >= 0)
handle = file.windowsHandle;
// Windows GUI applications have a fd but not a valid Windows HANDLE.
if (handle is null || handle == INVALID_HANDLE_VALUE || handle == _NO_CONSOLE_FILENO)
handle = GetStdHandle(stdHandle);
DWORD dwFlags;
if (GetHandleInformation(handle, &dwFlags))
{
if (!(dwFlags & HANDLE_FLAG_INHERIT))
{
if (!SetHandleInformation(handle,
HANDLE_FLAG_INHERIT,
HANDLE_FLAG_INHERIT))
{
throw new StdioException(
"Failed to make "~which~" stream inheritable by child process ("
~generateSysErrorMsg() ~ ')',
0);
}
}
}
}
int stdinFD = -1, stdoutFD = -1, stderrFD = -1;
prepareStream(stdin, STD_INPUT_HANDLE, "stdin" , stdinFD, startinfo.hStdInput );
prepareStream(stdout, STD_OUTPUT_HANDLE, "stdout", stdoutFD, startinfo.hStdOutput);
prepareStream(stderr, STD_ERROR_HANDLE, "stderr", stderrFD, startinfo.hStdError );
if ((startinfo.hStdInput != null && startinfo.hStdInput != INVALID_HANDLE_VALUE)
|| (startinfo.hStdOutput != null && startinfo.hStdOutput != INVALID_HANDLE_VALUE)
|| (startinfo.hStdError != null && startinfo.hStdError != INVALID_HANDLE_VALUE))
startinfo.dwFlags = STARTF_USESTDHANDLES;
// Create process.
PROCESS_INFORMATION pi;
DWORD dwCreationFlags =
CREATE_UNICODE_ENVIRONMENT |
((config.flags & Config.Flags.suppressConsole) ? CREATE_NO_WINDOW : 0);
// workaround until https://issues.dlang.org/show_bug.cgi?id=14696 is fixed
auto pworkDir = workDir.tempCStringW();
if (!CreateProcessW(null, commandLine.tempCStringW().buffPtr,
null, null, true, dwCreationFlags,
envz, workDir.length ? pworkDir : null, &startinfo, &pi))
throw ProcessException.newFromLastError("Failed to spawn process \"" ~ cast(string) program ~ '"');
// figure out if we should close any of the streams
if (!(config.flags & Config.Flags.retainStdin ) && stdinFD > STDERR_FILENO
&& stdinFD != getFD(std.stdio.stdin ))
stdin.close();
if (!(config.flags & Config.Flags.retainStdout) && stdoutFD > STDERR_FILENO
&& stdoutFD != getFD(std.stdio.stdout))
stdout.close();
if (!(config.flags & Config.Flags.retainStderr) && stderrFD > STDERR_FILENO
&& stderrFD != getFD(std.stdio.stderr))
stderr.close();
// close the thread handle in the process info structure
CloseHandle(pi.hThread);
if (config.flags & Config.Flags.detached)
{
CloseHandle(pi.hProcess);
return new Pid(pi.dwProcessId);
}
return new Pid(pi.dwProcessId, pi.hProcess);
}
// Converts childEnv to a zero-terminated array of zero-terminated strings
// on the form "name=value", optionally adding those of the current process'
// environment strings that are not present in childEnv. If the parent's
// environment should be inherited without modification, this function
// returns environ directly.
version (Posix)
private const(char*)* createEnv(const string[string] childEnv,
bool mergeWithParentEnv)
{
// Determine the number of strings in the parent's environment.
int parentEnvLength = 0;
auto environ = getEnvironPtr;
if (mergeWithParentEnv)
{
if (childEnv.length == 0) return environ;
while (environ[parentEnvLength] != null) ++parentEnvLength;
}
// Convert the "new" variables to C-style strings.
auto envz = new const(char)*[parentEnvLength + childEnv.length + 1];
int pos = 0;
foreach (var, val; childEnv)
envz[pos++] = (var~'='~val~'\0').ptr;
// Add the parent's environment.
foreach (environStr; environ[0 .. parentEnvLength])
{
int eqPos = 0;
while (environStr[eqPos] != '=' && environStr[eqPos] != '\0') ++eqPos;
if (environStr[eqPos] != '=') continue;
auto var = environStr[0 .. eqPos];
if (var in childEnv) continue;
envz[pos++] = environStr;
}
envz[pos] = null;
return envz.ptr;
}
version (Posix) @system unittest
{
auto e1 = createEnv(null, false);
assert(e1 != null && *e1 == null);
auto e2 = createEnv(null, true);
assert(e2 != null);
int i = 0;
auto environ = getEnvironPtr;
for (; environ[i] != null; ++i)
{
assert(e2[i] != null);
import core.stdc.string;
assert(strcmp(e2[i], environ[i]) == 0);
}
assert(e2[i] == null);
auto e3 = createEnv(["foo" : "bar", "hello" : "world"], false);
assert(e3 != null && e3[0] != null && e3[1] != null && e3[2] == null);
assert((e3[0][0 .. 8] == "foo=bar\0" && e3[1][0 .. 12] == "hello=world\0")
|| (e3[0][0 .. 12] == "hello=world\0" && e3[1][0 .. 8] == "foo=bar\0"));
}
// Converts childEnv to a Windows environment block, which is on the form
// "name1=value1\0name2=value2\0...nameN=valueN\0\0", optionally adding
// those of the current process' environment strings that are not present
// in childEnv. Returns null if the parent's environment should be
// inherited without modification, as this is what is expected by
// CreateProcess().
version (Windows)
private LPVOID createEnv(const string[string] childEnv,
bool mergeWithParentEnv)
{
if (mergeWithParentEnv && childEnv.length == 0) return null;
import std.array : appender;
import std.uni : toUpper;
auto envz = appender!(wchar[])();
void put(string var, string val)
{
envz.put(var);
envz.put('=');
envz.put(val);
envz.put(cast(wchar) '\0');
}
// Add the variables in childEnv, removing them from parentEnv
// if they exist there too.
auto parentEnv = mergeWithParentEnv ? environment.toAA() : null;
foreach (k, v; childEnv)
{
auto uk = toUpper(k);
put(uk, v);
if (uk in parentEnv) parentEnv.remove(uk);
}
// Add remaining parent environment variables.
foreach (k, v; parentEnv) put(k, v);
// Two final zeros are needed in case there aren't any environment vars,
// and the last one does no harm when there are.
envz.put("\0\0"w);
return envz.data.ptr;
}
version (Windows) @system unittest
{
assert(createEnv(null, true) == null);
assert((cast(wchar*) createEnv(null, false))[0 .. 2] == "\0\0"w);
auto e1 = (cast(wchar*) createEnv(["foo":"bar", "ab":"c"], false))[0 .. 14];
assert(e1 == "FOO=bar\0AB=c\0\0"w || e1 == "AB=c\0FOO=bar\0\0"w);
}
// Searches the PATH variable for the given executable file,
// (checking that it is in fact executable).
version (Posix)
package(std) string searchPathFor(scope const(char)[] executable)
@safe
{
import std.algorithm.iteration : splitter;
import std.conv : to;
import std.path : chainPath;
typeof(return) result;
environment.getImpl("PATH",
(scope const(char)[] path)
{
if (!path)
return;
foreach (dir; splitter(path, ":"))
{
auto execPath = chainPath(dir, executable);
if (isExecutable(execPath))
{
result = execPath.to!(typeof(result));
return;
}
}
});
return result;
}
// Checks whether the file exists and can be executed by the
// current user.
version (Posix)
private bool isExecutable(R)(R path) @trusted nothrow @nogc
if (isSomeFiniteCharInputRange!R)
{
return (access(path.tempCString(), X_OK) == 0);
}
version (Posix) @safe unittest
{
import std.algorithm;
auto lsPath = searchPathFor("ls");
assert(!lsPath.empty);
assert(lsPath[0] == '/');
assert(lsPath.endsWith("ls"));
auto unlikely = searchPathFor("lkmqwpoialhggyaofijadsohufoiqezm");
assert(unlikely is null, "Are you kidding me?");
}
// Sets or unsets the FD_CLOEXEC flag on the given file descriptor.
version (Posix)
private void setCLOEXEC(int fd, bool on) nothrow @nogc
{
import core.sys.posix.fcntl : fcntl, F_GETFD, FD_CLOEXEC, F_SETFD;
auto flags = fcntl(fd, F_GETFD);
if (flags >= 0)
{
if (on) flags |= FD_CLOEXEC;
else flags &= ~(cast(typeof(flags)) FD_CLOEXEC);
flags = fcntl(fd, F_SETFD, flags);
}
assert(flags != -1 || .errno == EBADF);
}
@system unittest // Command line arguments in spawnProcess().
{
version (Windows) TestScript prog =
"if not [%~1]==[foo] ( exit 1 )
if not [%~2]==[bar] ( exit 2 )
exit 0";
else version (Posix) TestScript prog =
`if test "$1" != "foo"; then exit 1; fi
if test "$2" != "bar"; then exit 2; fi
exit 0`;
assert(wait(spawnProcess(prog.path)) == 1);
assert(wait(spawnProcess([prog.path])) == 1);
assert(wait(spawnProcess([prog.path, "foo"])) == 2);
assert(wait(spawnProcess([prog.path, "foo", "baz"])) == 2);
assert(wait(spawnProcess([prog.path, "foo", "bar"])) == 0);
}
// test that file descriptors are correctly closed / left open.
// ideally this would be done by the child process making libc
// calls, but we make do...
version (Posix) @system unittest
{
import core.stdc.errno : errno;
import core.sys.posix.fcntl : open, O_RDONLY;
import core.sys.posix.unistd : close;
import std.algorithm.searching : canFind, findSplitBefore;
import std.array : split;
import std.conv : to;
static import std.file;
import std.functional : reverseArgs;
import std.path : buildPath;
auto directory = uniqueTempPath();
std.file.mkdir(directory);
scope(exit) std.file.rmdirRecurse(directory);
auto path = buildPath(directory, "tmp");
std.file.write(path, null);
errno = 0;
auto fd = open(path.tempCString, O_RDONLY);
if (fd == -1)
{
import core.stdc.string : strerror;
import std.stdio : stderr;
import std.string : fromStringz;
// For the CI logs
stderr.writefln("%s: could not open '%s': %s",
__FUNCTION__, path, strerror(errno).fromStringz);
// TODO: should we retry here instead?
return;
}
scope(exit) close(fd);
// command >&2 (or any other number) checks whethether that number
// file descriptor is open.
// Can't use this for arbitrary descriptors as many shells only support
// single digit fds.
TestScript testDefaults = `command >&0 && command >&1 && command >&2`;
assert(execute(testDefaults.path).status == 0);
assert(execute(testDefaults.path, null, Config.inheritFDs).status == 0);
// Try a few different methods to check whether there are any
// incorrectly-open files.
void testFDs()
{
// try /proc/<pid>/fd/ on linux
version (linux)
{
TestScript proc = "ls /proc/$$/fd";
auto procRes = execute(proc.path, null);
if (procRes.status == 0)
{
auto fdStr = fd.to!string;
assert(!procRes.output.split.canFind(fdStr));
assert(execute(proc.path, null, Config.inheritFDs)
.output.split.canFind(fdStr));
return;
}
}
// try fuser (might sometimes need permissions)
TestScript fuser = "echo $$ && fuser -f " ~ path;
auto fuserRes = execute(fuser.path, null);
if (fuserRes.status == 0)
{
assert(!reverseArgs!canFind(fuserRes
.output.findSplitBefore("\n").expand));
assert(reverseArgs!canFind(execute(fuser.path, null, Config.inheritFDs)
.output.findSplitBefore("\n").expand));
return;
}
// last resort, try lsof (not available on all Posix)
TestScript lsof = "lsof -p$$";
auto lsofRes = execute(lsof.path, null);
if (lsofRes.status == 0)
{
assert(!lsofRes.output.canFind(path));
auto lsofOut = execute(lsof.path, null, Config.inheritFDs).output;
if (!lsofOut.canFind(path))
{
std.stdio.stderr.writeln(__FILE__, ':', __LINE__,
": Warning: unexpected lsof output:", lsofOut);
}
return;
}
std.stdio.stderr.writeln(__FILE__, ':', __LINE__,
": Warning: Couldn't find any way to check open files");
}
testFDs();
}
@system unittest // Environment variables in spawnProcess().
{
// We really should use set /a on Windows, but Wine doesn't support it.
version (Windows) TestScript envProg =
`if [%STD_PROCESS_UNITTEST1%] == [1] (
if [%STD_PROCESS_UNITTEST2%] == [2] (exit 3)
exit 1
)
if [%STD_PROCESS_UNITTEST1%] == [4] (
if [%STD_PROCESS_UNITTEST2%] == [2] (exit 6)
exit 4
)
if [%STD_PROCESS_UNITTEST2%] == [2] (exit 2)
exit 0`;
version (Posix) TestScript envProg =
`if test "$std_process_unittest1" = ""; then
std_process_unittest1=0
fi
if test "$std_process_unittest2" = ""; then
std_process_unittest2=0
fi
exit $(($std_process_unittest1+$std_process_unittest2))`;
environment.remove("std_process_unittest1"); // Just in case.
environment.remove("std_process_unittest2");
assert(wait(spawnProcess(envProg.path)) == 0);
assert(wait(spawnProcess(envProg.path, null, Config.newEnv)) == 0);
environment["std_process_unittest1"] = "1";
assert(wait(spawnProcess(envProg.path)) == 1);
assert(wait(spawnProcess(envProg.path, null, Config.newEnv)) == 0);
auto env = ["std_process_unittest2" : "2"];
assert(wait(spawnProcess(envProg.path, env)) == 3);
assert(wait(spawnProcess(envProg.path, env, Config.newEnv)) == 2);
env["std_process_unittest1"] = "4";
assert(wait(spawnProcess(envProg.path, env)) == 6);
assert(wait(spawnProcess(envProg.path, env, Config.newEnv)) == 6);
environment.remove("std_process_unittest1");
assert(wait(spawnProcess(envProg.path, env)) == 6);
assert(wait(spawnProcess(envProg.path, env, Config.newEnv)) == 6);
}
@system unittest // Stream redirection in spawnProcess().
{
import std.path : buildPath;
import std.string;
version (Windows) TestScript prog =
"set /p INPUT=
echo %INPUT% output %~1
echo %INPUT% error %~2 1>&2
echo done > %3";
else version (Posix) TestScript prog =
"read INPUT
echo $INPUT output $1
echo $INPUT error $2 >&2
echo done > \"$3\"";
// Pipes
void testPipes(Config config)
{
import std.file, std.uuid, core.thread, std.exception;
auto pipei = pipe();
auto pipeo = pipe();
auto pipee = pipe();
auto done = buildPath(tempDir(), randomUUID().toString());
auto pid = spawnProcess([prog.path, "foo", "bar", done],
pipei.readEnd, pipeo.writeEnd, pipee.writeEnd, null, config);
pipei.writeEnd.writeln("input");
pipei.writeEnd.flush();
assert(pipeo.readEnd.readln().chomp() == "input output foo");
assert(pipee.readEnd.readln().chomp().stripRight() == "input error bar");
if (config.flags & Config.Flags.detached)
while (!done.exists) Thread.sleep(10.msecs);
else
wait(pid);
while (remove(done).collectException) Thread.sleep(10.msecs);
}
// Files
void testFiles(Config config)
{
import std.ascii, std.file, std.uuid, core.thread, std.exception;
auto pathi = buildPath(tempDir(), randomUUID().toString());
auto patho = buildPath(tempDir(), randomUUID().toString());
auto pathe = buildPath(tempDir(), randomUUID().toString());
std.file.write(pathi, "INPUT"~std.ascii.newline);
auto filei = File(pathi, "r");
auto fileo = File(patho, "w");
auto filee = File(pathe, "w");
auto done = buildPath(tempDir(), randomUUID().toString());
auto pid = spawnProcess([prog.path, "bar", "baz", done], filei, fileo, filee, null, config);
if (config.flags & Config.Flags.detached)
while (!done.exists) Thread.sleep(10.msecs);
else
wait(pid);
assert(readText(patho).chomp() == "INPUT output bar");
assert(readText(pathe).chomp().stripRight() == "INPUT error baz");
while (remove(pathi).collectException) Thread.sleep(10.msecs);
while (remove(patho).collectException) Thread.sleep(10.msecs);
while (remove(pathe).collectException) Thread.sleep(10.msecs);
while (remove(done).collectException) Thread.sleep(10.msecs);
}
testPipes(Config.none);
testFiles(Config.none);
testPipes(Config.detached);
testFiles(Config.detached);
}
@system unittest // Error handling in spawnProcess()
{
import std.algorithm.searching : canFind;
import std.exception : assertThrown, collectExceptionMsg;
static void testNotFoundException(string program)
{
assert(collectExceptionMsg!ProcessException(spawnProcess(program)).canFind(program));
assert(collectExceptionMsg!ProcessException(spawnProcess(program, null, Config.detached)).canFind(program));
}
testNotFoundException("ewrgiuhrifuheiohnmnvqweoijwf");
testNotFoundException("./rgiuhrifuheiohnmnvqweoijwf");
// can't execute malformed file with executable permissions
version (Posix)
{
import std.path : buildPath;
import std.file : remove, write, setAttributes, tempDir;
import core.sys.posix.sys.stat : S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH;
import std.conv : to;
string deleteme = buildPath(tempDir(), "deleteme.std.process.unittest.pid") ~ to!string(thisProcessID);
write(deleteme, "");
scope(exit) remove(deleteme);
setAttributes(deleteme, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH);
assertThrown!ProcessException(spawnProcess(deleteme));
assertThrown!ProcessException(spawnProcess(deleteme, null, Config.detached));
}
}
@system unittest // Specifying a working directory.
{
import std.path;
import std.file;
TestScript prog = "echo foo>bar";
auto directory = uniqueTempPath();
mkdir(directory);
scope(exit) rmdirRecurse(directory);
auto pid = spawnProcess([prog.path], null, Config.none, directory);
wait(pid);
assert(exists(buildPath(directory, "bar")));
}
@system unittest // Specifying a bad working directory.
{
import std.exception : assertThrown;
import std.file;
TestScript prog = "echo";
auto directory = uniqueTempPath();
assertThrown!ProcessException(spawnProcess([prog.path], null, Config.none, directory));
assertThrown!ProcessException(spawnProcess([prog.path], null, Config.detached, directory));
std.file.write(directory, "foo");
scope(exit) remove(directory);
assertThrown!ProcessException(spawnProcess([prog.path], null, Config.none, directory));
assertThrown!ProcessException(spawnProcess([prog.path], null, Config.detached, directory));
// can't run in directory if user does not have search permission on this directory
version (Posix)
{
if (core.sys.posix.unistd.getuid() != 0)
{
import core.sys.posix.sys.stat : S_IRUSR;
auto directoryNoSearch = uniqueTempPath();
mkdir(directoryNoSearch);
scope(exit) rmdirRecurse(directoryNoSearch);
setAttributes(directoryNoSearch, S_IRUSR);
assertThrown!ProcessException(spawnProcess(prog.path, null, Config.none, directoryNoSearch));
assertThrown!ProcessException(spawnProcess(prog.path, null, Config.detached, directoryNoSearch));
}
}
}
@system unittest // Specifying empty working directory.
{
TestScript prog = "";
string directory = "";
assert(directory.ptr && !directory.length);
spawnProcess([prog.path], null, Config.none, directory).wait();
}
// Reopening the standard streams (https://issues.dlang.org/show_bug.cgi?id=13258)
@system unittest
{
import std.string;
import std.file;
void fun()
{
spawnShell("echo foo").wait();
spawnShell("echo bar").wait();
}
auto tmpFile = uniqueTempPath();
scope(exit) if (exists(tmpFile)) remove(tmpFile);
{
auto oldOut = std.stdio.stdout;
scope(exit) std.stdio.stdout = oldOut;
std.stdio.stdout = File(tmpFile, "w");
fun();
std.stdio.stdout.close();
}
auto lines = readText(tmpFile).splitLines();
assert(lines == ["foo", "bar"]);
}
// MSVCRT workaround (https://issues.dlang.org/show_bug.cgi?id=14422)
version (Windows)
@system unittest
{
auto fn = uniqueTempPath();
scope(exit) if (exists(fn)) remove(fn);
std.file.write(fn, "AAAAAAAAAA");
auto f = File(fn, "a");
spawnProcess(["cmd", "/c", "echo BBBBB"], std.stdio.stdin, f).wait();
auto data = readText(fn);
assert(data == "AAAAAAAAAABBBBB\r\n", data);
}
// https://issues.dlang.org/show_bug.cgi?id=20765
// Test that running processes with relative path works in conjunction
// with indicating a workDir.
version (Posix) @system unittest
{
import std.file : mkdir, write, setAttributes, rmdirRecurse;
import std.conv : octal;
auto dir = uniqueTempPath();
mkdir(dir);
scope(exit) rmdirRecurse(dir);
write(dir ~ "/program", "#!/bin/sh\necho Hello");
setAttributes(dir ~ "/program", octal!700);
assert(execute(["./program"], null, Config.none, size_t.max, dir).output == "Hello\n");
}
/**
A variation on $(LREF spawnProcess) that runs the given _command through
the current user's preferred _command interpreter (aka. shell).
The string `command` is passed verbatim to the shell, and is therefore
subject to its rules about _command structure, argument/filename quoting
and escaping of special characters.
The path to the shell executable defaults to $(LREF nativeShell).
In all other respects this function works just like `spawnProcess`.
Please refer to the $(LREF spawnProcess) documentation for descriptions
of the other function parameters, the return value and any exceptions
that may be thrown.
---
// Run the command/program "foo" on the file named "my file.txt", and
// redirect its output into foo.log.
auto pid = spawnShell(`foo "my file.txt" > foo.log`);
wait(pid);
---
See_also:
$(LREF escapeShellCommand), which may be helpful in constructing a
properly quoted and escaped shell _command line for the current platform.
*/
Pid spawnShell(scope const(char)[] command,
File stdin = std.stdio.stdin,
File stdout = std.stdio.stdout,
File stderr = std.stdio.stderr,
scope const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null,
scope string shellPath = nativeShell)
@trusted // See reason below
{
version (Windows)
{
// CMD does not parse its arguments like other programs.
// It does not use CommandLineToArgvW.
// Instead, it treats the first and last quote specially.
// See CMD.EXE /? for details.
const commandLine = escapeShellFileName(shellPath)
~ ` ` ~ shellSwitch ~ ` "` ~ command ~ `"`;
return spawnProcessWin(commandLine, shellPath, stdin, stdout, stderr, env, config, workDir);
}
else version (Posix)
{
const(char)[][3] args;
args[0] = shellPath;
args[1] = shellSwitch;
args[2] = command;
/* The passing of args converts the static array, which is initialized with `scope` pointers,
* to a dynamic array, which is also a scope parameter. So, it is a scope pointer to a
* scope pointer, which although is safely used here, D doesn't allow transitive scope.
* See https://github.com/dlang/dmd/pull/10951
*/
return spawnProcessPosix(args, stdin, stdout, stderr, env, config, workDir);
}
else
static assert(0);
}
/// ditto
Pid spawnShell(scope const(char)[] command,
scope const string[string] env,
Config config = Config.none,
scope const(char)[] workDir = null,
scope string shellPath = nativeShell)
@trusted // TODO: Should be @safe
{
return spawnShell(command,
std.stdio.stdin,
std.stdio.stdout,
std.stdio.stderr,
env,
config,
workDir,
shellPath);
}
@system unittest
{
version (Windows)
auto cmd = "echo %FOO%";
else version (Posix)
auto cmd = "echo $foo";
import std.file;
auto tmpFile = uniqueTempPath();
scope(exit) if (exists(tmpFile)) remove(tmpFile);
auto redir = "> \""~tmpFile~'"';
auto env = ["foo" : "bar"];
assert(wait(spawnShell(cmd~redir, env)) == 0);
auto f = File(tmpFile, "a");
version (CRuntime_Microsoft) f.seek(0, SEEK_END); // MSVCRT probably seeks to the end when writing, not before
assert(wait(spawnShell(cmd, std.stdio.stdin, f, std.stdio.stderr, env)) == 0);
f.close();
auto output = std.file.readText(tmpFile);
assert(output == "bar\nbar\n" || output == "bar\r\nbar\r\n");
}
version (Windows)
@system unittest
{
import std.string;
import std.conv : text;
TestScript prog = "echo %0 %*";
auto outputFn = uniqueTempPath();
scope(exit) if (exists(outputFn)) remove(outputFn);
auto args = [`a b c`, `a\b\c\`, `a"b"c"`];
auto result = executeShell(
escapeShellCommand([prog.path] ~ args)
~ " > " ~
escapeShellFileName(outputFn));
assert(result.status == 0);
auto args2 = outputFn.readText().strip().parseCommandLine()[1..$];
assert(args == args2, text(args2));
}
/**
Options that control the behaviour of process creation functions in this
module. Most options only apply to $(LREF spawnProcess) and
$(LREF spawnShell).
Example:
---
auto logFile = File("myapp_error.log", "w");
// Start program, suppressing the console window (Windows only),
// redirect its error stream to logFile, and leave logFile open
// in the parent process as well.
auto pid = spawnProcess("myapp", stdin, stdout, logFile,
Config.retainStderr | Config.suppressConsole);
scope(exit)
{
auto exitCode = wait(pid);
logFile.writeln("myapp exited with code ", exitCode);
logFile.close();
}
---
*/
struct Config
{
/**
Flag options.
Use bitwise OR to combine flags.
**/
enum Flags
{
none = 0,
/**
By default, the child process inherits the parent's environment,
and any environment variables passed to $(LREF spawnProcess) will
be added to it. If this flag is set, the only variables in the
child process' environment will be those given to spawnProcess.
*/
newEnv = 1,
/**
Unless the child process inherits the standard input/output/error
streams of its parent, one almost always wants the streams closed
in the parent when $(LREF spawnProcess) returns. Therefore, by
default, this is done. If this is not desirable, pass any of these
options to spawnProcess.
*/
retainStdin = 2,
retainStdout = 4, /// ditto
retainStderr = 8, /// ditto
/**
On Windows, if the child process is a console application, this
flag will prevent the creation of a console window. Otherwise,
it will be ignored. On POSIX, `suppressConsole` has no effect.
*/
suppressConsole = 16,
/**
On POSIX, open $(LINK2 http://en.wikipedia.org/wiki/File_descriptor,file descriptors)
are by default inherited by the child process. As this may lead
to subtle bugs when pipes or multiple threads are involved,
$(LREF spawnProcess) ensures that all file descriptors except the
ones that correspond to standard input/output/error are closed
in the child process when it starts. Use `inheritFDs` to prevent
this.
On Windows, this option has no effect, and any handles which have been
explicitly marked as inheritable will always be inherited by the child
process.
*/
inheritFDs = 32,
/**
Spawn process in detached state. This removes the need in calling
$(LREF wait) to clean up the process resources.
Note:
Calling $(LREF wait) or $(LREF kill) with the resulting `Pid` is invalid.
*/
detached = 64,
/**
By default, the $(LREF execute) and $(LREF executeShell) functions
will capture child processes' both stdout and stderr. This can be
undesirable if the standard output is to be processed or otherwise
used by the invoking program, as `execute`'s result would then
contain a mix of output and warning/error messages.
Specify this flag when calling `execute` or `executeShell` to
cause invoked processes' stderr stream to be sent to $(REF stderr,
std,stdio), and only capture and return standard output.
This flag has no effect on $(LREF spawnProcess) or $(LREF spawnShell).
*/
stderrPassThrough = 128,
}
Flags flags; /// ditto
/**
For backwards compatibility, and cases when only flags need to
be specified in the `Config`, these allow building `Config`
instances using flag names only.
*/
enum Config none = Config.init;
enum Config newEnv = Config(Flags.newEnv); /// ditto
enum Config retainStdin = Config(Flags.retainStdin); /// ditto
enum Config retainStdout = Config(Flags.retainStdout); /// ditto
enum Config retainStderr = Config(Flags.retainStderr); /// ditto
enum Config suppressConsole = Config(Flags.suppressConsole); /// ditto
enum Config inheritFDs = Config(Flags.inheritFDs); /// ditto
enum Config detached = Config(Flags.detached); /// ditto
enum Config stderrPassThrough = Config(Flags.stderrPassThrough); /// ditto
Config opUnary(string op)()
if (is(typeof(mixin(op ~ q{flags}))))
{
return Config(mixin(op ~ q{flags}));
} /// ditto
Config opBinary(string op)(Config other)
if (is(typeof(mixin(q{flags} ~ op ~ q{other.flags}))))
{
return Config(mixin(q{flags} ~ op ~ q{other.flags}));
} /// ditto
Config opOpAssign(string op)(Config other)
if (is(typeof(mixin(q{flags} ~ op ~ q{=other.flags}))))
{
return Config(mixin(q{flags} ~ op ~ q{=other.flags}));
} /// ditto
version (StdDdoc)
{
/**
A function that is called before `exec` in $(LREF spawnProcess).
It returns `true` if succeeded and otherwise returns `false`.
$(RED Warning:
Please note that the code in this function must only use
async-signal-safe functions.)
On Windows, this member is not available.
*/
bool function() nothrow @nogc @safe preExecFunction;
}
else version (Posix)
{
bool function() nothrow @nogc @safe preExecFunction;
}
}
// https://issues.dlang.org/show_bug.cgi?id=22125
@safe unittest
{
Config c = Config.retainStdin;
c |= Config.retainStdout;
c |= Config.retainStderr;
c &= ~Config.retainStderr;
assert(c == (Config.retainStdin | Config.retainStdout));
}
/// A handle that corresponds to a spawned process.
final class Pid
{
/**
The process ID number.
This is a number that uniquely identifies the process on the operating
system, for at least as long as the process is running. Once $(LREF wait)
has been called on the $(LREF Pid), this method will return an
invalid (negative) process ID.
*/
@property int processID() const @safe pure nothrow
{
return _processID;
}
/**
An operating system handle to the process.
This handle is used to specify the process in OS-specific APIs.
On POSIX, this function returns a `core.sys.posix.sys.types.pid_t`
with the same value as $(LREF Pid.processID), while on Windows it returns
a `core.sys.windows.windows.HANDLE`.
Once $(LREF wait) has been called on the $(LREF Pid), this method
will return an invalid handle.
*/
// Note: Since HANDLE is a reference, this function cannot be const.
version (Windows)
@property HANDLE osHandle() @nogc @safe pure nothrow
{
return _handle;
}
else version (Posix)
@property pid_t osHandle() @nogc @safe pure nothrow
{
return _processID;
}
private:
/*
Pid.performWait() does the dirty work for wait() and nonBlockingWait().
If block == true, this function blocks until the process terminates,
sets _processID to terminated, and returns the exit code or terminating
signal as described in the wait() documentation.
If block == false, this function returns immediately, regardless
of the status of the process. If the process has terminated, the
function has the exact same effect as the blocking version. If not,
it returns 0 and does not modify _processID.
*/
version (Posix)
int performWait(bool block) @trusted
{
import std.exception : enforce;
enforce!ProcessException(owned, "Can't wait on a detached process");
if (_processID == terminated) return _exitCode;
int exitCode;
while (true)
{
int status;
auto check = waitpid(_processID, &status, block ? 0 : WNOHANG);
if (check == -1)
{
if (errno == ECHILD)
{
throw new ProcessException(
"Process does not exist or is not a child process.");
}
else
{
// waitpid() was interrupted by a signal. We simply
// restart it.
assert(errno == EINTR);
continue;
}
}
if (!block && check == 0) return 0;
if (WIFEXITED(status))
{
exitCode = WEXITSTATUS(status);
break;
}
else if (WIFSIGNALED(status))
{
exitCode = -WTERMSIG(status);
break;
}
// We check again whether the call should be blocking,
// since we don't care about other status changes besides
// "exited" and "terminated by signal".
if (!block) return 0;
// Process has stopped, but not terminated, so we continue waiting.
}
// Mark Pid as terminated, and cache and return exit code.
_processID = terminated;
_exitCode = exitCode;
return exitCode;
}
else version (Windows)
{
int performWait(const bool block, const DWORD timeout = INFINITE) @trusted
{
import std.exception : enforce;
enforce!ProcessException(owned, "Can't wait on a detached process");
if (_processID == terminated) return _exitCode;
assert(_handle != INVALID_HANDLE_VALUE);
if (block)
{
auto result = WaitForSingleObject(_handle, timeout);
if (result != WAIT_OBJECT_0)
{
// Wait time exceeded `timeout` milliseconds?
if (result == WAIT_TIMEOUT && timeout != INFINITE)
return 0;
throw ProcessException.newFromLastError("Wait failed.");
}
}
if (!GetExitCodeProcess(_handle, cast(LPDWORD)&_exitCode))
throw ProcessException.newFromLastError();
if (!block && _exitCode == STILL_ACTIVE) return 0;
CloseHandle(_handle);
_handle = INVALID_HANDLE_VALUE;
_processID = terminated;
return _exitCode;
}
int performWait(Duration timeout) @safe
{
import std.exception : enforce;
const msecs = timeout.total!"msecs";
// Limit this implementation the maximum wait time offered by
// WaitForSingleObject. One could theoretically break up larger
// durations into multiple waits but (DWORD.max - 1).msecs
// (> 7 weeks, 17 hours) should be enough for the usual case.
// DWORD.max is reserved for INFINITE
enforce!ProcessException(msecs < DWORD.max, "Timeout exceeds maximum wait time!");
return performWait(true, cast(DWORD) msecs);
}
~this()
{
if (_handle != INVALID_HANDLE_VALUE)
{
CloseHandle(_handle);
_handle = INVALID_HANDLE_VALUE;
}
}
}
// Special values for _processID.
enum invalid = -1, terminated = -2;
// OS process ID number. Only nonnegative IDs correspond to
// running processes.
int _processID = invalid;
// Exit code cached by wait(). This is only expected to hold a
// sensible value if _processID == terminated.
int _exitCode;
// Whether the process can be waited for by wait() for or killed by kill().
// False if process was started as detached. True otherwise.
bool owned;
// Pids are only meant to be constructed inside this module, so
// we make the constructor private.
version (Windows)
{
HANDLE _handle = INVALID_HANDLE_VALUE;
this(int pid, HANDLE handle) @safe pure nothrow
{
_processID = pid;
_handle = handle;
this.owned = true;
}
this(int pid) @safe pure nothrow
{
_processID = pid;
this.owned = false;
}
}
else
{
this(int id, bool owned) @safe pure nothrow
{
_processID = id;
this.owned = owned;
}
}
}
/**
Waits for the process associated with `pid` to terminate, and returns
its exit status.
In general one should always _wait for child processes to terminate
before exiting the parent process unless the process was spawned as detached
(that was spawned with `Config.detached` flag).
Otherwise, they may become "$(HTTP en.wikipedia.org/wiki/Zombie_process,zombies)"
– processes that are defunct, yet still occupy a slot in the OS process table.
You should not and must not wait for detached processes, since you don't own them.
If the process has already terminated, this function returns directly.
The exit code is cached, so that if wait() is called multiple times on
the same $(LREF Pid) it will always return the same value.
POSIX_specific:
If the process is terminated by a signal, this function returns a
negative number whose absolute value is the signal number.
Since POSIX restricts normal exit codes to the range 0-255, a
negative return value will always indicate termination by signal.
Signal codes are defined in the `core.sys.posix.signal` module
(which corresponds to the `signal.h` POSIX header).
Throws:
$(LREF ProcessException) on failure or on attempt to wait for detached process.
Example:
See the $(LREF spawnProcess) documentation.
See_also:
$(LREF tryWait), for a non-blocking function.
*/
int wait(Pid pid) @safe
{
assert(pid !is null, "Called wait on a null Pid.");
return pid.performWait(true);
}
@system unittest // Pid and wait()
{
version (Windows) TestScript prog = "exit %~1";
else version (Posix) TestScript prog = "exit $1";
assert(wait(spawnProcess([prog.path, "0"])) == 0);
assert(wait(spawnProcess([prog.path, "123"])) == 123);
auto pid = spawnProcess([prog.path, "10"]);
assert(pid.processID > 0);
version (Windows) assert(pid.osHandle != INVALID_HANDLE_VALUE);
else version (Posix) assert(pid.osHandle == pid.processID);
assert(wait(pid) == 10);
assert(wait(pid) == 10); // cached exit code
assert(pid.processID < 0);
version (Windows) assert(pid.osHandle == INVALID_HANDLE_VALUE);
else version (Posix) assert(pid.osHandle < 0);
}
private import std.typecons : Tuple;
/**
Waits until either the process associated with `pid` terminates or the
elapsed time exceeds the given timeout.
If the process terminates within the given duration it behaves exactly like
`wait`, except that it returns a tuple `(true, exit code)`.
If the process does not terminate within the given duration it will stop
waiting and return `(false, 0).`
The timeout may not exceed `(uint.max - 1).msecs` (~ 7 weeks, 17 hours).
$(BLUE This function is Windows-Only.)
Returns:
An $(D std.typecons.Tuple!(bool, "terminated", int, "status")).
Throws:
$(LREF ProcessException) on failure or on attempt to wait for detached process.
Example:
See the $(LREF spawnProcess) documentation.
See_also:
$(LREF wait), for a blocking function without timeout.
$(LREF tryWait), for a non-blocking function without timeout.
*/
version (StdDdoc)
Tuple!(bool, "terminated", int, "status") waitTimeout(Pid pid, Duration timeout) @safe;
else version (Windows)
Tuple!(bool, "terminated", int, "status") waitTimeout(Pid pid, Duration timeout) @safe
{
assert(pid !is null, "Called wait on a null Pid.");
auto code = pid.performWait(timeout);
return typeof(return)(pid._processID == Pid.terminated, code);
}
version (Windows)
@system unittest // Pid and waitTimeout()
{
import std.exception : collectException;
import std.typecons : tuple;
TestScript prog = ":Loop\ngoto Loop;";
auto pid = spawnProcess(prog.path);
// Doesn't block longer than one second
assert(waitTimeout(pid, 1.seconds) == tuple(false, 0));
kill(pid);
assert(waitTimeout(pid, 1.seconds) == tuple(true, 1)); // exit 1 because the process is killed
// Rejects timeouts exceeding the Windows API capabilities
const dur = DWORD.max.msecs;
const ex = collectException!ProcessException(waitTimeout(pid, dur));
assert(ex);
assert(ex.msg == "Timeout exceeds maximum wait time!");
}
/**
A non-blocking version of $(LREF wait).
If the process associated with `pid` has already terminated,
`tryWait` has the exact same effect as `wait`.
In this case, it returns a tuple where the `terminated` field
is set to `true` and the `status` field has the same
interpretation as the return value of `wait`.
If the process has $(I not) yet terminated, this function differs
from `wait` in that does not wait for this to happen, but instead
returns immediately. The `terminated` field of the returned
tuple will then be set to `false`, while the `status` field
will always be 0 (zero). `wait` or `tryWait` should then be
called again on the same `Pid` at some later time; not only to
get the exit code, but also to avoid the process becoming a "zombie"
when it finally terminates. (See $(LREF wait) for details).
Returns:
An $(D std.typecons.Tuple!(bool, "terminated", int, "status")).
Throws:
$(LREF ProcessException) on failure or on attempt to wait for detached process.
Example:
---
auto pid = spawnProcess("dmd myapp.d");
scope(exit) wait(pid);
...
auto dmd = tryWait(pid);
if (dmd.terminated)
{
if (dmd.status == 0) writeln("Compilation succeeded!");
else writeln("Compilation failed");
}
else writeln("Still compiling...");
...
---
Note that in this example, the first `wait` call will have no
effect if the process has already terminated by the time `tryWait`
is called. In the opposite case, however, the `scope` statement
ensures that we always wait for the process if it hasn't terminated
by the time we reach the end of the scope.
*/
auto tryWait(Pid pid) @safe
{
import std.typecons : Tuple;
assert(pid !is null, "Called tryWait on a null Pid.");
auto code = pid.performWait(false);
return Tuple!(bool, "terminated", int, "status")(pid._processID == Pid.terminated, code);
}
// unittest: This function is tested together with kill() below.
/**
Attempts to terminate the process associated with `pid`.
The effect of this function, as well as the meaning of `codeOrSignal`,
is highly platform dependent. Details are given below. Common to all
platforms is that this function only $(I initiates) termination of the process,
and returns immediately. It does not wait for the process to end,
nor does it guarantee that the process does in fact get terminated.
Always call $(LREF wait) to wait for a process to complete, even if `kill`
has been called on it.
Windows_specific:
The process will be
$(LINK2 http://msdn.microsoft.com/en-us/library/windows/desktop/ms686714%28v=vs.100%29.aspx,
forcefully and abruptly terminated). If `codeOrSignal` is specified, it
must be a nonnegative number which will be used as the exit code of the process.
If not, the process wil exit with code 1. Do not use $(D codeOrSignal = 259),
as this is a special value (aka. $(LINK2 http://msdn.microsoft.com/en-us/library/windows/desktop/ms683189.aspx,STILL_ACTIVE))
used by Windows to signal that a process has in fact $(I not) terminated yet.
---
auto pid = spawnProcess("some_app");
kill(pid, 10);
assert(wait(pid) == 10);
---
POSIX_specific:
A $(LINK2 http://en.wikipedia.org/wiki/Unix_signal,signal) will be sent to
the process, whose value is given by `codeOrSignal`. Depending on the
signal sent, this may or may not terminate the process. Symbolic constants
for various $(LINK2 http://en.wikipedia.org/wiki/Unix_signal#POSIX_signals,
POSIX signals) are defined in `core.sys.posix.signal`, which corresponds to the
$(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html,
`signal.h` POSIX header). If `codeOrSignal` is omitted, the
`SIGTERM` signal will be sent. (This matches the behaviour of the
$(LINK2 http://pubs.opengroup.org/onlinepubs/9699919799/utilities/kill.html,
`_kill`) shell command.)
---
import core.sys.posix.signal : SIGKILL;
auto pid = spawnProcess("some_app");
kill(pid, SIGKILL);
assert(wait(pid) == -SIGKILL); // Negative return value on POSIX!
---
Throws:
$(LREF ProcessException) on error (e.g. if codeOrSignal is invalid).
or on attempt to kill detached process.
Note that failure to terminate the process is considered a "normal"
outcome, not an error.$(BR)
*/
void kill(Pid pid)
{
version (Windows) kill(pid, 1);
else version (Posix)
{
import core.sys.posix.signal : SIGTERM;
kill(pid, SIGTERM);
}
}
/// ditto
void kill(Pid pid, int codeOrSignal)
{
import std.exception : enforce;
enforce!ProcessException(pid.owned, "Can't kill detached process");
version (Windows)
{
if (codeOrSignal < 0) throw new ProcessException("Invalid exit code");
// On Windows, TerminateProcess() appears to terminate the
// *current* process if it is passed an invalid handle...
if (pid.osHandle == INVALID_HANDLE_VALUE)
throw new ProcessException("Invalid process handle");
if (!TerminateProcess(pid.osHandle, codeOrSignal))
throw ProcessException.newFromLastError();
}
else version (Posix)
{
import core.sys.posix.signal : kill;
if (kill(pid.osHandle, codeOrSignal) == -1)
throw ProcessException.newFromErrno();
}
}
@system unittest // tryWait() and kill()
{
import core.thread;
import std.exception : assertThrown;
// The test script goes into an infinite loop.
version (Windows)
{
TestScript prog = ":loop
goto loop";
}
else version (Posix)
{
import core.sys.posix.signal : SIGTERM, SIGKILL;
TestScript prog = "while true; do sleep 1; done";
}
auto pid = spawnProcess(prog.path);
// Android appears to automatically kill sleeping processes very quickly,
// so shorten the wait before killing here.
version (Android)
Thread.sleep(dur!"msecs"(5));
else
Thread.sleep(dur!"msecs"(500));
kill(pid);
version (Windows) assert(wait(pid) == 1);
else version (Posix) assert(wait(pid) == -SIGTERM);
pid = spawnProcess(prog.path);
Thread.sleep(dur!"msecs"(500));
auto s = tryWait(pid);
assert(!s.terminated && s.status == 0);
assertThrown!ProcessException(kill(pid, -123)); // Negative code not allowed.
version (Windows) kill(pid, 123);
else version (Posix) kill(pid, SIGKILL);
do { s = tryWait(pid); } while (!s.terminated);
version (Windows) assert(s.status == 123);
else version (Posix) assert(s.status == -SIGKILL);
assertThrown!ProcessException(kill(pid));
}
@system unittest // wait() and kill() detached process
{
import core.thread;
import std.exception : assertThrown;
TestScript prog = "exit 0";
auto pid = spawnProcess([prog.path], null, Config.detached);
/*
This sleep is needed because we can't wait() for detached process to end
and therefore TestScript destructor may run at the same time as /bin/sh tries to start the script.
This leads to the annoying message like "/bin/sh: 0: Can't open /tmp/std.process temporary file" to appear when running tests.
It does not happen in unittests with non-detached processes because we always wait() for them to finish.
*/
Thread.sleep(500.msecs);
assert(!pid.owned);
version (Windows) assert(pid.osHandle == INVALID_HANDLE_VALUE);
assertThrown!ProcessException(wait(pid));
assertThrown!ProcessException(kill(pid));
}
/**
Creates a unidirectional _pipe.
Data is written to one end of the _pipe and read from the other.
---
auto p = pipe();
p.writeEnd.writeln("Hello World");
p.writeEnd.flush();
assert(p.readEnd.readln().chomp() == "Hello World");
---
Pipes can, for example, be used for interprocess communication
by spawning a new process and passing one end of the _pipe to
the child, while the parent uses the other end.
(See also $(LREF pipeProcess) and $(LREF pipeShell) for an easier
way of doing this.)
---
// Use cURL to download the dlang.org front page, pipe its
// output to grep to extract a list of links to ZIP files,
// and write the list to the file "D downloads.txt":
auto p = pipe();
auto outFile = File("D downloads.txt", "w");
auto cpid = spawnProcess(["curl", "http://dlang.org/download.html"],
std.stdio.stdin, p.writeEnd);
scope(exit) wait(cpid);
auto gpid = spawnProcess(["grep", "-o", `http://\S*\.zip`],
p.readEnd, outFile);
scope(exit) wait(gpid);
---
Returns:
A $(LREF Pipe) object that corresponds to the created _pipe.
Throws:
$(REF StdioException, std,stdio) on failure.
*/
version (Posix)
Pipe pipe() @trusted //TODO: @safe
{
import core.sys.posix.stdio : fdopen;
int[2] fds;
if (core.sys.posix.unistd.pipe(fds) != 0)
throw new StdioException("Unable to create pipe");
Pipe p;
auto readFP = fdopen(fds[0], "r");
if (readFP == null)
throw new StdioException("Cannot open read end of pipe");
p._read = File(readFP, null);
auto writeFP = fdopen(fds[1], "w");
if (writeFP == null)
throw new StdioException("Cannot open write end of pipe");
p._write = File(writeFP, null);
return p;
}
else version (Windows)
Pipe pipe() @trusted //TODO: @safe
{
// use CreatePipe to create an anonymous pipe
HANDLE readHandle;
HANDLE writeHandle;
if (!CreatePipe(&readHandle, &writeHandle, null, 0))
{
throw new StdioException(
"Error creating pipe (" ~ generateSysErrorMsg() ~ ')',
0);
}
scope(failure)
{
CloseHandle(readHandle);
CloseHandle(writeHandle);
}
try
{
Pipe p;
p._read .windowsHandleOpen(readHandle , "r");
p._write.windowsHandleOpen(writeHandle, "a");
return p;
}
catch (Exception e)
{
throw new StdioException("Error attaching pipe (" ~ e.msg ~ ")",
0);
}
}
/// An interface to a pipe created by the $(LREF pipe) function.
struct Pipe
{
/// The read end of the pipe.
@property File readEnd() @safe nothrow { return _read; }
/// The write end of the pipe.
@property File writeEnd() @safe nothrow { return _write; }
/**
Closes both ends of the pipe.
Normally it is not necessary to do this manually, as $(REF File, std,stdio)
objects are automatically closed when there are no more references
to them.
Note that if either end of the pipe has been passed to a child process,
it will only be closed in the parent process. (What happens in the
child process is platform dependent.)
Throws:
$(REF ErrnoException, std,exception) if an error occurs.
*/
void close() @safe
{
_read.close();
_write.close();
}
private:
File _read, _write;
}
@system unittest
{
import std.string;
auto p = pipe();
p.writeEnd.writeln("Hello World");
p.writeEnd.flush();
assert(p.readEnd.readln().chomp() == "Hello World");
p.close();
assert(!p.readEnd.isOpen);
assert(!p.writeEnd.isOpen);
}
/**
Starts a new process, creating pipes to redirect its standard
input, output and/or error streams.
`pipeProcess` and `pipeShell` are convenient wrappers around
$(LREF spawnProcess) and $(LREF spawnShell), respectively, and
automate the task of redirecting one or more of the child process'
standard streams through pipes. Like the functions they wrap,
these functions return immediately, leaving the child process to
execute in parallel with the invoking process. It is recommended
to always call $(LREF wait) on the returned $(LREF ProcessPipes.pid),
as detailed in the documentation for `wait`.
The `args`/`program`/`command`, `env` and `config`
parameters are forwarded straight to the underlying spawn functions,
and we refer to their documentation for details.
Params:
args = An array which contains the program name as the zeroth element
and any command-line arguments in the following elements.
(See $(LREF spawnProcess) for details.)
program = The program name, $(I without) command-line arguments.
(See $(LREF spawnProcess) for details.)
command = A shell command which is passed verbatim to the command
interpreter. (See $(LREF spawnShell) for details.)
redirect = Flags that determine which streams are redirected, and
how. See $(LREF Redirect) for an overview of available
flags.
env = Additional environment variables for the child process.
(See $(LREF spawnProcess) for details.)
config = Flags that control process creation. See $(LREF Config)
for an overview of available flags, and note that the
`retainStd...` flags have no effect in this function.
workDir = The working directory for the new process.
By default the child process inherits the parent's working
directory.
shellPath = The path to the shell to use to run the specified program.
By default this is $(LREF nativeShell).
Returns:
A $(LREF ProcessPipes) object which contains $(REF File, std,stdio)
handles that communicate with the redirected streams of the child
process, along with a $(LREF Pid) object that corresponds to the
spawned process.
Throws:
$(LREF ProcessException) on failure to start the process.$(BR)
$(REF StdioException, std,stdio) on failure to redirect any of the streams.$(BR)
Example:
---
// my_application writes to stdout and might write to stderr
auto pipes = pipeProcess("my_application", Redirect.stdout | Redirect.stderr);
scope(exit) wait(pipes.pid);
// Store lines of output.
string[] output;
foreach (line; pipes.stdout.byLine) output ~= line.idup;
// Store lines of errors.
string[] errors;
foreach (line; pipes.stderr.byLine) errors ~= line.idup;
// sendmail expects to read from stdin
pipes = pipeProcess(["/usr/bin/sendmail", "-t"], Redirect.stdin);
pipes.stdin.writeln("To: you");
pipes.stdin.writeln("From: me");
pipes.stdin.writeln("Subject: dlang");
pipes.stdin.writeln("");
pipes.stdin.writeln(message);
// a single period tells sendmail we are finished
pipes.stdin.writeln(".");
// but at this point sendmail might not see it, we need to flush
pipes.stdin.flush();
// sendmail happens to exit on ".", but some you have to close the file:
pipes.stdin.close();
// otherwise this wait will wait forever
wait(pipes.pid);
---
*/
ProcessPipes pipeProcess(scope const(char[])[] args,
Redirect redirect = Redirect.all,
const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null)
@safe
{
return pipeProcessImpl!spawnProcess(args, redirect, env, config, workDir);
}
/// ditto
ProcessPipes pipeProcess(scope const(char)[] program,
Redirect redirect = Redirect.all,
const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null)
@safe
{
return pipeProcessImpl!spawnProcess(program, redirect, env, config, workDir);
}
/// ditto
ProcessPipes pipeShell(scope const(char)[] command,
Redirect redirect = Redirect.all,
const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null,
string shellPath = nativeShell)
@safe
{
return pipeProcessImpl!spawnShell(command,
redirect,
env,
config,
workDir,
shellPath);
}
// Implementation of the pipeProcess() family of functions.
private ProcessPipes pipeProcessImpl(alias spawnFunc, Cmd, ExtraSpawnFuncArgs...)
(scope Cmd command,
Redirect redirectFlags,
const string[string] env = null,
Config config = Config.none,
scope const(char)[] workDir = null,
ExtraSpawnFuncArgs extraArgs = ExtraSpawnFuncArgs.init)
@trusted //TODO: @safe
{
File childStdin, childStdout, childStderr;
ProcessPipes pipes;
pipes._redirectFlags = redirectFlags;
if (redirectFlags & Redirect.stdin)
{
auto p = pipe();
childStdin = p.readEnd;
pipes._stdin = p.writeEnd;
}
else
{
childStdin = std.stdio.stdin;
}
if (redirectFlags & Redirect.stdout)
{
if ((redirectFlags & Redirect.stdoutToStderr) != 0)
throw new StdioException("Cannot create pipe for stdout AND "
~"redirect it to stderr", 0);
auto p = pipe();
childStdout = p.writeEnd;
pipes._stdout = p.readEnd;
}
else
{
childStdout = std.stdio.stdout;
}
if (redirectFlags & Redirect.stderr)
{
if ((redirectFlags & Redirect.stderrToStdout) != 0)
throw new StdioException("Cannot create pipe for stderr AND "
~"redirect it to stdout", 0);
auto p = pipe();
childStderr = p.writeEnd;
pipes._stderr = p.readEnd;
}
else
{
childStderr = std.stdio.stderr;
}
if (redirectFlags & Redirect.stdoutToStderr)
{
if (redirectFlags & Redirect.stderrToStdout)
{
// We know that neither of the other options have been
// set, so we assign the std.stdio.std* streams directly.
childStdout = std.stdio.stderr;
childStderr = std.stdio.stdout;
}
else
{
childStdout = childStderr;
}
}
else if (redirectFlags & Redirect.stderrToStdout)
{
childStderr = childStdout;
}
config.flags &= ~(Config.Flags.retainStdin | Config.Flags.retainStdout | Config.Flags.retainStderr);
pipes._pid = spawnFunc(command, childStdin, childStdout, childStderr,
env, config, workDir, extraArgs);
return pipes;
}
/**
Flags that can be passed to $(LREF pipeProcess) and $(LREF pipeShell)
to specify which of the child process' standard streams are redirected.
Use bitwise OR to combine flags.
*/
enum Redirect
{
/// Redirect the standard input, output or error streams, respectively.
stdin = 1,
stdout = 2, /// ditto
stderr = 4, /// ditto
/**
Redirect _all three streams. This is equivalent to
$(D Redirect.stdin | Redirect.stdout | Redirect.stderr).
*/
all = stdin | stdout | stderr,
/**
Redirect the standard error stream into the standard output stream.
This can not be combined with `Redirect.stderr`.
*/
stderrToStdout = 8,
/**
Redirect the standard output stream into the standard error stream.
This can not be combined with `Redirect.stdout`.
*/
stdoutToStderr = 16,
}
@system unittest
{
import std.string;
version (Windows) TestScript prog =
"call :sub %~1 %~2 0
call :sub %~1 %~2 1
call :sub %~1 %~2 2
call :sub %~1 %~2 3
exit 3
:sub
set /p INPUT=
if -%INPUT%-==-stop- ( exit %~3 )
echo %INPUT% %~1
echo %INPUT% %~2 1>&2";
else version (Posix) TestScript prog =
`for EXITCODE in 0 1 2 3; do
read INPUT
if test "$INPUT" = stop; then break; fi
echo "$INPUT $1"
echo "$INPUT $2" >&2
done
exit $EXITCODE`;
auto pp = pipeProcess([prog.path, "bar", "baz"]);
pp.stdin.writeln("foo");
pp.stdin.flush();
assert(pp.stdout.readln().chomp() == "foo bar");
assert(pp.stderr.readln().chomp().stripRight() == "foo baz");
pp.stdin.writeln("1234567890");
pp.stdin.flush();
assert(pp.stdout.readln().chomp() == "1234567890 bar");
assert(pp.stderr.readln().chomp().stripRight() == "1234567890 baz");
pp.stdin.writeln("stop");
pp.stdin.flush();
assert(wait(pp.pid) == 2);
pp = pipeProcess([prog.path, "12345", "67890"],
Redirect.stdin | Redirect.stdout | Redirect.stderrToStdout);
pp.stdin.writeln("xyz");
pp.stdin.flush();
assert(pp.stdout.readln().chomp() == "xyz 12345");
assert(pp.stdout.readln().chomp().stripRight() == "xyz 67890");
pp.stdin.writeln("stop");
pp.stdin.flush();
assert(wait(pp.pid) == 1);
pp = pipeShell(escapeShellCommand(prog.path, "AAAAA", "BBB"),
Redirect.stdin | Redirect.stdoutToStderr | Redirect.stderr);
pp.stdin.writeln("ab");
pp.stdin.flush();
assert(pp.stderr.readln().chomp() == "ab AAAAA");
assert(pp.stderr.readln().chomp().stripRight() == "ab BBB");
pp.stdin.writeln("stop");
pp.stdin.flush();
assert(wait(pp.pid) == 1);
}
@system unittest
{
import std.exception : assertThrown;
TestScript prog = "exit 0";
assertThrown!StdioException(pipeProcess(
prog.path,
Redirect.stdout | Redirect.stdoutToStderr));
assertThrown!StdioException(pipeProcess(
prog.path,
Redirect.stderr | Redirect.stderrToStdout));
auto p = pipeProcess(prog.path, Redirect.stdin);
assertThrown!Error(p.stdout);
assertThrown!Error(p.stderr);
wait(p.pid);
p = pipeProcess(prog.path, Redirect.stderr);
assertThrown!Error(p.stdin);
assertThrown!Error(p.stdout);
wait(p.pid);
}
/**
Object which contains $(REF File, std,stdio) handles that allow communication
with a child process through its standard streams.
*/
struct ProcessPipes
{
/// The $(LREF Pid) of the child process.
@property Pid pid() @safe nothrow
{
return _pid;
}
/**
An $(REF File, std,stdio) that allows writing to the child process'
standard input stream.
Throws:
$(OBJECTREF Error) if the child process' standard input stream hasn't
been redirected.
*/
@property File stdin() @safe nothrow
{
if ((_redirectFlags & Redirect.stdin) == 0)
throw new Error("Child process' standard input stream hasn't "
~"been redirected.");
return _stdin;
}
/**
An $(REF File, std,stdio) that allows reading from the child process'
standard output stream.
Throws:
$(OBJECTREF Error) if the child process' standard output stream hasn't
been redirected.
*/
@property File stdout() @safe nothrow
{
if ((_redirectFlags & Redirect.stdout) == 0)
throw new Error("Child process' standard output stream hasn't "
~"been redirected.");
return _stdout;
}
/**
An $(REF File, std,stdio) that allows reading from the child process'
standard error stream.
Throws:
$(OBJECTREF Error) if the child process' standard error stream hasn't
been redirected.
*/
@property File stderr() @safe nothrow
{
if ((_redirectFlags & Redirect.stderr) == 0)
throw new Error("Child process' standard error stream hasn't "
~"been redirected.");
return _stderr;
}
private:
Redirect _redirectFlags;
Pid _pid;
File _stdin, _stdout, _stderr;
}
/**
Executes the given program or shell command and returns its exit
code and output.
`execute` and `executeShell` start a new process using
$(LREF spawnProcess) and $(LREF spawnShell), respectively, and wait
for the process to complete before returning. The functions capture
what the child process prints to both its standard output and
standard error streams, and return this together with its exit code.
---
auto dmd = execute(["dmd", "myapp.d"]);
if (dmd.status != 0) writeln("Compilation failed:\n", dmd.output);
auto ls = executeShell("ls -l");
if (ls.status != 0) writeln("Failed to retrieve file listing");
else writeln(ls.output);
---
The `args`/`program`/`command`, `env` and `config`
parameters are forwarded straight to the underlying spawn functions,
and we refer to their documentation for details.
Params:
args = An array which contains the program name as the zeroth element
and any command-line arguments in the following elements.
(See $(LREF spawnProcess) for details.)
program = The program name, $(I without) command-line arguments.
(See $(LREF spawnProcess) for details.)
command = A shell command which is passed verbatim to the command
interpreter. (See $(LREF spawnShell) for details.)
env = Additional environment variables for the child process.
(See $(LREF spawnProcess) for details.)
config = Flags that control process creation. See $(LREF Config)
for an overview of available flags, and note that the
`retainStd...` flags have no effect in this function.
maxOutput = The maximum number of bytes of output that should be
captured.
workDir = The working directory for the new process.
By default the child process inherits the parent's working
directory.
shellPath = The path to the shell to use to run the specified program.
By default this is $(LREF nativeShell).
Returns:
An $(D std.typecons.Tuple!(int, "status", string, "output")).
POSIX_specific:
If the process is terminated by a signal, the `status` field of
the return value will contain a negative number whose absolute
value is the signal number. (See $(LREF wait) for details.)
Throws:
$(LREF ProcessException) on failure to start the process.$(BR)
$(REF StdioException, std,stdio) on failure to capture output.
*/
auto execute(scope const(char[])[] args,
const string[string] env = null,
Config config = Config.none,
size_t maxOutput = size_t.max,
scope const(char)[] workDir = null)
@safe
{
return executeImpl!pipeProcess(args, env, config, maxOutput, workDir);
}
/// ditto
auto execute(scope const(char)[] program,
const string[string] env = null,
Config config = Config.none,
size_t maxOutput = size_t.max,
scope const(char)[] workDir = null)
@safe
{
return executeImpl!pipeProcess(program, env, config, maxOutput, workDir);
}
/// ditto
auto executeShell(scope const(char)[] command,
const string[string] env = null,
Config config = Config.none,
size_t maxOutput = size_t.max,
scope const(char)[] workDir = null,
string shellPath = nativeShell)
@safe
{
return executeImpl!pipeShell(command,
env,
config,
maxOutput,
workDir,
shellPath);
}
// Does the actual work for execute() and executeShell().
private auto executeImpl(alias pipeFunc, Cmd, ExtraPipeFuncArgs...)(
Cmd commandLine,
const string[string] env = null,
Config config = Config.none,
size_t maxOutput = size_t.max,
scope const(char)[] workDir = null,
ExtraPipeFuncArgs extraArgs = ExtraPipeFuncArgs.init)
@trusted //TODO: @safe
{
import std.algorithm.comparison : min;
import std.array : appender;
import std.typecons : Tuple;
auto redirect = (config.flags & Config.Flags.stderrPassThrough)
? Redirect.stdout
: Redirect.stdout | Redirect.stderrToStdout;
auto p = pipeFunc(commandLine, redirect,
env, config, workDir, extraArgs);
auto a = appender!string;
enum size_t defaultChunkSize = 4096;
immutable chunkSize = min(maxOutput, defaultChunkSize);
// Store up to maxOutput bytes in a.
foreach (ubyte[] chunk; p.stdout.byChunk(chunkSize))
{
immutable size_t remain = maxOutput - a.data.length;
if (chunk.length < remain) a.put(chunk);
else
{
a.put(chunk[0 .. remain]);
break;
}
}
// Exhaust the stream, if necessary.
foreach (ubyte[] chunk; p.stdout.byChunk(defaultChunkSize)) { }
return Tuple!(int, "status", string, "output")(wait(p.pid), a.data);
}
@system unittest
{
import std.string;
// To avoid printing the newline characters, we use the echo|set trick on
// Windows, and printf on POSIX (neither echo -n nor echo \c are portable).
version (Windows) TestScript prog =
"echo|set /p=%~1
echo|set /p=%~2 1>&2
exit 123";
else version (Android) TestScript prog =
`echo -n $1
echo -n $2 >&2
exit 123`;
else version (Posix) TestScript prog =
`printf '%s' $1
printf '%s' $2 >&2
exit 123`;
auto r = execute([prog.path, "foo", "bar"]);
assert(r.status == 123);
assert(r.output.stripRight() == "foobar");
auto s = execute([prog.path, "Hello", "World"]);
assert(s.status == 123);
assert(s.output.stripRight() == "HelloWorld");
}
@safe unittest
{
import std.string;
auto r1 = executeShell("echo foo");
assert(r1.status == 0);
assert(r1.output.chomp() == "foo");
auto r2 = executeShell("echo bar 1>&2");
assert(r2.status == 0);
assert(r2.output.chomp().stripRight() == "bar");
auto r3 = executeShell("exit 123");
assert(r3.status == 123);
assert(r3.output.empty);
}
@system unittest
{
// Temporarily disable output to stderr so as to not spam the build log.
import std.stdio : stderr;
import std.typecons : Tuple;
import std.file : readText, exists, remove;
import std.traits : ReturnType;
ReturnType!executeShell r;
auto tmpname = uniqueTempPath;
scope(exit) if (exists(tmpname)) remove(tmpname);
auto t = stderr;
// Open a new scope to minimize code ran with stderr redirected.
{
stderr.open(tmpname, "w");
scope(exit) stderr = t;
r = executeShell("echo D rox>&2", null, Config.stderrPassThrough);
}
assert(r.status == 0);
assert(r.output.empty);
auto witness = readText(tmpname);
import std.ascii : newline;
assert(witness == "D rox" ~ newline, "'" ~ witness ~ "'");
}
@safe unittest
{
import std.typecons : Tuple;
void foo() //Just test the compilation
{
auto ret1 = execute(["dummy", "arg"]);
auto ret2 = executeShell("dummy arg");
static assert(is(typeof(ret1) == typeof(ret2)));
Tuple!(int, string) ret3 = execute(["dummy", "arg"]);
}
}
/// An exception that signals a problem with starting or waiting for a process.
class ProcessException : Exception
{
import std.exception : basicExceptionCtors;
mixin basicExceptionCtors;
// Creates a new ProcessException based on errno.
static ProcessException newFromErrno(string customMsg = null,
string file = __FILE__,
size_t line = __LINE__)
{
import core.stdc.errno : errno;
return newFromErrno(errno, customMsg, file, line);
}
// ditto, but error number is provided by caller
static ProcessException newFromErrno(int error,
string customMsg = null,
string file = __FILE__,
size_t line = __LINE__)
{
import std.exception : errnoString;
auto errnoMsg = errnoString(error);
auto msg = customMsg.empty ? errnoMsg
: customMsg ~ " (" ~ errnoMsg ~ ')';
return new ProcessException(msg, file, line);
}
// Creates a new ProcessException based on GetLastError() (Windows only).
version (Windows)
static ProcessException newFromLastError(string customMsg = null,
string file = __FILE__,
size_t line = __LINE__)
{
auto lastMsg = generateSysErrorMsg();
auto msg = customMsg.empty ? lastMsg
: customMsg ~ " (" ~ lastMsg ~ ')';
return new ProcessException(msg, file, line);
}
}
/**
Determines the path to the current user's preferred command interpreter.
On Windows, this function returns the contents of the COMSPEC environment
variable, if it exists. Otherwise, it returns the result of $(LREF nativeShell).
On POSIX, `userShell` returns the contents of the SHELL environment
variable, if it exists and is non-empty. Otherwise, it returns the result of
$(LREF nativeShell).
*/
@property string userShell() @safe
{
version (Windows) return environment.get("COMSPEC", nativeShell);
else version (Posix) return environment.get("SHELL", nativeShell);
}
/**
The platform-specific native shell path.
This function returns `"cmd.exe"` on Windows, `"/bin/sh"` on POSIX, and
`"/system/bin/sh"` on Android.
*/
@property string nativeShell() @safe @nogc pure nothrow
{
version (Windows) return "cmd.exe";
else version (Android) return "/system/bin/sh";
else version (Posix) return "/bin/sh";
}
// A command-line switch that indicates to the shell that it should
// interpret the following argument as a command to be executed.
version (Posix) private immutable string shellSwitch = "-c";
version (Windows) private immutable string shellSwitch = "/C";
// Unittest support code: TestScript takes a string that contains a
// shell script for the current platform, and writes it to a temporary
// file. On Windows the file name gets a .cmd extension, while on
// POSIX its executable permission bit is set. The file is
// automatically deleted when the object goes out of scope.
version (StdUnittest)
private struct TestScript
{
this(string code) @system
{
// @system due to chmod
import std.ascii : newline;
import std.file : write;
version (Windows)
{
auto ext = ".cmd";
auto firstLine = "@echo off";
}
else version (Posix)
{
auto ext = "";
auto firstLine = "#!" ~ nativeShell;
}
path = uniqueTempPath()~ext;
write(path, firstLine ~ newline ~ code ~ newline);
version (Posix)
{
import core.sys.posix.sys.stat : chmod;
import std.conv : octal;
chmod(path.tempCString(), octal!777);
}
}
~this()
{
import std.file : remove, exists;
if (!path.empty && exists(path))
{
try { remove(path); }
catch (Exception e)
{
debug std.stdio.stderr.writeln(e.msg);
}
}
}
string path;
}
// =============================================================================
// Functions for shell command quoting/escaping.
// =============================================================================
/*
Command line arguments exist in three forms:
1) string or char* array, as received by main.
Also used internally on POSIX systems.
2) Command line string, as used in Windows'
CreateProcess and CommandLineToArgvW functions.
A specific quoting and escaping algorithm is used
to distinguish individual arguments.
3) Shell command string, as written at a shell prompt
or passed to cmd /C - this one may contain shell
control characters, e.g. > or | for redirection /
piping - thus, yet another layer of escaping is
used to distinguish them from program arguments.
Except for escapeWindowsArgument, the intermediary
format (2) is hidden away from the user in this module.
*/
/**
Escapes an argv-style argument array to be used with $(LREF spawnShell),
$(LREF pipeShell) or $(LREF executeShell).
---
string url = "http://dlang.org/";
executeShell(escapeShellCommand("wget", url, "-O", "dlang-index.html"));
---
Concatenate multiple `escapeShellCommand` and
$(LREF escapeShellFileName) results to use shell redirection or
piping operators.
---
executeShell(
escapeShellCommand("curl", "http://dlang.org/download.html") ~
"|" ~
escapeShellCommand("grep", "-o", `http://\S*\.zip`) ~
">" ~
escapeShellFileName("D download links.txt"));
---
Throws:
$(OBJECTREF Exception) if any part of the command line contains unescapable
characters (NUL on all platforms, as well as CR and LF on Windows).
*/
string escapeShellCommand(scope const(char[])[] args...) @safe pure
{
if (args.empty)
return null;
version (Windows)
{
// Do not ^-escape the first argument (the program path),
// as the shell parses it differently from parameters.
// ^-escaping a program path that contains spaces will fail.
string result = escapeShellFileName(args[0]);
if (args.length > 1)
{
result ~= " " ~ escapeShellCommandString(
escapeShellArguments(args[1..$]));
}
return result;
}
version (Posix)
{
return escapeShellCommandString(escapeShellArguments(args));
}
}
@safe unittest
{
// This is a simple unit test without any special requirements,
// in addition to the unittest_burnin one below which requires
// special preparation.
struct TestVector { string[] args; string windows, posix; }
TestVector[] tests =
[
{
args : ["foo bar"],
windows : `"foo bar"`,
posix : `'foo bar'`
},
{
args : ["foo bar", "hello"],
windows : `"foo bar" hello`,
posix : `'foo bar' 'hello'`
},
{
args : ["foo bar", "hello world"],
windows : `"foo bar" ^"hello world^"`,
posix : `'foo bar' 'hello world'`
},
{
args : ["foo bar", "hello", "world"],
windows : `"foo bar" hello world`,
posix : `'foo bar' 'hello' 'world'`
},
{
args : ["foo bar", `'"^\`],
windows : `"foo bar" ^"'\^"^^\\^"`,
posix : `'foo bar' ''\''"^\'`
},
];
foreach (test; tests)
version (Windows)
assert(escapeShellCommand(test.args) == test.windows);
else
assert(escapeShellCommand(test.args) == test.posix );
}
private string escapeShellCommandString(return scope string command) @safe pure
{
version (Windows)
return escapeWindowsShellCommand(command);
else
return command;
}
private string escapeWindowsShellCommand(scope const(char)[] command) @safe pure
{
import std.array : appender;
auto result = appender!string();
result.reserve(command.length);
foreach (c; command)
switch (c)
{
case '\0':
throw new Exception("Cannot put NUL in command line");
case '\r':
case '\n':
throw new Exception("CR/LF are not escapable");
case '\x01': .. case '\x09':
case '\x0B': .. case '\x0C':
case '\x0E': .. case '\x1F':
case '"':
case '^':
case '&':
case '<':
case '>':
case '|':
result.put('^');
goto default;
default:
result.put(c);
}
return result.data;
}
private string escapeShellArguments(scope const(char[])[] args...)
@trusted pure nothrow
{
import std.exception : assumeUnique;
char[] buf;
@safe nothrow
char[] allocator(size_t size)
{
if (buf.length == 0)
return buf = new char[size];
else
{
auto p = buf.length;
buf.length = buf.length + 1 + size;
buf[p++] = ' ';
return buf[p .. p+size];
}
}
foreach (arg; args)
escapeShellArgument!allocator(arg);
return assumeUnique(buf);
}
private auto escapeShellArgument(alias allocator)(scope const(char)[] arg) @safe nothrow
{
// The unittest for this function requires special
// preparation - see below.
version (Windows)
return escapeWindowsArgumentImpl!allocator(arg);
else
return escapePosixArgumentImpl!allocator(arg);
}
/**
Quotes a command-line argument in a manner conforming to the behavior of
$(LINK2 http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx,
CommandLineToArgvW).
*/
string escapeWindowsArgument(scope const(char)[] arg) @trusted pure nothrow
{
// Rationale for leaving this function as public:
// this algorithm of escaping paths is also used in other software,
// e.g. DMD's response files.
import std.exception : assumeUnique;
auto buf = escapeWindowsArgumentImpl!charAllocator(arg);
return assumeUnique(buf);
}
private char[] charAllocator(size_t size) @safe pure nothrow
{
return new char[size];
}
private char[] escapeWindowsArgumentImpl(alias allocator)(scope const(char)[] arg)
@safe nothrow
if (is(typeof(allocator(size_t.init)[0] = char.init)))
{
// References:
// * http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
// * http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx
// Check if the string needs to be escaped,
// and calculate the total string size.
// Trailing backslashes must be escaped
bool escaping = true;
bool needEscape = false;
// Result size = input size + 2 for surrounding quotes + 1 for the
// backslash for each escaped character.
size_t size = 1 + arg.length + 1;
foreach_reverse (char c; arg)
{
if (c == '"')
{
needEscape = true;
escaping = true;
size++;
}
else
if (c == '\\')
{
if (escaping)
size++;
}
else
{
if (c == ' ' || c == '\t')
needEscape = true;
escaping = false;
}
}
import std.ascii : isDigit;
// Empty arguments need to be specified as ""
if (!arg.length)
needEscape = true;
else
// Arguments ending with digits need to be escaped,
// to disambiguate with 1>file redirection syntax
if (isDigit(arg[$-1]))
needEscape = true;
if (!needEscape)
return allocator(arg.length)[] = arg;
// Construct result string.
auto buf = allocator(size);
size_t p = size;
buf[--p] = '"';
escaping = true;
foreach_reverse (char c; arg)
{
if (c == '"')
escaping = true;
else
if (c != '\\')
escaping = false;
buf[--p] = c;
if (escaping)
buf[--p] = '\\';
}
buf[--p] = '"';
assert(p == 0);
return buf;
}
version (Windows) version (StdUnittest)
{
private:
import core.stdc.stddef;
import core.stdc.wchar_ : wcslen;
import core.sys.windows.shellapi : CommandLineToArgvW;
import core.sys.windows.winbase;
import core.sys.windows.winnt;
import std.array;
string[] parseCommandLine(string line)
{
import std.algorithm.iteration : map;
import std.array : array;
import std.conv : to;
auto lpCommandLine = (to!(WCHAR[])(line) ~ '\0').ptr;
int numArgs;
auto args = CommandLineToArgvW(lpCommandLine, &numArgs);
scope(exit) LocalFree(args);
return args[0 .. numArgs]
.map!(arg => to!string(arg[0 .. wcslen(arg)]))
.array();
}
@system unittest
{
import std.conv : text;
string[] testStrings = [
`Hello`,
`Hello, world`,
`Hello, "world"`,
`C:\`,
`C:\dmd`,
`C:\Program Files\`,
];
enum CHARS = `_x\" *&^` ~ "\t"; // _ is placeholder for nothing
foreach (c1; CHARS)
foreach (c2; CHARS)
foreach (c3; CHARS)
foreach (c4; CHARS)
testStrings ~= [c1, c2, c3, c4].replace("_", "");
foreach (s; testStrings)
{
auto q = escapeWindowsArgument(s);
auto args = parseCommandLine("Dummy.exe " ~ q);
assert(args.length == 2, s ~ " => " ~ q ~ " #" ~ text(args.length-1));
assert(args[1] == s, s ~ " => " ~ q ~ " => " ~ args[1]);
}
}
}
private string escapePosixArgument(scope const(char)[] arg) @trusted pure nothrow
{
import std.exception : assumeUnique;
auto buf = escapePosixArgumentImpl!charAllocator(arg);
return assumeUnique(buf);
}
private char[] escapePosixArgumentImpl(alias allocator)(scope const(char)[] arg)
@safe nothrow
if (is(typeof(allocator(size_t.init)[0] = char.init)))
{
// '\'' means: close quoted part of argument, append an escaped
// single quote, and reopen quotes
// Below code is equivalent to:
// return `'` ~ std.array.replace(arg, `'`, `'\''`) ~ `'`;
size_t size = 1 + arg.length + 1;
foreach (char c; arg)
if (c == '\'')
size += 3;
auto buf = allocator(size);
size_t p = 0;
buf[p++] = '\'';
foreach (char c; arg)
if (c == '\'')
{
buf[p .. p+4] = `'\''`;
p += 4;
}
else
buf[p++] = c;
buf[p++] = '\'';
assert(p == size);
return buf;
}
/**
Escapes a filename to be used for shell redirection with $(LREF spawnShell),
$(LREF pipeShell) or $(LREF executeShell).
*/
string escapeShellFileName(scope const(char)[] fileName) @trusted pure nothrow
{
// The unittest for this function requires special
// preparation - see below.
version (Windows)
{
// If a file starts with &, it can cause cmd.exe to misinterpret
// the file name as the stream redirection syntax:
// command > "&foo.txt"
// gets interpreted as
// command >&foo.txt
// Prepend .\ to disambiguate.
if (fileName.length && fileName[0] == '&')
return cast(string)(`".\` ~ fileName ~ '"');
return cast(string)('"' ~ fileName ~ '"');
}
else
return escapePosixArgument(fileName);
}
// Loop generating strings with random characters
//version = unittest_burnin;
version (unittest_burnin)
@system unittest
{
// There are no readily-available commands on all platforms suitable
// for properly testing command escaping. The behavior of CMD's "echo"
// built-in differs from the POSIX program, and Windows ports of POSIX
// environments (Cygwin, msys, gnuwin32) may interfere with their own
// "echo" ports.
// To run this unit test, create std_process_unittest_helper.d with the
// following content and compile it:
// import std.stdio, std.array; void main(string[] args) { write(args.join("\0")); }
// Then, test this module with:
// rdmd --main -unittest -version=unittest_burnin process.d
auto helper = absolutePath("std_process_unittest_helper");
assert(executeShell(helper ~ " hello").output.split("\0")[1..$] == ["hello"], "Helper malfunction");
void test(string[] s, string fn)
{
string e;
string[] g;
e = escapeShellCommand(helper ~ s);
{
scope(failure) writefln("executeShell() failed.\nExpected:\t%s\nEncoded:\t%s", s, [e]);
auto result = executeShell(e);
assert(result.status == 0, "std_process_unittest_helper failed");
g = result.output.split("\0")[1..$];
}
assert(s == g, format("executeShell() test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
e = escapeShellCommand(helper ~ s) ~ ">" ~ escapeShellFileName(fn);
{
scope(failure) writefln(
"executeShell() with redirect failed.\nExpected:\t%s\nFilename:\t%s\nEncoded:\t%s", s, [fn], [e]);
auto result = executeShell(e);
assert(result.status == 0, "std_process_unittest_helper failed");
assert(!result.output.length, "No output expected, got:\n" ~ result.output);
g = readText(fn).split("\0")[1..$];
}
remove(fn);
assert(s == g,
format("executeShell() with redirect test failed.\nExpected:\t%s\nGot:\t\t%s\nEncoded:\t%s", s, g, [e]));
}
while (true)
{
string[] args;
foreach (n; 0 .. uniform(1, 4))
{
string arg;
foreach (l; 0 .. uniform(0, 10))
{
dchar c;
while (true)
{
version (Windows)
{
// As long as DMD's system() uses CreateProcessA,
// we can't reliably pass Unicode
c = uniform(0, 128);
}
else
c = uniform!ubyte();
if (c == 0)
continue; // argv-strings are zero-terminated
version (Windows)
if (c == '\r' || c == '\n')
continue; // newlines are unescapable on Windows
break;
}
arg ~= c;
}
args ~= arg;
}
// generate filename
string fn;
foreach (l; 0 .. uniform(1, 10))
{
dchar c;
while (true)
{
version (Windows)
c = uniform(0, 128); // as above
else
c = uniform!ubyte();
if (c == 0 || c == '/')
continue; // NUL and / are the only characters
// forbidden in POSIX filenames
version (Windows)
if (c < '\x20' || c == '<' || c == '>' || c == ':' ||
c == '"' || c == '\\' || c == '|' || c == '?' || c == '*')
continue; // http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
break;
}
fn ~= c;
}
fn = fn[0..$/2] ~ "_testfile_" ~ fn[$/2..$];
test(args, fn);
}
}
// =============================================================================
// Everything below this line was part of the old std.process, and most of
// it will be deprecated and removed.
// =============================================================================
/*
Copyright: Copyright The D Language Foundation 2007 - 2009.
License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors: $(HTTP digitalmars.com, Walter Bright),
$(HTTP erdani.org, Andrei Alexandrescu),
$(HTTP thecybershadow.net, Vladimir Panteleev)
Source: $(PHOBOSSRC std/_process.d)
*/
/*
Copyright The D Language Foundation 2007 - 2009.
Distributed under the Boost Software License, Version 1.0.
(See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
*/
import core.stdc.errno;
import core.stdc.stdlib;
import core.stdc.string;
import core.thread;
version (Windows)
{
import std.file, std.format, std.random;
}
version (Posix)
{
import core.sys.posix.stdlib;
}
private void toAStringz(in string[] a, const(char)**az)
{
import std.string : toStringz;
foreach (string s; a)
{
*az++ = toStringz(s);
}
*az = null;
}
/* ========================================================== */
//version (Windows)
//{
// int spawnvp(int mode, string pathname, string[] argv)
// {
// char** argv_ = cast(char**) core.stdc.stdlib.malloc((char*).sizeof * (1 + argv.length));
// scope(exit) core.stdc.stdlib.free(argv_);
//
// toAStringz(argv, argv_);
//
// return spawnvp(mode, pathname.tempCString(), argv_);
// }
//}
// Incorporating idea (for spawnvp() on Posix) from Dave Fladebo
enum { _P_WAIT, _P_NOWAIT, _P_OVERLAY }
version (Windows) extern(C) int spawnvp(int, scope const(char) *, scope const(char*)*);
alias P_WAIT = _P_WAIT;
alias P_NOWAIT = _P_NOWAIT;
/* ========================================================== */
version (StdDdoc)
{
/**
Replaces the current process by executing a command, `pathname`, with
the arguments in `argv`.
$(BLUE This function is Posix-Only.)
Typically, the first element of `argv` is
the command being executed, i.e. $(D argv[0] == pathname). The 'p'
versions of `exec` search the PATH environment variable for $(D
pathname). The 'e' versions additionally take the new process'
environment variables as an array of strings of the form key=value.
Does not return on success (the current process will have been
replaced). Returns -1 on failure with no indication of the
underlying error.
Windows_specific:
These functions are only supported on POSIX platforms, as the Windows
operating systems do not provide the ability to overwrite the current
process image with another. In single-threaded programs it is possible
to approximate the effect of `execv*` by using $(LREF spawnProcess)
and terminating the current process once the child process has returned.
For example:
---
auto commandLine = [ "program", "arg1", "arg2" ];
version (Posix)
{
execv(commandLine[0], commandLine);
throw new Exception("Failed to execute program");
}
else version (Windows)
{
import core.stdc.stdlib : _Exit;
_Exit(wait(spawnProcess(commandLine)));
}
---
This is, however, NOT equivalent to POSIX' `execv*`. For one thing, the
executed program is started as a separate process, with all this entails.
Secondly, in a multithreaded program, other threads will continue to do
work while the current thread is waiting for the child process to complete.
A better option may sometimes be to terminate the current program immediately
after spawning the child process. This is the behaviour exhibited by the
$(LINK2 http://msdn.microsoft.com/en-us/library/431x4c1w.aspx,`__exec`)
functions in Microsoft's C runtime library, and it is how D's now-deprecated
Windows `execv*` functions work. Example:
---
auto commandLine = [ "program", "arg1", "arg2" ];
version (Posix)
{
execv(commandLine[0], commandLine);
throw new Exception("Failed to execute program");
}
else version (Windows)
{
spawnProcess(commandLine);
import core.stdc.stdlib : _exit;
_exit(0);
}
---
*/
int execv(in string pathname, in string[] argv);
///ditto
int execve(in string pathname, in string[] argv, in string[] envp);
/// ditto
int execvp(in string pathname, in string[] argv);
/// ditto
int execvpe(in string pathname, in string[] argv, in string[] envp);
}
else version (Posix)
{
int execv(in string pathname, in string[] argv)
{
return execv_(pathname, argv);
}
int execve(in string pathname, in string[] argv, in string[] envp)
{
return execve_(pathname, argv, envp);
}
int execvp(in string pathname, in string[] argv)
{
return execvp_(pathname, argv);
}
int execvpe(in string pathname, in string[] argv, in string[] envp)
{
return execvpe_(pathname, argv, envp);
}
}
// Move these C declarations to druntime if we decide to keep the D wrappers
extern(C)
{
int execv(scope const(char) *, scope const(char *)*);
int execve(scope const(char)*, scope const(char*)*, scope const(char*)*);
int execvp(scope const(char)*, scope const(char*)*);
version (Windows) int execvpe(scope const(char)*, scope const(char*)*, scope const(char*)*);
}
private int execv_(in string pathname, in string[] argv)
{
import core.exception : OutOfMemoryError;
import std.exception : enforce;
auto argv_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + argv.length));
enforce!OutOfMemoryError(argv_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(argv_);
toAStringz(argv, argv_);
return execv(pathname.tempCString(), argv_);
}
private int execve_(in string pathname, in string[] argv, in string[] envp)
{
import core.exception : OutOfMemoryError;
import std.exception : enforce;
auto argv_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + argv.length));
enforce!OutOfMemoryError(argv_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(argv_);
auto envp_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + envp.length));
enforce!OutOfMemoryError(envp_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(envp_);
toAStringz(argv, argv_);
toAStringz(envp, envp_);
return execve(pathname.tempCString(), argv_, envp_);
}
private int execvp_(in string pathname, in string[] argv)
{
import core.exception : OutOfMemoryError;
import std.exception : enforce;
auto argv_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + argv.length));
enforce!OutOfMemoryError(argv_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(argv_);
toAStringz(argv, argv_);
return execvp(pathname.tempCString(), argv_);
}
private int execvpe_(in string pathname, in string[] argv, in string[] envp)
{
version (Posix)
{
import std.array : split;
import std.conv : to;
// Is pathname rooted?
if (pathname[0] == '/')
{
// Yes, so just call execve()
return execve(pathname, argv, envp);
}
else
{
// No, so must traverse PATHs, looking for first match
string[] envPaths = split(
to!string(core.stdc.stdlib.getenv("PATH")), ":");
int iRet = 0;
// Note: if any call to execve() succeeds, this process will cease
// execution, so there's no need to check the execve() result through
// the loop.
foreach (string pathDir; envPaths)
{
string composite = cast(string) (pathDir ~ "/" ~ pathname);
iRet = execve(composite, argv, envp);
}
if (0 != iRet)
{
iRet = execve(pathname, argv, envp);
}
return iRet;
}
}
else version (Windows)
{
import core.exception : OutOfMemoryError;
import std.exception : enforce;
auto argv_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + argv.length));
enforce!OutOfMemoryError(argv_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(argv_);
auto envp_ = cast(const(char)**)core.stdc.stdlib.malloc((char*).sizeof * (1 + envp.length));
enforce!OutOfMemoryError(envp_ !is null, "Out of memory in std.process.");
scope(exit) core.stdc.stdlib.free(envp_);
toAStringz(argv, argv_);
toAStringz(envp, envp_);
return execvpe(pathname.tempCString(), argv_, envp_);
}
else
{
static assert(0);
} // version
}
version (StdDdoc)
{
/****************************************
* Start up the browser and set it to viewing the page at url.
*/
void browse(scope const(char)[] url);
}
else
version (Windows)
{
import core.sys.windows.shellapi, core.sys.windows.winuser;
pragma(lib,"shell32.lib");
void browse(scope const(char)[] url) nothrow @nogc @trusted
{
ShellExecuteW(null, "open", url.tempCStringW(), null, null, SW_SHOWNORMAL);
}
}
else version (Posix)
{
import core.stdc.stdio;
import core.stdc.string;
import core.sys.posix.unistd;
void browse(scope const(char)[] url) nothrow @nogc @safe
{
const buffer = url.tempCString(); // Retain buffer until end of scope
const(char)*[3] args;
// Trusted because it's called with a zero-terminated literal
const(char)* browser = (() @trusted => core.stdc.stdlib.getenv("BROWSER"))();
if (browser)
{
// String already zero-terminated
browser = (() @trusted => strdup(browser))();
args[0] = browser;
}
else
{
version (OSX)
{
args[0] = "open";
}
else
{
//args[0] = "x-www-browser"; // doesn't work on some systems
args[0] = "xdg-open";
}
}
args[1] = buffer;
args[2] = null;
auto childpid = core.sys.posix.unistd.fork();
if (childpid == 0)
{
// Trusted because args and all entries are always zero-terminated
(() @trusted =>
core.sys.posix.unistd.execvp(args[0], &args[0]) ||
perror(args[0]) // failed to execute
)();
return;
}
if (browser)
// Trusted because it's allocated via strdup above
(() @trusted => free(cast(void*) browser))();
version (StdUnittest)
{
// Verify that the test script actually suceeds
int status;
const check = (() @trusted => waitpid(childpid, &status, 0))();
assert(check != -1);
assert(status == 0);
}
}
}
else
static assert(0, "os not supported");
// Verify attributes are consistent between all implementations
@safe @nogc nothrow unittest
{
if (false)
browse("");
}
version (Windows) { /* Doesn't use BROWSER */ }
else
@system unittest
{
import std.conv : text;
import std.range : repeat;
immutable string url = text("http://", repeat('x', 249));
TestScript prog = `if [ "$1" != "` ~ url ~ `" ]; then exit 1; fi`;
environment["BROWSER"] = prog.path;
browse(url);
}