blob: 701de1180b37c3a3854e894f394acfc1614e480b [file] [log] [blame]
Philipp Schraderfa8fc492023-09-26 14:52:02 -07001#include <signal.h>
2#include <sys/types.h>
3
4#include <filesystem>
5
Philipp Schraderc8e779e2024-01-25 16:32:39 -08006#include "gmock/gmock.h"
Philipp Schraderfa8fc492023-09-26 14:52:02 -07007#include "gtest/gtest.h"
8
9#include "aos/events/shm_event_loop.h"
10#include "aos/starter/subprocess.h"
11#include "aos/testing/path.h"
12#include "aos/testing/tmpdir.h"
13#include "aos/util/file.h"
14
15namespace aos::starter::testing {
16
17namespace {
18void Wait(pid_t pid) {
19 int status;
20 if (waitpid(pid, &status, 0) != pid) {
21 if (errno != ECHILD) {
22 PLOG(ERROR) << "Failed to wait for PID " << pid << ": " << status;
23 FAIL();
24 }
25 }
26 LOG(INFO) << "Succesfully waited for PID " << pid;
27}
28
29} // namespace
30
31// Validates that killing a child process right after startup doesn't have any
32// unexpected consequences. The child process should exit even if it hasn't
33// `exec()`d yet.
34TEST(SubprocessTest, KillDuringStartup) {
35 const std::string config_file =
36 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
37 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
38 aos::configuration::ReadConfig(config_file);
39 aos::ShmEventLoop event_loop(&config.message());
40
41 // Run an application that takes a really long time to exit. The exact details
42 // here don't matter. We just need to to survive long enough until we can call
43 // Terminate() below.
44 auto application =
45 std::make_unique<Application>("sleep", "sleep", &event_loop, []() {});
46 application->set_args({"100"});
47
48 // Track whether we exit via our own timer callback. We don't want to exit
49 // because of any strange interactions with the child process.
50 bool exited_as_expected = false;
51
52 // Here's the sequence of events that we expect to see:
53 // 1. Start child process.
54 // 2. Stop child process (via `Terminate()`).
55 // 3. Wait 1 second.
56 // 4. Set `exited_as_expected` to `true`.
57 // 5. Exit the event loop.
58 //
59 // At the end, if `exited_as_expected` is `false`, then something unexpected
60 // happened and we failed the test here.
61 aos::TimerHandler *shutdown_timer = event_loop.AddTimer([&]() {
62 exited_as_expected = true;
63 event_loop.Exit();
64 });
65 aos::TimerHandler *trigger_timer = event_loop.AddTimer([&]() {
66 application->Start();
67 application->Terminate();
68 shutdown_timer->Schedule(event_loop.monotonic_now() +
69 std::chrono::seconds(1));
70 });
71 trigger_timer->Schedule(event_loop.monotonic_now());
72 event_loop.Run();
73 application->Stop();
74 Wait(application->get_pid());
75
76 EXPECT_TRUE(exited_as_expected) << "It looks like we killed ourselves.";
77}
78
79// Validates that the code in subprocess.cc doesn't accidentally block signals
80// in the child process.
81TEST(SubprocessTest, CanKillAfterStartup) {
82 const std::string config_file =
83 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
84 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
85 aos::configuration::ReadConfig(config_file);
86 aos::ShmEventLoop event_loop(&config.message());
87
88 // We create a directory in which we create some files so this test here and
89 // the subsequently created child process can "signal" one another. We roughly
90 // expect the following sequence of events:
91 // 1. Start the child process.
92 // 2. Test waits for "startup" file to be created by child.
93 // 3. Child process creates the "startup" file.
94 // 4. Test sees "startup" file being created, sends SIGTERM to child.
95 // 5. Child sees SIGTERM, creates "shutdown" file, and exits.
96 // 6. Test waits for child to exit.
97 // 7. Test validates that the "shutdown" file was created by the child.
98 auto signal_dir = std::filesystem::path(aos::testing::TestTmpDir()) /
99 "startup_file_signals";
100 ASSERT_TRUE(std::filesystem::create_directory(signal_dir));
101 auto startup_signal_file = signal_dir / "startup";
102 auto shutdown_signal_file = signal_dir / "shutdown";
103
104 auto application = std::make_unique<Application>("/bin/bash", "/bin/bash",
105 &event_loop, []() {});
106 application->set_args(
107 {"-c", absl::StrCat("cleanup() { touch ", shutdown_signal_file.string(),
108 "; exit 0; }; trap cleanup SIGTERM; touch ",
109 startup_signal_file.string(),
110 "; while true; do sleep 0.1; done;")});
111
112 // Wait for the child process to create the "startup" file.
113 ASSERT_FALSE(std::filesystem::exists(startup_signal_file));
114 application->Start();
115 while (!std::filesystem::exists(startup_signal_file)) {
116 std::this_thread::sleep_for(std::chrono::milliseconds(50));
117 }
118 ASSERT_FALSE(std::filesystem::exists(shutdown_signal_file));
119
120 // Manually kill the application here. The Stop() and Terminate() helpers
121 // trigger some timeout behaviour that interferes with the test here. This
122 // should cause the child to exit and create the "shutdown" file.
123 PCHECK(kill(application->get_pid(), SIGTERM) == 0);
124 Wait(application->get_pid());
125 ASSERT_TRUE(std::filesystem::exists(shutdown_signal_file));
126}
127
Philipp Schraderc8e779e2024-01-25 16:32:39 -0800128// Validates that a process that is known to take a while to stop can shut down
129// gracefully without being killed.
130TEST(SubprocessTest, CanSlowlyStopGracefully) {
131 const std::string config_file =
132 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
133 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
134 aos::configuration::ReadConfig(config_file);
135 aos::ShmEventLoop event_loop(&config.message());
136
137 // Use a file to signal that the subprocess has started up properly and that
138 // the exit handler has been installed. Otherwise we risk killing the process
139 // uncleanly before the signal handler got installed.
140 auto signal_dir = std::filesystem::path(aos::testing::TestTmpDir()) /
141 "slow_death_startup_file_signals";
142 ASSERT_TRUE(std::filesystem::create_directory(signal_dir));
143 auto startup_signal_file = signal_dir / "startup";
144
145 // Create an application that should never get killed automatically. It should
146 // have plenty of time to shut down on its own. In this case, we use 2 seconds
147 // to mean "plenty of time".
148 auto application = std::make_unique<Application>("/bin/bash", "/bin/bash",
149 &event_loop, [] {});
150 application->set_args(
151 {"-c",
152 absl::StrCat(
153 "trap 'echo got int; sleep 2; echo shutting down; exit 0' SIGINT; "
154 "while true; do sleep 0.1; touch ",
155 startup_signal_file.string(), "; done;")});
156 application->set_capture_stdout(true);
157 application->set_stop_grace_period(std::chrono::seconds(999));
158 application->AddOnChange([&] {
159 if (application->status() == aos::starter::State::STOPPED) {
160 event_loop.Exit();
161 }
162 });
163 application->Start();
164 event_loop
165 .AddTimer([&] {
166 if (std::filesystem::exists(startup_signal_file)) {
167 // Now that the subprocess has properly started up, let's kill it.
168 application->Stop();
169 }
170 })
171 ->Schedule(event_loop.monotonic_now(), std::chrono::milliseconds(100));
172 event_loop.Run();
173
174 EXPECT_EQ(application->exit_code(), 0);
175 EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("got int"));
176 EXPECT_THAT(application->GetStdout(), ::testing::HasSubstr("shutting down"));
177}
178
Philipp Schraderfa8fc492023-09-26 14:52:02 -0700179} // namespace aos::starter::testing