aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--butl/config2
-rw-r--r--butl/fdstream.cxx2
-rw-r--r--butl/pager.cxx2
-rw-r--r--butl/process77
-rw-r--r--butl/process.cxx292
-rw-r--r--butl/process.ixx8
-rw-r--r--butl/timestamp2
-rw-r--r--tests/process/driver.cxx4
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.
}