blob: 25f0767d42e69742ae360a079538b9b3d8c06006 [file] [edit]
/*
* 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