diff options
-rw-r--r-- | butl/config | 2 | ||||
-rw-r--r-- | butl/fdstream.cxx | 2 | ||||
-rw-r--r-- | butl/pager.cxx | 2 | ||||
-rw-r--r-- | butl/process | 77 | ||||
-rw-r--r-- | butl/process.cxx | 292 | ||||
-rw-r--r-- | butl/process.ixx | 8 | ||||
-rw-r--r-- | butl/timestamp | 2 | ||||
-rw-r--r-- | tests/process/driver.cxx | 4 |
8 files changed, 345 insertions, 44 deletions
diff --git a/butl/config b/butl/config index dabbdc7..98546d9 100644 --- a/butl/config +++ b/butl/config @@ -33,7 +33,7 @@ // // Clang's libc++ seems to have it for a while (but not before 1200) so we // assume it's there from 1200 (it doesn't define a feature test macros). But -// not for Mac OS X, where it is explicitly marked unavailable until version +// not for MacOS, where it is explicitly marked as unavailable until MacOS // 10.12. // #elif defined(_LIBCPP_VERSION) && _LIBCPP_VERSION >= 1200 diff --git a/butl/fdstream.cxx b/butl/fdstream.cxx index 6ebfaf2..8ca2422 100644 --- a/butl/fdstream.cxx +++ b/butl/fdstream.cxx @@ -685,7 +685,7 @@ namespace butl of |= mode (fdopen_mode::binary) ? _O_BINARY : _O_TEXT; // According to Microsoft _sopen() should not change the permissions of an - // existing file. Meanwhile it does if we pass them (reproduced on Windows + // existing file. However it does if we pass them (reproduced on Windows // XP, 7, and 8). And we must pass them if we have _O_CREATE. So we need // to take care of preserving the permissions ourselves. Note that Wine's // implementation of _sopen() works properly. diff --git a/butl/pager.cxx b/butl/pager.cxx index a03582c..d281d5c 100644 --- a/butl/pager.cxx +++ b/butl/pager.cxx @@ -32,7 +32,7 @@ namespace butl // Create successfully exited process. Will "wait" for it if fallback to // non-interactive execution path. // - : p_ (optional<process::status_type> (0)) + : p_ (process_exit (0)) { // If we are using the default pager, try to get the terminal width // so that we can center the output. diff --git a/butl/process b/butl/process index ff22577..e6d36dc 100644 --- a/butl/process +++ b/butl/process @@ -105,17 +105,74 @@ namespace butl const char** args0_ = nullptr; }; + // Process exit information. + // + struct LIBBUTL_EXPORT process_exit + { + // Status type is the raw exit value as returned by GetExitCodeProcess() + // (NTSTATUS value that represents exit or error codes; MSDN refers to the + // error code as "value of the exception that caused the termination") or + // waitpid(1). Code type is the return value if the process exited + // normally. + // +#ifndef _WIN32 + using status_type = int; + using code_type = std::uint8_t; +#else + using status_type = std::uint32_t; // Win32 DWORD + using code_type = std::uint16_t; // Win32 WORD +#endif + + status_type status; + + process_exit () = default; + + explicit + process_exit (code_type); + + enum as_status_type {as_status}; + process_exit (status_type s, as_status_type): status (s) {} + + // Return false if the process exited abnormally. + // + bool + normal () const; + + code_type + code () const; + + // Abnormal termination information. + // +#ifndef _WIN32 + // Return the signal number that caused the termination or 0 if no such + // information is available. + // + int + signal () const; + + // Return true if the core file was generated. + // + bool + core () const; +#endif + + // Return a description of the reason that caused the process to terminate + // abnormally. On POSIX this is the signal name, on Windows -- the summary + // produced from the corresponding error identifier defined in ntstatus.h. + // + std::string + description () const; + }; + class LIBBUTL_EXPORT process { public: #ifndef _WIN32 using handle_type = pid_t; using id_type = pid_t; - using status_type = int; #else - using handle_type = void*; // Win32 HANDLE - using id_type = std::uint32_t; // Win32 DWORD - using status_type = std::uint32_t; // Win32 DWORD + using handle_type = void*; // Win32 HANDLE + using id_type = std::uint32_t; // Win32 DWORD #endif // Start another process using the specified command line. The default @@ -187,7 +244,7 @@ namespace butl process&, int = 1, int = 2); // Wait for the process to terminate. Return true if the process - // terminated normally and with the zero exit status. Unless ignore_error + // terminated normally and with the zero exit code. Unless ignore_error // is true, throw process_error if anything goes wrong. This function can // be called multiple times with subsequent calls simply returning the // status. @@ -215,10 +272,10 @@ namespace butl process& operator= (const process&) = delete; // Create an empty or "already terminated" process. By default the - // termination status is abnormal but you can change that. + // termination status is unknown but you can change that. // explicit - process (optional<status_type> status = nullopt); + process (optional<process_exit> = nullopt); // Resolve process' paths based on the initial path in args0. If recall // differs from initial, adjust args0 to point to the recall path. If @@ -292,7 +349,11 @@ namespace butl public: handle_type handle; - optional<status_type> status; // Absence means terminated abnormally. + + // Absence means that the exit information is not (yet) known. This can be + // because you haven't called wait() yet or because wait() failed. + // + optional<process_exit> exit; // Use the following file descriptors to communicate with the new process's // standard streams. diff --git a/butl/process.cxx b/butl/process.cxx index df9fb1f..74b370d 100644 --- a/butl/process.cxx +++ b/butl/process.cxx @@ -25,19 +25,18 @@ # define STDERR_FILENO 2 # endif // _MSC_VER -# include <memory> // unique_ptr -# include <cstdlib> // __argv[] +# include <cstdlib> // __argv[] # include <butl/win32-utility> #endif #include <errno.h> -#include <ios> // ios_base::failure +#include <ios> // ios_base::failure #include <cassert> -#include <cstddef> // size_t -#include <cstring> // strlen(), strchr() -#include <utility> // move() +#include <cstddef> // size_t +#include <cstring> // strlen(), strchr() +#include <utility> // move() #include <ostream> #include <butl/utility> // casecmp() @@ -51,6 +50,8 @@ using namespace butl::win32; namespace butl { + // process + // static process_path path_search (const char*, const dir_path&); @@ -374,23 +375,23 @@ namespace butl { if (handle != 0) { - status_type es; + int es; int r (waitpid (handle, &es, 0)); handle = 0; // We have tried. if (r == -1) { - // If ignore errors then just leave status nullopt, so it has the same - // semantics as for abnormally terminated process. + // If ignore errors then just leave exit nullopt, so it has "no exit + // information available" semantics. // if (!ie) throw process_error (errno, false); } - else if (WIFEXITED (es)) - status = WEXITSTATUS (es); + else + exit = process_exit (es, process_exit::as_status); } - return status && *status == 0; + return exit && exit->normal () && exit->code () == 0; } bool process:: @@ -398,7 +399,7 @@ namespace butl { if (handle != 0) { - status_type es; + int es; int r (waitpid (handle, &es, WNOHANG)); if (r == 0) // Not exited yet. @@ -409,11 +410,10 @@ namespace butl if (r == -1) throw process_error (errno, false); - if (WIFEXITED (es)) - status = WEXITSTATUS (es); + exit = process_exit (es, process_exit::as_status); } - s = status && *status == 0; + s = exit && exit->normal () && exit->code () == 0; return true; } @@ -423,6 +423,135 @@ namespace butl return getpid (); } + // process_exit + // + process_exit:: + process_exit (code_type c) + // + // Note that such an initialization is not portable as POSIX doesn't + // specify the bits layout for the value returned by waitpid(). However + // for the major POSIX systems (Linux, FreeBSD, MacOS) it is the + // following: + // + // [0, 7) - terminating signal + // [7, 8) - coredump flag + // [8, 16) - program exit code + // + // Also the lowest 7 bits value is used to distinguish the normal and + // abnormal process terminations. If it is zero then the program exited + // normally and the exit code is available. + // + : status (c << 8) + { + } + + // Make sure the bits layout we stick to (read above) correlates to the W*() + // macros implementations for the current platform. + // + namespace details + { + // W* macros may require an argument to be lvalue (for example for glibc). + // + static const process_exit::status_type status_code (0xFF00); + + static_assert (WIFEXITED (status_code) && + WEXITSTATUS (status_code) == 0xFF && + !WIFSIGNALED (status_code), + "unexpected process exit status bits layout"); + } + + bool process_exit:: + normal () const + { + return WIFEXITED (status); + } + + process_exit::code_type process_exit:: + code () const + { + assert (normal ()); + return WEXITSTATUS (status); + } + + int process_exit:: + signal () const + { + assert (!normal ()); + + // WEXITSTATUS() and WIFSIGNALED() can both return false for the same + // status, so we have neither exit code nor signal. We return zero for + // such a case. + // + return WIFSIGNALED (status) ? WTERMSIG (status) : 0; + } + + bool process_exit:: + core () const + { + assert (!normal ()); + + // Not a POSIX macro (available on Linux, FreeBSD, MacOS). + // +#ifdef WCOREDUMP + return WIFSIGNALED (status) && WCOREDUMP (status); +#else + return false; +#endif + } + + string process_exit:: + description () const + { + assert (!normal ()); + + // It would be convenient to use strsignal() or sys_siglist[] to obtain a + // signal name for the number, but the function is not thread-safe and the + // array is not POSIX. So we will use the custom mapping of POSIX signals + // (IEEE Std 1003.1-2008, 2016 Edition) to their names (as they appear in + // glibc). + // + switch (signal ()) + { + case SIGHUP: return "hangup (SIGHUP)"; + case SIGINT: return "interrupt (SIGINT)"; + case SIGQUIT: return "quit (SIGQUIT)"; + case SIGILL: return "illegal instruction (SIGILL)"; + case SIGABRT: return "aborted (SIGABRT)"; + case SIGFPE: return "floating point exception (SIGFPE)"; + case SIGKILL: return "killed (SIGKILL)"; + case SIGSEGV: return "segmentation fault (SIGSEGV)"; + case SIGPIPE: return "broken pipe (SIGPIPE)"; + case SIGALRM: return "alarm clock (SIGALRM)"; + case SIGTERM: return "terminated (SIGTERM)"; + case SIGUSR1: return "user defined signal 1 (SIGUSR1)"; + case SIGUSR2: return "user defined signal 2 (SIGUSR2)"; + case SIGCHLD: return "child exited (SIGCHLD)"; + case SIGCONT: return "continued (SIGCONT)"; + case SIGSTOP: return "stopped (process; SIGSTOP)"; + case SIGTSTP: return "stopped (typed at terminal; SIGTSTP)"; + case SIGTTIN: return "stopped (tty input; SIGTTIN)"; + case SIGTTOU: return "stopped (tty output; SIGTTOU)"; + case SIGBUS: return "bus error (SIGBUS)"; + + // Unavailabe on MacOS 10.11. + // +#ifdef SIGPOLL + case SIGPOLL: return "I/O possible (SIGPOLL)"; +#endif + + case SIGPROF: return "profiling timer expired (SIGPROF)"; + case SIGSYS: return "bad system call (SIGSYS)"; + case SIGTRAP: return "trace/breakpoint trap (SIGTRAP)"; + case SIGURG: return "urgent I/O condition (SIGURG)"; + case SIGVTALRM: return "virtual timer expired (SIGVTALRM)"; + case SIGXCPU: return "CPU time limit exceeded (SIGXCPU)"; + case SIGXFSZ: return "file size limit exceeded (SIGXFSZ)"; + + case 0: return "status unknown"; + default: return "unknown signal " + to_string (signal ()); + } + } + #else // _WIN32 static process_path @@ -864,28 +993,31 @@ namespace butl { if (handle != 0) { - DWORD s; + DWORD es; DWORD e (NO_ERROR); if (WaitForSingleObject (handle, INFINITE) != WAIT_OBJECT_0 || - !GetExitCodeProcess (handle, &s)) + !GetExitCodeProcess (handle, &es)) e = GetLastError (); auto_handle h (handle); // Auto-deleter. handle = 0; // We have tried. if (e == NO_ERROR) - status = s; + { + exit = process_exit (); + exit->status = es; + } else { - // If ignore errors then just leave status nullopt, so it has the same - // semantics as for abnormally terminated process. + // If ignore errors then just leave exit nullopt, so it has "no exit + // information available" semantics. // if (!ie) throw process_error (error_msg (e)); } } - return status && *status == 0; + return exit && exit->normal () && exit->code () == 0; } bool process:: @@ -897,9 +1029,9 @@ namespace butl if (r == WAIT_TIMEOUT) return false; - DWORD s; + DWORD es; DWORD e (NO_ERROR); - if (r != WAIT_OBJECT_0 || !GetExitCodeProcess (handle, &s)) + if (r != WAIT_OBJECT_0 || !GetExitCodeProcess (handle, &es)) e = GetLastError (); auto_handle h (handle); @@ -908,10 +1040,11 @@ namespace butl if (e != NO_ERROR) throw process_error (error_msg (e)); - status = s; + exit = process_exit (); + exit->status = es; } - s = status && *status == 0; + s = exit && exit->normal () && exit->code () == 0; return true; } @@ -921,5 +1054,112 @@ namespace butl return GetCurrentProcessId (); } + // process_exit + // + process_exit:: + process_exit (code_type c) + // + // The NTSTATUS value returned by GetExitCodeProcess() has the following + // layout of bits: + // + // [ 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, + // 11 - error) + // + : status (c) + { + } + + bool process_exit:: + normal () const + { + // We consider status values with severities other than 0 not being + // returned by the process and so denoting the abnormal termination. + // + return ((status >> 30) & 0x3) == 0; + } + + process_exit::code_type process_exit:: + code () const + { + assert (normal ()); + return status & 0xFFFF; + } + + string process_exit:: + description () const + { + assert (!normal ()); + + // Error codes (or, as MSDN calls them, exception codes) are defined in + // ntstatus.h. It is possible to obtain message descriptions for them + // using FormatMessage() with the FORMAT_MESSAGE_FROM_HMODULE flag and the + // handle returned by LoadLibrary("NTDLL.DLL") call. However, the returned + // messages are pretty much useless being format strings. For example for + // STATUS_ACCESS_VIOLATION error code the message string is "The + // instruction at 0x%p referenced memory at 0x%p. The memory could not be + // %s.". Also under Wine (1.9.8) it is not possible to obtain such a + // descriptions at all for some reason. + // + // Let's use a custom code-to-message mapping for the most common error + // codes, and extend it as needed. + // + // Note that the error code most likely will be messed up if the abnormal + // termination of a process is intercepted with the "searching for + // available solution" message box or debugger invocation. Also note that + // the same failure can result in different exit codes for a process being + // run on Windows nativelly and under Wine. For example under Wine 1.9.8 a + // process that fails due to the stack overflow exits normally with 0 + // status but prints the "err:seh:setup_exception stack overflow ..." + // message to stderr. + // + switch (status) + { + case STATUS_ACCESS_VIOLATION: return "access violation"; + case STATUS_STACK_OVERFLOW: return "stack overflow"; + case STATUS_INTEGER_DIVIDE_BY_ZERO: return "integer divided by zero"; + + // VC-compiled program that calls abort() terminates with this error code + // (0xC0000409). That differs from MinGW GCC-compiled one, that exits + // normally with status 3 (conforms to MSDN). Under Wine (1.9.8) such a + // program exits with status 3 for both VC and MinGW GCC. Sounds weird. + // + case STATUS_STACK_BUFFER_OVERRUN: return "stack buffer overrun"; + + default: + { + string desc ("unknown error 0x"); + + // Add error code hex representation (as it is defined in ntstatus.h). + // + // Strange enough, there is no easy way to convert a number into the + // hex string representation (not using streams). + // + const char digits[] = "0123456789ABCDEF"; + bool skip (true); // Skip leading zeros. + + auto add = [&desc, &digits, &skip] (unsigned char d, bool force) + { + if (d != 0 || !skip || force) + { + desc += digits[d]; + skip = false; + } + }; + + for (int i (sizeof (status) - 1); i >= 0 ; --i) + { + unsigned char c ((status >> (i * 8)) & 0xFF); + add ((c >> 4) & 0xF, false); // Convert the high 4 bits to a digit. + add (c & 0xF, i == 0); // Convert the low 4 bits to a digit. + } + + return desc; + } + } + } + #endif // _WIN32 } diff --git a/butl/process.ixx b/butl/process.ixx index 1bc259c..d360f7e 100644 --- a/butl/process.ixx +++ b/butl/process.ixx @@ -100,9 +100,9 @@ namespace butl } inline process:: - process (optional<status_type> s) + process (optional<process_exit> e) : handle (0), - status (s), + exit (std::move (e)), out_fd (-1), in_ofd (-1), in_efd (-1) @@ -138,7 +138,7 @@ namespace butl inline process:: process (process&& p) : handle (p.handle), - status (p.status), + exit (std::move (p.exit)), out_fd (std::move (p.out_fd)), in_ofd (std::move (p.in_ofd)), in_efd (std::move (p.in_efd)) @@ -155,7 +155,7 @@ namespace butl wait (); handle = p.handle; - status = std::move (p.status); + exit = std::move (p.exit); out_fd = std::move (p.out_fd); in_ofd = std::move (p.in_ofd); in_efd = std::move (p.in_efd); diff --git a/butl/timestamp b/butl/timestamp index 04a65b7..fe67bff 100644 --- a/butl/timestamp +++ b/butl/timestamp @@ -122,7 +122,7 @@ namespace butl // // Note that internally from_string() calls strptime(), which behaves // according to the process' C locale (set with std::setlocale()) and not - // the C++ locale (set with std::locale::global()). Meanwhile the behaviour + // the C++ locale (set with std::locale::global()). However the behaviour // can be affected by std::locale::global() as well, as it itself calls // std::setlocale() for the locale with a name. // diff --git a/tests/process/driver.cxx b/tests/process/driver.cxx index ac8f54d..d2a8172 100644 --- a/tests/process/driver.cxx +++ b/tests/process/driver.cxx @@ -265,12 +265,12 @@ main (int argc, const char* argv[]) // Note that if to create as just process(0) then the // process(const char* args[], int=0, int=1, int=2) ctor is being called. // - process p (optional<process::status_type> (0)); + process p (optional<process_exit> (process_exit (0))); assert (p.wait ()); // "Exited" successfully. } { - process p (optional<process::status_type> (1)); + process p (optional<process_exit> (process_exit (1))); assert (!p.wait ()); // "Exited" with an error. } |