aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-10-08 21:24:00 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-11-05 18:23:39 +0300
commite197ae6fae73719266fd4747f499cd6106fbff4e (patch)
tree3cf4e18310469492f371045ae83c28cb25fab4e7
parent7a8386289b18678c2ee49ffdfcf71e3a2abd3258 (diff)
Add process::term() and implement process::kill() on Windows
-rw-r--r--libbutl/process.cxx57
-rw-r--r--libbutl/process.mxx30
-rw-r--r--tests/process-term/buildfile6
-rw-r--r--tests/process-term/driver.cxx413
-rw-r--r--tests/process-term/testscript4
5 files changed, 486 insertions, 24 deletions
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<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 +1991,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::
@@ -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 <typename R, typename P>
optional<bool>
timed_wait (const std::chrono::duration<R, P>&);
- // 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 <time.h>
+# include <signal.h>
+# include <unistd.h>
+# include <sys/types.h>
+#else
+# include <libbutl/win32-utility.hxx>
+#endif
+
+#include <cassert>
+
+#ifndef __cpp_lib_modules_ts
+#include <string>
+#include <cerrno> // ERANGE
+#include <utility> // move()
+#include <cstdlib> // atexit(), exit(), strtoull()
+#include <cstring> // memset()
+#include <cstdint> // uint64_t
+#include <iostream>
+#ifndef _WIN32
+# include <chrono>
+#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 <libbutl/process.mxx>
+#include <libbutl/optional.mxx>
+#include <libbutl/fdstream.mxx>
+#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 <sec> [-t (ignore|exit|default)] [-e] [-c <num>]
+//
+// 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 <sec>
+// 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 <num>
+// 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<uint64_t> sec;
+
+#ifndef _WIN32
+ enum class sig_action
+ {
+ ignore,
+ exit,
+ default_
+ };
+
+ optional<sig_action> 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<int> (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<time_t> (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<DWORD> (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
+
+$*