blob: 894b6feefe6cda53c3be9ec707941779c8d70594 [file] [log] [blame]
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <chrono>
#include <filesystem>
#include <memory>
#include <ostream>
#include <string>
#include <thread>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/str_cat.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "aos/configuration.h"
#include "aos/events/event_loop.h"
#include "aos/events/shm_event_loop.h"
#include "aos/flatbuffers.h"
#include "aos/starter/starter_generated.h"
#include "aos/starter/subprocess.h"
#include "aos/testing/path.h"
#include "aos/testing/tmpdir.h"
namespace aos::starter::testing {
namespace {
void Wait(pid_t pid) {
int status;
if (waitpid(pid, &status, 0) != pid) {
if (errno != ECHILD) {
PLOG(ERROR) << "Failed to wait for PID " << pid << ": " << status;
FAIL();
}
}
LOG(INFO) << "Succesfully waited for PID " << pid;
}
} // namespace
// Validates that killing a child process right after startup doesn't have any
// unexpected consequences. The child process should exit even if it hasn't
// `exec()`d yet.
TEST(SubprocessTest, KillDuringStartup) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
// Run an application that takes a really long time to exit. The exact details
// here don't matter. We just need to to survive long enough until we can call
// Terminate() below.
auto application =
std::make_unique<Application>("sleep", "sleep", &event_loop, []() {});
application->set_args({"100"});
// Track whether we exit via our own timer callback. We don't want to exit
// because of any strange interactions with the child process.
bool exited_as_expected = false;
// Here's the sequence of events that we expect to see:
// 1. Start child process.
// 2. Stop child process (via `Terminate()`).
// 3. Wait 1 second.
// 4. Set `exited_as_expected` to `true`.
// 5. Exit the event loop.
//
// At the end, if `exited_as_expected` is `false`, then something unexpected
// happened and we failed the test here.
aos::TimerHandler *shutdown_timer = event_loop.AddTimer([&]() {
exited_as_expected = true;
event_loop.Exit();
});
aos::TimerHandler *trigger_timer = event_loop.AddTimer([&]() {
application->Start();
application->Terminate();
shutdown_timer->Schedule(event_loop.monotonic_now() +
std::chrono::seconds(1));
});
trigger_timer->Schedule(event_loop.monotonic_now());
event_loop.Run();
application->Stop();
Wait(application->get_pid());
EXPECT_TRUE(exited_as_expected) << "It looks like we killed ourselves.";
}
// Validates that the code in subprocess.cc doesn't accidentally block signals
// in the child process.
TEST(SubprocessTest, CanKillAfterStartup) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
// We create a directory in which we create some files so this test here and
// the subsequently created child process can "signal" one another. We roughly
// expect the following sequence of events:
// 1. Start the child process.
// 2. Test waits for "startup" file to be created by child.
// 3. Child process creates the "startup" file.
// 4. Test sees "startup" file being created, sends SIGTERM to child.
// 5. Child sees SIGTERM, creates "shutdown" file, and exits.
// 6. Test waits for child to exit.
// 7. Test validates that the "shutdown" file was created by the child.
auto signal_dir = std::filesystem::path(aos::testing::TestTmpDir()) /
"startup_file_signals";
ASSERT_TRUE(std::filesystem::create_directory(signal_dir));
auto startup_signal_file = signal_dir / "startup";
auto shutdown_signal_file = signal_dir / "shutdown";
auto application = std::make_unique<Application>("/bin/bash", "/bin/bash",
&event_loop, []() {});
application->set_args(
{"-c", absl::StrCat("cleanup() { touch ", shutdown_signal_file.string(),
"; exit 0; }; trap cleanup SIGTERM; touch ",
startup_signal_file.string(),
"; while true; do sleep 0.1; done;")});
// Wait for the child process to create the "startup" file.
ASSERT_FALSE(std::filesystem::exists(startup_signal_file));
application->Start();
while (!std::filesystem::exists(startup_signal_file)) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
ASSERT_FALSE(std::filesystem::exists(shutdown_signal_file));
// Manually kill the application here. The Stop() and Terminate() helpers
// trigger some timeout behaviour that interferes with the test here. This
// should cause the child to exit and create the "shutdown" file.
PCHECK(kill(application->get_pid(), SIGTERM) == 0);
Wait(application->get_pid());
ASSERT_TRUE(std::filesystem::exists(shutdown_signal_file));
}
// Validates that a process that is known to take a while to stop can shut down
// gracefully without being killed.
TEST(SubprocessTest, CanSlowlyStopGracefully) {
const std::string config_file =
::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(config_file);
aos::ShmEventLoop event_loop(&config.message());
// Use a file to signal that the subprocess has started up properly and that
// the exit handler has been installed. Otherwise we risk killing the process
// uncleanly before the signal handler got installed.
auto signal_dir = std::filesystem::path(aos::testing::TestTmpDir()) /
"slow_death_startup_file_signals";
ASSERT_TRUE(std::filesystem::create_directory(signal_dir));
auto startup_signal_file = signal_dir / "startup";
// Create an application that should never get killed automatically. It should
// have plenty of time to shut down on its own. In this case, we use 2 seconds
// to mean "plenty of time".
auto application = std::make_unique<Application>("/bin/bash", "/bin/bash",
&event_loop, [] {});
application->set_args(
{"-c",
absl::StrCat(
"trap 'echo got int; sleep 2; echo shutting down; exit 0' SIGINT; "
"while true; do sleep 0.1; touch ",
startup_signal_file.string(), "; done;")});
application->set_capture_stdout(true);
application->set_stop_grace_period(std::chrono::seconds(999));
application->AddOnChange([&] {
if (application->status() == aos::starter::State::STOPPED) {
event_loop.Exit();
}
});
application->Start();
event_loop
.AddTimer([&] {
if (std::filesystem::exists(startup_signal_file)) {
// Now that the subprocess has properly started up, let's kill it.
application->Stop();
}
})
->Schedule(event_loop.monotonic_now(), std::chrono::milliseconds(100));
event_loop.Run();
EXPECT_EQ(application->exit_code(), 0);
EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("got int"));
EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("shutting down"));
}
} // namespace aos::starter::testing