From 52064fdd1b9ed25f7432f14abdce9dac10e4e507 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Wed, 3 Sep 2014 18:24:29 -0400 Subject: [PATCH] Add the MultiprocessExec test and its test. TEST=util_test MultiprocessExec.* R=rsesek@chromium.org Review URL: https://codereview.chromium.org/531203002 --- util/test/executable_path.h | 29 +++++ util/test/executable_path_mac.cc | 37 ++++++ util/test/executable_path_test.cc | 30 +++++ util/test/multiprocess.h | 2 +- util/test/multiprocess_exec.cc | 145 ++++++++++++++++++++++ util/test/multiprocess_exec.h | 73 +++++++++++ util/test/multiprocess_exec_test.cc | 61 +++++++++ util/test/multiprocess_exec_test_child.cc | 51 ++++++++ util/test/posix/close_multiple.cc | 14 ++- util/test/posix/close_multiple.h | 5 +- util/util.gyp | 13 ++ 11 files changed, 452 insertions(+), 8 deletions(-) create mode 100644 util/test/executable_path.h create mode 100644 util/test/executable_path_mac.cc create mode 100644 util/test/executable_path_test.cc create mode 100644 util/test/multiprocess_exec.cc create mode 100644 util/test/multiprocess_exec.h create mode 100644 util/test/multiprocess_exec_test.cc create mode 100644 util/test/multiprocess_exec_test_child.cc diff --git a/util/test/executable_path.h b/util/test/executable_path.h new file mode 100644 index 00000000..4db0bea5 --- /dev/null +++ b/util/test/executable_path.h @@ -0,0 +1,29 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_TEST_EXECUTABLE_PATH_H_ +#define CRASHPAD_UTIL_TEST_EXECUTABLE_PATH_H_ + +#include "base/files/file_path.h" + +namespace crashpad { +namespace test { + +//! \brief Returns the pathname of the currently-running test executable. +base::FilePath ExecutablePath(); + +} // namespace test +} // namespace crashpad + +#endif // CRASHPAD_UTIL_TEST_EXECUTABLE_PATH_H_ diff --git a/util/test/executable_path_mac.cc b/util/test/executable_path_mac.cc new file mode 100644 index 00000000..9da7cc99 --- /dev/null +++ b/util/test/executable_path_mac.cc @@ -0,0 +1,37 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/executable_path.h" + +#include +#include + +#include "base/logging.h" + +namespace crashpad { +namespace test { + +base::FilePath ExecutablePath() { + uint32_t executable_length = 0; + _NSGetExecutablePath(NULL, &executable_length); + DCHECK_GT(executable_length, 1u); + std::string executable_path(executable_length, std::string::value_type()); + int rv = _NSGetExecutablePath(&executable_path[0], &executable_length); + DCHECK_EQ(rv, 0); + + return base::FilePath(executable_path); +} + +} // namespace test +} // namespace crashpad diff --git a/util/test/executable_path_test.cc b/util/test/executable_path_test.cc new file mode 100644 index 00000000..b99d3d91 --- /dev/null +++ b/util/test/executable_path_test.cc @@ -0,0 +1,30 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/executable_path.h" + +#include "base/files/file_path.h" +#include "gtest/gtest.h" + +namespace { + +using namespace crashpad::test; + +TEST(ExecutablePath, ExecutablePath) { + base::FilePath executable_path = ExecutablePath(); + base::FilePath executable_name = executable_path.BaseName(); + EXPECT_EQ("util_test", executable_name.value()); +} + +} // namespace diff --git a/util/test/multiprocess.h b/util/test/multiprocess.h index 80ee8415..40ab9faf 100644 --- a/util/test/multiprocess.h +++ b/util/test/multiprocess.h @@ -64,7 +64,7 @@ class Multiprocess { //! \code //! virtual void PreFork() override { //! Multiprocess::PreFork(); - //! if (testing::Test::HasFatalAssertions()) { + //! if (testing::Test::HasFatalFailure()) { //! return; //! } //! diff --git a/util/test/multiprocess_exec.cc b/util/test/multiprocess_exec.cc new file mode 100644 index 00000000..4cad7e0d --- /dev/null +++ b/util/test/multiprocess_exec.cc @@ -0,0 +1,145 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/multiprocess_exec.h" + +#include +#include +#include + +#include "base/posix/eintr_wrapper.h" +#include "gtest/gtest.h" +#include "util/misc/scoped_forbid_return.h" +#include "util/test/errors.h" +#include "util/test/posix/close_multiple.h" + +namespace crashpad { +namespace test { + +using namespace testing; + +MultiprocessExec::MultiprocessExec() + : Multiprocess(), + command_(), + arguments_(), + argv_() { +} + +void MultiprocessExec::SetChildCommand( + const std::string& command, const std::vector* arguments) { + command_ = command; + if (arguments) { + arguments_ = *arguments; + } else { + arguments_.clear(); + } +} + +MultiprocessExec::~MultiprocessExec() { +} + +void MultiprocessExec::PreFork() { + Multiprocess::PreFork(); + if (testing::Test::HasFatalFailure()) { + return; + } + + ASSERT_FALSE(command_.empty()); + + // Build up the argv vector. This is done in PreFork() instead of + // MultiprocessChild() because although the result is only needed in the child + // process, building it is a hazardous operation in that process. + ASSERT_TRUE(argv_.empty()); + + argv_.push_back(command_.c_str()); + for (const std::string& argument : arguments_) { + argv_.push_back(argument.c_str()); + } + argv_.push_back(NULL); +} + +void MultiprocessExec::MultiprocessChild() { + // Make sure that stdin, stdout, and stderr are FDs 0, 1, and 2, respectively. + // All FDs above this will be closed. + COMPILE_ASSERT(STDIN_FILENO == 0, stdin_must_be_fd_0); + COMPILE_ASSERT(STDOUT_FILENO == 1, stdout_must_be_fd_1); + COMPILE_ASSERT(STDERR_FILENO == 2, stderr_must_be_fd_2); + + // Move the read pipe to stdin. + int read_fd = ReadPipeFD(); + ASSERT_NE(read_fd, STDIN_FILENO); + ASSERT_NE(read_fd, STDOUT_FILENO); + ASSERT_EQ(STDIN_FILENO, fileno(stdin)); + + int rv = fpurge(stdin); + ASSERT_EQ(0, rv) << ErrnoMessage("fpurge"); + + rv = HANDLE_EINTR(dup2(read_fd, STDIN_FILENO)); + ASSERT_EQ(STDIN_FILENO, rv) << ErrnoMessage("dup2"); + + // Move the write pipe to stdout. + int write_fd = WritePipeFD(); + ASSERT_NE(write_fd, STDIN_FILENO); + ASSERT_NE(write_fd, STDOUT_FILENO); + ASSERT_EQ(STDOUT_FILENO, fileno(stdout)); + + // Make a copy of the original stdout file descriptor so that in case there’s + // an execv() failure, the original stdout can be restored so that gtest + // messages directed to stdout go to the right place. Mark it as + // close-on-exec, so that the child won’t see it after a successful exec(), + // but it will still be available in this process after an unsuccessful + // exec(). + int dup_orig_stdout_fd = dup(STDOUT_FILENO); + ASSERT_GE(dup_orig_stdout_fd, 0) << ErrnoMessage("dup"); + + rv = fcntl(dup_orig_stdout_fd, F_SETFD, FD_CLOEXEC); + ASSERT_NE(rv, -1) << ErrnoMessage("fcntl"); + + rv = HANDLE_EINTR(fflush(stdout)); + ASSERT_EQ(0, rv) << ErrnoMessage("fflush"); + + rv = HANDLE_EINTR(dup2(write_fd, STDOUT_FILENO)); + ASSERT_EQ(STDOUT_FILENO, rv) << ErrnoMessage("dup2"); + + CloseMultipleNowOrOnExec(STDERR_FILENO + 1, dup_orig_stdout_fd); + + // Start the new program, replacing this one. execv() has a weird declaration + // where its argv argument is declared as char* const*. In reality, the + // implementation behaves as if the argument were const char* const*, and this + // behavior is required by the standard. See + // http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html + // (search for “constant”). + execv(argv_[0], const_cast(&argv_[0])); + + // This should not normally be reached. Getting here means that execv() + // failed. + + // Be sure not to return until FAIL() is reached. + ScopedForbidReturn forbid_return; + + // Put the original stdout back. Close the copy of the write pipe FD that’s + // currently on stdout first, so that in case the dup2() that restores the + // original stdout fails, stdout isn’t left attached to the pipe when the + // FAIL() statement executes. + HANDLE_EINTR(fflush(stdout)); + IGNORE_EINTR(close(STDOUT_FILENO)); + HANDLE_EINTR(dup2(dup_orig_stdout_fd, STDOUT_FILENO)); + IGNORE_EINTR(close(dup_orig_stdout_fd)); + + forbid_return.Disarm(); + FAIL() << ErrnoMessage("execv") << ": " << argv_[0]; +} + +} // namespace test +} // namespace crashpad diff --git a/util/test/multiprocess_exec.h b/util/test/multiprocess_exec.h new file mode 100644 index 00000000..a5c0f11c --- /dev/null +++ b/util/test/multiprocess_exec.h @@ -0,0 +1,73 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_TEST_MULTIPROCESS_EXEC_H_ +#define CRASHPAD_UTIL_TEST_MULTIPROCESS_EXEC_H_ + +#include +#include + +#include "base/basictypes.h" +#include "util/test/multiprocess.h" + +namespace crashpad { +namespace test { + +//! \brief Manages an `exec()`-based multiprocess test. +//! +//! These tests are based on `fork()` and `exec()`. The parent process is able +//! to communicate with the child in the same manner as a base-class +//! Multiprocess parent. The read and write pipes appear in the child process on +//! stdin and stdout, respectively. +//! +//! Subclasses are expected to implement the parent in the same was as a +//! base-class Multiprocess parent. The child must be implemented in an +//! executable to be set by SetChildCommand(). +class MultiprocessExec : public Multiprocess { + public: + MultiprocessExec(); + + //! \brief Sets the command to `exec()` in the child. + //! + //! This method must be called before the test can be Run(). + //! + //! \param[in] command The executable’s pathname. + //! \param[in] arguments The command-line arguments to pass to the child + //! process in its `argv[]` vector. This vector must begin at `argv[1]`, + //! as \a command is implicitly used as `argv[0]`. This argument may be + //! `NULL` if no command-line arguments are to be passed. + void SetChildCommand(const std::string& command, + const std::vector* arguments); + + protected: + ~MultiprocessExec(); + + // Multiprocess: + virtual void PreFork() override; + + private: + // Multiprocess: + virtual void MultiprocessChild() override; + + std::string command_; + std::vector arguments_; + std::vector argv_; + + DISALLOW_COPY_AND_ASSIGN(MultiprocessExec); +}; + +} // namespace test +} // namespace crashpad + +#endif // CRASHPAD_UTIL_TEST_MULTIPROCESS_EXEC_H_ diff --git a/util/test/multiprocess_exec_test.cc b/util/test/multiprocess_exec_test.cc new file mode 100644 index 00000000..0edbbcf0 --- /dev/null +++ b/util/test/multiprocess_exec_test.cc @@ -0,0 +1,61 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/test/multiprocess_exec.h" + +#include + +#include "base/basictypes.h" +#include "gtest/gtest.h" +#include "util/file/fd_io.h" +#include "util/test/errors.h" +#include "util/test/executable_path.h" + +namespace { + +using namespace crashpad; +using namespace crashpad::test; + +class TestMultiprocessExec final : public MultiprocessExec { + public: + TestMultiprocessExec() : MultiprocessExec() {} + + ~TestMultiprocessExec() {} + + private: + virtual void MultiprocessParent() override { + int write_fd = WritePipeFD(); + char c = 'z'; + ssize_t rv = WriteFD(write_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("write"); + + int read_fd = ReadPipeFD(); + rv = ReadFD(read_fd, &c, 1); + ASSERT_EQ(1, rv) << ErrnoMessage("read"); + EXPECT_EQ('Z', c); + } + + DISALLOW_COPY_AND_ASSIGN(TestMultiprocessExec); +}; + +TEST(MultiprocessExec, MultiprocessExec) { + TestMultiprocessExec multiprocess_exec; + base::FilePath test_executable = ExecutablePath(); + std::string child_test_executable = + test_executable.value() + "_multiprocess_exec_test_child"; + multiprocess_exec.SetChildCommand(child_test_executable, NULL); + multiprocess_exec.Run(); +} + +} // namespace diff --git a/util/test/multiprocess_exec_test_child.cc b/util/test/multiprocess_exec_test_child.cc new file mode 100644 index 00000000..e1a06122 --- /dev/null +++ b/util/test/multiprocess_exec_test_child.cc @@ -0,0 +1,51 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include + +#include + +int main(int argc, char* argv[]) { + // Make sure that there’s nothing open at any FD higher than 3. All FDs other + // than stdin, stdout, and stderr should have been closed prior to or at + // exec(). + int max_fd = std::max(static_cast(sysconf(_SC_OPEN_MAX)), OPEN_MAX); + max_fd = std::max(max_fd, getdtablesize()); + for (int fd = STDERR_FILENO + 1; fd < max_fd; ++fd) { + if (close(fd) == 0 || errno != EBADF) { + abort(); + } + } + + // Read a byte from stdin, expecting it to be a specific value. + char c; + ssize_t rv = read(STDIN_FILENO, &c, 1); + if (rv != 1 || c != 'z') { + abort(); + } + + // Write a byte to stdout. + c = 'Z'; + rv = write(STDOUT_FILENO, &c, 1); + if (rv != 1) { + abort(); + } + + return 0; +} diff --git a/util/test/posix/close_multiple.cc b/util/test/posix/close_multiple.cc index 01555302..9c70e183 100644 --- a/util/test/posix/close_multiple.cc +++ b/util/test/posix/close_multiple.cc @@ -77,7 +77,7 @@ typedef scoped_ptr ScopedDIR; // system-specific FD directory to determine which file descriptors are open. // This is an advantage over looping over all possible file descriptors, because // no attempt needs to be made to close file descriptors that are not open. -bool CloseMultipleNowOrOnExecUsingFDDir(int fd) { +bool CloseMultipleNowOrOnExecUsingFDDir(int fd, int preserve_fd) { #if defined(OS_MACOSX) const char kFDDir[] = "/dev/fd"; #elif defined(OS_LINUX) @@ -120,7 +120,7 @@ bool CloseMultipleNowOrOnExecUsingFDDir(int fd) { return false; } - if (entry_fd >= fd && entry_fd != dir_fd) { + if (entry_fd >= fd && entry_fd != preserve_fd && entry_fd != dir_fd) { CloseNowOrOnExec(entry_fd, false); } } @@ -130,9 +130,9 @@ bool CloseMultipleNowOrOnExecUsingFDDir(int fd) { } // namespace -void CloseMultipleNowOrOnExec(int fd) { - if (CloseMultipleNowOrOnExecUsingFDDir(fd)) { - return; +void CloseMultipleNowOrOnExec(int fd, int preserve_fd) { + if (CloseMultipleNowOrOnExecUsingFDDir(fd, preserve_fd)) { + return; } // Fallback: close every file descriptor starting at |fd| and ending at the @@ -147,7 +147,9 @@ void CloseMultipleNowOrOnExec(int fd) { max_fd = std::max(max_fd, getdtablesize()); for (int entry_fd = fd; entry_fd < max_fd; ++entry_fd) { - CloseNowOrOnExec(entry_fd, true); + if (entry_fd != preserve_fd) { + CloseNowOrOnExec(entry_fd, true); + } } } diff --git a/util/test/posix/close_multiple.h b/util/test/posix/close_multiple.h index f0199c16..c2206bd0 100644 --- a/util/test/posix/close_multiple.h +++ b/util/test/posix/close_multiple.h @@ -34,7 +34,10 @@ namespace crashpad { //! protected against `close()`. //! //! \param[in] fd The lowest file descriptor to close or set as close-on-exec. -void CloseMultipleNowOrOnExec(int fd); +//! \param[in] preserve_fd A file descriptor to preserve and not close (or set +//! as close-on-exec), even if it is open and its value is greater than \a +//! fd. To not preserve any file descriptor, pass `-1` for this parameter. +void CloseMultipleNowOrOnExec(int fd, int preserve_fd); } // namespace crashpad diff --git a/util/util.gyp b/util/util.gyp index 7d3cf9f0..08eef382 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -100,12 +100,16 @@ 'sources': [ 'test/errors.cc', 'test/errors.h', + 'test/executable_path.h', + 'test/executable_path_mac.cc', 'test/mac/mach_errors.cc', 'test/mac/mach_errors.h', 'test/mac/mach_multiprocess.cc', 'test/mac/mach_multiprocess.h', 'test/multiprocess.cc', 'test/multiprocess.h', + 'test/multiprocess_exec.cc', + 'test/multiprocess_exec.h', 'test/posix/close_multiple.cc', 'test/posix/close_multiple.h', ], @@ -144,9 +148,18 @@ 'posix/process_util_test.cc', 'stdlib/strlcpy_test.cc', 'stdlib/strnlen_test.cc', + 'test/executable_path_test.cc', 'test/mac/mach_multiprocess_test.cc', + 'test/multiprocess_exec_test.cc', 'test/multiprocess_test.cc', ], }, + { + 'target_name': 'util_test_multiprocess_exec_test_child', + 'type': 'executable', + 'sources': [ + 'test/multiprocess_exec_test_child.cc', + ], + }, ], }