| /* |
| * SPDX-FileCopyrightText: Copyright OpenBMC Authors |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include "DbusMockTestBase.hpp" |
| |
| #include "MctpRequester.hpp" |
| |
| #include <linux/prctl.h> |
| #include <sys/prctl.h> |
| #include <sys/wait.h> |
| #include <systemd/sd-bus.h> |
| #include <unistd.h> |
| |
| #include <boost/asio/io_context.hpp> |
| #include <sdbusplus/asio/connection.hpp> |
| #include <sdbusplus/asio/object_server.hpp> |
| #include <sdbusplus/bus.hpp> |
| |
| // NOLINTNEXTLINE(modernize-deprecated-headers): kill()/SIGKILL are POSIX |
| #include <signal.h> |
| // NOLINTNEXTLINE(modernize-deprecated-headers): mkdtemp() is POSIX |
| #include <stdlib.h> |
| |
| #include <array> |
| #include <cerrno> |
| #include <chrono> |
| #include <cstdio> |
| #include <cstring> |
| #include <exception> |
| #include <filesystem> |
| #include <functional> |
| #include <memory> |
| #include <string> |
| #include <system_error> |
| #include <thread> |
| #include <type_traits> |
| |
| #include <gtest/gtest.h> |
| |
| [[maybe_unused]] static ::testing::Environment* const dbusEnv = |
| ::testing::AddGlobalTestEnvironment(new DbusEnvironment); |
| |
| boost::asio::io_context* DbusMockTestBase::io = nullptr; |
| std::shared_ptr<sdbusplus::asio::connection> DbusMockTestBase::conn; |
| std::unique_ptr<sdbusplus::asio::object_server> DbusMockTestBase::objectServer; |
| std::unique_ptr<mctp::MctpRequester> DbusMockTestBase::mctpRequester; |
| bool DbusMockTestBase::available = false; |
| std::string DbusMockTestBase::bringUpFailure; |
| |
| namespace |
| { |
| pid_t daemonPid = -1; |
| std::filesystem::path socketDir; |
| |
| // Long enough that a reply already on its way back through the daemon is not |
| // missed on a loaded machine. Paid once, when the binary is shutting down. |
| constexpr auto replyDrainSlice = std::chrono::milliseconds(200); |
| |
| pid_t spawnDaemon(const std::filesystem::path& dir, std::string& addr, |
| std::string& error); |
| void stopDaemon(pid_t& pid); |
| void removeSocketDir(); |
| } // namespace |
| |
| void DbusEnvironment::SetUp() |
| { |
| DbusMockTestBase::io = new boost::asio::io_context; |
| |
| if (!DbusMockTestBase::tryConnect()) |
| { |
| return; |
| } |
| |
| DbusMockTestBase::objectServer = |
| std::make_unique<sdbusplus::asio::object_server>( |
| DbusMockTestBase::conn); |
| DbusMockTestBase::mctpRequester = |
| std::make_unique<mctp::MctpRequester>(*DbusMockTestBase::io); |
| |
| DbusMockTestBase::available = true; |
| } |
| |
| void DbusEnvironment::TearDown() |
| { |
| if (!DbusMockTestBase::available) |
| { |
| return; |
| } |
| |
| // sdbusplus reclaims the state behind an async call only when the reply |
| // is dispatched, and stopping the loop outright abandons whatever is |
| // still queued. Drain it first, or a test that dropped its subject while |
| // a call was in flight leaves that state behind for LeakSanitizer to |
| // fail the whole binary over. |
| DbusMockTestBase::conn->flush(); |
| if (DbusMockTestBase::io->stopped()) |
| { |
| DbusMockTestBase::io->restart(); |
| } |
| while (DbusMockTestBase::io->run_one_for(replyDrainSlice) != 0) |
| {} |
| |
| DbusMockTestBase::io->stop(); |
| |
| DbusMockTestBase::mctpRequester.reset(); |
| DbusMockTestBase::objectServer.reset(); |
| DbusMockTestBase::conn.reset(); |
| |
| stopDaemon(daemonPid); |
| removeSocketDir(); |
| |
| DbusMockTestBase::available = false; |
| } |
| |
| std::shared_ptr<sdbusplus::asio::connection>& DbusMockTestBase::bus() |
| { |
| return conn; |
| } |
| |
| sdbusplus::asio::object_server& DbusMockTestBase::objects() |
| { |
| return *objectServer; |
| } |
| |
| mctp::MctpRequester& DbusMockTestBase::requester() |
| { |
| return *mctpRequester; |
| } |
| |
| boost::asio::io_context& DbusMockTestBase::ioContext() |
| { |
| return *io; |
| } |
| |
| bool DbusMockTestBase::hasBus() |
| { |
| return available; |
| } |
| |
| void DbusMockTestBase::SetUp() |
| { |
| if (!available) |
| { |
| GTEST_SKIP() << "No D-Bus session available: " << bringUpFailure; |
| } |
| } |
| |
| void DbusMockTestBase::TearDown() |
| { |
| if (!available) |
| { |
| return; |
| } |
| |
| // Flush and dispatch everything pending so the next test starts clean. |
| conn->flush(); |
| while (conn->process_discard()) |
| {} |
| } |
| |
| void DbusMockTestBase::drainPendingAsync() |
| { |
| if (io->stopped()) |
| { |
| io->restart(); |
| } |
| io->poll(); |
| } |
| |
| bool DbusMockTestBase::pumpIoUntil(const std::function<bool()>& done, |
| const std::chrono::seconds timeout) |
| { |
| constexpr auto pumpSlice = std::chrono::milliseconds(50); |
| const auto deadline = std::chrono::steady_clock::now() + timeout; |
| |
| while (!done()) |
| { |
| if (std::chrono::steady_clock::now() >= deadline) |
| { |
| return false; |
| } |
| if (io->stopped()) |
| { |
| io->restart(); |
| } |
| io->run_one_for(pumpSlice); |
| } |
| return true; |
| } |
| |
| bool DbusMockTestBase::tryConnect() |
| { |
| std::string dirTemplate = ::testing::TempDir() + "dbus-mock-XXXXXX"; |
| if (mkdtemp(dirTemplate.data()) == nullptr) |
| { |
| bringUpFailure = std::string("mkdtemp failed: ") + strerror(errno); |
| return false; |
| } |
| socketDir = dirTemplate; |
| |
| std::string addr; |
| daemonPid = spawnDaemon(socketDir, addr, bringUpFailure); |
| if (daemonPid < 0) |
| { |
| removeSocketDir(); |
| return false; |
| } |
| |
| if (!addr.starts_with("unix:")) |
| { |
| bringUpFailure = "dbus-daemon printed no usable address"; |
| stopDaemon(daemonPid); |
| removeSocketDir(); |
| return false; |
| } |
| |
| // Connect to the printed address directly; setenv(DBUS_SESSION_BUS_ADDRESS) |
| // + bus::new_user() traps under valgrind (hardened glibc setenv). |
| // NOLINTNEXTLINE(misc-include-cleaner): sd_bus comes from systemd/sd-bus.h |
| sd_bus* rawBus = nullptr; |
| int rc = sd_bus_new(&rawBus); |
| if (rc >= 0) |
| { |
| rc = sd_bus_set_address(rawBus, addr.c_str()); |
| } |
| if (rc >= 0) |
| { |
| rc = sd_bus_set_bus_client(rawBus, 1); |
| } |
| if (rc >= 0) |
| { |
| // Must run before bus_t wraps the bus: its constructor calls |
| // get_unique_name(), which needs a started bus. |
| rc = sd_bus_start(rawBus); |
| } |
| if (rc < 0) |
| { |
| bringUpFailure = std::string("bus connect failed: ") + strerror(-rc); |
| sd_bus_unref(rawBus); |
| stopDaemon(daemonPid); |
| removeSocketDir(); |
| return false; |
| } |
| |
| try |
| { |
| // bus_t adopts rawBus and unrefs it on every path. |
| conn = std::make_shared<sdbusplus::asio::connection>( |
| *io, sdbusplus::bus_t(rawBus, std::false_type{})); |
| return true; |
| } |
| catch (const std::exception& e) |
| { |
| bringUpFailure = std::string("connection failed: ") + e.what(); |
| stopDaemon(daemonPid); |
| removeSocketDir(); |
| return false; |
| } |
| } |
| |
| namespace |
| { |
| |
| pid_t spawnDaemon(const std::filesystem::path& dir, std::string& addr, |
| std::string& error) |
| { |
| std::array<int, 2> pipeFds{}; |
| if (pipe(pipeFds.data()) < 0) |
| { |
| error = std::string("pipe failed: ") + strerror(errno); |
| return -1; |
| } |
| |
| const pid_t parentPid = getpid(); |
| const pid_t pid = fork(); |
| if (pid < 0) |
| { |
| error = std::string("fork failed: ") + strerror(errno); |
| close(pipeFds[0]); |
| close(pipeFds[1]); |
| return -1; |
| } |
| |
| if (pid == 0) |
| { |
| // Die with the test binary even when it crashes past teardown. |
| // No --fork: it would reparent the daemon and clear the death signal. |
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg): fixed C API |
| prctl(PR_SET_PDEATHSIG, SIGTERM); |
| // The signal only arms after prctl; bail if the parent already died. |
| if (getppid() != parentPid) |
| { |
| _exit(1); |
| } |
| dup2(pipeFds[1], STDOUT_FILENO); |
| close(pipeFds[0]); |
| close(pipeFds[1]); |
| |
| // execvp wants mutable argument strings. |
| std::string arg0 = "dbus-daemon"; |
| std::string arg1 = "--session"; |
| std::string arg2 = "--print-address=1"; |
| std::string arg3 = "--address=unix:tmpdir=" + dir.string(); |
| std::array<char*, 5> argv = {arg0.data(), arg1.data(), arg2.data(), |
| arg3.data(), nullptr}; |
| execvp(arg0.c_str(), argv.data()); |
| _exit(127); |
| } |
| |
| // Read the address line; EOF (empty addr) means execvp failed. |
| close(pipeFds[1]); |
| FILE* const out = fdopen(pipeFds[0], "r"); |
| if (out == nullptr) |
| { |
| error = "fdopen failed"; |
| close(pipeFds[0]); |
| return pid; |
| } |
| |
| std::array<char, 512> buffer{}; |
| if (fgets(buffer.data(), static_cast<int>(buffer.size()), out) != nullptr) |
| { |
| addr = buffer.data(); |
| if (!addr.empty() && addr.back() == '\n') |
| { |
| addr.pop_back(); |
| } |
| } |
| fclose(out); |
| |
| return pid; |
| } |
| |
| void stopDaemon(pid_t& pid) |
| { |
| if (pid <= 0) |
| { |
| return; |
| } |
| |
| kill(pid, SIGTERM); |
| |
| // Direct fork() child, so waitpid is authoritative (no pid-reuse races). |
| constexpr auto stopPollInterval = std::chrono::milliseconds(20); |
| constexpr auto stopTimeout = std::chrono::seconds(1); |
| const auto deadline = std::chrono::steady_clock::now() + stopTimeout; |
| |
| while (std::chrono::steady_clock::now() < deadline) |
| { |
| const pid_t reaped = waitpid(pid, nullptr, WNOHANG); |
| if (reaped == pid || (reaped < 0 && errno == ECHILD)) |
| { |
| pid = -1; |
| return; |
| } |
| std::this_thread::sleep_for(stopPollInterval); |
| } |
| |
| // SIGKILL cannot be ignored; a blocking waitpid reaps promptly. |
| kill(pid, SIGKILL); |
| waitpid(pid, nullptr, 0); |
| pid = -1; |
| } |
| |
| void removeSocketDir() |
| { |
| if (socketDir.empty()) |
| { |
| return; |
| } |
| std::error_code ec; |
| std::filesystem::remove_all(socketDir, ec); |
| socketDir.clear(); |
| } |
| |
| } // namespace |