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