| #include "systemd_handler.hpp" |
| |
| #include "log.hpp" |
| |
| #include <fmt/printf.h> |
| |
| #include <boost/interprocess/file_mapping.hpp> |
| #include <boost/interprocess/mapped_region.hpp> |
| |
| #include <regex> |
| |
| namespace boot_time_monitor |
| { |
| namespace systemd |
| { |
| |
| namespace btm = boot_time_monitor; |
| |
| constexpr std::string_view kSystemdFirmwareTime = "FirmwareTimestampMonotonic"; |
| constexpr std::string_view kSystemdLoaderTime = "LoaderTimestampMonotonic"; |
| constexpr std::string_view kSystemdKernelTime = "KernelTimestampMonotonic"; |
| constexpr std::string_view kSystemdInitRDTime = "InitRDTimestampMonotonic"; |
| constexpr std::string_view kSystemdUserspaceTime = |
| "UserspaceTimestampMonotonic"; |
| constexpr std::string_view kSystemdFinishTime = "FinishTimestampMonotonic"; |
| |
| constexpr int64_t kMillisecondPerSecond = 1000; |
| |
| struct SystemdTimestamp |
| { |
| uint64_t firmware; |
| uint64_t loader; |
| uint64_t kernel; |
| uint64_t initRD; |
| uint64_t userspace; |
| uint64_t finish; |
| }; |
| |
| struct SystemdDuration |
| { |
| int64_t firmware; |
| int64_t loader; |
| int64_t kernel; |
| int64_t initRD; |
| int64_t userspace; |
| }; |
| |
| namespace |
| { |
| // How to calculate duration for each stage: |
| // https://github.com/systemd/systemd/blob/82b7bf8c1c8c6ded6f56b43998c803843a3b944b/src/analyze/analyze-time-data.c#L176-L186 |
| // Special calculation for kernel duration: |
| // https://github.com/systemd/systemd/blob/82b7bf8c1c8c6ded6f56b43998c803843a3b944b/src/analyze/analyze-time-data.c#L84-L87 |
| inline SystemdDuration CalculateSystemdDuration(SystemdTimestamp times) |
| { |
| int64_t firmware = static_cast<int64_t>(times.firmware); |
| int64_t loader = static_cast<int64_t>(times.loader); |
| int64_t initRD = static_cast<int64_t>(times.initRD); |
| int64_t userspace = static_cast<int64_t>(times.userspace); |
| int64_t finish = static_cast<int64_t>(times.finish); |
| SystemdDuration durations; |
| durations.kernel = userspace; |
| |
| if (firmware != 0) |
| { |
| durations.firmware = firmware - loader; |
| } |
| |
| if (loader != 0) |
| { |
| durations.loader = loader; |
| } |
| |
| if (initRD > 0) |
| { |
| durations.kernel = initRD; |
| } |
| |
| if (initRD != 0) |
| { |
| durations.initRD = userspace - initRD; |
| } |
| |
| if (userspace != 0) |
| { |
| durations.userspace = finish - userspace; |
| } |
| return durations; |
| } |
| |
| inline std::string convertName(std::string_view name) |
| { |
| return std::regex_replace(std::string(name), |
| std::regex("TimestampMonotonic"), ""); |
| } |
| |
| } // namespace |
| |
| Handler::Handler(std::shared_ptr<sdbusplus::asio::connection> conn, |
| std::shared_ptr<btm::api::IBoottimeApi> api) : |
| mApi(std::move(api)), mBMCFinishTimer(conn->get_io_context()) |
| { |
| mBMCFinishTimer.expires_after(std::chrono::seconds(0)); |
| mBMCFinishTimer.async_wait( |
| [this, &conn](const boost::system::error_code& ec) { |
| CheckBmcFinish(ec, conn); |
| }); |
| } |
| |
| void Handler::CheckBmcFinish(const boost::system::error_code& ec, |
| std::shared_ptr<sdbusplus::asio::connection>& conn) |
| { |
| if (ec == boost::asio::error::operation_aborted) |
| { |
| fmt::print(stderr, |
| "[CheckBmcFinish] BmcFinishTimer is being " |
| "canceled, Error: {}\n", |
| ec.message().c_str()); |
| return; |
| } |
| |
| if (ec) |
| { |
| fmt::print(stderr, "[CheckBmcFinish] Timer Error: {}\n", |
| ec.message().c_str()); |
| return; |
| } |
| |
| // The raw pointer here is safe since it points to an existing instance |
| // instead of allocating a memory space. |
| // We can't use `std::share_ptr<uint64_t>(×.firmware)` because the |
| // `times` will free its space while `share_ptr` will also free the same |
| // space again when this constructor completed. It will cause |
| // `free(): double free detected` or `free(): invalid pointer` error. |
| SystemdTimestamp times; |
| std::array<std::pair<std::string, uint64_t*>, 6> bootTimestamp = { |
| std::make_pair( // Firmware |
| std::string(kSystemdFirmwareTime), ×.firmware), |
| std::make_pair( // Loader |
| std::string(kSystemdLoaderTime), ×.loader), |
| std::make_pair( // Kernel |
| std::string(kSystemdKernelTime), ×.kernel), |
| std::make_pair( // InitRD |
| std::string(kSystemdInitRDTime), ×.initRD), |
| std::make_pair( // Userspace |
| std::string(kSystemdUserspaceTime), ×.userspace), |
| std::make_pair( // Finish |
| std::string(kSystemdFinishTime), ×.finish)}; |
| |
| for (const auto& timestamp : bootTimestamp) |
| { |
| auto method = conn->new_method_call( |
| "org.freedesktop.systemd1", // Systemd Service |
| "/org/freedesktop/systemd1", // Systemd Path |
| "org.freedesktop.DBus.Properties", // Systemd Iface |
| "Get"); // Systemd Func |
| method.append("org.freedesktop.systemd1.Manager", |
| std::string(timestamp.first)); |
| std::variant<uint64_t> result; |
| try |
| { |
| conn->call(method).read(result); |
| } |
| catch (const sdbusplus::exception::SdBusError& e) |
| { |
| fmt::print( |
| stderr, |
| "[CheckBmcFinish] Can't get systemd property {}. ERROR={}\n", |
| std::string(timestamp.first), e.what()); |
| // Restart the timer to keep polling the APML_ALERT status |
| mBMCFinishTimer.expires_after(kCheckBmcFinishInterval); |
| mBMCFinishTimer.async_wait( |
| [this, &conn](const boost::system::error_code& ec) { |
| CheckBmcFinish(ec, conn); |
| }); |
| } |
| *timestamp.second = std::get<uint64_t>(result); |
| } |
| |
| // This daemon may start before the userspace is fully ready. So we need |
| // to confirm if userspace is fully ready by checking `times.finish` |
| // equals zero or not. |
| if (times.finish == 0) |
| { |
| fmt::print( |
| stderr, |
| "[CheckBmcFinish] `FinishTimestampMonotonic` is not ready yet\n"); |
| mBMCFinishTimer.expires_after(kCheckBmcFinishInterval); |
| mBMCFinishTimer.async_wait( |
| [this, &conn](const boost::system::error_code& ec) { |
| CheckBmcFinish(ec, conn); |
| }); |
| } |
| |
| absl::Status status; |
| SystemdDuration durations = CalculateSystemdDuration(times); |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, convertName(kSystemdFirmwareTime), |
| durations.firmware / kMillisecondPerSecond); |
| btm::log::LogIfError(status); |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, convertName(kSystemdLoaderTime), |
| durations.loader / kMillisecondPerSecond); |
| btm::log::LogIfError(status); |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, convertName(kSystemdKernelTime), |
| durations.kernel / kMillisecondPerSecond); |
| btm::log::LogIfError(status); |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, convertName(kSystemdInitRDTime), |
| durations.initRD / kMillisecondPerSecond); |
| btm::log::LogIfError(status); |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, convertName(kSystemdUserspaceTime), |
| durations.userspace / kMillisecondPerSecond); |
| btm::log::LogIfError(status); |
| |
| #ifdef NPCM7XX_OR_NEWER |
| // NPCM7XX or newer Nuvoton BMC has a register that starts counting from |
| // SoC power on. Also uptime starts counting when kernel is up. Thus we |
| // can get (Firmware + Loader) time by `value[SEC_CNT_ADDR] - uptime`. |
| constexpr uint32_t SEC_CNT_ADDR = 0xF0801068; |
| |
| auto powerOnSec = readMem4Bytes(SEC_CNT_ADDR); |
| auto upTimeMS = getUpTimeInMs(); |
| if (powerOnSec != std::nullopt && upTimeMS != std::nullopt) |
| { |
| status = mApi->SetNodesDurationByTag( |
| btm::kBootTimeTagBMC, "FirmwarePlusLoader", |
| powerOnSec.value() * 1000 - upTimeMS.value()); |
| btm::log::LogIfError(status); |
| } |
| #endif |
| |
| // BMC marks the boot process as `completed` automatically if we do *NOT* have |
| // external daemon to do so. |
| #ifdef AUTO_BMC_COMPLETE |
| status = mApi->SetNodesCheckpointByTag(btm::kBootTimeTagBMC, "UserspaceEnd", |
| 0, 0); |
| btm::log::LogIfError(status); |
| status = mApi->NotifyNodesCompleteByTag(btm::kBootTimeTagBMC); |
| btm::log::LogIfError(status); |
| #endif |
| } |
| |
| #ifdef NPCM7XX_OR_NEWER |
| std::optional<uint32_t> Handler::readMem4Bytes(uint32_t target) |
| { |
| // Typically the pageSize will be 4K/8K for 32 bit operating systems |
| uint32_t pageSize = boost::interprocess::mapped_region::get_page_size(); |
| const uint32_t kPageMask = pageSize - 1; |
| |
| uint32_t pageOffset = target & (~kPageMask); |
| uint32_t offsetInPage = target & kPageMask; |
| |
| // Map `/dev/mem` to a region. |
| std::unique_ptr<boost::interprocess::file_mapping> fileMapping; |
| std::unique_ptr<boost::interprocess::mapped_region> MappedRegion; |
| try |
| { |
| fileMapping = std::make_unique<boost::interprocess::file_mapping>( |
| "/dev/mem", boost::interprocess::read_only); |
| // No need to unmap in the end. |
| MappedRegion = std::make_unique<boost::interprocess::mapped_region>( |
| *fileMapping, boost::interprocess::read_only, pageOffset, |
| pageSize * 2); |
| |
| // MappedRegion->get_address() returns (void*) which needs to covert |
| // into (char*) to make `+ offsetInPage` work. |
| // Then converts it again into (uint32_t*) since we read 4 bytes. |
| return *(reinterpret_cast<uint32_t*>( |
| static_cast<char*>(MappedRegion->get_address()) + offsetInPage)); |
| } |
| catch (const std::exception& e) |
| { |
| fmt::print(stderr, "[{}]: Throw exception: %s\n", __FUNCTION__, |
| e.what()); |
| return std::nullopt; |
| } |
| |
| fmt::print(stderr, "[{}]: Shouldn't go to this line.\n", __FUNCTION__); |
| return std::nullopt; |
| } |
| |
| std::optional<int64_t> Handler::getUpTimeInMs() |
| { |
| constexpr int32_t kMillisecondPerSecond = 1000; |
| std::ifstream fin("/proc/uptime"); |
| if (!fin.is_open()) |
| { |
| fmt::print(stderr, "[{}]: Can not read \"/proc/uptime\"\n", |
| __FUNCTION__); |
| return std::nullopt; |
| } |
| double uptimeSec; |
| fin >> uptimeSec; |
| fin.close(); |
| |
| return static_cast<int64_t>(uptimeSec * kMillisecondPerSecond); |
| } |
| #endif // NPCM7XX_OR_NEWER |
| |
| } // namespace systemd |
| } // namespace boot_time_monitor |