diff options
Diffstat (limited to 'libbutl/process.cxx')
-rw-r--r-- | libbutl/process.cxx | 410 |
1 files changed, 248 insertions, 162 deletions
diff --git a/libbutl/process.cxx b/libbutl/process.cxx index 0695493..e416807 100644 --- a/libbutl/process.cxx +++ b/libbutl/process.cxx @@ -1,9 +1,7 @@ // file : libbutl/process.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file -#ifndef __cpp_modules_ts -#include <libbutl/process.mxx> -#endif +#include <libbutl/process.hxx> #include <errno.h> @@ -49,6 +47,14 @@ # elif defined(__NetBSD__) && __NetBSD__ >= 6 # define LIBBUTL_POSIX_SPAWN // +// On OpenBSD posix_spawn() appeared in 5.2 (see the man page for details). +// +# elif defined(__OpenBSD__) +# include <sys/param.h> // OpenBSD (yyyymm) +# if OpenBSD >= 201211 // 5.2 released on 1 Nov 2012. +# define LIBBUTL_POSIX_SPAWN +# endif +// // posix_spawn() appeared in Version 3 of the Single UNIX Specification that // was implemented in MacOS 10.5 (see the man page for details). // @@ -87,29 +93,20 @@ # endif // _MSC_VER #endif -#include <cassert> - -#ifndef __cpp_lib_modules_ts -#include <string> -#include <vector> -#include <chrono> -#include <cstdint> -#include <cstddef> -#include <system_error> - #include <ios> // ios_base::failure -#include <cstring> // strlen(), strchr(), strncmp() +#include <memory> // unique_ptr +#include <cstring> // strlen(), strchr(), strpbrk(), strncmp() #include <utility> // move() #include <ostream> +#include <cassert> #ifndef _WIN32 -#include <thread> // this_thread::sleep_for() +# include <thread> // this_thread::sleep_for() #else -#include <map> -#include <ratio> // milli -#include <cstdlib> // __argv[] -#include <algorithm> // find() -#endif +# include <map> +# include <ratio> // milli +# include <cstdlib> // __argv[] +# include <algorithm> // find() #endif #include <libbutl/process-details.hxx> @@ -119,32 +116,8 @@ namespace butl shared_mutex process_spawn_mutex; // Out of module purview. } -#ifdef __cpp_modules_ts -module butl.process; - -// Only imports additional to interface. -#ifdef __clang__ -#ifdef __cpp_lib_modules_ts -import std.core; -import std.io; -import std.threading; // Clang wants it in purview (see process-details.hxx). -#endif -import butl.path; -import butl.fdstream; -import butl.vector_view; -import butl.small_vector; -#endif - -#ifndef _WIN32 -import std.threading; -#endif - -import butl.utility; // icasecmp() -import butl.fdstream; // fdopen_null() -#else -#include <libbutl/utility.mxx> -#include <libbutl/fdstream.mxx> -#endif +#include <libbutl/utility.hxx> // icasecmp() +#include <libbutl/fdstream.hxx> // fdopen_null() using namespace std; @@ -217,7 +190,7 @@ namespace butl } void process:: - print (ostream& o, const char* const args[], size_t n) + print (ostream& o, const char* const* args, size_t n) { size_t m (0); const char* const* p (args); @@ -253,6 +226,35 @@ namespace butl } while (*p != nullptr); } +#if defined(LIBBUTL_POSIX_SPAWN) || defined(_WIN32) + // Return true if the NULL-terminated variable list contains an (un)set of + // the specified variable. The NULL list argument denotes an empty list. + // + // Note that on Windows variable names are case-insensitive. + // + static inline bool + contains_envvar (const char* const* vs, const char* v, size_t n) + { + if (vs != nullptr) + { + // Note that we don't expect the number of variables to (un)set to be + // large, so the linear search is OK. + // + while (const char* v1 = *vs++) + { +#ifdef _WIN32 + if (icasecmp (v1, v, n) == 0 && (v1[n] == '=' || v1[n] == '\0')) +#else + if (strncmp (v1, v, n) == 0 && (v1[n] == '=' || v1[n] == '\0')) +#endif + return true; + } + } + + return false; + } +#endif + #ifndef _WIN32 static process_path @@ -381,10 +383,10 @@ namespace butl } process:: - process (const process_path& pp, const char* args[], + process (const process_path& pp, const char* const* args, pipe pin, pipe pout, pipe perr, const char* cwd, - const char* const* envvars) + const char* const* evars) { int in (pin.in); int out (pout.out); @@ -452,6 +454,8 @@ namespace butl else if (err == -2) in_efd.out = open_null (); + const char* const* tevars (thread_env ()); + // The posix_spawn()-based implementation. // #ifdef LIBBUTL_POSIX_SPAWN @@ -540,47 +544,45 @@ namespace butl fail (r); #endif - // Set/unset environment variables if requested. + // Set/unset the child process environment variables if requested. // - small_vector<const char*, 8> new_env; + vector<const char*> new_env; - if (envvars != nullptr) + if (tevars != nullptr || evars != nullptr) { - for (const char* const* env (environ); *env != nullptr; ++env) + // Copy the non-overridden process environment variables into the + // child's environment. + // + for (const char* const* ev (environ); *ev != nullptr; ++ev) { - // Lookup the existing variable among those that are requested to be - // (un)set. If not present, than add it to the child process - // environment. - // - // Note that on POSIX variable names are case-sensitive. - // - // Alse note that we don't expect the number of variables to (un)set - // to be large, so the linear search is OK. - // - const char* cv (*env); - const char* eq (strchr (cv, '=')); - size_t n (eq != nullptr ? eq - cv : strlen (cv)); - - const char* const* ev (envvars); - for (; *ev != nullptr; ++ev) - { - const char* v (*ev); - if (strncmp (cv, v, n) == 0 && (v[n] == '=' || v[n] == '\0')) - break; - } + const char* v (*ev); + const char* e (strchr (v, '=')); + size_t n (e != nullptr ? e - v : strlen (v)); - if (*ev == nullptr) - new_env.push_back (cv); + if (!contains_envvar (tevars, v, n) && + !contains_envvar (evars, v, n)) + new_env.push_back (v); } - // Copy the environment variables that are requested to be set. + // Copy non-overridden variable assignments into the child's + // environment. // - for (const char* const* ev (envvars); *ev != nullptr; ++ev) + auto set_vars = [&new_env] (const char* const* vs, + const char* const* ovs = nullptr) { - const char* v (*ev); - if (strchr (v, '=') != nullptr) - new_env.push_back (v); - } + if (vs != nullptr) + { + while (const char* v = *vs++) + { + const char* e (strchr (v, '=')); + if (e != nullptr && !contains_envvar (ovs, v, e - v)) + new_env.push_back (v); + } + } + }; + + set_vars (tevars, evars); + set_vars (evars); new_env.push_back (nullptr); } @@ -598,9 +600,9 @@ namespace butl &fa, nullptr /* attrp */, const_cast<char* const*> (&args[0]), - envvars != nullptr - ? const_cast<char* const*> (new_env.data ()) - : environ); + new_env.empty () + ? environ + : const_cast<char* const*> (new_env.data ())); if (r != 0) fail (r); } // Release the lock in parent. @@ -641,6 +643,10 @@ namespace butl { // Child. // + // NOTE: make sure not to call anything that may acquire a mutex that + // could be already acquired in another thread, most notably + // malloc(). @@ What about exceptions (all the fail() calls)? + // Duplicate the user-supplied (fd > -1) or the created pipe descriptor // to the standard stream descriptor (read end for STDIN_FILENO, write // end otherwise). Close the pipe afterwards. @@ -688,27 +694,38 @@ namespace butl if (cwd != nullptr && *cwd != '\0' && chdir (cwd) != 0) fail (true /* child */); - // Set/unset environment variables if requested. + // Set/unset environment variables. // - if (envvars != nullptr) + auto set_vars = [] (const char* const* vs) { - while (const char* ev = *envvars++) + if (vs != nullptr) { - const char* v (strchr (ev, '=')); - - try + while (const char* v = *vs++) { - if (v != nullptr) - setenv (string (ev, v - ev), v + 1); - else - unsetenv (ev); - } - catch (const system_error& e) - { - throw process_child_error (e.code ().value ()); + const char* e (strchr (v, '=')); + + try + { + // @@ TODO: redo without allocation (PATH_MAX?) Maybe + // also using C API to avoid exceptions. + // + if (e != nullptr) + setenv (string (v, e - v), e + 1); + else + unsetenv (v); + } + catch (const system_error& e) + { + // @@ Should we assume this cannot throw? + // + throw process_child_error (e.code ().value ()); + } } } - } + }; + + set_vars (tevars); + set_vars (evars); // Try to re-exec after the "text file busy" failure for 450ms. // @@ -741,6 +758,13 @@ namespace butl { if (handle != 0) { + // First close any open pipe ends for good measure but ignore any + // errors. + // + out_fd.reset (); + in_ofd.reset (); + in_efd.reset (); + int es; int r (waitpid (handle, &es, 0)); handle = 0; // We have tried. @@ -805,13 +829,15 @@ namespace butl void process:: kill () { - if (handle != 0) - { - if (::kill (handle, SIGKILL) == -1) - throw process_error (errno); + if (handle != 0 && ::kill (handle, SIGKILL) == -1) + throw process_error (errno); + } - wait (); - } + void process:: + term () + { + if (handle != 0 && ::kill (handle, SIGTERM) == -1) + throw process_error (errno); } process::id_type process:: @@ -820,6 +846,12 @@ namespace butl return getpid (); } + process::handle_type process:: + current_handle () + { + return getpid (); + } + // process_exit // process_exit:: @@ -1272,13 +1304,30 @@ namespace butl }; const char* process:: - quote_argument (const char* a, string& s) + quote_argument (const char* a, string& s, bool bat) { - // On Windows we need to protect values with spaces using quotes. - // Since there could be actual quotes in the value, we need to - // escape them. + // On Windows we need to protect values with spaces using quotes. Since + // there could be actual quotes in the value, we need to escape them. // - bool q (*a == '\0' || strchr (a, ' ') != nullptr); + // For batch files we also protect equal (`=`), comma (`,`) and semicolon + // (`;`) since otherwise an argument containing any of these will be split + // into several as if they were spaces (that is, the parts will appear in + // %1 %2, etc., instead of all in %1). This of course could break some + // batch files that rely on this semantics (for example, to automatically + // handle --foo=bar as --foo bar) but overall seeing a single argument + // (albeit quoted) is closer to the behavior of real executables. So we do + // this by default and if it becomes a problem we can invent a flag + // (probably in process_env) to disable this quoting (and while at it we + // may add a flag to disable all quoting since the user may need to quote + // some arguments but not others). + // + // While `()` and `[]` are not special characters, some "subsystems" + // (e.g., Cygwin/MSYS2) try to interpret them in certain contexts (e.g., + // relative paths). So we quote them as well (over-quoting seems to be + // harmless according to the "Parsing C Command-Line Arguments" MSDN + // article). + // + bool q (*a == '\0' || strpbrk (a, bat ? " =,;" : " ()[]") != nullptr); if (!q && strchr (a, '"') == nullptr) return a; @@ -1289,8 +1338,8 @@ namespace butl s += '"'; // Note that backslashes don't need escaping, unless they immediately - // precede the double quote (see `Parsing C Command-Line Arguments` MSDN - // article for more details). For example: + // precede the double quote (see "Parsing C Command-Line Arguments" MSDN + // article for details). For example: // // -DPATH="C:\\foo\\" -> -DPATH=\"C:\\foo\\\\\" // -DPATH=C:\foo bar\ -> "-DPATH=C:\foo bar\\" @@ -1329,10 +1378,10 @@ namespace butl static map<string, bool> detect_msys_cache_; process:: - process (const process_path& pp, const char* args[], + process (const process_path& pp, const char* const* args, pipe pin, pipe pout, pipe perr, const char* cwd, - const char* const* envvars) + const char* const* evars) { int in (pin.in); int out (pout.out); @@ -1354,7 +1403,9 @@ namespace butl // vector<char> new_env; - if (envvars != nullptr) + const char* const* tevars (thread_env ()); + + if (tevars != nullptr || evars != nullptr) { // The environment block contains the variables in the following format: // @@ -1363,7 +1414,7 @@ namespace butl // Note the trailing NULL character that follows the last variable // (null-terminated) string. // - unique_ptr<char, void (*)(char*)> cvars ( + unique_ptr<char, void (*)(char*)> pevars ( GetEnvironmentStringsA (), [] (char* p) { @@ -1374,50 +1425,45 @@ namespace butl assert (false); }); - if (cvars.get () == nullptr) + if (pevars.get () == nullptr) fail (); - const char* cv (cvars.get ()); - - // Copy the current environment variables. + // Copy the non-overridden process environment variables into the + // child's environment. // - while (*cv != '\0') + for (const char* v (pevars.get ()); *v != '\0'; ) { - // Lookup the existing variable among those that are requested to be - // (un)set. If not present, than copy it to the new block. - // - // Note that on Windows variable names are case-insensitive. - // - // Alse note that we don't expect the number of variables to (un)set - // to be large, so the linear search is OK. - // - size_t n (strlen (cv) + 1); // Includes NULL character. + size_t n (strlen (v) + 1); // Includes NULL character. - const char* eq (strchr (cv, '=')); - size_t nn (eq != nullptr ? eq - cv : n - 1); - const char* const* ev (envvars); + const char* e (strchr (v, '=')); + size_t nn (e != nullptr ? e - v : n - 1); - for (; *ev != nullptr; ++ev) - { - const char* v (*ev); - if (icasecmp (cv, v, nn) == 0 && (v[nn] == '=' || v[nn] == '\0')) - break; - } + if (!contains_envvar (tevars, v, nn) && + !contains_envvar (evars, v, nn)) + new_env.insert (new_env.end (), v, v + n); - if (*ev == nullptr) - new_env.insert (new_env.end (), cv, cv + n); - - cv += n; + v += n; } - // Copy the environment variables that are requested to be set. + // Copy non-overridden variable assignments into the child's + // environment. // - for (const char* const* ev (envvars); *ev != nullptr; ++ev) + auto set_vars = [&new_env] (const char* const* vs, + const char* const* ovs = nullptr) { - const char* v (*ev); - if (strchr (v, '=') != nullptr) - new_env.insert (new_env.end (), v, v + strlen (v) + 1); - } + if (vs != nullptr) + { + while (const char* v = *vs++) + { + const char* e (strchr (v, '=')); + if (e != nullptr && !contains_envvar (ovs, v, e - v)) + new_env.insert (new_env.end (), v, v + strlen (v) + 1); + } + } + }; + + set_vars (tevars, evars); + set_vars (evars); new_env.push_back ('\0'); // Terminate the new environment block. } @@ -1514,12 +1560,12 @@ namespace butl // string cmd_line; { - auto append = [&cmd_line, buf = string ()] (const char* a) mutable + auto append = [&batch, &cmd_line, buf = string ()] (const char* a) mutable { if (!cmd_line.empty ()) cmd_line += ' '; - cmd_line += quote_argument (a, buf); + cmd_line += quote_argument (a, buf, batch.has_value ()); }; if (batch) @@ -1761,7 +1807,6 @@ namespace butl using namespace chrono; - // Retry for about 1 hour. // system_clock::duration timeout (1h); @@ -1774,7 +1819,7 @@ namespace butl 0, // Primary thread security attributes. true, // Inherit handles. 0, // Creation flags. - envvars != nullptr ? new_env.data () : nullptr, + new_env.empty () ? nullptr : new_env.data (), cwd != nullptr && *cwd != '\0' ? cwd : nullptr, &si, &pi)) @@ -1847,7 +1892,7 @@ namespace butl return PeekNamedPipe (h, &c, 1, &n, nullptr, nullptr) && n == 1; }; - // Hidden by butl::duration that is introduced via fdstream.mxx. + // Hidden by butl::duration that is introduced via fdstream.hxx. // using milli_duration = chrono::duration<DWORD, milli>; @@ -1928,6 +1973,10 @@ namespace butl { if (handle != 0) { + out_fd.reset (); + in_ofd.reset (); + in_efd.reset (); + DWORD es; DWORD e (NO_ERROR); if (WaitForSingleObject (handle, INFINITE) != WAIT_OBJECT_0 || @@ -1958,9 +2007,16 @@ namespace butl optional<bool> process:: try_wait () { + return timed_wait (chrono::milliseconds (0)); + } + + template <> + optional<bool> process:: + timed_wait (const chrono::milliseconds& t) + { if (handle != 0) { - DWORD r (WaitForSingleObject (handle, 0)); + DWORD r (WaitForSingleObject (handle, static_cast<DWORD> (t.count ()))); if (r == WAIT_TIMEOUT) return nullopt; @@ -1982,17 +2038,33 @@ namespace butl return exit ? static_cast<bool> (*exit) : optional<bool> (); } - template <> - optional<bool> process:: - timed_wait (const chrono::milliseconds&) + void process:: + kill () { - throw process_error (ENOTSUP); + // Note that TerminateProcess() requires an exit code the process will be + // terminated with. We could probably craft a custom exit code that will + // be treated by the normal() function as an abnormal termination. + // However, let's keep it simple and reuse the existing (semantically + // close) error code. + // + if (handle != 0 && !TerminateProcess (handle, DBG_TERMINATE_PROCESS)) + { + DWORD e (GetLastError ()); + if (e != ERROR_ACCESS_DENIED) + throw process_error (error_msg (e)); + + // Handle the case when the process has already terminated or is still + // exiting (potentially after being killed). + // + if (!try_wait ()) + throw process_error (error_msg (e), EPERM); + } } void process:: - kill () + term () { - throw process_error (ENOTSUP); + kill (); } process::id_type process:: @@ -2012,6 +2084,15 @@ namespace butl return GetCurrentProcessId (); } + process::handle_type process:: + current_handle () + { + // Note that the returned handle is a pseudo handle (-1) that does not + // need to be closed. + // + return GetCurrentProcess (); + } + // process_exit // process_exit:: @@ -2023,7 +2104,7 @@ namespace butl // [ 0, 16) - program exit code or exception code // [16, 29) - facility // [29, 30) - flag indicating if the status value is customer-defined - // [30, 31) - severity (00 -success, 01 - informational, 10 - warning, + // [30, 31] - severity (00 -success, 01 - informational, 10 - warning, // 11 - error) // : status (c) @@ -2077,6 +2158,7 @@ namespace butl { case STATUS_ACCESS_VIOLATION: return "access violation"; case STATUS_DLL_INIT_FAILED: return "DLL initialization failed"; + case STATUS_DLL_NOT_FOUND: return "unable to find DLL"; case STATUS_INTEGER_DIVIDE_BY_ZERO: return "integer divided by zero"; // If a VC-compiled program exits with the STATUS_STACK_BUFFER_OVERRUN @@ -2094,6 +2176,10 @@ namespace butl case STATUS_STACK_BUFFER_OVERRUN: return "aborted"; case STATUS_STACK_OVERFLOW: return "stack overflow"; + // Presumably the kill() function was called for the process. + // + case DBG_TERMINATE_PROCESS: return "killed"; + default: { string desc ("unknown error 0x"); |