mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 13:10:35 +03:00
4615 lines
153 KiB
D
4615 lines
153 KiB
D
// 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;
|
||
}
|
||
|
||
|
||
// Some of the following should be moved to druntime.
|
||
private
|
||
{
|
||
// Microsoft Visual C Runtime (MSVCRT) declarations.
|
||
version (CRuntime_Microsoft)
|
||
{
|
||
import core.stdc.stdint;
|
||
enum
|
||
{
|
||
STDIN_FILENO = 0,
|
||
STDOUT_FILENO = 1,
|
||
STDERR_FILENO = 2,
|
||
}
|
||
}
|
||
|
||
// POSIX API declarations.
|
||
version (Posix)
|
||
{
|
||
import core.sys.posix.unistd : getEnvironPtr = 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
|
||
{
|
||
if (name is null)
|
||
return false;
|
||
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
|
||
{
|
||
// fix issue https://issues.dlang.org/show_bug.cgi?id=24549
|
||
if (name is null)
|
||
return sink(null);
|
||
|
||
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);
|
||
}
|
||
|
||
// https://issues.dlang.org/show_bug.cgi?id=24549
|
||
@safe unittest
|
||
{
|
||
import std.exception : assertThrown;
|
||
assert(environment.get(null) is null);
|
||
assertThrown(environment[null]);
|
||
assert(!(null 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 @nogc //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 @nogc //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,
|
||
closefds_dup2,
|
||
}
|
||
|
||
/*
|
||
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]);
|
||
auto 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))
|
||
{
|
||
version (FreeBSD)
|
||
import core.sys.freebsd.unistd : closefrom;
|
||
else version (OpenBSD)
|
||
import core.sys.openbsd.unistd : closefrom;
|
||
|
||
static if (!__traits(compiles, closefrom))
|
||
{
|
||
void fallback (int lowfd)
|
||
{
|
||
import core.sys.posix.dirent : dirent, opendir, readdir, closedir, DIR;
|
||
import core.sys.posix.unistd : close;
|
||
import core.sys.posix.stdlib : atoi, malloc, free;
|
||
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;
|
||
|
||
// Missing druntime declaration
|
||
pragma(mangle, "dirfd")
|
||
extern(C) nothrow @nogc int dirfd(DIR* dir);
|
||
|
||
DIR* dir = null;
|
||
|
||
// We read from /dev/fd or /proc/self/fd only if the limit is high enough
|
||
if (maxDescriptors > 128*1024)
|
||
{
|
||
// Try to open the directory /dev/fd or /proc/self/fd
|
||
dir = opendir("/dev/fd");
|
||
if (dir is null) dir = opendir("/proc/self/fd");
|
||
}
|
||
|
||
// If we have a directory, close all file descriptors except stdin, stdout, and stderr
|
||
if (dir)
|
||
{
|
||
scope(exit) closedir(dir);
|
||
|
||
int opendirfd = dirfd(dir);
|
||
|
||
// Iterate over all file descriptors
|
||
while (true)
|
||
{
|
||
dirent* entry = readdir(dir);
|
||
|
||
if (entry is null) break;
|
||
if (entry.d_name[0] == '.') continue;
|
||
|
||
int fd = atoi(cast(char*) entry.d_name);
|
||
|
||
// Don't close stdin, stdout, stderr, or the directory file descriptor
|
||
if (fd < lowfd || fd == opendirfd) continue;
|
||
|
||
close(fd);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// This is going to allocate 8 bytes for each possible file descriptor from lowfd to r.rlim_cur
|
||
if (maxDescriptors <= 128*1024)
|
||
{
|
||
// 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;
|
||
|
||
immutable maxToClose = maxDescriptors - lowfd;
|
||
|
||
// 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 + lowfd;
|
||
pfds[i].events = 0;
|
||
pfds[i].revents = 0;
|
||
}
|
||
|
||
if (poll(pfds, maxToClose, 0) < 0)
|
||
// couldn't use poll, use the slow path.
|
||
goto LslowClose;
|
||
|
||
foreach (i; 0 .. maxToClose)
|
||
{
|
||
// POLLNVAL will be set if the file descriptor is invalid.
|
||
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LslowClose:
|
||
// Fall back to closing everything.
|
||
foreach (i; lowfd .. maxDescriptors)
|
||
{
|
||
close(i);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// closefrom may not be available on the version of glibc we build against.
|
||
// Until we find a way to perform this check we will try to use dlsym to
|
||
// check for the function. See: https://github.com/dlang/phobos/pull/9048
|
||
version (CRuntime_Glibc)
|
||
{
|
||
void closefrom (int lowfd) {
|
||
static bool tryGlibcClosefrom (int lowfd) {
|
||
import core.sys.posix.dlfcn : dlopen, dlclose, dlsym, dlerror, RTLD_LAZY;
|
||
|
||
void *handle = dlopen("libc.so.6", RTLD_LAZY);
|
||
if (!handle)
|
||
return false;
|
||
scope(exit) dlclose(handle);
|
||
|
||
// Clear errors
|
||
dlerror();
|
||
alias closefromT = extern(C) void function(int) @nogc @system nothrow;
|
||
auto closefrom = cast(closefromT) dlsym(handle, "closefrom");
|
||
if (dlerror())
|
||
return false;
|
||
|
||
closefrom(lowfd);
|
||
return true;
|
||
}
|
||
|
||
if (!tryGlibcClosefrom(lowfd))
|
||
fallback(lowfd);
|
||
}
|
||
}
|
||
else
|
||
alias closefrom = fallback;
|
||
}
|
||
|
||
// We need to close all open file descriptors excluding std{in,out,err}
|
||
// and forkPipeOut because we still need it.
|
||
// Since the various libc's provide us with `closefrom` move forkPipeOut
|
||
// to position 3, right after STDERR_FILENO, and close all FDs following that.
|
||
if (dup2(forkPipeOut, 3) == -1)
|
||
abortOnError(forkPipeOut, InternalError.closefds_dup2, .errno);
|
||
forkPipeOut = 3;
|
||
// forkPipeOut needs to be closed after we call `exec`.
|
||
setCLOEXEC(forkPipeOut, true);
|
||
closefrom(forkPipeOut + 1);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
if (config.preExecDelegate !is null)
|
||
{
|
||
if (config.preExecDelegate() != true)
|
||
{
|
||
abortOnError(forkPipeOut, InternalError.preExec, .errno);
|
||
}
|
||
}
|
||
|
||
// Execute program.
|
||
core.sys.posix.unistd.execve(argz[0], argz.ptr, envz is null ? getEnvironPtr : 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 or preExecDelegate";
|
||
break;
|
||
case InternalError.closefds_dup2:
|
||
assert(!(config.flags & Config.Flags.inheritFDs));
|
||
errorMsg = "Failed to close inherited file descriptors";
|
||
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);
|
||
}
|
||
|
||
version (Posix)
|
||
@system unittest
|
||
{
|
||
__gshared int j;
|
||
foreach (i; 0 .. 3)
|
||
{
|
||
auto config = Config(
|
||
preExecFunction: function() @trusted {
|
||
j = 1;
|
||
return true;
|
||
},
|
||
preExecDelegate: delegate() @trusted {
|
||
// j should now be 1, as preExecFunction is called before
|
||
// preExecDelegate is.
|
||
_Exit(i + j);
|
||
return true;
|
||
},
|
||
);
|
||
auto pid = spawnProcess(["false"], config: config);
|
||
assert(wait(pid) == i + 1);
|
||
}
|
||
}
|
||
|
||
/*
|
||
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 null.
|
||
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 null;
|
||
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);
|
||
|
||
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 : tempDir, exists, remove;
|
||
import std.uuid : randomUUID;
|
||
import std.exception : collectException;
|
||
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 : newline;
|
||
import std.file : tempDir, exists, remove, readText, write;
|
||
import std.uuid : randomUUID;
|
||
import std.exception : collectException;
|
||
auto pathi = buildPath(tempDir(), randomUUID().toString());
|
||
auto patho = buildPath(tempDir(), randomUUID().toString());
|
||
auto pathe = buildPath(tempDir(), randomUUID().toString());
|
||
write(pathi, "INPUT" ~ 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.)
|
||
|
||
If $(LREF preExecDelegate) is also set, it is called last.
|
||
|
||
On Windows, this member is not available.
|
||
*/
|
||
bool function() nothrow @nogc @safe preExecFunction;
|
||
|
||
/**
|
||
A delegate 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.)
|
||
|
||
If $(LREF preExecFunction) is also set, it is called first.
|
||
|
||
On Windows, this member is not available.
|
||
*/
|
||
bool delegate() nothrow @nogc @safe preExecDelegate;
|
||
}
|
||
else version (Posix)
|
||
{
|
||
bool function() nothrow @nogc @safe preExecFunction;
|
||
bool delegate() nothrow @nogc @safe preExecDelegate;
|
||
}
|
||
}
|
||
|
||
// 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\r\n" ~ "goto 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 (pid.osHandle == Pid.invalid)
|
||
throw new ProcessException("Pid is invalid");
|
||
if (pid.osHandle == Pid.terminated)
|
||
throw new ProcessException("Pid is already terminated");
|
||
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)); // Already terminated
|
||
}
|
||
|
||
@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' ''\''"^\'`
|
||
},
|
||
{
|
||
args : ["foo bar", ""],
|
||
windows : `"foo bar" ^"^"`,
|
||
posix : `'foo bar' ''`
|
||
},
|
||
{
|
||
args : ["foo bar", "2"],
|
||
windows : `"foo bar" ^"2^"`,
|
||
posix : `'foo bar' '2'`
|
||
},
|
||
];
|
||
|
||
foreach (test; tests)
|
||
{
|
||
auto actual = escapeShellCommand(test.args);
|
||
version (Windows)
|
||
string expected = test.windows;
|
||
else
|
||
string expected = test.posix;
|
||
assert(actual == expected, "\nExpected: " ~ expected ~ "\nGot: " ~ actual);
|
||
}
|
||
}
|
||
|
||
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)))
|
||
{
|
||
bool needQuoting = {
|
||
import std.ascii : isAlphaNum, isDigit;
|
||
import std.algorithm.comparison : among;
|
||
|
||
// Empty arguments need to be specified as ''
|
||
if (arg.length == 0)
|
||
return true;
|
||
// Arguments ending with digits need to be escaped,
|
||
// to disambiguate with 1>file redirection syntax
|
||
if (isDigit(arg[$-1]))
|
||
return true;
|
||
|
||
// Obtained using:
|
||
// for n in $(seq 1 255) ; do
|
||
// c=$(printf \\$(printf "%o" $n))
|
||
// q=$(/bin/printf '%q' "$c")
|
||
// if [[ "$q" == "$c" ]] ; then printf "%s, " "'$c'" ; fi
|
||
// done
|
||
// printf '\n'
|
||
foreach (char c; arg)
|
||
if (!isAlphaNum(c) && !c.among('%', '+', ',', '-', '.', '/', ':', '@', ']', '_'))
|
||
return true;
|
||
return false;
|
||
}();
|
||
if (!needQuoting)
|
||
{
|
||
auto buf = allocator(arg.length);
|
||
buf[] = arg;
|
||
return buf;
|
||
}
|
||
|
||
// '\'' 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
|
||
|
||
import std.file : readText, remove;
|
||
import std.format : format;
|
||
import std.path : absolutePath;
|
||
import std.random : uniform;
|
||
|
||
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 const(char)** toAStringz(in string[] a)
|
||
{
|
||
import std.string : toStringz;
|
||
auto p = (new const(char)*[1 + a.length]).ptr;
|
||
foreach (i, string s; a)
|
||
p[i] = toStringz(s);
|
||
p[a.length] = null;
|
||
return p;
|
||
}
|
||
|
||
|
||
/* ========================================================== */
|
||
|
||
//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)
|
||
{
|
||
return execv(pathname.tempCString(), toAStringz(argv));
|
||
}
|
||
|
||
private int execve_(in string pathname, in string[] argv, in string[] envp)
|
||
{
|
||
return execve(pathname.tempCString(), toAStringz(argv), toAStringz(envp));
|
||
}
|
||
|
||
private int execvp_(in string pathname, in string[] argv)
|
||
{
|
||
return execvp(pathname.tempCString(), toAStringz(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)
|
||
{
|
||
return execvpe(pathname.tempCString(), toAStringz(argv), toAStringz(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]);
|
||
core.sys.posix.unistd._exit(1);
|
||
})();
|
||
assert(0, "Child failed to exec");
|
||
}
|
||
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);
|
||
}
|