blob: d2dc3b0c428ba91de612f3ea3e2ceda20ffdb56d [file] [log] [blame]
James Kuszmauld42edb42022-01-07 18:00:16 -08001#include "aos/starter/subprocess.h"
2
Adam Snaider70deaf22023-08-11 13:58:34 -07003#include <signal.h>
Stephan Pleinesf581a072024-05-23 20:59:27 -07004#include <stdlib.h>
5#include <sys/stat.h>
Adam Snaider70deaf22023-08-11 13:58:34 -07006
Stephan Pleinesf581a072024-05-23 20:59:27 -07007#include <ostream>
8
Austin Schuh99f7c6a2024-06-25 22:07:44 -07009#include "absl/flags/flag.h"
10#include "absl/log/check.h"
11#include "absl/log/log.h"
Stephan Pleinesf581a072024-05-23 20:59:27 -070012#include "absl/strings/str_cat.h"
James Kuszmaul37a56af2023-07-29 15:15:16 -070013#include "absl/strings/str_join.h"
14#include "gmock/gmock.h"
Philipp Schrader790cb542023-07-05 21:06:52 -070015#include "gtest/gtest.h"
16
Stephan Pleinesf581a072024-05-23 20:59:27 -070017#include "aos/configuration.h"
James Kuszmauld42edb42022-01-07 18:00:16 -080018#include "aos/events/shm_event_loop.h"
Stephan Pleinesf581a072024-05-23 20:59:27 -070019#include "aos/flatbuffers.h"
20#include "aos/ipc_lib/shm_base.h"
James Kuszmauld42edb42022-01-07 18:00:16 -080021#include "aos/testing/path.h"
22#include "aos/testing/tmpdir.h"
23#include "aos/util/file.h"
James Kuszmauld42edb42022-01-07 18:00:16 -080024
25namespace aos::starter::testing {
26
27class SubprocessTest : public ::testing::Test {
28 protected:
Austin Schuh99f7c6a2024-06-25 22:07:44 -070029 SubprocessTest() {
James Kuszmauld42edb42022-01-07 18:00:16 -080030 // Nuke the shm dir:
Austin Schuh99f7c6a2024-06-25 22:07:44 -070031 aos::util::UnlinkRecursive(absl::GetFlag(FLAGS_shm_base));
James Kuszmauld42edb42022-01-07 18:00:16 -080032 }
James Kuszmauld42edb42022-01-07 18:00:16 -080033};
34
35TEST_F(SubprocessTest, CaptureOutputs) {
36 const std::string config_file =
37 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
38
39 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
40 aos::configuration::ReadConfig(config_file);
41 aos::ShmEventLoop event_loop(&config.message());
42 bool observed_stopped = false;
43 Application echo_stdout(
44 "echo", "echo", &event_loop, [&observed_stopped, &echo_stdout]() {
45 if (echo_stdout.status() == aos::starter::State::STOPPED) {
46 observed_stopped = true;
47 }
48 });
49 ASSERT_FALSE(echo_stdout.autorestart());
50 echo_stdout.set_args({"abcdef"});
51 echo_stdout.set_capture_stdout(true);
52 echo_stdout.set_capture_stderr(true);
53
54 echo_stdout.Start();
55 aos::TimerHandler *exit_timer =
56 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
57 event_loop.OnRun([&event_loop, exit_timer]() {
Austin Schuh63851a42022-05-16 13:31:37 -070058 // Note: we are using the backup poll in this test to capture SIGCHLD. This
59 // runs at 1 hz, so make sure we let it run at least once.
Philipp Schradera6712522023-07-05 20:25:11 -070060 exit_timer->Schedule(event_loop.monotonic_now() +
61 std::chrono::milliseconds(1500));
James Kuszmauld42edb42022-01-07 18:00:16 -080062 });
63
64 event_loop.Run();
65
James Kuszmauld42edb42022-01-07 18:00:16 -080066 ASSERT_EQ("abcdef\n", echo_stdout.GetStdout());
67 ASSERT_TRUE(echo_stdout.GetStderr().empty());
68 EXPECT_TRUE(observed_stopped);
69 EXPECT_EQ(aos::starter::State::STOPPED, echo_stdout.status());
70
71 observed_stopped = false;
72
73 // Run again, the output should've been cleared.
74 echo_stdout.set_args({"ghijkl"});
75 echo_stdout.Start();
76 event_loop.Run();
77 ASSERT_EQ("ghijkl\n", echo_stdout.GetStdout());
78 EXPECT_TRUE(observed_stopped);
79}
80
81TEST_F(SubprocessTest, CaptureStderr) {
82 const std::string config_file =
83 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
84
85 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
86 aos::configuration::ReadConfig(config_file);
87 aos::ShmEventLoop event_loop(&config.message());
88 bool observed_stopped = false;
89 Application echo_stderr(
90 "echo", "sh", &event_loop, [&observed_stopped, &echo_stderr]() {
91 if (echo_stderr.status() == aos::starter::State::STOPPED) {
92 observed_stopped = true;
93 }
94 });
95 echo_stderr.set_args({"-c", "echo abcdef >&2"});
96 echo_stderr.set_capture_stdout(true);
97 echo_stderr.set_capture_stderr(true);
98
99 echo_stderr.Start();
Austin Schuh63851a42022-05-16 13:31:37 -0700100 // Note: we are using the backup poll in this test to capture SIGCHLD. This
101 // runs at 1 hz, so make sure we let it run at least once.
James Kuszmauld42edb42022-01-07 18:00:16 -0800102 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); })
Philipp Schradera6712522023-07-05 20:25:11 -0700103 ->Schedule(event_loop.monotonic_now() + std::chrono::milliseconds(1500));
James Kuszmauld42edb42022-01-07 18:00:16 -0800104
105 event_loop.Run();
106
107 ASSERT_EQ("abcdef\n", echo_stderr.GetStderr());
108 ASSERT_TRUE(echo_stderr.GetStdout().empty());
109 ASSERT_TRUE(observed_stopped);
110 ASSERT_EQ(aos::starter::State::STOPPED, echo_stderr.status());
111}
112
James Kuszmaulb740f452023-11-14 17:44:29 -0800113// Checks that when a child application crashing results in the starter printing
114// out its own version by default.
115TEST_F(SubprocessTest, PrintNoTimingReportVersionString) {
116 const std::string config_file =
117 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
118
119 ::testing::internal::CaptureStderr();
120
121 // Set up application without quiet flag active
122 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
123 aos::configuration::ReadConfig(config_file);
124 aos::ShmEventLoop event_loop(&config.message());
125 event_loop.SetVersionString("version_string");
126 bool observed_stopped = false;
127 Application error_out(
128 "false", "bash", &event_loop,
129 [&observed_stopped, &error_out]() {
130 if (error_out.status() == aos::starter::State::STOPPED) {
131 observed_stopped = true;
132 }
133 },
134 Application::QuietLogging::kNo);
135 error_out.set_args({"-c", "sleep 3; false"});
136
137 error_out.Start();
138 aos::TimerHandler *exit_timer =
139 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
140 event_loop.OnRun([&event_loop, exit_timer]() {
141 exit_timer->Schedule(event_loop.monotonic_now() +
142 std::chrono::milliseconds(5000));
143 });
144
145 event_loop.Run();
146
147 // Ensure presence of logs without quiet flag
148 std::string output = ::testing::internal::GetCapturedStderr();
149 std::string expected = "starter version 'version_string'";
150
151 ASSERT_TRUE(output.find(expected) != std::string::npos) << output;
152 EXPECT_TRUE(observed_stopped);
153 EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
154}
155
156TEST_F(SubprocessTest, PrintFailedToStartVersionString) {
157 const std::string config_file =
158 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
159
160 ::testing::internal::CaptureStderr();
161
162 // Set up application without quiet flag active
163 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
164 aos::configuration::ReadConfig(config_file);
165 aos::ShmEventLoop event_loop(&config.message());
166 event_loop.SetVersionString("version_string");
167 bool observed_stopped = false;
168 Application error_out(
169 "false", "false", &event_loop,
170 [&observed_stopped, &error_out]() {
171 if (error_out.status() == aos::starter::State::STOPPED) {
172 observed_stopped = true;
173 }
174 },
175 Application::QuietLogging::kNo);
176
177 error_out.Start();
178 aos::TimerHandler *exit_timer =
179 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
180 event_loop.OnRun([&event_loop, exit_timer]() {
181 exit_timer->Schedule(event_loop.monotonic_now() +
182 std::chrono::milliseconds(1500));
183 });
184
185 event_loop.Run();
186
187 // Ensure presence of logs without quiet flag
188 std::string output = ::testing::internal::GetCapturedStderr();
189 std::string expected = "starter version 'version_string'";
190
191 ASSERT_TRUE(output.find(expected) != std::string::npos) << output;
192 EXPECT_TRUE(observed_stopped);
193 EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
194}
195
payton.rehl2841b1c2023-05-25 17:23:55 -0700196TEST_F(SubprocessTest, UnactiveQuietFlag) {
197 const std::string config_file =
198 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
199
200 ::testing::internal::CaptureStderr();
201
202 // Set up application without quiet flag active
203 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
204 aos::configuration::ReadConfig(config_file);
205 aos::ShmEventLoop event_loop(&config.message());
206 bool observed_stopped = false;
207 Application error_out(
208 "false", "false", &event_loop,
209 [&observed_stopped, &error_out]() {
210 if (error_out.status() == aos::starter::State::STOPPED) {
211 observed_stopped = true;
212 }
213 },
214 Application::QuietLogging::kNo);
215 ASSERT_FALSE(error_out.autorestart());
216
217 error_out.Start();
218 aos::TimerHandler *exit_timer =
219 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
220 event_loop.OnRun([&event_loop, exit_timer]() {
221 exit_timer->Schedule(event_loop.monotonic_now() +
222 std::chrono::milliseconds(1500));
223 });
224
225 event_loop.Run();
226
227 // Ensure presence of logs without quiet flag
228 std::string output = ::testing::internal::GetCapturedStderr();
229 std::string expectedStart = "Failed to start 'false'";
230 std::string expectedRun = "exited unexpectedly with status";
231
232 ASSERT_TRUE(output.find(expectedStart) != std::string::npos ||
James Kuszmaulb740f452023-11-14 17:44:29 -0800233 output.find(expectedRun) != std::string::npos)
234 << output;
payton.rehl2841b1c2023-05-25 17:23:55 -0700235 EXPECT_TRUE(observed_stopped);
236 EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
237}
238
239TEST_F(SubprocessTest, ActiveQuietFlag) {
240 const std::string config_file =
241 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
242
243 ::testing::internal::CaptureStderr();
244
245 // Set up application with quiet flag active
246 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
247 aos::configuration::ReadConfig(config_file);
248 aos::ShmEventLoop event_loop(&config.message());
249 bool observed_stopped = false;
250 Application error_out(
251 "false", "false", &event_loop,
252 [&observed_stopped, &error_out]() {
253 if (error_out.status() == aos::starter::State::STOPPED) {
254 observed_stopped = true;
255 }
256 },
257 Application::QuietLogging::kYes);
258 ASSERT_FALSE(error_out.autorestart());
259
260 error_out.Start();
261 aos::TimerHandler *exit_timer =
262 event_loop.AddTimer([&event_loop]() { event_loop.Exit(); });
263 event_loop.OnRun([&event_loop, exit_timer]() {
264 exit_timer->Schedule(event_loop.monotonic_now() +
265 std::chrono::milliseconds(1500));
266 });
267
268 event_loop.Run();
269
270 // Ensure lack of logs with quiet flag
271 ASSERT_TRUE(::testing::internal::GetCapturedStderr().empty());
272 EXPECT_TRUE(observed_stopped);
273 EXPECT_EQ(aos::starter::State::STOPPED, error_out.status());
274}
275
Adam Snaider70deaf22023-08-11 13:58:34 -0700276// Tests that Nothing Badâ„¢ happens if the event loop outlives the Application.
277//
278// Note that this is a bit of a hope test, as there is no guarantee that we
279// will trigger a crash even if the resources tied to the event loop in the
280// aos::Application aren't properly released.
281TEST_F(SubprocessTest, ShortLivedApp) {
282 const std::string config_file =
283 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
284
285 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
286 aos::configuration::ReadConfig(config_file);
287 aos::ShmEventLoop event_loop(&config.message());
288
289 auto application =
290 std::make_unique<Application>("sleep", "sleep", &event_loop, []() {});
291 application->set_args({"10"});
292 application->Start();
293 pid_t pid = application->get_pid();
294
295 int ticks = 0;
296 aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop, &ticks,
297 &application, pid]() {
298 ticks++;
299 if (application && application->status() == aos::starter::State::RUNNING) {
300 // Kill the application, it will autorestart.
301 kill(pid, SIGTERM);
302 application.reset();
303 }
304
305 // event loop lives for longer.
306 if (ticks >= 5) {
307 // Now we exit.
308 event_loop.Exit();
309 }
310 });
311
312 event_loop.OnRun([&event_loop, exit_timer]() {
313 exit_timer->Schedule(event_loop.monotonic_now(),
314 std::chrono::milliseconds(1000));
315 });
316
317 event_loop.Run();
318}
James Kuszmaul37a56af2023-07-29 15:15:16 -0700319
320// Test that if the binary changes out from under us that we note it in the
321// FileState.
322TEST_F(SubprocessTest, ChangeBinaryContents) {
323 const std::string config_file =
324 ::aos::testing::ArtifactPath("aos/events/pingpong_config.json");
325
326 aos::FlatbufferDetachedBuffer<aos::Configuration> config =
327 aos::configuration::ReadConfig(config_file);
328 aos::ShmEventLoop event_loop(&config.message());
329
330 // Create a local copy of the sleep binary so that we can delete it.
331 const std::filesystem::path full_executable_path =
332 absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_binary");
333 aos::util::WriteStringToFileOrDie(
334 full_executable_path.native(),
335 aos::util::ReadFileToStringOrDie(ResolvePath("sleep").native()), S_IRWXU);
336
337 const std::filesystem::path executable_name =
338 absl::StrCat(aos::testing::TestTmpDir(), "/", "sleep_symlink");
339 // Create a symlink that points to the actual binary, and test that a
340 // Creating a symlink in particular lets us ensure that our logic actually
341 // pays attention to the target file that we are running rather than the
342 // symlink itself (it also saves us having to cp a binary somewhere where we
343 // can overwrite it).
344 std::filesystem::create_symlink(full_executable_path.native(),
345 executable_name);
346
347 // Wait until we are running, go through and test that various variations in
348 // file state result in the expected behavior, and then exit.
349 Application sleep(
350 "sleep", executable_name.native(), &event_loop,
351 [&sleep, &event_loop, executable_name, full_executable_path]() {
352 switch (sleep.status()) {
353 case aos::starter::State::RUNNING:
354 EXPECT_EQ(aos::starter::FileState::NO_CHANGE,
355 sleep.UpdateFileState());
356 // Delete the symlink; this should have no effect, because the
357 // Application class should be looking at the original path.
358 std::filesystem::remove(executable_name);
359 EXPECT_EQ(aos::starter::FileState::NO_CHANGE,
360 sleep.UpdateFileState());
361 // Delete the executable; it should be changed.
362 std::filesystem::remove(full_executable_path);
363 EXPECT_EQ(aos::starter::FileState::CHANGED,
364 sleep.UpdateFileState());
365 // Replace the executable itself; it should be changed.
366 aos::util::WriteStringToFileOrDie(full_executable_path.native(),
367 "abcdef");
368 EXPECT_EQ(aos::starter::FileState::CHANGED,
369 sleep.UpdateFileState());
370 // Terminate.
371 event_loop.Exit();
372 break;
373 case aos::starter::State::WAITING:
374 case aos::starter::State::STARTING:
375 case aos::starter::State::STOPPING:
376 case aos::starter::State::STOPPED:
377 EXPECT_EQ(aos::starter::FileState::NOT_RUNNING,
378 sleep.UpdateFileState());
379 break;
380 }
381 });
382 ASSERT_FALSE(sleep.autorestart());
383 // Ensure that the subprocess will run longer than we care about (we just call
384 // Terminate() below to stop it).
385 sleep.set_args({"1000"});
386
387 sleep.Start();
388 aos::TimerHandler *exit_timer = event_loop.AddTimer([&event_loop]() {
389 event_loop.Exit();
390 FAIL() << "We should have already exited.";
391 });
392 event_loop.OnRun([&event_loop, exit_timer]() {
393 exit_timer->Schedule(event_loop.monotonic_now() +
394 std::chrono::milliseconds(5000));
395 });
396
397 event_loop.Run();
398 sleep.Terminate();
399}
400
401class ResolvePathTest : public ::testing::Test {
402 protected:
403 ResolvePathTest() {
404 // Before doing anything else,
405 if (getenv("PATH") != nullptr) {
406 original_path_ = getenv("PATH");
407 PCHECK(0 == unsetenv("PATH"));
408 }
409 }
410
411 ~ResolvePathTest() {
412 if (!original_path_.empty()) {
413 PCHECK(0 == setenv("PATH", original_path_.c_str(), /*overwrite=*/1));
414 } else {
415 PCHECK(0 == unsetenv("PATH"));
416 }
417 }
418
419 std::filesystem::path GetLocalPath(const std::string filename) {
420 return absl::StrCat(aos::testing::TestTmpDir(), "/", filename);
421 }
422
423 std::filesystem::path CreateFile(const std::string filename) {
424 const std::filesystem::path file = GetLocalPath(filename);
425 VLOG(2) << "Creating file at " << file;
426 util::WriteStringToFileOrDie(file.native(), "contents");
427 return file;
428 }
429
430 void SetPath(const std::vector<std::string> &path) {
431 PCHECK(0 ==
432 setenv("PATH", absl::StrJoin(path, ":").c_str(), /*overwrite=*/1));
433 }
434
435 // Keep track of original PATH environment variable so that we can restore
436 // it.
437 std::string original_path_;
438};
439
440// Tests that we can resolve paths when there is no PATH environment variable.
441TEST_F(ResolvePathTest, ResolveWithUnsetPath) {
442 const std::filesystem::path local_echo = CreateFile("echo");
443 // Because the default path will be in /bin and /usr/bin (typically), we have
444 // to choose some utility that we can reasonably expect to be available in the
445 // test environment.
446 const std::filesystem::path echo_path = ResolvePath("echo");
447 EXPECT_THAT((std::vector<std::string>{"/bin/echo", "/usr/bin/echo"}),
448 ::testing::Contains(echo_path.native()));
449
450 // Test that a file with /'s in the name ignores the PATH.
451 const std::filesystem::path local_echo_path =
452 ResolvePath(local_echo.native());
453 EXPECT_EQ(local_echo_path, local_echo);
454}
455
456// Test that when the PATH environment variable is set that we can use it.
457TEST_F(ResolvePathTest, ResolveWithPath) {
458 const std::filesystem::path local_folder = GetLocalPath("bin/");
459 const std::filesystem::path local_folder2 = GetLocalPath("bin2/");
460 aos::util::MkdirP(local_folder.native(), S_IRWXU);
461 aos::util::MkdirP(local_folder2.native(), S_IRWXU);
462 SetPath({local_folder, local_folder2});
463 const std::filesystem::path binary = CreateFile("bin/binary");
464 const std::filesystem::path duplicate_binary = CreateFile("bin2/binary");
465 const std::filesystem::path other_name = CreateFile("bin2/other_name");
466
467 EXPECT_EQ(binary, ResolvePath("binary"));
468 EXPECT_EQ(other_name, ResolvePath("other_name"));
469 // And check that if we specify the full path for the duplicate binary that we
470 // can find it.
471 EXPECT_EQ(duplicate_binary, ResolvePath(duplicate_binary.native()));
472}
473
474// Test that we fail to find non-existent files.
475TEST_F(ResolvePathTest, DieOnFakeFile) {
476 // Fail to find something that searches the PATH.
477 SetPath({"foo", "bar"});
478 EXPECT_DEATH(ResolvePath("fake_file"), "Unable to resolve");
479
480 // Fail to find a local file.
481 EXPECT_DEATH(ResolvePath("./fake_file"), "./fake_file does not exist");
482}
483
James Kuszmauld42edb42022-01-07 18:00:16 -0800484} // namespace aos::starter::testing