Commit 16d41c4b authored by Chad Austin's avatar Chad Austin Committed by Facebook Github Bot

add Subprocess::waitOrTerminateOrKill

Summary: `waitOrTerminateOrKill` fills the gap between `wait()` and `terminateOrKill`. It allows giving the subprocess a chance to shut down cleanly (after closing its stdin pipe, for example), but avoids waiting forever or leaking a child process.

Reviewed By: yfeldblum, mzhaom

Differential Revision: D17769090

fbshipit-source-id: 8940fd63f7eb9c09ef293c89a5e97f69805735ff
parent b797de38
...@@ -666,23 +666,17 @@ void Subprocess::waitChecked() { ...@@ -666,23 +666,17 @@ void Subprocess::waitChecked() {
checkStatus(returnCode_); checkStatus(returnCode_);
} }
void Subprocess::sendSignal(int signal) { ProcessReturnCode Subprocess::waitTimeout(TimeoutDuration timeout) {
returnCode_.enforce(ProcessReturnCode::RUNNING);
int r = ::kill(pid_, signal);
checkUnixError(r, "kill");
}
ProcessReturnCode Subprocess::terminateOrKill(TimeoutDuration sigtermTimeout) {
returnCode_.enforce(ProcessReturnCode::RUNNING); returnCode_.enforce(ProcessReturnCode::RUNNING);
DCHECK_GT(pid_, 0) << "The subprocess has been waited already"; DCHECK_GT(pid_, 0) << "The subprocess has been waited already";
// 1. Send SIGTERM to kill the process
terminate(); auto pollUntil = std::chrono::steady_clock::now() + timeout;
// 2. check whether subprocess has terminated using non-blocking waitpid for (;;) {
auto pollUntil = std::chrono::steady_clock::now() + sigtermTimeout; // Always call waitpid once after the full timeout has elapsed.
do { auto now = std::chrono::steady_clock::now();
int status; int status;
pid_t found; pid_t found;
// warp waitpid in the while loop to deal with EINTR.
do { do {
found = ::waitpid(pid_, &status, WNOHANG); found = ::waitpid(pid_, &status, WNOHANG);
} while (found == -1 && errno == EINTR); } while (found == -1 && errno == EINTR);
...@@ -696,10 +690,45 @@ ProcessReturnCode Subprocess::terminateOrKill(TimeoutDuration sigtermTimeout) { ...@@ -696,10 +690,45 @@ ProcessReturnCode Subprocess::terminateOrKill(TimeoutDuration sigtermTimeout) {
pid_ = -1; pid_ = -1;
return returnCode_; return returnCode_;
} }
if (now > pollUntil) {
// Timed out: still running().
return returnCode_;
}
// The subprocess is still running, sleep for 100ms // The subprocess is still running, sleep for 100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds{100});
} while (std::chrono::steady_clock::now() <= pollUntil); }
}
void Subprocess::sendSignal(int signal) {
returnCode_.enforce(ProcessReturnCode::RUNNING);
int r = ::kill(pid_, signal);
checkUnixError(r, "kill");
}
ProcessReturnCode Subprocess::waitOrTerminateOrKill(
TimeoutDuration waitTimeout,
TimeoutDuration sigtermTimeout) {
returnCode_.enforce(ProcessReturnCode::RUNNING);
DCHECK_GT(pid_, 0) << "The subprocess has been waited already";
this->waitTimeout(waitTimeout);
if (returnCode_.running()) {
return terminateOrKill(sigtermTimeout);
}
return returnCode_;
}
ProcessReturnCode Subprocess::terminateOrKill(TimeoutDuration sigtermTimeout) {
returnCode_.enforce(ProcessReturnCode::RUNNING);
DCHECK_GT(pid_, 0) << "The subprocess has been waited already";
// 1. Send SIGTERM to kill the process
terminate();
// 2. check whether subprocess has terminated using non-blocking waitpid
waitTimeout(sigtermTimeout);
if (!returnCode_.running()) {
return returnCode_;
}
// 3. If we are at this point, we have waited enough time after // 3. If we are at this point, we have waited enough time after
// sending SIGTERM, we have to use nuclear option SIGKILL to kill // sending SIGTERM, we have to use nuclear option SIGKILL to kill
// the subprocess. // the subprocess.
......
...@@ -593,7 +593,7 @@ class Subprocess { ...@@ -593,7 +593,7 @@ class Subprocess {
/** /**
* Wait for the process to terminate and return its status. Like poll(), * Wait for the process to terminate and return its status. Like poll(),
* the only exception this can throw is std::logic_error if you call this * the only exception this can throw is std::logic_error if you call this
* on a Subprocess whose status is RUNNING. Aborts on egregious * on a Subprocess whose status is not RUNNING. Aborts on egregious
* violations of contract, like an out-of-band waitpid(p.pid(), 0, 0). * violations of contract, like an out-of-band waitpid(p.pid(), 0, 0).
*/ */
ProcessReturnCode wait(); ProcessReturnCode wait();
...@@ -603,6 +603,16 @@ class Subprocess { ...@@ -603,6 +603,16 @@ class Subprocess {
*/ */
void waitChecked(); void waitChecked();
using TimeoutDuration = std::chrono::milliseconds;
/**
* Call `waitpid` non-blockingly up to `timeout`. Throws std::logic_error if
* called on a Subprocess whose status is not RUNNING.
*
* The return code will be running() if waiting timed out.
*/
ProcessReturnCode waitTimeout(TimeoutDuration timeout);
/** /**
* Send a signal to the child. Shortcuts for the commonly used Unix * Send a signal to the child. Shortcuts for the commonly used Unix
* signals are below. * signals are below.
...@@ -615,7 +625,14 @@ class Subprocess { ...@@ -615,7 +625,14 @@ class Subprocess {
sendSignal(SIGKILL); sendSignal(SIGKILL);
} }
using TimeoutDuration = std::chrono::milliseconds; /**
* Call `waitpid` non-blockingly up to `waitTimeout`. If the process hasn't
* terminated after that, fall back on `terminateOrKill` with
* `sigtermTimeoutSeconds`.
*/
ProcessReturnCode waitOrTerminateOrKill(
TimeoutDuration waitTimeout,
TimeoutDuration sigtermTimeout);
/** /**
* Send the SIGTERM to terminate the process, poll `waitpid` non-blockingly * Send the SIGTERM to terminate the process, poll `waitpid` non-blockingly
......
...@@ -174,6 +174,20 @@ TEST(SimpleSubprocessTest, ChangeChildDirectoryWithError) { ...@@ -174,6 +174,20 @@ TEST(SimpleSubprocessTest, ChangeChildDirectoryWithError) {
} }
} }
TEST(SimpleSubprocessTest, waitOrTerminateOrKill_waits_if_process_exits) {
Subprocess proc(std::vector<std::string>{"/bin/sleep", "0.1"});
auto retCode = proc.waitOrTerminateOrKill(1s, 1s);
EXPECT_TRUE(retCode.exited());
EXPECT_EQ(0, retCode.exitStatus());
}
TEST(SimpleSubprocessTest, waitOrTerminateOrKill_terminates_if_timeout) {
Subprocess proc(std::vector<std::string>{"/bin/sleep", "60"});
auto retCode = proc.waitOrTerminateOrKill(1s, 1s);
EXPECT_TRUE(retCode.killed());
EXPECT_EQ(SIGTERM, retCode.killSignal());
}
// This method verifies terminateOrKill shouldn't affect the exit // This method verifies terminateOrKill shouldn't affect the exit
// status if the process has exitted already. // status if the process has exitted already.
TEST(SimpleSubprocessTest, TerminateAfterProcessExit) { TEST(SimpleSubprocessTest, TerminateAfterProcessExit) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment