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