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