From e197ae6fae73719266fd4747f499cd6106fbff4e Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 8 Oct 2020 21:24:00 +0300 Subject: Add process::term() and implement process::kill() on Windows --- libbutl/process.cxx | 57 ++++-- libbutl/process.mxx | 30 ++- tests/process-term/buildfile | 6 + tests/process-term/driver.cxx | 413 ++++++++++++++++++++++++++++++++++++++++++ tests/process-term/testscript | 4 + 5 files changed, 486 insertions(+), 24 deletions(-) create mode 100644 tests/process-term/buildfile create mode 100644 tests/process-term/driver.cxx create mode 100644 tests/process-term/testscript diff --git a/libbutl/process.cxx b/libbutl/process.cxx index 0695493..3383a73 100644 --- a/libbutl/process.cxx +++ b/libbutl/process.cxx @@ -805,13 +805,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:: @@ -1958,9 +1960,16 @@ namespace butl optional process:: try_wait () { + return timed_wait (chrono::milliseconds (0)); + } + + template <> + optional process:: + timed_wait (const chrono::milliseconds& t) + { if (handle != 0) { - DWORD r (WaitForSingleObject (handle, 0)); + DWORD r (WaitForSingleObject (handle, static_cast (t.count ()))); if (r == WAIT_TIMEOUT) return nullopt; @@ -1982,17 +1991,33 @@ namespace butl return exit ? static_cast (*exit) : optional (); } - template <> - optional 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:: @@ -2023,7 +2048,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) @@ -2094,6 +2119,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"); diff --git a/libbutl/process.mxx b/libbutl/process.mxx index 54abdec..9106549 100644 --- a/libbutl/process.mxx +++ b/libbutl/process.mxx @@ -360,27 +360,37 @@ LIBBUTL_MODEXPORT namespace butl // duration. Return the same result as wait() if the process has // terminated in this timeframe and nullopt otherwise. // - // Note: not yet implemented on Windows. - // template optional timed_wait (const std::chrono::duration&); - // Terminate the process. + // Note that the destructor will wait for the process but will ignore + // any errors and the exit status. + // + ~process () {if (handle != 0) wait (true);} + + // Process termination. // - // On POSIX send SIGKILL to the process and wait until it terminates. The - // process exit information is available after the call returns. Noop for - // an already terminated process. + + // Send SIGKILL to the process on POSIX and call TerminateProcess() with + // DBG_TERMINATE_PROCESS exit code on Windows. Noop for an already + // terminated process. + // + // Note that if the process is killed, it terminates as if it has called + // abort() (functions registered with atexit() are not called, etc). // - // Note: not yet implemented on Windows. + // Also note that on Windows calling this function for a terminating + // process results in the EPERM process_error exception. // void kill (); - // Note that the destructor will wait for the process but will ignore - // any errors and the exit status. + // Send SIGTERM to the process on POSIX and call kill() on Windows (where + // there is no general way to terminate a console process gracefully). + // Noop for an already terminated process. // - ~process () {if (handle != 0) wait (true);} + void + term (); // Moveable-only type. // diff --git a/tests/process-term/buildfile b/tests/process-term/buildfile new file mode 100644 index 0000000..e710179 --- /dev/null +++ b/tests/process-term/buildfile @@ -0,0 +1,6 @@ +# file : tests/process-term/buildfile +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} + +exe{driver}: {hxx cxx}{*} $libs testscript diff --git a/tests/process-term/driver.cxx b/tests/process-term/driver.cxx new file mode 100644 index 0000000..0e92c2b --- /dev/null +++ b/tests/process-term/driver.cxx @@ -0,0 +1,413 @@ +// file : tests/process-term/driver.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef _WIN32 +# include +# include +# include +# include +#else +# include +#endif + +#include + +#ifndef __cpp_lib_modules_ts +#include +#include // ERANGE +#include // move() +#include // atexit(), exit(), strtoull() +#include // memset() +#include // uint64_t +#include +#ifndef _WIN32 +# include +#endif +#endif + +// Other includes. + +#ifdef __cpp_modules_ts +#ifdef __cpp_lib_modules_ts +import std.core; +import std.io; +#endif +import butl.process; +import butl.optional; +import butl.fdstream; +#else +#include +#include +#include +#endif + +using namespace std; +using namespace butl; + +void +atexit_func () +{ + cout << "exiting"; +} + +#ifndef _WIN32 + +volatile sig_atomic_t term_sig = 0; + +static void +term (int sig) +{ + term_sig = sig; +} +#endif + +// Usages: +// +// argv[0] +// argv[0] -s [-t (ignore|exit|default)] [-e] [-c ] +// +// In the first form run some basic process termination tests, running its +// child in the second form. +// +// In the second form optionally register the SIGTERM signal handler +// (POSIX-only) and the atexit function, then sleep for the requested number +// of seconds and exit with the specified status. +// +// -s +// Sleep for the specified timeout. +// +// -t (ignore|exit|default) +// Register the SIGTERM signal handler. If the signal is received than +// either ignore it, interrupt the sleep and exit, or call the default +// handler. +// +// -e +// Register the function with atexit() that prints the 'exiting' string to +// stdout. +// +// -c +// Exit with the specified status (zero by default). +// +int +main (int argc, const char* argv[]) +{ + using butl::optional; + + auto num = [] (const string& s) + { + assert (!s.empty ()); + + char* e (nullptr); + uint64_t r (strtoull (s.c_str (), &e, 10)); // Can't throw. + assert (errno != ERANGE && e == s.c_str () + s.size ()); + + return r; + }; + + int ec (0); + optional sec; + +#ifndef _WIN32 + enum class sig_action + { + ignore, + exit, + default_ + }; + + optional term_action; + + struct sigaction def_handler; +#endif + + for (int i (1); i != argc; ++i) + { + string o (argv[i]); + + if (o == "-s") + { + assert (++i != argc); + sec = num (argv[i]); + } + else if (o == "-c") + { + assert (++i != argc); + ec = static_cast (num (argv[i])); + } + else if (o == "-e") + { + assert (atexit (atexit_func) == 0); + } + else if (o == "-t") + { + assert (++i != argc); + +#ifndef _WIN32 + string v (argv[i]); + + if (v == "ignore") + term_action = sig_action::ignore; + else if (v == "exit") + term_action = sig_action::exit; + else if (v == "default") + term_action = sig_action::default_; + else + assert (false); + + struct sigaction action; + memset (&action, 0, sizeof (action)); + action.sa_handler = term; + assert (sigaction (SIGTERM, &action, &def_handler) == 0); +#endif + } + else + assert (false); + } + +#ifndef _WIN32 + auto sleep = [&term_action, &def_handler] (uint64_t sec) + { + // Wait until timeout expires or SIGTERM is received and is not ignored. + // + for (timespec tm {static_cast (sec), 0}; + nanosleep (&tm, &tm) == -1; ) + { + assert (term_action && errno == EINTR && term_sig == SIGTERM); + + if (*term_action == sig_action::ignore) + continue; + + if (*term_action == sig_action::default_) + { + assert (sigaction (term_sig, &def_handler, nullptr) == 0); + kill (getpid (), term_sig); + } + + break; + } + }; +#else + auto sleep = [] (uint64_t sec) + { + Sleep (static_cast (sec) * 1000); + }; +#endif + + // Child process. + // + if (sec) + { + if (*sec != 0) + sleep (*sec); + + return ec; + } + + // Main process. + // + + // Return true if the child process has written the specified string to + // stdout, represented by the reading end of the specified pipe. + // + auto test_out = [] (fdpipe&& pipe, const char* out) + { + pipe.out.close (); + + ifdstream is (move (pipe.in)); + bool r (is.read_text () == out); + is.close (); + return r; + }; + +#ifndef _WIN32 + // Terminate a process with the default SIGTERM handler. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10, "-e")); + + sleep (2); // Give the child some time to initialize. + p.term (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + assert (p.exit->signal () == SIGTERM); + } + + // Terminate a process that exits on SIGTERM. Make sure it exits normally + // and atexit function is called. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, + argv[0], "-s", 10, "-t", "exit", "-e", "-c", 5)); + + sleep (2); // Give the child some time to initialize. + p.term (); + + assert (test_out (move (pipe), "exiting")); + + assert (!p.wait ()); + assert (p.exit); + assert (p.exit->normal ()); + assert (p.exit->code () == 5); + } + + // Terminate a process that calls the default handler on SIGTERM. + // + { + fdpipe pipe (fdopen_pipe ()); + process p ( + process_start (0, pipe, 2, + argv[0], "-s", 10, "-t", "default", "-e", "-c", 5)); + + sleep (2); // Give the child some time to initialize. + p.term (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + assert (p.exit->signal () == SIGTERM); + } + + // Terminate and then kill still running process. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, + argv[0], "-s", 10, "-t", "ignore", "-e")); + + sleep (2); // Give the child some time to initialize. + p.term (); + + assert (!p.timed_wait (chrono::seconds (1))); + + p.kill (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + assert (p.exit->signal () == SIGKILL); + } + + // Terminate an already terminated process. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 0, "-c", 5)); + + sleep (4); + p.term (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (p.exit->normal ()); + assert (p.exit->code () == 5); + } + + // Terminate a process being terminated. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10)); + + p.term (); + p.term (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + } + + // Kill a process being terminated. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10)); + + p.term (); + p.kill (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + assert (p.exit->signal () == SIGTERM || p.exit->signal () == SIGKILL); + } + + // Kill a process being killed. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10)); + + p.kill (); + p.kill (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + } +#endif + + // Terminate and wait a process. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10, "-e")); + + p.term (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + } + + // Kill and wait a process. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 10, "-e")); + + p.kill (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (!p.exit->normal ()); + } + + // Kill a terminated process. + // + { + fdpipe pipe (fdopen_pipe ()); + process p (process_start (0, pipe, 2, argv[0], "-s", 0, "-c", 5)); + + sleep (4); + p.kill (); + + assert (test_out (move (pipe), "")); + + assert (!p.wait ()); + assert (p.exit); + assert (p.exit->normal ()); + assert (p.exit->code () == 5); + } +} diff --git a/tests/process-term/testscript b/tests/process-term/testscript new file mode 100644 index 0000000..f61899c --- /dev/null +++ b/tests/process-term/testscript @@ -0,0 +1,4 @@ +# file : tests/process-term/testscript +# license : MIT; see accompanying LICENSE file + +$* -- cgit v1.1