Major changes for supporting multihost, including some code refactoring.
Local Unit Test:
https://paste.googleplex.com/5874223018672128
Tested on AMD single-host:
https://paste.googleplex.com/5323461163220992
Tested on ARM multi-host:
https://paste.googleplex.com/5179956709294080
Google-Bug-Id: 378795793
Change-Id: I904a3dee8a5c1812649bd1ba610b328d16f98e09
diff --git a/include/api.hpp b/include/api.hpp
new file mode 100644
index 0000000..52cb283
--- /dev/null
+++ b/include/api.hpp
@@ -0,0 +1,207 @@
+#pragma once
+
+#include "node_config.hpp"
+#include "resource.hpp"
+
+#include <map>
+#include <memory>
+#include <string_view>
+
+namespace boot_time_monitor
+{
+
+namespace api
+{
+
+namespace btm = boot_time_monitor;
+
+/**
+ * @brief Interface for managing boot time monitoring data for different nodes.
+ *
+ * This interface defines the contract for setting and retrieving boot
+ * checkpoints and durations for specific nodes or groups of nodes identified
+ * by tags. It also handles notifications for boot completion and provides
+ * methods to check the reboot status of a node.
+ */
+class IApi
+{
+ public:
+ virtual ~IApi() = default;
+
+ /**
+ * @brief Sets a boot checkpoint for a specific node.
+ *
+ * @param nodeConfig The configuration of the target node.
+ * @param checkpointName The name of the checkpoint.
+ * @param wallTime The wall clock time (epoch time in ms) when the
+ * checkpoint was reached. If 0, the current time is used.
+ * @param selfMeasuredDuration The duration (in ms) measured by the
+ * component reporting the checkpoint. If
+ * non-zero, an additional checkpoint marking the beginning of this duration
+ * might be recorded.
+ */
+ virtual void SetNodeCheckpoint(const btm::NodeConfig& nodeConfig,
+ std::string_view checkpointName,
+ int64_t wallTime,
+ int64_t selfMeasuredDuration) = 0;
+
+ /**
+ * @brief Sets a boot checkpoint for all nodes matching a specific tag.
+ *
+ * @param tag The tag to identify the target nodes.
+ * @param checkpointName The name of the checkpoint.
+ * @param wallTime The wall clock time (epoch time in ms) when the
+ * checkpoint was reached. If 0, the current time is used.
+ * @param selfMeasuredDuration The duration (in ms) measured by the
+ * component reporting the checkpoint.
+ */
+ virtual void SetNodesCheckpointByTag(std::string_view tag,
+ std::string_view checkpointName,
+ int64_t wallTime,
+ int64_t selfMeasuredDuration) = 0;
+
+ /**
+ * @brief Sets a boot duration measurement for a specific node.
+ *
+ * @param nodeConfig The configuration of the target node.
+ * @param durationName The name of the duration measurement.
+ * @param duration The measured duration in milliseconds.
+ */
+ virtual void SetNodeDuration(const btm::NodeConfig& nodeConfig,
+ std::string_view durationName,
+ int64_t duration) = 0;
+
+ /**
+ * @brief Sets a boot duration measurement for all nodes matching a specific
+ * tag.
+ *
+ * @param tag The tag to identify the target nodes.
+ * @param durationName The name of the duration measurement.
+ * @param duration The measured duration in milliseconds.
+ */
+ virtual void SetNodesDurationByTag(std::string_view tag,
+ std::string_view durationName,
+ int64_t duration) = 0;
+
+ /**
+ * @brief Sets a Power Supply Unit (PSU) related boot checkpoint for all
+ * nodes. PSU events are typically broadcast to all nodes.
+ *
+ * @param checkpointName The name of the PSU checkpoint.
+ * @param wallTime The wall clock time (epoch time in ms).
+ * @param selfMeasuredDuration The self-measured duration (in ms).
+ */
+ virtual void SetPSUCheckpoint(std::string_view checkpointName,
+ int64_t wallTime,
+ int64_t selfMeasuredDuration) = 0;
+
+ /**
+ * @brief Sets a Power Supply Unit (PSU) related boot duration for all
+ * nodes. PSU events are typically broadcast to all nodes.
+ *
+ * @param durationName The name of the PSU duration.
+ * @param duration The measured duration in milliseconds.
+ */
+ virtual void SetPSUDuration(std::string_view durationName,
+ int64_t duration) = 0;
+
+ /**
+ * @brief Notifies that the boot process for a specific node is complete.
+ * This typically involves finalizing the current boot record.
+ *
+ * @param nodeConfig The configuration of the completed node.
+ */
+ virtual void NotifyNodeComplete(const btm::NodeConfig& nodeConfig) = 0;
+
+ /**
+ * @brief Notifies that the boot process for all nodes matching a specific
+ * tag is complete.
+ *
+ * @param tag The tag identifying the completed nodes.
+ */
+ virtual void NotifyNodesCompleteByTag(std::string_view tag) = 0;
+
+ /**
+ * @brief Retrieves the list of recorded checkpoints for a specific node.
+ *
+ * @param nodeConfig The configuration of the target node.
+ * @return A vector of tuples, each containing (checkpoint name, wall time
+ * ms, monotonic time ms).
+ */
+ virtual std::vector<std::tuple<std::string, int64_t, int64_t>>
+ GetNodeCheckpointList(const btm::NodeConfig& nodeConfig) = 0;
+
+ /**
+ * @brief Retrieves the list of recorded additional durations for a specific
+ * node.
+ *
+ * @param nodeConfig The configuration of the target node.
+ * @return A vector of tuples, each containing (duration name, duration ms).
+ */
+ virtual std::vector<std::tuple<std::string, int64_t>>
+ GetNodeAdditionalDurations(const btm::NodeConfig& nodeConfig) = 0;
+
+ /**
+ * @brief Checks if a specific node is currently in the process of
+ * rebooting.
+ *
+ * @param nodeConfig The configuration of the target node.
+ * @return True if the node is considered to be rebooting, false otherwise.
+ */
+ virtual bool IsNodeRebooting(const btm::NodeConfig& nodeConfig) = 0;
+};
+
+/**
+ * @brief Concrete implementation of the IApi interface.
+ *
+ * This class manages multiple `IResource` instances (one per node)
+ * and provides thread-safe access to them via a mutex. It acts as the central
+ * point for interacting with boot time data storage.
+ */
+class Api : public IApi
+{
+ public:
+ virtual ~Api() override;
+ void SetNodeCheckpoint(const btm::NodeConfig& nodeConfig,
+ std::string_view checkpointName, int64_t wallTime,
+ int64_t selfMeasuredDuration) override;
+ void SetNodesCheckpointByTag(std::string_view tag,
+ std::string_view checkpointName,
+ int64_t wallTime,
+ int64_t selfMeasuredDuration) override;
+ void SetNodeDuration(const btm::NodeConfig& nodeConfig,
+ std::string_view durationName,
+ int64_t duration) override;
+ void SetNodesDurationByTag(std::string_view tag,
+ std::string_view durationName,
+ int64_t duration) override;
+ void SetPSUCheckpoint(std::string_view checkpointName, int64_t wallTime,
+ int64_t selfMeasuredDuration) override;
+ void SetPSUDuration(std::string_view durationName,
+ int64_t duration) override;
+ void NotifyNodeComplete(const btm::NodeConfig& nodeConfig) override;
+ void NotifyNodesCompleteByTag(std::string_view tag) override;
+ std::vector<std::tuple<std::string, int64_t, int64_t>>
+ GetNodeCheckpointList(const btm::NodeConfig& nodeConfig) override;
+ std::vector<std::tuple<std::string, int64_t>>
+ GetNodeAdditionalDurations(const btm::NodeConfig& nodeConfig) override;
+ bool IsNodeRebooting(const btm::NodeConfig& nodeConfig) override;
+
+ /**
+ * @brief Registers a new node with the API.
+ *
+ * This creates a corresponding resource handler (e.g., a File resource)
+ * for the given node configuration.
+ *
+ * @param nodeConfig The configuration of the node to register.
+ */
+ void RegisterNode(const btm::NodeConfig& nodeConfig);
+
+ private:
+ // Map storing the resource handler for each registered node.
+ std::map<NodeConfig, std::unique_ptr<resource::IResource>> mResourceMap;
+ std::mutex mApiMutex; // An api mutex is required for protecting
+ // misordering of the record.
+};
+} // namespace api
+} // namespace boot_time_monitor
diff --git a/include/bmc_monitor_app.hpp b/include/bmc_monitor_app.hpp
deleted file mode 100644
index 2ff50fc..0000000
--- a/include/bmc_monitor_app.hpp
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-
-#include "boot_manager.hpp"
-#include "dbus_handler.hpp"
-#include "utils.hpp"
-
-#include <sdbusplus/asio/connection.hpp>
-#include <sdbusplus/bus.hpp>
-
-#include <memory>
-#include <string_view>
-
-namespace boot_time_monitor
-{
-
-class BMCMonitorApp
-{
- public:
- constexpr static std::string_view kNodeName = "bmc";
- constexpr static std::string_view kObjPath =
- "/xyz/openbmc_project/time/boot/bmc";
-
- BMCMonitorApp(sdbusplus::bus::bus& bus,
- const std::shared_ptr<sdbusplus::asio::connection>& conn);
-
- private:
- sdbusplus::server::manager::manager objManager;
- std::shared_ptr<Util> util;
- std::shared_ptr<FileUtil> cpCSV;
- std::shared_ptr<FileUtil> durCSV;
- std::shared_ptr<BootManager> bootManager;
- std::shared_ptr<DbusHandler> dbusHandler;
-};
-
-} // namespace boot_time_monitor
diff --git a/include/boot_manager.hpp b/include/boot_manager.hpp
deleted file mode 100644
index 32c0022..0000000
--- a/include/boot_manager.hpp
+++ /dev/null
@@ -1,108 +0,0 @@
-#pragma once
-
-#include "utils.hpp"
-
-#include <cstdint>
-#include <memory>
-#include <string_view>
-
-namespace boot_time_monitor
-{
-
-/**
- * An interface class for the Boot Manager APIs
- */
-class BootManagerIface
-{
- public:
- virtual ~BootManagerIface() = default;
-
- /**
- * Set a new checkpoint to Boot Manager
- *
- * @param[in] cpName - Checkpoint name
- * @param[in] externalWallTime - Current epoch time in milliseconds
- * @param[in] duration - Self measured duration in milliseconds
- */
- virtual void setCheckpoint(std::string_view cpName,
- int64_t externalWallTime, int64_t duration) = 0;
-
- /**
- * Set a new duration to Boot Manager
- *
- * @param[in] durName - Duration name
- * @param[in] duration - Self measured duration in milliseconds
- */
- virtual void setDuration(std::string_view durName, int64_t duration) = 0;
-
- /**
- * Inform Boot Manager that current reboot process has completed.
- */
- virtual void notifyComplete() = 0;
-
- /**
- * Check if we are in a reboot process
- *
- * @return true if it's rebooting
- */
- virtual bool isRebooting() = 0;
-
- /**
- * Get current checkpoints
- *
- * @return vector of current checkpoints
- */
- virtual const std::vector<Checkpoint>& getCheckpoints() const = 0;
-
- /**
- * Get current durations
- *
- * @return vector of current durations
- */
- virtual const std::vector<Duration>& getDurations() const = 0;
-
- /**
- * Get previous checkpoints
- *
- * @return vector of previous checkpoints
- */
- virtual const std::vector<Checkpoint>& getPreCheckpoints() const = 0;
-
- /**
- * Get previous durations
- *
- * @return vector of previous durations
- */
- virtual const std::vector<Duration>& getPreDurations() const = 0;
-};
-
-class BootManager : public BootManagerIface
-{
- public:
- constexpr static std::string_view kBeginStageSuffix = ":BEGIN";
-
- BootManager(std::shared_ptr<UtilIface> util,
- const std::shared_ptr<FileUtilIface>& cpCSV,
- const std::shared_ptr<FileUtilIface>& durCSV);
- void setCheckpoint(std::string_view cpName, int64_t externalWallTime,
- int64_t duration) override;
- void setDuration(std::string_view durName, int64_t duration) override;
- void notifyComplete() override;
- bool isRebooting() override;
- const std::vector<Checkpoint>& getCheckpoints() const override;
- const std::vector<Duration>& getDurations() const override;
- const std::vector<Checkpoint>& getPreCheckpoints() const override;
- const std::vector<Duration>& getPreDurations() const override;
-
- private:
- std::vector<Checkpoint> checkpoints;
- std::vector<Duration> durations;
- std::vector<Checkpoint> preCheckpoints;
- std::vector<Duration> preDurations;
-
- std::shared_ptr<UtilIface> util;
- std::shared_ptr<FileUtilIface> cpCSV;
- std::shared_ptr<FileUtilIface> durCSV;
-};
-
-} // namespace boot_time_monitor
diff --git a/include/dbus_handler.hpp b/include/dbus_handler.hpp
index 9767bab..b8d39e1 100644
--- a/include/dbus_handler.hpp
+++ b/include/dbus_handler.hpp
@@ -1,7 +1,7 @@
#pragma once
+#include "api.hpp"
-#include "boot_manager.hpp"
-#include "utils.hpp"
+#include <fmt/printf.h>
#include <sdbusplus/asio/object_server.hpp>
#include <sdbusplus/bus/match.hpp>
@@ -16,7 +16,32 @@
namespace boot_time_monitor
{
-class DbusHandler :
+namespace dbus
+{
+
+namespace btm = boot_time_monitor;
+
+/**
+ * @brief Configuration for the D-Bus handler.
+ */
+struct DbusConfig
+{
+ /** @brief The D-Bus object path for this handler instance. */
+ std::string dbusObjPath;
+};
+
+/**
+ * @brief Handles D-Bus requests related to boot time monitoring for a specific
+ * node.
+ *
+ * This class implements the D-Bus interfaces for Checkpoint, Duration, and
+ * Statistic defined under `xyz.openbmc_project.Time.Boot`. It acts as a
+ * bridge between incoming D-Bus calls and the underlying boot time monitoring
+ * API (`api::IApi`). Each instance of this handler is associated with a
+ * specific node configuration (`NodeConfig`) and interacts with the central API
+ * to record or retrieve boot time data for that node.
+ */
+class Handler :
sdbusplus::server::object::object<
sdbusplus::server::xyz::openbmc_project::time::boot::Duration>,
sdbusplus::server::object::object<
@@ -25,23 +50,84 @@
sdbusplus::server::xyz::openbmc_project::time::boot::Statistic>
{
public:
- DbusHandler(sdbusplus::bus::bus& dbus, const std::string& objPath,
- std::shared_ptr<BootManagerIface> bootManager,
- std::shared_ptr<UtilIface> util);
+ /**
+ * @brief Constructs the D-Bus handler.
+ *
+ * @param dbus The sdbusplus bus connection.
+ * @param nodeConfig Configuration of the node this handler manages.
+ * @param dbusConfig D-Bus specific configuration (object path).
+ * @param api Shared pointer to the central boot time monitoring API.
+ */
+ Handler(sdbusplus::bus::bus& dbus, const btm::NodeConfig& nodeConfig,
+ const btm::dbus::DbusConfig& dbusConfig,
+ std::shared_ptr<btm::api::IApi> api);
+ /**
+ * @brief D-Bus method implementation for setting a boot checkpoint.
+ *
+ * Forwards the request to the appropriate API method
+ * (`SetNodeCheckpoint` or `SetPSUCheckpoint`).
+ *
+ * @param checkpointName The name of the checkpoint.
+ * @param wallTime The wall clock time (epoch time in ms).
+ * @param selfMeasuredDuration Optional self-measured duration (ms).
+ */
void setCheckpoint(std::string checkpointName, int64_t wallTime,
int64_t selfMeasuredDuration) override;
+
+ /**
+ * @brief D-Bus method implementation for retrieving the checkpoint list.
+ *
+ * Retrieves the data from the API via `GetNodeCheckpointList`.
+ *
+ * @return A vector of tuples: (checkpoint name, wall time ms, monotonic
+ * time ms).
+ */
std::vector<std::tuple<std::string, int64_t, int64_t>>
getCheckpointList() override;
+
+ /**
+ * @brief D-Bus method implementation to signal boot completion.
+ *
+ * Calls `NotifyNodeComplete` on the API if the node is currently rebooting.
+ */
void rebootComplete() override;
+
+ /**
+ * @brief D-Bus method implementation for setting a boot duration.
+ *
+ * Forwards the request to the appropriate API method
+ * (`SetNodeDuration` or `SetPSUDuration`).
+ *
+ * @param durationName The name of the duration measurement.
+ * @param duration The measured duration in milliseconds.
+ */
void setDuration(std::string durationName, int64_t duration) override;
+
+ /**
+ * @brief D-Bus method implementation for retrieving additional durations.
+ *
+ * Retrieves the data from the API via `GetNodeAdditionalDurations`.
+ *
+ * @return A vector of tuples: (duration name, duration ms).
+ */
std::vector<std::tuple<std::string, int64_t>>
getAdditionalDurations() override;
+
+ /**
+ * @brief D-Bus property getter implementation for checking reboot status.
+ *
+ * Retrieves the status from the API via `IsNodeRebooting`.
+ *
+ * @return True if the node is considered to be rebooting, false otherwise.
+ */
bool isRebooting() const override;
private:
- std::shared_ptr<BootManagerIface> bootManager;
- std::shared_ptr<UtilIface> util;
+ /** @brief Configuration of the node associated with this handler. */
+ NodeConfig mNodeConfig;
+ /** @brief Shared pointer to the central boot time monitoring API. */
+ std::shared_ptr<api::IApi> mApi;
};
-
+} // namespace dbus
} // namespace boot_time_monitor
diff --git a/include/gen.hpp b/include/gen.hpp
new file mode 100644
index 0000000..3102410
--- /dev/null
+++ b/include/gen.hpp
@@ -0,0 +1,128 @@
+#pragma once
+
+#include "dbus_handler.hpp"
+#include "node_config.hpp"
+#include "psm_handler.hpp"
+
+#include <fmt/printf.h>
+
+#include <string_view>
+#include <vector>
+
+namespace boot_time_monitor
+{
+/**
+ * @brief Contains helper functions for generating configuration objects and
+ * names.
+ *
+ * This namespace provides utility functions to create configuration structures
+ * (like PSMConfig and DbusConfig) and standardized node names based on system
+ * properties like the number of hosts or BMCs and their indices.
+ */
+namespace gen
+{
+namespace btm = boot_time_monitor;
+
+/**
+ * @brief Generates the PSM (Platform State Manager) configuration for a
+ * specific node.
+ *
+ * This function determines the correct D-Bus service names and object paths
+ * for monitoring host state and OS status based on whether the system is
+ * single-host or multi-host. For multi-host systems, it uses the node's
+ * index within the provided list to construct unique identifiers.
+ *
+ * @param nodeConfigs A vector containing the configurations of all nodes.
+ * @param nodeConfig The configuration of the specific node for which to
+ * generate the PSM config.
+ * @return psm::PSMConfig The generated PSM configuration containing D-Bus
+ * service names and object paths. Returns an empty
+ * config if the node is not found or the list is empty.
+ */
+psm::PSMConfig GenPSMConfig(const std::vector<btm::NodeConfig>& nodeConfigs,
+ const btm::NodeConfig& nodeConfig)
+{
+ if (nodeConfigs.size() == 0)
+ {
+ fmt::print("[{}] node list is empty!\n", __FUNCTION__);
+ return {};
+ }
+
+ // Singlehost rule
+ if (nodeConfigs.size() == 1)
+ {
+ return {
+ .hostStateDbusService = "xyz.openbmc_project.State.Host",
+ .hostStateDbusObjPath = "/xyz/openbmc_project/state/host0",
+ .osStateDbusService = "xyz.openbmc_project.State.OperatingSystem",
+ .osStateDbusObjPath = "/xyz/openbmc_project/state/os",
+ };
+ }
+
+ // Multihost platforms have a different PSM Dbus Service and Path rule.
+ // Create dBus path depends on its relative indices.
+ auto it = std::find(nodeConfigs.begin(), nodeConfigs.end(), nodeConfig);
+ if (it == nodeConfigs.end())
+ {
+ fmt::print("[{}] node config not found!\n", __FUNCTION__);
+ return {};
+ }
+
+ int idx = std::distance(nodeConfigs.begin(), it) + 1;
+
+ return {
+ .hostStateDbusService = fmt::format("xyz.openbmc_project.State.Host{}",
+ idx),
+ .hostStateDbusObjPath = fmt::format("/xyz/openbmc_project/state/host{}",
+ idx),
+ .osStateDbusService =
+ fmt::format("xyz.openbmc_project.State.OperatingSystem{}", idx),
+ .osStateDbusObjPath = "/xyz/openbmc_project/state/os",
+ };
+}
+
+/**
+ * @brief Generates the D-Bus configuration for a specific node.
+ *
+ * Constructs the D-Bus object path for the boot time monitor interfaces
+ * based on the node's name.
+ *
+ * @param nodeConfig The configuration of the node.
+ * @return dbus::DbusConfig The generated D-Bus configuration containing the
+ * object path.
+ */
+dbus::DbusConfig GenDbusConfig(const NodeConfig& nodeConfig)
+{
+ return {.dbusObjPath = fmt::format("/xyz/openbmc_project/time/boot/{}",
+ nodeConfig.nodeName)};
+}
+
+/**
+ * @brief Generates a standard host node name based on its index.
+ *
+ * @param idx The zero-based index of the host.
+ * @return std::string The generated host node name (e.g., "host0", "host1").
+ */
+std::string GenHostNodeName(int idx)
+{
+ return fmt::format("host{}", idx);
+}
+
+/**
+ * @brief Generates a standard BMC node name based on the total number of BMCs
+ * and its index.
+ * @param bmcAmount The total number of BMCs in the system.
+ * @param idx The zero-based index of this BMC.
+ * @return std::string The generated BMC node name (e.g., "bmc" for single BMC,
+ * "bmc0", "bmc1" for multiple).
+ */
+std::string GenBmcNodeName(int bmcAmount, int idx)
+{
+ if (bmcAmount == 1)
+ return "bmc";
+
+ return fmt::format("bmc{}", idx);
+}
+
+} // namespace gen
+} // namespace boot_time_monitor
diff --git a/include/host_monitor_app.hpp b/include/host_monitor_app.hpp
deleted file mode 100644
index 850237d..0000000
--- a/include/host_monitor_app.hpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma once
-
-#include "boot_manager.hpp"
-#include "dbus_handler.hpp"
-#include "utils.hpp"
-
-#include <sdbusplus/bus.hpp>
-#include <sdbusplus/bus/match.hpp>
-#include <sdbusplus/server/manager.hpp>
-
-#include <memory>
-#include <string_view>
-
-namespace boot_time_monitor
-{
-
-class HostMonitorApp
-{
- public:
- HostMonitorApp(sdbusplus::bus::bus& bus, uint32_t hostNum);
-
- private:
- const std::string kNodeName;
- const std::string kObjPath;
- sdbusplus::server::manager::manager objManager;
- std::unique_ptr<sdbusplus::bus::match::match> hostStateWatcher;
- std::unique_ptr<sdbusplus::bus::match::match> osStatusWatcher;
-
- std::shared_ptr<Util> util;
- std::shared_ptr<FileUtil> cpCSV;
- std::shared_ptr<FileUtil> durCSV;
- std::shared_ptr<BootManager> bootManager;
- std::shared_ptr<DbusHandler> dbusHandler;
-};
-
-} // namespace boot_time_monitor
diff --git a/include/node_config.hpp b/include/node_config.hpp
new file mode 100644
index 0000000..595da02
--- /dev/null
+++ b/include/node_config.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <iostream>
+#include <string>
+#include <string_view>
+
+namespace boot_time_monitor
+{
+/** @brief Tag identifying a BMC node. Used for grouping operations. */
+inline constexpr std::string_view kBootTimeTagBMC = "BMC";
+/** @brief Tag identifying a Host node. Used for grouping operations. */
+inline constexpr std::string_view kBootTimeTagHost = "Host";
+
+/**
+ * @brief Represents the configuration for a specific node being monitored.
+ *
+ * This structure holds the unique name of a node (e.g., "host0", "bmc") and
+ * an optional tag (e.g., "Host", "BMC") used for grouping nodes. It provides
+ * comparison operators for use in containers like `std::map` and an output
+ * stream operator for easy printing.
+ */
+struct NodeConfig
+{
+ /**
+ * @brief The unique identifier name for the node (e.g., "host0", "bmc").
+ */
+ std::string nodeName;
+ /** @brief An optional tag for grouping nodes (e.g., "Host", "BMC"). */
+ std::string tag = "";
+
+ /**
+ * @brief Less-than comparison operator based on `nodeName`.
+ * @param other The NodeConfig to compare against.
+ * @return True if this nodeName is lexicographically less than
+ * other.nodeName.
+ */
+ bool operator<(const NodeConfig& other) const
+ {
+ return nodeName < other.nodeName;
+ }
+
+ /**
+ * @brief Equality comparison operator based on `nodeName`.
+ * @param other The NodeConfig to compare against.
+ * @return True if nodeNames are equal.
+ */
+ bool operator==(const NodeConfig& other) const
+ {
+ return nodeName == other.nodeName;
+ }
+
+ /**
+ * @brief Output stream operator for printing the NodeConfig.
+ * @param os The output stream.
+ * @param k The NodeConfig object to print.
+ * @return The output stream with the NodeConfig printed in the format:
+ * `{"<nodeName>", "<tag>"}`.
+ */
+ friend std::ostream& operator<<(std::ostream& os, const NodeConfig& k)
+ {
+ os << "{\"" << k.nodeName << "\", \"" << k.tag << "\"}";
+ return os;
+ }
+};
+} // namespace boot_time_monitor
diff --git a/include/psm_handler.hpp b/include/psm_handler.hpp
new file mode 100644
index 0000000..db2b5ab
--- /dev/null
+++ b/include/psm_handler.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+#include "api.hpp"
+#include "node_config.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <string>
+
+namespace boot_time_monitor
+{
+/**
+ * @brief Contains functionality related to interacting with the
+ * [Phosphor-state-manager](https://github.com/openbmc/phosphor-state-manager)
+ * (PSM).
+ */
+namespace psm
+{
+
+namespace btm = boot_time_monitor;
+
+/**
+ * @brief Configuration for interacting with PSM D-Bus interfaces.
+ *
+ * Stores the necessary D-Bus service names and object paths required to
+ * monitor host state and operating system status properties.
+ */
+struct PSMConfig
+{
+ /** @brief D-Bus service name for the Host State interface (e.g.,
+ * "xyz.openbmc_project.State.Host"). */
+ std::string hostStateDbusService;
+ /** @brief D-Bus object path for the Host State interface (e.g.,
+ * "/xyz/openbmc_project/state/host0"). */
+ std::string hostStateDbusObjPath;
+ /** @brief D-Bus service name for the OS State interface (e.g.,
+ * "xyz.openbmc_project.State.OperatingSystem"). */
+ std::string osStateDbusService;
+ /** @brief D-Bus object path for the OS State interface (e.g.,
+ * "/xyz/openbmc_project/state/os"). */
+ std::string osStateDbusObjPath;
+};
+
+/**
+ * @brief Handles monitoring of Platform State Manager (PSM) D-Bus signals for a
+ * specific node.
+ *
+ * This class sets up D-Bus match rules to listen for property changes on
+ * the Host State (`xyz.openbmc_project.State.Host`) and OS Status
+ * (`xyz.openbmc_project.State.OperatingSystem.Status`) interfaces defined
+ * in the `PSMConfig`. When a relevant property changes (e.g.,
+ * `CurrentHostState`, `OperatingSystemState`), it translates the new state
+ * into a checkpoint name and records it using the provided `IApi` instance
+ * for the associated `NodeConfig`.
+ */
+class Handler
+{
+ public:
+ /**
+ * @brief Constructs the PSM Handler.
+ *
+ * Initializes previous state variables by querying current D-Bus properties
+ * and sets up D-Bus match rules to monitor for future changes.
+ *
+ * @param bus The sdbusplus bus connection.
+ * @param nodeConfig Configuration of the node this handler monitors.
+ * @param psmConfig D-Bus configuration for PSM interfaces.
+ * @param api Shared pointer to the central boot time monitoring API.
+ */
+ Handler(sdbusplus::bus::bus& bus, const btm::NodeConfig& nodeConfig,
+ const btm::psm::PSMConfig& psmConfig,
+ std::shared_ptr<btm::api::IApi> api);
+
+ private:
+ /** @brief D-Bus match rule watcher for Host State property changes. */
+ std::unique_ptr<sdbusplus::bus::match::match> hostStateWatcher;
+ /** @brief D-Bus match rule watcher for OS Status property changes. */
+ std::unique_ptr<sdbusplus::bus::match::match> osStatusWatcher;
+
+ /** @brief Stores the previously observed Host State to detect changes. */
+ std::string mPreHostState;
+ /** @brief Stores the previously observed OS State/Status to detect changes.
+ */
+ std::string mPreOSState;
+ /** @brief Configuration of the node associated with this handler. */
+ btm::NodeConfig mNodeConfig;
+ /** @brief Shared pointer to the central boot time monitoring API. */
+ std::shared_ptr<btm::api::IApi> mApi;
+};
+} // namespace psm
+} // namespace boot_time_monitor
diff --git a/include/resource.hpp b/include/resource.hpp
new file mode 100644
index 0000000..a7c339c
--- /dev/null
+++ b/include/resource.hpp
@@ -0,0 +1,280 @@
+#pragma once
+
+#include <fstream>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+namespace boot_time_monitor
+{
+
+/**
+ * @brief Contains interfaces and implementations for resource management (data
+ * storage).
+ *
+ * This namespace defines how boot time data (checkpoints and durations) is
+ * stored and retrieved. Currently, it provides a file-based implementation.
+ */
+namespace resource
+{
+
+namespace btm = boot_time_monitor;
+
+/** @brief Default directory for storing boot time monitor data files. */
+extern std::string BTMonitorDir;
+
+/** @brief Suffix appended to filenames when a boot cycle is completed. */
+const std::string kCompletedSuffix = ".completed";
+/** @brief Suffix added to a checkpoint name to indicate the start of a
+ * self-measured duration. */
+const std::string kBeginStageSuffix = ":BEGIN";
+/** @brief Base filename for storing checkpoint data. */
+const std::string kCheckpointFile = "checkpoints.csv";
+/** @brief Base filename for storing duration data. */
+const std::string kDurationFile = "durations.csv";
+
+/**
+ * @brief Represents a single boot checkpoint record.
+ */
+struct Checkpoint
+{
+ /** @brief The name identifying the checkpoint. */
+ std::string name;
+ /** @brief The wall clock time (epoch time in ms) when the checkpoint was
+ * reached. */
+ int64_t wallTime;
+ /** @brief The monotonic clock time (uptime in ms) when the checkpoint was
+ * reached. */
+ int64_t monoTime;
+
+ /** @brief Constructs a Checkpoint object. */
+ Checkpoint(std::string name, int64_t wallTime, int64_t monoTime) :
+ name(std::move(name)), wallTime(wallTime), monoTime(monoTime)
+ {}
+};
+
+/**
+ * @brief Represents a single boot duration measurement record.
+ */
+struct Duration
+{
+ /** @brief The name identifying the duration measurement. */
+ std::string name;
+ /** @brief The measured duration in milliseconds. */
+ int64_t duration;
+
+ /** @brief Constructs a Duration object. */
+ Duration(std::string name, int64_t duration) :
+ name(std::move(name)), duration(duration)
+ {}
+};
+
+/**
+ * @brief Interface for managing boot time data storage for a single node.
+ *
+ * Defines the contract for storing checkpoints and durations, retrieving cached
+ * data, marking a boot cycle as complete, and checking the current reboot
+ * status.
+ */
+class IResource
+{
+ public:
+ virtual ~IResource() = default;
+
+ /**
+ * @brief Records a boot checkpoint.
+ *
+ * @param name The name of the checkpoint.
+ * @param epochTime The wall clock time (epoch time in ms). If 0, current
+ * time is used.
+ * @param duration The self-measured duration (in ms). If non-zero, a
+ * ":BEGIN" checkpoint might be added.
+ */
+ virtual void SetCheckpoint(std::string_view name, int64_t epochTime,
+ int64_t duration) = 0;
+
+ /**
+ * @brief Records a boot duration measurement.
+ *
+ * @param name The name of the duration measurement.
+ * @param duration The measured duration in milliseconds.
+ */
+ virtual void SetDuration(std::string_view name, int64_t duration) = 0;
+
+ /**
+ * @brief Retrieves the cached list of checkpoints for the current or last
+ * completed boot.
+ *
+ * @return A vector of Checkpoint objects. Returns current boot data if
+ * IsRebooting() is true, otherwise returns data from the last completed
+ * boot.
+ */
+ virtual std::vector<btm::resource::Checkpoint> GetCheckpointCache() = 0;
+
+ /**
+ * @brief Retrieves the cached list of durations for the current or last
+ * completed boot.
+ *
+ * @return A vector of Duration objects. Returns current boot data if
+ * IsRebooting() is true, otherwise returns data from the last completed
+ * boot.
+ */
+ virtual std::vector<btm::resource::Duration> GetDurationCache() = 0;
+
+ /**
+ * @brief Marks the current boot cycle as complete.
+ * This typically involves finalizing storage (e.g., renaming files)
+ * and clearing current caches.
+ */
+ virtual void MarkComplete() = 0;
+
+ /**
+ * @brief Checks if the node is currently considered to be in a reboot
+ * cycle.
+ *
+ * @return True if data is being actively recorded for the current boot,
+ * false otherwise.
+ */
+ virtual bool IsRebooting() = 0;
+};
+
+/**
+ * @brief File-based implementation of the IResource interface.
+ *
+ * Stores checkpoint and duration data in separate CSV files per node.
+ * Manages current boot data and data from the last completed boot cycle.
+ * Provides caching for efficient retrieval.
+ */
+class File : public IResource
+{
+ public:
+ /**
+ * @brief Constructs a File resource handler for a specific node.
+ *
+ * Initializes file streams and loads data from existing files (current and
+ * completed).
+ *
+ * @param nodeName The name of the node this resource belongs to.
+ */
+ File(std::string_view nodeName);
+
+ ~File() = default;
+
+ // Declare noncopyable
+ File(const File&) = delete;
+ File& operator=(const File&) = delete;
+
+ // Declare nonmoveable
+ File(File&&) = delete;
+ File& operator=(File&&) = delete;
+
+ /** @copydoc IResource::SetCheckpoint */
+ void SetCheckpoint(std::string_view name, int64_t epochTime,
+ int64_t duration) override;
+
+ /** @copydoc IResource::SetDuration */
+ void SetDuration(std::string_view name, int64_t duration) override;
+
+ /** @copydoc IResource::GetCheckpointCache */
+ std::vector<btm::resource::Checkpoint> GetCheckpointCache() override;
+
+ /** @copydoc IResource::GetDurationCache */
+ std::vector<btm::resource::Duration> GetDurationCache() override;
+
+ /** @copydoc IResource::MarkComplete */
+ void MarkComplete() override;
+
+ /** @copydoc IResource::IsRebooting */
+ bool IsRebooting() override;
+
+ private:
+ /** @brief Mutex protecting file access and cache modifications. */
+ std::mutex mFileMutex;
+ /** @brief The name of the node associated with this resource. */
+ std::string mNodeName;
+ /** @brief Output file stream for the current checkpoint file. */
+ std::ofstream mCpOfs;
+ /** @brief Output file stream for the current duration file. */
+ std::ofstream mDurOfs;
+ /** @brief Cache for checkpoints recorded during the current boot cycle. */
+ std::vector<btm::resource::Checkpoint> mCheckpointCache;
+ /** @brief Cache for checkpoints from the last completed boot cycle. */
+ std::vector<btm::resource::Checkpoint> mCheckpointCompletedCache;
+ /** @brief Cache for durations recorded during the current boot cycle. */
+ std::vector<btm::resource::Duration> mDurationCache;
+ /** @brief Cache for durations from the last completed boot cycle. */
+ std::vector<btm::resource::Duration> mDurationCompletedCache;
+
+ /**
+ * @brief Appends a checkpoint record to the current checkpoint file and
+ * cache.
+ * @param name Checkpoint name.
+ * @param wallTimeInMs Wall clock time (ms).
+ * @param monoTimeInMs Monotonic clock time (ms).
+ */
+ void AppendCheckpointToFile(std::string name, int64_t wallTimeInMs,
+ int64_t monoTimeInMs);
+ /**
+ * @brief Appends a duration record to the current duration file and cache.
+ * @param name Duration name.
+ * @param durationInMs Duration value (ms).
+ */
+ void AppendDurationToFile(std::string name, int64_t durationInMs);
+ /**
+ * @brief Opens or creates a file for appending and returns the stream.
+ * @param filename Full path to the file.
+ * @return std::ofstream The opened file stream.
+ * @throws std::invalid_argument if the stream cannot be opened.
+ */
+ std::ofstream StartFileInstance(std::string_view filename);
+ /**
+ * @brief Finalizes a data file by closing it and renaming it with the
+ * ".complete" suffix. Reopens the original filename for the next boot.
+ * @param ofs The output stream to close and manage.
+ * @param filename The base filename (without suffix).
+ */
+ void CompleteCurrentFile(std::ofstream& ofs, std::string filename);
+ /**
+ * @brief Checks if the file associated with the stream is currently empty.
+ * @param ofs The output stream to check.
+ * @return True if the file is empty, false otherwise.
+ * @throws std::runtime_error if `tellp` fails.
+ */
+ bool IsEmptyFile(std::ofstream& ofs);
+ /**
+ * @brief Loads checkpoint data from a specified file (current or
+ * completed).
+ * @param loadComplete If true, loads from the ".complete" file; otherwise,
+ * loads from the current file.
+ * @return std::vector<btm::resource::Checkpoint> The loaded checkpoints.
+ */
+ std::vector<btm::resource::Checkpoint> LoadCheckpoints(bool loadComplete);
+ /**
+ * @brief Loads duration data from a specified file (current or completed).
+ * @param loadComplete If true, loads from the ".complete" file; otherwise,
+ * loads from the current file.
+ * @return std::vector<btm::resource::Duration> The loaded durations.
+ */
+ std::vector<btm::resource::Duration> LoadDurations(bool loadComplete);
+ /**
+ * @brief Gets the current system uptime in milliseconds from /proc/uptime.
+ * @return std::optional<int64_t> Uptime in ms, or std::nullopt if reading
+ * fails.
+ */
+ std::optional<int64_t> GetUpTimeInMs();
+
+ /** @brief Gets the current wall clock time (epoch) in milliseconds. */
+ inline int64_t GetWallTimeInMs();
+ /** @brief Validates if a name contains only allowed characters
+ * ([0-9a-zA-Z_:]). */
+ inline bool IsValidName(std::string_view name);
+ /** @brief Constructs the full path for the checkpoint file for this node.
+ */
+ inline std::string GetCpFullFileName();
+ /** @brief Constructs the full path for the duration file for this node. */
+ inline std::string GetDurFullFileName();
+};
+} // namespace resource
+} // namespace boot_time_monitor
diff --git a/include/systemd_handler.hpp b/include/systemd_handler.hpp
new file mode 100644
index 0000000..4b5a0e0
--- /dev/null
+++ b/include/systemd_handler.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "api.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+
+#include <functional>
+#include <vector>
+
+namespace boot_time_monitor
+{
+/**
+ * @brief Contains functionality related to interacting with systemd for boot
+ * time information.
+ */
+namespace systemd
+{
+
+namespace btm = boot_time_monitor;
+
+/**
+ * @brief Handles monitoring of systemd boot timestamps for BMC nodes.
+ *
+ * This class periodically queries systemd's D-Bus interface
+ * (`org.freedesktop.systemd1.Manager`) to retrieve various monotonic
+ * timestamps related to the boot process (Firmware, Loader, Kernel, InitRD,
+ * Userspace, Finish). It waits until the `FinishTimestampMonotonic` is
+ * available, indicating the userspace boot is complete. Once available, it
+ * calculates the duration of each stage and records these durations using the
+ * provided `IApi` instance, specifically targeting nodes tagged with
+ * `kBootTimeTagBMC`.
+ */
+class Handler
+{
+ public:
+ /**
+ * @brief Constructs the systemd Handler.
+ *
+ * Initializes the handler and starts a timer (`m_bmc_finish_timer`) to
+ * periodically check for systemd boot completion.
+ *
+ * @param conn Shared pointer to the sdbusplus ASIO connection.
+ * @param api Shared pointer to the central boot time monitoring API.
+ */
+ Handler(std::shared_ptr<sdbusplus::asio::connection> conn,
+ std::shared_ptr<btm::api::IApi> api);
+
+ private:
+ /** @brief Interval for polling systemd properties until boot is finished.
+ */
+ const std::chrono::seconds kCheckBmcFinishInterval =
+ std::chrono::seconds(10);
+ /**
+ * @brief Timer callback function to check systemd boot timestamps.
+ *
+ * Queries systemd D-Bus properties for boot timestamps. If the finish
+ * timestamp is available, calculates stage durations and records them via
+ * `mApi`. Otherwise, reschedules the timer.
+ *
+ * @param ec Boost error code from the timer operation.
+ * @param conn Shared pointer to the sdbusplus ASIO connection.
+ */
+ void CheckBmcFinish(const boost::system::error_code& ec,
+ std::shared_ptr<sdbusplus::asio::connection>& conn);
+
+ /** @brief Shared pointer to the central boot time monitoring API. */
+ std::shared_ptr<btm::api::IApi> mApi;
+ /** @brief ASIO timer used to periodically check for systemd boot
+ * completion. */
+ boost::asio::steady_timer mBMCFinishTimer;
+};
+} // namespace systemd
+} // namespace boot_time_monitor
diff --git a/include/utils.hpp b/include/utils.hpp
deleted file mode 100644
index 97aad84..0000000
--- a/include/utils.hpp
+++ /dev/null
@@ -1,200 +0,0 @@
-#pragma once
-
-#include "config.h"
-
-#include <cstdint>
-#include <fstream>
-#include <memory>
-#include <optional>
-#include <string>
-#include <string_view>
-#include <vector>
-
-namespace boot_time_monitor
-{
-
-const std::string kBTMonitorDir = bootTimeDataDir;
-const std::string kCheckpointFile = "checkpoints.csv";
-const std::string kDurationFile = "durations.csv";
-const std::string kCompletedSuffix = ".completed";
-
-struct Checkpoint
-{
- std::string name;
- int64_t wallTime;
- int64_t monoTime;
-
- Checkpoint(std::string name, int64_t wallTime, int64_t monoTime) :
- name(std::move(name)), wallTime(wallTime), monoTime(monoTime)
- {}
-};
-
-struct Duration
-{
- std::string name;
- int64_t duration;
-
- Duration(std::string name, int64_t duration) :
- name(std::move(name)), duration(duration)
- {}
-};
-
-/**
- * An interface class for the Util APIs
- */
-class UtilIface
-{
- public:
- virtual ~UtilIface() = default;
-
- /**
- * Read uptime from `/proc/uptime`
- *
- * @return Current uptime in milliseconds
- */
- virtual std::optional<int64_t> getUpTimeInMs() = 0;
-
- /**
- * Get system time and covert it to the epoch time in milliseconds
- *
- * @return Current epoch time in milliseconds
- */
- virtual int64_t getWallTimeInMs() = 0;
-
- /**
- * Check if the input name is valid. This can be used to check both
- * checkpoint name and duration name
- *
- * @return true if the input name is valid
- */
- virtual bool isValidName(std::string_view name) = 0;
-
- /**
- * Get full file path of checkpoint record for a specific node
- *
- * @param[in] nodeName - Node name
- * @param[in] wantCompleted - Want `kCompletedSuffix` in the file path or
- * not
- * @return Full file path
- */
- virtual std::string getCPPath(std::string_view nodeName,
- bool wantCompleted) = 0;
-
- /**
- * Get full file path of duration record for a specific node
- *
- * @param[in] nodeName - Node name
- * @param[in] wantCompleted - Want `kCompletedSuffix` in the file path or
- * not
- * @return Full file path
- */
- virtual std::string getDurPath(std::string_view nodeName,
- bool wantCompleted) = 0;
-
- /**
- * Read 4 bytes data from target address
- *
- * @param[in] target - Target address
- * @return 4 bytes data from the target address or std::nullopt if any error
- * happens during `readMem4Bytes`
- */
- virtual std::optional<uint32_t> readMem4Bytes(uint32_t target) = 0;
-};
-
-/**
- * Utility implementation class
- */
-class Util : public UtilIface
-{
- public:
- std::optional<int64_t> getUpTimeInMs() override;
- int64_t getWallTimeInMs() override;
- bool isValidName(std::string_view name) override;
- std::string getCPPath(std::string_view nodeName,
- bool wantCompleted) override;
- std::string getDurPath(std::string_view nodeName,
- bool wantCompleted) override;
- std::optional<uint32_t> readMem4Bytes(uint32_t target) override;
-};
-
-/**
- * An interface class for the FileUtil APIs
- */
-class FileUtilIface
-{
- public:
- virtual ~FileUtilIface() = default;
-
- /**
- * Add a new checkpoint record in file
- *
- * @param[in] name - Checkpoint name
- * @param[in] wallTimeInMs - Current epoch time in milliseconds
- * @param[in] monoTimeInMs - Current monotonic time in milliseconds
- * (Typically it's from `/proc/uptime`)
- */
- virtual void addCheckpoint(std::string_view name, int64_t wallTimeInMs,
- int64_t monoTimeInMs) = 0;
-
- /**
- * Add a new duration record in file
- *
- * @param[in] name - Duration name
- * @param[in] durationInMs - Duration in milliseconds
- */
- virtual void addDuration(std::string_view name, int64_t durationInMs) = 0;
-
- /**
- * Close current file and rename it with suffix `kCompletedSuffix`
- */
- virtual void completeCurrent() = 0;
-
- /**
- * Load checkpoints from file
- *
- * @param[in] loadCompleted - Load file with `kCompletedSuffix` or not
- * @return Smart pointer of vector of checkpoints
- */
- virtual std::unique_ptr<std::vector<Checkpoint>>
- loadCheckpoints(bool loadCompleted) = 0;
-
- /**
- * Load durations from file
- *
- * @param[in] loadCompleted - Load file with `kCompletedSuffix` or not
- * @return Smart pointer of vector of durations
- */
- virtual std::unique_ptr<std::vector<Duration>>
- loadDurations(bool loadCompleted) = 0;
-
- /**
- * Is current file empty
- *
- * @return true if file is empty
- */
- virtual bool isEmpty() = 0;
-};
-
-/**
- * FileUtil implementation class
- */
-class FileUtil : public FileUtilIface
-{
- public:
- explicit FileUtil(std::string_view filename);
- void addCheckpoint(std::string_view name, int64_t wallTimeInMs,
- int64_t monoTimeInMs) override;
- void addDuration(std::string_view name, int64_t durationInMs) override;
- void completeCurrent() override;
- std::unique_ptr<std::vector<Checkpoint>>
- loadCheckpoints(bool loadCompleted) override;
- std::unique_ptr<std::vector<Duration>>
- loadDurations(bool loadCompleted) override;
- bool isEmpty() override;
-
- protected:
- std::string filename;
- std::unique_ptr<std::ofstream> ofs;
-};
-
-} // namespace boot_time_monitor
diff --git a/meson.build b/meson.build
index 3f03f92..823e9da 100644
--- a/meson.build
+++ b/meson.build
@@ -48,4 +48,4 @@
if not get_option('tests').disabled()
subdir('test')
-endif
+endif
\ No newline at end of file
diff --git a/src/api.cpp b/src/api.cpp
new file mode 100644
index 0000000..c085907
--- /dev/null
+++ b/src/api.cpp
@@ -0,0 +1,201 @@
+#include "api.hpp"
+
+#include <fmt/printf.h>
+
+namespace boot_time_monitor
+{
+
+namespace api
+{
+
+namespace btm = boot_time_monitor;
+
+Api::~Api() = default;
+
+void Api::SetNodeCheckpoint(const btm::NodeConfig& nodeConfig,
+ std::string_view checkpointName, int64_t wallTime,
+ int64_t selfMeasuredDuration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return;
+ }
+
+ it->second.get()->SetCheckpoint(checkpointName, wallTime,
+ selfMeasuredDuration);
+}
+
+void Api::SetNodeDuration(const btm::NodeConfig& nodeConfig,
+ std::string_view durationName, int64_t duration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return;
+ }
+
+ it->second.get()->SetDuration(durationName, duration);
+}
+
+void Api::SetNodesCheckpointByTag(std::string_view tag,
+ std::string_view checkpointName,
+ int64_t wallTime,
+ int64_t selfMeasuredDuration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ for (const auto& [config, node] : mResourceMap)
+ {
+ if (config.tag == tag)
+ {
+ node.get()->SetCheckpoint(checkpointName, wallTime,
+ selfMeasuredDuration);
+ }
+ }
+}
+
+void Api::SetNodesDurationByTag(std::string_view tag,
+ std::string_view durationName, int64_t duration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ for (const auto& [config, node] : mResourceMap)
+ {
+ if (config.tag == tag)
+ {
+ node.get()->SetDuration(durationName, duration);
+ }
+ }
+}
+
+void Api::SetPSUCheckpoint(std::string_view checkpointName, int64_t wallTime,
+ int64_t selfMeasuredDuration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ // For "Power Supply Unit" checkpoint should broadcast to all nodes.
+ for (const auto& [_, node] : mResourceMap)
+ {
+ node.get()->SetCheckpoint(checkpointName, wallTime,
+ selfMeasuredDuration);
+ }
+}
+
+void Api::SetPSUDuration(std::string_view durationName, int64_t duration)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ // For "Power Supply Unit" duration should broadcast to all nodes.
+ for (const auto& [_, node] : mResourceMap)
+ {
+ node.get()->SetDuration(durationName, duration);
+ }
+}
+
+void Api::NotifyNodeComplete(const btm::NodeConfig& nodeConfig)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return;
+ }
+
+ it->second.get()->MarkComplete();
+}
+
+void Api::NotifyNodesCompleteByTag(std::string_view tag)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ for (const auto& [config, node] : mResourceMap)
+ {
+ if (config.tag == tag)
+ {
+ node.get()->MarkComplete();
+ }
+ }
+}
+
+std::vector<std::tuple<std::string, int64_t, int64_t>>
+ Api::GetNodeCheckpointList(const btm::NodeConfig& nodeConfig)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return {};
+ }
+
+ std::vector<std::tuple<std::string, int64_t, int64_t>> result;
+
+ for (const auto& cp : it->second.get()->GetCheckpointCache())
+ {
+ result.emplace_back(std::make_tuple(cp.name, cp.wallTime, cp.monoTime));
+ }
+
+ return result;
+}
+std::vector<std::tuple<std::string, int64_t>>
+ Api::GetNodeAdditionalDurations(const btm::NodeConfig& nodeConfig)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return {};
+ }
+
+ std::vector<std::tuple<std::string, int64_t>> result;
+
+ for (const auto& dur : it->second.get()->GetDurationCache())
+ {
+ result.emplace_back(std::make_tuple(dur.name, dur.duration));
+ }
+
+ return result;
+}
+
+bool Api::IsNodeRebooting(const btm::NodeConfig& nodeConfig)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.find(nodeConfig);
+
+ if (it == mResourceMap.end())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ nodeConfig.nodeName);
+ return false;
+ }
+
+ return it->second.get()->IsRebooting();
+}
+
+void Api::RegisterNode(const btm::NodeConfig& nodeConfig)
+{
+ std::lock_guard<std::mutex> guard(mApiMutex);
+ auto it = mResourceMap.try_emplace(
+ nodeConfig, std::make_unique<resource::File>(nodeConfig.nodeName));
+
+ if (!it.second)
+ {
+ fmt::print(
+ "[{}] Failed to register `{}` due to the key duplicated. Skip it.\n",
+ __FUNCTION__, nodeConfig.nodeName);
+ }
+}
+} // namespace api
+} // namespace boot_time_monitor
diff --git a/src/bmc_monitor_app.cpp b/src/bmc_monitor_app.cpp
deleted file mode 100644
index 85371e3..0000000
--- a/src/bmc_monitor_app.cpp
+++ /dev/null
@@ -1,199 +0,0 @@
-#include "bmc_monitor_app.hpp"
-
-#include "boot_manager.hpp"
-#include "dbus_handler.hpp"
-#include "utils.hpp"
-
-#include <fmt/printf.h>
-
-#include <boost/asio/steady_timer.hpp>
-#include <sdbusplus/asio/connection.hpp>
-#include <sdbusplus/bus.hpp>
-
-#include <array>
-#include <memory>
-#include <regex>
-#include <string_view>
-
-namespace boot_time_monitor
-{
-
-constexpr std::string_view kSystemdService = "org.freedesktop.systemd1";
-constexpr std::string_view kSystemdPath = "/org/freedesktop/systemd1";
-constexpr std::string_view kSystemdIface = "org.freedesktop.DBus.Properties";
-constexpr std::string_view kSystemdFunc = "Get";
-constexpr std::string_view kSystemdParam1 = "org.freedesktop.systemd1.Manager";
-
-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";
-
-struct SystemdTimestamp
-{
- uint64_t firmware;
- uint64_t loader;
- uint64_t kernel;
- uint64_t initRD;
- uint64_t userspace;
- uint64_t finish;
-};
-
-namespace
-{
-
-inline std::string convertName(std::string_view name)
-{
- return std::regex_replace(name.data(), std::regex("TimestampMonotonic"),
- "");
-}
-
-} // namespace
-
-BMCMonitorApp::BMCMonitorApp(
- sdbusplus::bus::bus& bus,
- const std::shared_ptr<sdbusplus::asio::connection>& conn) :
- objManager(bus, kObjPath.data()), util(std::make_shared<Util>()),
- cpCSV(std::make_shared<FileUtil>(util->getCPPath(kNodeName, false))),
- durCSV(std::make_shared<FileUtil>(util->getDurPath(kNodeName, false))),
- bootManager(std::make_shared<BootManager>(util, cpCSV, durCSV)),
- dbusHandler(
- std::make_shared<DbusHandler>(bus, kObjPath.data(), bootManager, util))
-{
- constexpr auto kCheckBmcFinishInterval = std::chrono::seconds(10);
- static boost::asio::steady_timer BmcFinishTimer(conn->get_io_context());
- static std::function<void(const boost::system::error_code&)>
- checkBmcFinish = [this, conn, kCheckBmcFinishInterval](
- const boost::system::error_code& ec) {
- if (ec == boost::asio::error::operation_aborted)
- {
- fmt::print(stderr, "[checkBmcFinish] BmcFinishTimer is being "
- "canceled\n");
- // we're being canceled
- return;
- }
- if (ec)
- {
- fmt::print(stderr, "[checkBmcFinish] Timer Error: {}\n",
- ec.message().c_str());
- return;
- }
-
- constexpr uint64_t kMillisecondPerSecond = 1000;
- // 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_view, uint64_t*>, 6> bootTimestamp = {
- std::make_pair(kSystemdFirmwareTime.data(), ×.firmware),
- std::make_pair(kSystemdLoaderTime.data(), ×.loader),
- std::make_pair(kSystemdKernelTime.data(), ×.kernel),
- std::make_pair(kSystemdInitRDTime.data(), ×.initRD),
- std::make_pair(kSystemdUserspaceTime.data(), ×.userspace),
- std::make_pair(kSystemdFinishTime.data(), ×.finish)};
-
- for (const auto& timestamp : bootTimestamp)
- {
- auto method = conn->new_method_call(
- kSystemdService.data(), kSystemdPath.data(),
- kSystemdIface.data(), kSystemdFunc.data());
- method.append(kSystemdParam1.data(), timestamp.first.data());
- 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",
- timestamp.first.data(), e.what());
- // Restart the timer to keep polling the APML_ALERT status
- BmcFinishTimer.expires_after(kCheckBmcFinishInterval);
- BmcFinishTimer.async_wait(checkBmcFinish);
- return;
- }
- *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");
- BmcFinishTimer.expires_after(kCheckBmcFinishInterval);
- BmcFinishTimer.async_wait(checkBmcFinish);
- return;
- }
-
- // 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
- bootManager->setDuration(
- convertName(kSystemdFirmwareTime),
- times.firmware != 0
- ? static_cast<int64_t>((times.firmware - times.loader) /
- kMillisecondPerSecond)
- : 0);
- bootManager->setDuration(
- convertName(kSystemdLoaderTime),
- times.loader != 0
- ? static_cast<int64_t>(times.loader / kMillisecondPerSecond)
- : 0);
- bootManager->setDuration(
- convertName(kSystemdKernelTime),
- static_cast<int64_t>(
- (times.initRD > 0 ? times.initRD : times.userspace) /
- kMillisecondPerSecond));
- bootManager->setDuration(
- convertName(kSystemdInitRDTime),
- times.initRD != 0
- ? static_cast<int64_t>((times.userspace - times.initRD) /
- kMillisecondPerSecond)
- : 0);
- bootManager->setDuration(
- convertName(kSystemdUserspaceTime),
- times.userspace != 0
- ? static_cast<int64_t>((times.finish - times.userspace) /
- kMillisecondPerSecond)
- : 0);
-
-#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 = util->readMem4Bytes(SEC_CNT_ADDR);
- auto upTimeMS = util->getUpTimeInMs();
- if (powerOnSec != std::nullopt && upTimeMS != std::nullopt)
- {
- bootManager->setDuration("FirmwarePlusLoader",
- powerOnSec.value() * 1000 -
- upTimeMS.value());
- }
-#endif
-
-// BMC marks the boot process as `completed` automatically if we do *NOT* have
-// external daemon to do so.
-#ifdef AUTO_BMC_COMPLETE
- bootManager->setCheckpoint("UserspaceEnd", 0, 0);
- bootManager->notifyComplete();
-#endif
- };
- BmcFinishTimer.expires_after(std::chrono::seconds(0));
- BmcFinishTimer.async_wait(checkBmcFinish);
-}
-
-} // namespace boot_time_monitor
diff --git a/src/boot_manager.cpp b/src/boot_manager.cpp
deleted file mode 100644
index 4ead565..0000000
--- a/src/boot_manager.cpp
+++ /dev/null
@@ -1,151 +0,0 @@
-#include "boot_manager.hpp"
-
-#include "utils.hpp"
-
-#include <fmt/printf.h>
-
-#include <string_view>
-#include <vector>
-
-namespace boot_time_monitor
-{
-
-namespace
-{
-constexpr uint32_t kMaxCheckpointCnt = 100;
-constexpr uint32_t kMaxDurationCnt = 100;
-} // namespace
-
-/**
- * BootManager implementation class
- */
-BootManager::BootManager(std::shared_ptr<UtilIface> util,
- const std::shared_ptr<FileUtilIface>& cpCSV,
- const std::shared_ptr<FileUtilIface>& durCSV) :
- util(std::move(util)), cpCSV(cpCSV), durCSV(durCSV)
-{
- checkpoints = std::move(*cpCSV->loadCheckpoints(false));
- durations = std::move(*durCSV->loadDurations(false));
- preCheckpoints = std::move(*cpCSV->loadCheckpoints(true));
- preDurations = std::move(*durCSV->loadDurations(true));
-}
-
-void BootManager::setCheckpoint(std::string_view cpName,
- int64_t externalWallTime, int64_t duration)
-{
- auto wallTime = externalWallTime == 0 ? util->getWallTimeInMs()
- : externalWallTime;
- auto upTime = util->getUpTimeInMs();
- if (upTime == std::nullopt)
- {
- fmt::print("[{}] Can't get up time. Skip this checkpoint.\n",
- __FUNCTION__);
- return;
- }
- if (!util->isValidName(cpName))
- {
- fmt::print(
- "[{}] Name is invalid. Only allows [0-9a-zA-Z_:] but get: {}\n",
- __FUNCTION__, cpName);
- return;
- }
-
- if (checkpoints.size() >= kMaxCheckpointCnt)
- {
- fmt::print("[{}] Drop incoming checkpoint due to hit the limit. "
- "name={}, externalWallTime={}, duration={}\n",
- __FUNCTION__, cpName, externalWallTime, duration);
- return;
- }
- // `boot_manager` helps to calculate the transition time from one stage to
- // next stage if the user can provide a self measured duration for the
- // current stage.
- // For example, if the interval between checkpoint A and B is 10 seconds and
- // the user knows B stage only uses 8 seconds to boot up. Then that means 2
- // seconds transition time was cost from A stage to B stage.
- // `duration == 0` means "I don't have self measured duration, so no need to
- // calculate transition time for me".
- if (duration != 0)
- {
- checkpoints.emplace_back(cpName.data() + std::string{kBeginStageSuffix},
- wallTime - duration,
- upTime.value() - duration);
- cpCSV->addCheckpoint(checkpoints.back().name,
- checkpoints.back().wallTime,
- checkpoints.back().monoTime);
-
- if (checkpoints.size() >= kMaxCheckpointCnt)
- {
- fmt::print(
- "[{}] Incoming checkpoint has `duration` so `To_{}` has been "
- "created and added. Since the size is full so the incoming "
- "checkpoint has been dropped\n",
- __FUNCTION__, cpName);
- return;
- }
- }
-
- checkpoints.emplace_back(std::string{cpName}, wallTime, upTime.value());
- cpCSV->addCheckpoint(checkpoints.back().name, checkpoints.back().wallTime,
- checkpoints.back().monoTime);
-}
-
-void BootManager::setDuration(std::string_view durName, int64_t duration)
-{
- if (!util->isValidName(durName))
- {
- fmt::print(
- "[{}] Name is invalid. Only allows [0-9a-zA-Z_:] but get: {}\n",
- __FUNCTION__, durName);
- return;
- }
-
- if (durations.size() >= kMaxDurationCnt)
- {
- fmt::print("[{}] Drop incoming duration due to hit the limit. "
- "name={}, duration={}\n",
- __FUNCTION__, durName, duration);
- return;
- }
-
- durations.emplace_back(std::string(durName), duration);
- durCSV->addDuration(durations.back().name, durations.back().duration);
-}
-
-void BootManager::notifyComplete()
-{
- std::swap(preCheckpoints, checkpoints);
- checkpoints.clear();
- std::swap(preDurations, durations);
- durations.clear();
-
- cpCSV->completeCurrent();
- durCSV->completeCurrent();
-}
-
-bool BootManager::isRebooting()
-{
- return !(cpCSV->isEmpty() && durCSV->isEmpty());
-}
-
-const std::vector<Checkpoint>& BootManager::getCheckpoints() const
-{
- return checkpoints;
-}
-
-const std::vector<Duration>& BootManager::getDurations() const
-{
- return durations;
-}
-
-const std::vector<Checkpoint>& BootManager::getPreCheckpoints() const
-{
- return preCheckpoints;
-}
-
-const std::vector<Duration>& BootManager::getPreDurations() const
-{
- return preDurations;
-}
-
-} // namespace boot_time_monitor
diff --git a/src/dbus_handler.cpp b/src/dbus_handler.cpp
index fda63e3..9049b0e 100644
--- a/src/dbus_handler.cpp
+++ b/src/dbus_handler.cpp
@@ -1,58 +1,44 @@
#include "dbus_handler.hpp"
-#include "boot_manager.hpp"
-
-#include <fmt/printf.h>
-
-#include <boost/container/flat_map.hpp>
-#include <sdbusplus/asio/object_server.hpp>
-#include <sdbusplus/bus/match.hpp>
-#include <xyz/openbmc_project/Common/error.hpp>
-#include <xyz/openbmc_project/State/Host/server.hpp>
-#include <xyz/openbmc_project/Time/Boot/Checkpoint/server.hpp>
-#include <xyz/openbmc_project/Time/Boot/Duration/server.hpp>
-#include <xyz/openbmc_project/Time/Boot/Statistic/server.hpp>
-
-#include <tuple>
-
namespace boot_time_monitor
{
+namespace dbus
+{
-DbusHandler::DbusHandler(sdbusplus::bus::bus& dbus, const std::string& objPath,
- std::shared_ptr<BootManagerIface> bootManager,
- std::shared_ptr<UtilIface> util) :
- sdbusplus::server::object::object<Duration>(dbus, objPath.c_str()),
- sdbusplus::server::object::object<Checkpoint>(dbus, objPath.c_str()),
- sdbusplus::server::object::object<Statistic>(dbus, objPath.c_str()),
- bootManager(std::move(bootManager)), util(std::move(util))
+namespace btm = boot_time_monitor;
+
+constexpr std::string_view kPSUCheckpointName = "TrayPowerCycle";
+constexpr std::string_view kPSUDurationName = "TrayPowerCycle";
+
+Handler::Handler(sdbusplus::bus::bus& dbus, const btm::NodeConfig& nodeConfig,
+ const btm::dbus::DbusConfig& dbusConfig,
+ std::shared_ptr<btm::api::IApi> api) :
+ sdbusplus::server::object::object<Duration>(dbus,
+ dbusConfig.dbusObjPath.c_str()),
+ sdbusplus::server::object::object<Checkpoint>(
+ dbus, dbusConfig.dbusObjPath.c_str()),
+ sdbusplus::server::object::object<Statistic>(
+ dbus, dbusConfig.dbusObjPath.c_str()),
+ mNodeConfig(nodeConfig), mApi(api)
{}
-void DbusHandler::setCheckpoint(std::string checkpointName, int64_t wallTime,
- int64_t selfMeasuredDuration)
+void Handler::setCheckpoint(std::string checkpointName, int64_t wallTime,
+ int64_t selfMeasuredDuration)
{
- if (!util->isValidName(checkpointName))
- {
- throw sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument();
- }
-
- bootManager->setCheckpoint(checkpointName, wallTime, selfMeasuredDuration);
+ if (checkpointName == kPSUCheckpointName)
+ mApi->SetPSUCheckpoint(checkpointName, wallTime, selfMeasuredDuration);
+ else
+ mApi->SetNodeCheckpoint(mNodeConfig, checkpointName, wallTime,
+ selfMeasuredDuration);
}
std::vector<std::tuple<std::string, int64_t, int64_t>>
- DbusHandler::getCheckpointList()
+ Handler::getCheckpointList()
{
- std::vector<std::tuple<std::string, int64_t, int64_t>> result;
-
- for (const auto& cp :
- (bootManager->isRebooting() ? bootManager->getCheckpoints()
- : bootManager->getPreCheckpoints()))
- {
- result.emplace_back(std::make_tuple(cp.name, cp.wallTime, cp.monoTime));
- }
- return result;
+ return mApi->GetNodeCheckpointList(mNodeConfig);
}
-void DbusHandler::rebootComplete()
+void Handler::rebootComplete()
{
if (!isRebooting())
{
@@ -62,36 +48,25 @@
return;
}
- bootManager->notifyComplete();
+ mApi->NotifyNodeComplete(mNodeConfig);
}
-void DbusHandler::setDuration(std::string durationName, int64_t duration)
+void Handler::setDuration(std::string durationName, int64_t duration)
{
- if (!util->isValidName(durationName))
- {
- throw sdbusplus::xyz::openbmc_project::Common::Error::InvalidArgument();
- }
-
- bootManager->setDuration(durationName, duration);
+ if (durationName == kPSUDurationName)
+ mApi->SetPSUDuration(durationName, duration);
+ else
+ mApi->SetNodeDuration(mNodeConfig, durationName, duration);
}
-std::vector<std::tuple<std::string, int64_t>>
- DbusHandler::getAdditionalDurations()
+std::vector<std::tuple<std::string, int64_t>> Handler::getAdditionalDurations()
{
- std::vector<std::tuple<std::string, int64_t>> result;
-
- for (const auto& dur :
- (bootManager->isRebooting() ? bootManager->getDurations()
- : bootManager->getPreDurations()))
- {
- result.emplace_back(std::make_tuple(dur.name, dur.duration));
- }
- return result;
+ return mApi->GetNodeAdditionalDurations(mNodeConfig);
}
-bool DbusHandler::isRebooting() const
+bool Handler::isRebooting() const
{
- return bootManager->isRebooting();
+ return mApi->IsNodeRebooting(mNodeConfig);
}
-
+} // namespace dbus
} // namespace boot_time_monitor
diff --git a/src/host_monitor_app.cpp b/src/host_monitor_app.cpp
deleted file mode 100644
index 3c0dced..0000000
--- a/src/host_monitor_app.cpp
+++ /dev/null
@@ -1,164 +0,0 @@
-#include "host_monitor_app.hpp"
-
-#include "boot_manager.hpp"
-#include "dbus_handler.hpp"
-#include "utils.hpp"
-
-#include <fmt/printf.h>
-
-#include <boost/container/flat_map.hpp>
-#include <sdbusplus/bus.hpp>
-#include <sdbusplus/bus/match.hpp>
-#include <sdbusplus/message.hpp>
-
-#include <memory>
-#include <ranges>
-#include <string>
-#include <string_view>
-#include <variant>
-
-namespace boot_time_monitor
-{
-
-using BasicVariantType =
- std::variant<std::vector<std::string>, std::string, int64_t, uint64_t,
- double, int32_t, uint32_t, int16_t, uint16_t, uint8_t, bool>;
-
-constexpr std::string_view kHostService = "xyz.openbmc_project.State.Host";
-constexpr std::string_view kHostPath = "/xyz/openbmc_project/state/host0";
-constexpr std::string_view kHostIface = "xyz.openbmc_project.State.Host";
-constexpr std::string_view kHostProperty = "CurrentHostState";
-
-[[maybe_unused]] constexpr std::string_view kHostStateRunning =
- "xyz.openbmc_project.State.Host.HostState.Running";
-[[maybe_unused]] constexpr std::string_view kHostStateOff =
- "xyz.openbmc_project.State.Host.HostState.Off";
-
-constexpr std::string_view kOSStatusService =
- "xyz.openbmc_project.State.OperatingSystem";
-constexpr std::string_view kOSStatusPath = "/xyz/openbmc_project/state/os";
-constexpr std::string_view kOSStatusIface =
- "xyz.openbmc_project.State.OperatingSystem.Status";
-constexpr std::string_view kOSStatusProperty = "OperatingSystemState";
-
-std::string inline translateHostStateName(std::string_view state)
-{
- std::size_t found = state.find_last_of('.');
- return std::string("HostState:") + std::string(state.substr(found + 1));
-}
-
-std::string inline translateOSStatus(std::string_view status)
-{
- std::size_t found = status.find_last_of('.');
- return std::string("OSStatus:") + std::string(status.substr(found + 1));
-}
-
-HostMonitorApp::HostMonitorApp(sdbusplus::bus::bus& bus, uint32_t hostNum) :
- kNodeName(std::string{"host"} + std::to_string(hostNum)),
- kObjPath(std::string{"/xyz/openbmc_project/time/boot/"} + kNodeName),
- objManager(bus, kObjPath.c_str())
-{
- util = std::make_shared<Util>();
- cpCSV = std::make_shared<FileUtil>(util->getCPPath(kNodeName, false));
- durCSV = std::make_shared<FileUtil>(util->getDurPath(kNodeName, false));
- bootManager = std::make_shared<BootManager>(util, cpCSV, durCSV);
- dbusHandler = std::make_shared<DbusHandler>(bus, kObjPath.data(),
- bootManager, util);
- // Initialize preHostState
- static std::string preHostState;
- auto method = bus.new_method_call(kHostService.data(), kHostPath.data(),
- "org.freedesktop.DBus.Properties", "Get");
- method.append(kHostIface.data(), kHostProperty.data());
- BasicVariantType result;
- try
- {
- bus.call(method).read(result);
- preHostState = std::get<std::string>(result);
- }
- catch (const sdbusplus::exception::SdBusError& e)
- {
- fmt::print(stderr, "[{}] Failed to get `CurrentHostState`. ERROR={}\n",
- __FUNCTION__, e.what());
- fmt::print(
- stderr,
- "[{}] Initialized `preHostState` to empty string as default value\n",
- __FUNCTION__);
- }
-
- hostStateWatcher = std::make_unique<sdbusplus::bus::match::match>(
- bus,
- sdbusplus::bus::match::rules::propertiesChanged(kHostPath.data(),
- kHostIface.data()),
- [this](sdbusplus::message::message& message) {
- std::string objectName;
- boost::container::flat_map<
- std::string,
- std::variant<std::string, bool, int64_t, uint64_t, double>>
- values;
- message.read(objectName, values);
-
- auto findState = values.find(kHostProperty.data());
- if (findState != values.end())
- {
- const std::string curHostState =
- std::get<std::string>(findState->second);
- fmt::print(
- stderr,
- "[hostStateWatcher] CurrentHostState has changed from {} to {}\n",
- preHostState, curHostState);
- bootManager->setCheckpoint(translateHostStateName(curHostState), 0,
- 0);
- preHostState = curHostState;
- }
- });
-
- // Initialize preOSStatus
- static std::string preOSStatus;
- method = bus.new_method_call(kOSStatusService.data(), kOSStatusPath.data(),
- "org.freedesktop.DBus.Properties", "Get");
- method.append(kOSStatusIface.data(), kOSStatusProperty.data());
- try
- {
- bus.call(method).read(result);
- preOSStatus = std::get<std::string>(result);
- }
- catch (const sdbusplus::exception::SdBusError& e)
- {
- fmt::print(stderr,
- "[{}] Failed to get `OperatingSystemState`. ERROR={}\n",
- __FUNCTION__, e.what());
- fmt::print(
- stderr,
- "[{}] Initialized `preOSStatus` to empty string as default value\n",
- __FUNCTION__);
- }
-
- osStatusWatcher = std::make_unique<sdbusplus::bus::match::match>(
- bus,
- sdbusplus::bus::match::rules::propertiesChanged(kOSStatusPath.data(),
- kOSStatusIface.data()),
- [this](sdbusplus::message::message& message) {
- std::string objectName;
- boost::container::flat_map<
- std::string,
- std::variant<std::string, bool, int64_t, uint64_t, double>>
- values;
- message.read(objectName, values);
-
- auto findState = values.find(kOSStatusProperty.data());
- if (findState != values.end())
- {
- const std::string curOSStatus =
- std::get<std::string>(findState->second);
-
- fmt::print(
- stderr,
- "[osStatusWatcher] curOSStatus has changed from {} to {}\n",
- preOSStatus, curOSStatus);
- bootManager->setCheckpoint(translateOSStatus(curOSStatus), 0, 0);
- preOSStatus = curOSStatus;
- }
- });
-}
-
-} // namespace boot_time_monitor
diff --git a/src/main.cpp b/src/main.cpp
index 7a9fc47..4a551a5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,5 +1,11 @@
-#include "bmc_monitor_app.hpp"
-#include "host_monitor_app.hpp"
+#include "api.hpp"
+#include "dbus_handler.hpp"
+#include "gen.hpp"
+#include "psm_handler.hpp"
+#include "systemd_handler.hpp"
+
+#include <fmt/printf.h>
+#include <getopt.h> // For getopt_long
#include <boost/asio/io_service.hpp>
#include <sdbusplus/asio/connection.hpp>
@@ -7,19 +13,130 @@
#include <sdbusplus/bus.hpp>
#include <sdbusplus/server.hpp>
-using boot_time_monitor::BMCMonitorApp;
-using boot_time_monitor::HostMonitorApp;
+namespace btm = boot_time_monitor;
-int main()
+/*
+ * +-----------------+
+ * | main |
+ * +-----------------+
+ * |
+ * |
+ * v
+ * +-----------------+
+ * | data_sources |
+ * +-----------------+
+ * | | |
+ * | | psm_handler
+ * | dbus_handler
+ * systemd_handler
+ * |
+ * | interface between data source and actual checkpoint/duration records
+ * v
+ * +-----------------+
+ * | api |
+ * +-----------------+
+ * |
+ * | store checkpoints/durations
+ * v
+ * +-----------------+
+ * | files |
+ * +-----------------+
+ *
+ */
+
+void print_usage(const char* prog_name)
{
+ fmt::print(stderr, "Usage: {} [options]\n", prog_name);
+ fmt::print(stderr, "Options:\n");
+ fmt::print(stderr,
+ " -h, --help Show this help message and exit\n");
+ fmt::print(stderr,
+ " --host_count <num> Set number of hosts (default: 1)\n");
+ fmt::print(stderr,
+ " --bmc_count <num> Set number of BMCs (default: 1)\n");
+}
+
+int main(int argc, char* argv[])
+{
+ int hostCount = 1; // Default host count
+ int bmcCount = 1; // Default BMC count
+
+ const struct option long_options[] = {
+ {"help", no_argument, nullptr, 'h'},
+ {"host_count", required_argument, nullptr, 'o'}, // 'o' for hOst
+ {"bmc_count", required_argument, nullptr, 'b'}, // 'b' for Bmc
+ {nullptr, 0, nullptr, 0}};
+
+ int opt;
+ int option_index = 0;
+ while ((opt = getopt_long(argc, argv, "h", long_options, &option_index)) !=
+ -1)
+ {
+ switch (opt)
+ {
+ case 'h':
+ print_usage(argv[0]);
+ return 0;
+ case 'o':
+ hostCount = std::atoi(optarg);
+ break;
+ case 'b':
+ bmcCount = std::atoi(optarg);
+ break;
+ default: /* '?' */
+ print_usage(argv[0]);
+ return 1;
+ }
+ }
+
boost::asio::io_service io;
auto conn = std::make_shared<sdbusplus::asio::connection>(io);
conn->request_name("com.google.gbmc.boot_time_monitor");
auto server = std::make_shared<sdbusplus::asio::object_server>(conn);
auto& bus = static_cast<sdbusplus::bus::bus&>(*conn);
- HostMonitorApp host(bus, 0);
- BMCMonitorApp bmc(bus, conn);
+ std::vector<btm::NodeConfig> hostNodeConfigs;
+ std::vector<btm::NodeConfig> bmcNodeConfigs;
+
+ std::vector<std::unique_ptr<btm::dbus::Handler>> hostDbusHandlers;
+ std::vector<std::unique_ptr<btm::dbus::Handler>> bmcDbusHandlers;
+
+ std::vector<btm::psm::Handler> psmHandlers;
+
+ std::shared_ptr<btm::api::Api> api = std::make_shared<btm::api::Api>();
+
+ for (int i = 0; i < hostCount; i++)
+ {
+ hostNodeConfigs.emplace_back(btm::gen::GenHostNodeName(i),
+ std::string(btm::kBootTimeTagHost));
+ }
+
+ for (int i = 0; i < bmcCount; i++)
+ {
+ bmcNodeConfigs.emplace_back(btm::gen::GenBmcNodeName(bmcCount, i),
+ std::string(btm::kBootTimeTagBMC));
+ }
+
+ for (auto& nodeConfig : hostNodeConfigs)
+ {
+ api->RegisterNode(nodeConfig);
+
+ hostDbusHandlers.emplace_back(std::make_unique<btm::dbus::Handler>(
+ bus, nodeConfig, btm::gen::GenDbusConfig(nodeConfig), api));
+ psmHandlers.emplace_back(
+ bus, nodeConfig,
+ btm::gen::GenPSMConfig(hostNodeConfigs, nodeConfig), api);
+ }
+
+ for (auto& nodeConfig : bmcNodeConfigs)
+ {
+ api->RegisterNode(nodeConfig);
+
+ bmcDbusHandlers.emplace_back(std::make_unique<btm::dbus::Handler>(
+ bus, nodeConfig, btm::gen::GenDbusConfig(nodeConfig), api));
+ }
+
+ btm::systemd::Handler systemdHandler(conn, api);
io.run();
diff --git a/src/meson.build b/src/meson.build
index 09fb8c7..17538c7 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -25,11 +25,12 @@
boot_time_monitor_lib = static_library(
'boot_time_monitor',
- 'host_monitor_app.cpp',
- 'utils.cpp',
- 'boot_manager.cpp',
+ 'api.cpp',
'dbus_handler.cpp',
- 'bmc_monitor_app.cpp',
+ 'psm_handler.cpp',
+ 'resource.cpp',
+ 'systemd_handler.cpp',
+ generated_sources,
include_directories: boot_time_monitor_incs,
implicit_include_directories: false,
dependencies: boot_time_monitor_pre,
diff --git a/src/psm_handler.cpp b/src/psm_handler.cpp
new file mode 100644
index 0000000..285ddd6
--- /dev/null
+++ b/src/psm_handler.cpp
@@ -0,0 +1,144 @@
+#include "psm_handler.hpp"
+
+#include <fmt/printf.h>
+
+#include <variant>
+
+namespace boot_time_monitor
+{
+namespace psm
+{
+
+namespace btm = boot_time_monitor;
+
+using BasicVariantType =
+ std::variant<std::vector<std::string>, std::string, int64_t, uint64_t,
+ double, int32_t, uint32_t, int16_t, uint16_t, uint8_t, bool>;
+
+std::string inline translateHostStateName(std::string_view state)
+{
+ std::size_t found = state.find_last_of('.');
+ return std::string("HostState:") + std::string(state.substr(found + 1));
+}
+
+std::string inline translateOSStatus(std::string_view status)
+{
+ std::size_t found = status.find_last_of('.');
+ return std::string("OSStatus:") + std::string(status.substr(found + 1));
+}
+
+Handler::Handler(sdbusplus::bus::bus& bus, const btm::NodeConfig& nodeConfig,
+ const btm::psm::PSMConfig& psmConfig,
+ std::shared_ptr<btm::api::IApi> api) :
+ mNodeConfig(nodeConfig), mApi(api)
+{
+ // Initialize mPreHostState
+ auto method =
+ bus.new_method_call(psmConfig.hostStateDbusService.c_str(), // Service
+ psmConfig.hostStateDbusObjPath.c_str(), // Path
+ "org.freedesktop.DBus.Properties", // Iface
+ "Get"); // Function
+ method.append("xyz.openbmc_project.State.Host", "CurrentHostState");
+ BasicVariantType result;
+ try
+ {
+ bus.call(method).read(result);
+ mPreHostState = std::get<std::string>(result);
+ fmt::print("[{}] Get `CurrentHostState` success. value={}\n",
+ __FUNCTION__, mPreHostState);
+ }
+ catch (const sdbusplus::exception::SdBusError& e)
+ {
+ fmt::print(stderr, "[{}] Failed to get `CurrentHostState`. ERROR={}\n",
+ __FUNCTION__, e.what());
+ fmt::print(
+ stderr,
+ "[{}] Initialized `mPreHostState` to empty string as default value\n",
+ __FUNCTION__);
+ }
+
+ hostStateWatcher = std::make_unique<sdbusplus::bus::match::match>(
+ bus,
+ sdbusplus::bus::match::rules::propertiesChanged(
+ psmConfig.hostStateDbusObjPath.c_str(),
+ "xyz.openbmc_project.State.Host"),
+ [this](sdbusplus::message::message& message) {
+ std::string objectName;
+ boost::container::flat_map<
+ std::string,
+ std::variant<std::string, bool, int64_t, uint64_t, double>>
+ values;
+ message.read(objectName, values);
+
+ auto findState = values.find("CurrentHostState");
+ if (findState != values.end())
+ {
+ const std::string curHostState =
+ std::get<std::string>(findState->second);
+ fmt::print(
+ stderr,
+ "[hostStateWatcher] CurrentHostState has changed from {} to {}\n",
+ mPreHostState, curHostState);
+ mApi->SetNodeCheckpoint(mNodeConfig,
+ translateHostStateName(curHostState), 0, 0);
+ mPreHostState = curHostState;
+ }
+ });
+
+ // Initialize mPreOSState
+ method =
+ bus.new_method_call(psmConfig.osStateDbusService.c_str(), // Service
+ psmConfig.osStateDbusObjPath.c_str(), // Path
+ "org.freedesktop.DBus.Properties", // Iface
+ "Get"); // Function
+ method.append("xyz.openbmc_project.State.OperatingSystem.Status",
+ "OperatingSystemState");
+ try
+ {
+ bus.call(method).read(result);
+ mPreOSState = std::get<std::string>(result);
+ fmt::print("[{}] Get `OperatingSystemState` success. value={}\n",
+ __FUNCTION__, mPreOSState);
+ }
+ catch (const sdbusplus::exception::SdBusError& e)
+ {
+ fmt::print(stderr,
+ "[{}] Failed to get `OperatingSystemState`. ERROR={}\n",
+ __FUNCTION__, e.what());
+ fmt::print(
+ stderr,
+ "[{}] Initialized `mPreOSState` to empty string as default value\n",
+ __FUNCTION__);
+ }
+
+ osStatusWatcher = std::make_unique<sdbusplus::bus::match::match>(
+ bus,
+ sdbusplus::bus::match::rules::propertiesChanged(
+ psmConfig.osStateDbusObjPath.c_str(),
+ "xyz.openbmc_project.State.OperatingSystem.Status"),
+ [this](sdbusplus::message::message& message) {
+ std::string objectName;
+ boost::container::flat_map<
+ std::string,
+ std::variant<std::string, bool, int64_t, uint64_t, double>>
+ values;
+ message.read(objectName, values);
+
+ auto findState = values.find("OperatingSystemState");
+ if (findState != values.end())
+ {
+ const std::string curOSStatus =
+ std::get<std::string>(findState->second);
+
+ fmt::print(
+ stderr,
+ "[osStatusWatcher] curOSStatus has changed from {} to {}\n",
+ mPreOSState, curOSStatus);
+ mApi->SetNodeCheckpoint(mNodeConfig, translateOSStatus(curOSStatus),
+ 0, 0);
+ mPreOSState = curOSStatus;
+ }
+ });
+}
+} // namespace psm
+} // namespace boot_time_monitor
diff --git a/src/resource.cpp b/src/resource.cpp
new file mode 100644
index 0000000..385053b
--- /dev/null
+++ b/src/resource.cpp
@@ -0,0 +1,309 @@
+
+#include "resource.hpp"
+
+#include <fmt/printf.h>
+
+#include <chrono>
+#include <filesystem>
+#include <regex>
+#include <string>
+
+namespace boot_time_monitor
+{
+
+namespace resource
+{
+
+namespace fs = std::filesystem;
+namespace btm = boot_time_monitor;
+
+// Definition of the global variable
+std::string BTMonitorDir = "/var/google/boot_time_monitor/";
+
+constexpr uint32_t kMaxCheckpointCnt = 100;
+constexpr uint32_t kMaxDurationCnt = 100;
+
+File::File(std::string_view nodeName) : mNodeName(nodeName)
+{
+ mCpOfs = StartFileInstance(GetCpFullFileName());
+ mDurOfs = StartFileInstance(GetDurFullFileName());
+
+ mCheckpointCache = LoadCheckpoints(false);
+ mDurationCache = LoadDurations(false);
+ mCheckpointCompletedCache = LoadCheckpoints(true);
+ mDurationCompletedCache = LoadDurations(true);
+}
+
+void File::SetCheckpoint(std::string_view name, int64_t epochTime,
+ int64_t duration)
+{
+ auto wallTime = epochTime == 0 ? GetWallTimeInMs() : epochTime;
+ auto upTime = GetUpTimeInMs();
+ if (upTime == std::nullopt)
+ {
+ fmt::print("[{}] Can't get up time. Skip this checkpoint.\n",
+ __FUNCTION__);
+ return;
+ }
+ if (!IsValidName(name))
+ {
+ fmt::print(
+ "[{}] Name is invalid. Only allows [0-9a-zA-Z_:] but get: {}\n",
+ __FUNCTION__, name);
+ return;
+ }
+
+ if (mCheckpointCache.size() >= kMaxCheckpointCnt)
+ {
+ fmt::print("[{}] Drop incoming checkpoint due to hit the limit. "
+ "name={}, epochTime={}, duration={}\n",
+ __FUNCTION__, name, epochTime, duration);
+ return;
+ }
+ // `boot_manager` helps to calculate the transition time from one stage to
+ // next stage if the user can provide a self measured duration for the
+ // current stage.
+ // For example, if the interval between checkpoint A and B is 10 seconds and
+ // the user knows B stage only uses 8 seconds to boot up. Then that means 2
+ // seconds transition time was cost from A stage to B stage.
+ // `duration == 0` means "I don't have self measured duration, so no need to
+ // calculate transition time for me".
+ if (duration != 0)
+ {
+ AppendCheckpointToFile(std::string(name) + kBeginStageSuffix,
+ wallTime - duration, upTime.value() - duration);
+
+ if (mCheckpointCache.size() >= kMaxCheckpointCnt)
+ {
+ fmt::print(
+ "[{}] Incoming checkpoint has `duration` so `To_{}` has been "
+ "created and added. Since the size is full so the incoming "
+ "checkpoint has been dropped\n",
+ __FUNCTION__, name);
+ return;
+ }
+ }
+
+ AppendCheckpointToFile(std::string(name), wallTime, upTime.value());
+ // TODO(@huangweichieh): Check reboot complete.
+}
+
+void File::SetDuration(std::string_view name, int64_t duration)
+{
+ if (!IsValidName(name))
+ {
+ fmt::print(
+ "[{}] Name is invalid. Only allows [0-9a-zA-Z_:] but get: {}\n",
+ __FUNCTION__, name);
+ return;
+ }
+
+ if (mDurationCache.size() >= kMaxDurationCnt)
+ {
+ fmt::print("[{}] Drop incoming duration due to hit the limit. "
+ "name={}, duration={}\n",
+ __FUNCTION__, name, duration);
+ return;
+ }
+
+ AppendDurationToFile(std::string(name), duration);
+}
+
+std::vector<btm::resource::Checkpoint> File::GetCheckpointCache()
+{
+ return IsRebooting() ? mCheckpointCache : mCheckpointCompletedCache;
+}
+
+std::vector<btm::resource::Duration> File::GetDurationCache()
+{
+ return IsRebooting() ? mDurationCache : mDurationCompletedCache;
+}
+
+void File::MarkComplete()
+{
+ std::swap(mCheckpointCompletedCache, mCheckpointCache);
+ mCheckpointCache.clear();
+ std::swap(mDurationCompletedCache, mDurationCache);
+ mDurationCache.clear();
+
+ CompleteCurrentFile(mCpOfs, GetCpFullFileName());
+ CompleteCurrentFile(mDurOfs, GetDurFullFileName());
+}
+bool File::IsRebooting()
+{
+ return !(IsEmptyFile(mCpOfs) && IsEmptyFile(mDurOfs));
+}
+
+std::ofstream File::StartFileInstance(std::string_view filename)
+{
+ std::lock_guard<std::mutex> guard(mFileMutex);
+ fs::path dir = fs::path(filename).remove_filename();
+ if (!dir.empty() && !fs::exists(dir))
+ {
+ fs::create_directories(dir);
+ }
+ std::ofstream ofs = std::ofstream(std::string(filename),
+ std::fstream::out | std::fstream::app);
+ if (!ofs.good())
+ {
+ throw std::invalid_argument("ofstream is not available.");
+ }
+ return ofs;
+}
+
+bool File::IsEmptyFile(std::ofstream& ofs)
+{
+ std::lock_guard<std::mutex> guard(mFileMutex);
+ const auto result = ofs.tellp();
+ if (result < 0)
+ {
+ throw std::runtime_error("File can't be `tellp`");
+ }
+
+ return result == 0;
+}
+void File::CompleteCurrentFile(std::ofstream& ofs, std::string filename)
+{
+ std::lock_guard<std::mutex> guard(mFileMutex);
+ ofs.close();
+ fs::rename(filename, filename + kCompletedSuffix);
+
+ // Reopen for next reboot
+ ofs.open(filename.c_str(), std::fstream::out | std::fstream::app);
+ if (!ofs.good())
+ {
+ throw std::invalid_argument("ofstream is not available.");
+ }
+}
+
+void File::AppendCheckpointToFile(std::string name, int64_t wallTimeInMs,
+ int64_t monoTimeInMs)
+{
+ std::lock_guard<std::mutex> guard(mFileMutex);
+ mCheckpointCache.emplace_back(name, wallTimeInMs, monoTimeInMs);
+ mCpOfs << name << "," << wallTimeInMs << "," << monoTimeInMs << std::endl;
+ mCpOfs.flush();
+}
+
+void File::AppendDurationToFile(std::string name, int64_t durationInMs)
+{
+ std::lock_guard<std::mutex> guard(mFileMutex);
+ mDurationCache.emplace_back(name, durationInMs);
+ mDurOfs << name << "," << durationInMs << std::endl;
+ mDurOfs.flush();
+}
+
+std::vector<btm::resource::Checkpoint> File::LoadCheckpoints(bool loadCompleted)
+{
+ std::vector<btm::resource::Checkpoint> result =
+ std::vector<btm::resource::Checkpoint>();
+ std::string cpFilename = GetCpFullFileName() +
+ (loadCompleted ? kCompletedSuffix : "");
+ std::ifstream ifs(cpFilename);
+ if (!ifs.good())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ cpFilename);
+ return result;
+ }
+
+ constexpr uint32_t kBufSize = 1024;
+ constexpr uint32_t kMatchCount = 4;
+
+ std::array<char, kBufSize> buf;
+ std::cmatch match;
+ const std::regex reg("^([0-9a-zA-Z_:]*),([0-9]+),([0-9]+)$");
+ while (ifs.getline(buf.data(), kBufSize))
+ {
+ if (regex_match(buf.data(), match, reg))
+ {
+ if (match.size() != kMatchCount)
+ {
+ // This line is problematic. Skip it.
+ continue;
+ }
+
+ result.emplace_back(match[1].str(), std::stoll(match[2].str()),
+ std::stoll(match[3].str()));
+ }
+ }
+ return result;
+}
+
+std::vector<btm::resource::Duration> File::LoadDurations(bool loadComplete)
+{
+ std::vector<btm::resource::Duration> result =
+ std::vector<btm::resource::Duration>();
+ std::string durFilename = GetDurFullFileName() +
+ (loadComplete ? kCompletedSuffix : "");
+ std::ifstream ifs(durFilename);
+ if (!ifs.good())
+ {
+ fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
+ durFilename);
+ return result;
+ }
+
+ constexpr uint32_t kBufSize = 1024;
+ constexpr uint32_t kMatchCount = 3;
+
+ std::array<char, kBufSize> buf;
+ std::cmatch match;
+ const std::regex reg("^([0-9a-zA-Z_:]*),([0-9]+)$");
+ while (ifs.getline(buf.data(), kBufSize))
+ {
+ if (regex_match(buf.data(), match, reg))
+ {
+ if (match.size() != kMatchCount)
+ {
+ // This line is problematic. Skip it.
+ continue;
+ }
+
+ result.emplace_back(match[1].str(), std::stoll(match[2].str()));
+ }
+ }
+ return result;
+}
+
+std::optional<int64_t> File::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);
+}
+
+int64_t File::GetWallTimeInMs()
+{
+ return std::chrono::system_clock::now().time_since_epoch() /
+ std::chrono::milliseconds(1);
+}
+
+bool File::IsValidName(std::string_view name)
+{
+ return !std::regex_search(std::string(name).c_str(),
+ std::regex("[^0-9a-zA-Z_:]"));
+}
+
+inline std::string File::GetCpFullFileName()
+{
+ return std::string() + btm::resource::BTMonitorDir + mNodeName + "_" +
+ kCheckpointFile;
+}
+inline std::string File::GetDurFullFileName()
+{
+ return std::string() + btm::resource::BTMonitorDir + mNodeName + "_" +
+ kDurationFile;
+}
+} // namespace resource
+} // namespace boot_time_monitor
diff --git a/src/systemd_handler.cpp b/src/systemd_handler.cpp
new file mode 100644
index 0000000..859be37
--- /dev/null
+++ b/src/systemd_handler.cpp
@@ -0,0 +1,187 @@
+#include "systemd_handler.hpp"
+
+#include <fmt/printf.h>
+
+#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";
+
+struct SystemdTimestamp
+{
+ uint64_t firmware;
+ uint64_t loader;
+ uint64_t kernel;
+ uint64_t initRD;
+ uint64_t userspace;
+ uint64_t finish;
+};
+
+namespace
+{
+
+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::IApi> api) :
+ mApi(api), mBMCFinishTimer(conn->get_io_context())
+{
+ mBMCFinishTimer.expires_after(std::chrono::seconds(0));
+ mBMCFinishTimer.async_wait(std::bind(
+ &Handler::CheckBmcFinish, this, std::placeholders::_1, std::ref(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;
+ }
+ else if (ec)
+ {
+ fmt::print(stderr, "[CheckBmcFinish] Timer Error: {}\n",
+ ec.message().c_str());
+ return;
+ }
+
+ constexpr uint64_t kMillisecondPerSecond = 1000;
+ // 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<std::string, uint64_t*>( // Firmware
+ std::string(kSystemdFirmwareTime), ×.firmware),
+ std::make_pair<std::string, uint64_t*>( // Loader
+ std::string(kSystemdLoaderTime), ×.loader),
+ std::make_pair<std::string, uint64_t*>( // Kernel
+ std::string(kSystemdKernelTime), ×.kernel),
+ std::make_pair<std::string, uint64_t*>( // InitRD
+ std::string(kSystemdInitRDTime), ×.initRD),
+ std::make_pair<std::string, uint64_t*>( // Userspace
+ std::string(kSystemdUserspaceTime), ×.userspace),
+ std::make_pair<std::string, uint64_t*>( // 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(std::bind(&Handler::CheckBmcFinish, this,
+ std::placeholders::_1,
+ std::ref(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(std::bind(&Handler::CheckBmcFinish, this,
+ std::placeholders::_1,
+ std::ref(conn)));
+ }
+
+ // 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
+ mApi->SetNodesDurationByTag(
+ btm::kBootTimeTagBMC, convertName(kSystemdFirmwareTime),
+ times.firmware != 0
+ ? (times.firmware - times.loader) / kMillisecondPerSecond
+ : 0);
+ mApi->SetNodesDurationByTag(
+ btm::kBootTimeTagBMC, convertName(kSystemdLoaderTime),
+ times.loader != 0 ? times.loader / kMillisecondPerSecond : 0);
+ mApi->SetNodesDurationByTag(
+ btm::kBootTimeTagBMC, convertName(kSystemdKernelTime),
+ (times.initRD > 0 ? times.initRD : times.userspace) /
+ kMillisecondPerSecond);
+ mApi->SetNodesDurationByTag(
+ btm::kBootTimeTagBMC, convertName(kSystemdInitRDTime),
+ times.initRD != 0
+ ? (times.userspace - times.initRD) / kMillisecondPerSecond
+ : 0);
+ mApi->SetNodesDurationByTag(
+ btm::kBootTimeTagBMC, convertName(kSystemdUserspaceTime),
+ times.userspace != 0
+ ? (times.finish - times.userspace) / kMillisecondPerSecond
+ : 0);
+
+#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 = util->readMem4Bytes(SEC_CNT_ADDR);
+ auto upTimeMS = util->getUpTimeInMs();
+ if (powerOnSec != std::nullopt && upTimeMS != std::nullopt)
+ {
+ mApi->SetNodeDurationByTag(btm::kBootTimeTagBMC, "FirmwarePlusLoader",
+ powerOnSec.value() * 1000 -
+ upTimeMS.value());
+ }
+#endif
+
+// BMC marks the boot process as `completed` automatically if we do *NOT* have
+// external daemon to do so.
+#ifdef AUTO_BMC_COMPLETE
+ mApi->SetNodesCheckpointByTag(btm::kBootTimeTagBMC, "UserspaceEnd", 0, 0);
+ mApi->NotifyNodesCompleteByTag(btm::kBootTimeTagBMC);
+#endif
+ return;
+}
+} // namespace systemd
+} // namespace boot_time_monitor
diff --git a/src/utils.cpp b/src/utils.cpp
deleted file mode 100644
index b1afbef..0000000
--- a/src/utils.cpp
+++ /dev/null
@@ -1,230 +0,0 @@
-#include "utils.hpp"
-
-#include <fmt/printf.h>
-
-#include <boost/interprocess/file_mapping.hpp>
-#include <boost/interprocess/mapped_region.hpp>
-
-#include <array>
-#include <chrono>
-#include <filesystem>
-#include <fstream>
-#include <memory>
-#include <optional>
-#include <regex>
-#include <string>
-#include <string_view>
-#include <vector>
-
-namespace boot_time_monitor
-{
-
-namespace fs = std::filesystem;
-
-std::optional<int64_t> Util::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);
-}
-
-inline int64_t Util::getWallTimeInMs()
-{
- return std::chrono::system_clock::now().time_since_epoch() /
- std::chrono::milliseconds(1);
-}
-
-inline bool Util::isValidName(std::string_view name)
-{
- return !std::regex_search(name.data(), std::regex("[^0-9a-zA-Z_:]"));
-}
-
-inline std::string Util::getCPPath(std::string_view nodeName,
- bool wantCompleted)
-{
- return kBTMonitorDir + nodeName.data() + "_" + kCheckpointFile +
- (wantCompleted ? kCompletedSuffix : "");
-}
-
-inline std::string Util::getDurPath(std::string_view nodeName,
- bool wantCompleted)
-{
- return kBTMonitorDir + nodeName.data() + "_" + kDurationFile +
- (wantCompleted ? kCompletedSuffix : "");
-}
-
-std::optional<uint32_t> Util::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;
-}
-
-FileUtil::FileUtil(std::string_view filename) : filename(filename)
-{
- fs::path dir = fs::path(filename).remove_filename();
- if (!dir.empty() && !fs::exists(dir))
- {
- fs::create_directories(dir);
- }
- ofs = std::make_unique<std::ofstream>(
- filename.data(), std::fstream::out | std::fstream::app);
- if (!ofs->good())
- {
- throw std::invalid_argument("ofstream is not available.");
- }
-}
-
-void FileUtil::addCheckpoint(std::string_view name, int64_t wallTimeInMs,
- int64_t monoTimeInMs)
-{
- (*ofs) << name << "," << wallTimeInMs << "," << monoTimeInMs << '\n';
- ofs->flush();
-}
-
-void FileUtil::addDuration(std::string_view name, int64_t durationInMs)
-{
- (*ofs) << name << "," << durationInMs << '\n';
- ofs->flush();
-}
-
-void FileUtil::completeCurrent()
-{
- ofs->close();
- fs::rename(filename, filename + kCompletedSuffix);
-
- // Reopen for next reboot
- ofs->open(filename.c_str(), std::fstream::out | std::fstream::app);
- if (!ofs->good())
- {
- throw std::invalid_argument("ofstream is not available.");
- }
-}
-
-std::unique_ptr<std::vector<Checkpoint>>
- FileUtil::loadCheckpoints(bool loadCompleted)
-{
- std::unique_ptr<std::vector<Checkpoint>> result =
- std::make_unique<std::vector<Checkpoint>>();
- std::string cpFilename = filename + (loadCompleted ? kCompletedSuffix : "");
- std::ifstream ifs(cpFilename);
- if (!ifs.good())
- {
- fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
- cpFilename);
- return result;
- }
-
- constexpr uint32_t kBufSize = 1024;
- constexpr uint32_t kMatchCount = 4;
-
- std::array<char, kBufSize> buf;
- std::cmatch match;
- const std::regex reg("^([0-9a-zA-Z_:]*),([0-9]+),([0-9]+)$");
- while (ifs.getline(buf.data(), kBufSize))
- {
- if (regex_match(buf.data(), match, reg))
- {
- if (match.size() != kMatchCount)
- {
- // This line is problematic. Skip it.
- continue;
- }
-
- result->emplace_back(match[1].str(), std::stoll(match[2].str()),
- std::stoll(match[3].str()));
- }
- }
- return result;
-}
-
-std::unique_ptr<std::vector<Duration>>
- FileUtil::loadDurations(bool loadCompleted)
-{
- std::unique_ptr<std::vector<Duration>> result =
- std::make_unique<std::vector<Duration>>();
- std::string durFilename = filename +
- (loadCompleted ? kCompletedSuffix : "");
- std::ifstream ifs(durFilename);
- if (!ifs.good())
- {
- fmt::print("[{}] Failed to load `{}`. Skip it.\n", __FUNCTION__,
- durFilename);
- return result;
- }
-
- constexpr uint32_t kBufSize = 1024;
- constexpr uint32_t kMatchCount = 3;
-
- std::array<char, kBufSize> buf;
- std::cmatch match;
- const std::regex reg("^([0-9a-zA-Z_:]*),([0-9]+)$");
- while (ifs.getline(buf.data(), kBufSize))
- {
- if (regex_match(buf.data(), match, reg))
- {
- if (match.size() != kMatchCount)
- {
- // This line is problematic. Skip it.
- continue;
- }
-
- result->emplace_back(match[1].str(), std::stoll(match[2].str()));
- }
- }
- return result;
-}
-
-bool FileUtil::isEmpty()
-{
- const auto result = ofs->tellp();
- if (result < 0)
- {
- throw std::runtime_error("File can't be `tellp`");
- }
-
- return result == 0;
-}
-
-} // namespace boot_time_monitor
diff --git a/test/api_test.cpp b/test/api_test.cpp
new file mode 100644
index 0000000..c56d94d
--- /dev/null
+++ b/test/api_test.cpp
@@ -0,0 +1,323 @@
+#include "api.hpp"
+#include "resource.hpp"
+
+#include <fmt/format.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdlib>
+#include <filesystem>
+#include <fstream>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace boot_time_monitor
+{
+
+namespace btm = boot_time_monitor;
+
+namespace fs = std::filesystem;
+namespace
+{
+
+constexpr int kTestNodeCount = 6; // Number of nodes to create for testing
+
+class ApiTest : public ::testing::Test
+{
+ public:
+ // Wall time (second param) equals zero means BMC will get wall time by
+ // itself.
+ const std::vector<std::tuple<std::string_view, int64_t, int64_t>> inCP = {
+ {"reboot_start", 1000, 0},
+ {"Shutdown1", 0 /*will be 2000*/, 1000},
+ {"thisIs_2_shutdown", 0 /*will be 3000*/, 50},
+ {"000S0", 0 /*will be 4000*/, 0},
+ {"_PowerBack_", 0 /*will be 5000*/, 100},
+ {"start1", 0 /*will be 6000*/, 500},
+ {"start2", 0 /*will be 7000*/, 100},
+ {"reboot_end", 8000, 0},
+ };
+
+ const std::vector<std::tuple<std::string_view, int64_t>> inDur = {
+ {"nic1", 0},
+ {"A_nic", 1000},
+ {"_nextNIC", 4},
+ {"99NNN", 99},
+ };
+
+ ApiTest()
+ {
+ // Use a temporary directory for test files
+ // Prefer TMPDIR environment variable if set, otherwise use /tmp
+ const char* tmp_env = std::getenv("TMPDIR");
+ std::string base_tmp = (tmp_env != nullptr) ? tmp_env : "/tmp";
+ testDir = base_tmp + "/api_test_temp_" + std::to_string(getpid()) + "/";
+
+ // Clean up the temporary directory and its contents
+ // Check if the directory exists before removing
+ if (fs::exists(testDir))
+ fs::remove_all(testDir);
+ else
+ fs::create_directories(testDir); // Ensure the directory exists
+
+ // Use a temporary directory for test files
+ btm::resource::BTMonitorDir =
+ testDir; // Set the global monitor dir for this test scope
+ }
+
+ protected:
+ std::string testDir;
+};
+
+/*
+ * Verifies that setting multiple checkpoints, including those that generate
+ * an additional ":BEGIN" entry due to a non-zero duration, results in the
+ * correct total count when retrieved via GetNodeCheckpointList immediately
+ * after setting.
+ */
+TEST_F(ApiTest, checkpoint_set)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host0NodeConfig("host0",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host0NodeConfig);
+
+ // Calculate expected lines: checkpoints with duration != 0 add two lines
+ std::size_t expectedLines = 0;
+ for (const auto& cp : inCP)
+ {
+ expectedLines += (std::get<2>(cp) != 0) ? 2 : 1;
+ }
+
+ // Set checkpoints for host0
+ for (const auto& cp : inCP)
+ {
+ api->SetNodeCheckpoint(host0NodeConfig, std::get<0>(cp),
+ std::get<1>(cp), std::get<2>(cp));
+ }
+
+ EXPECT_EQ(api->GetNodeCheckpointList(host0NodeConfig).size(),
+ expectedLines);
+}
+
+/*
+ * Verifies that calling SetNodeCheckpoint with a non-zero duration specifically
+ * creates both the original checkpoint entry and the associated ":BEGIN" entry.
+ */
+TEST_F(ApiTest, checkpoint_set_with_duration)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host1NodeConfig("host1",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host1NodeConfig);
+
+ // Test on host1
+ const std::string_view testName = "StageWithDuration";
+ const int64_t testDuration = 500;
+ const std::string expectedBeginName = std::string(testName) +
+ btm::resource::kBeginStageSuffix;
+
+ api->SetNodeCheckpoint(host1NodeConfig, testName, 0, testDuration);
+
+ auto checkpoints = api->GetNodeCheckpointList(host1NodeConfig);
+ EXPECT_EQ(checkpoints.size(), 2);
+
+ // Check if both the original and the :BEGIN checkpoints exist
+ bool foundOriginal = false;
+ bool foundBegin = false;
+ for (const auto& cp : checkpoints)
+ {
+ if (std::get<0>(cp) == testName)
+ {
+ foundOriginal = true;
+ }
+ else if (std::get<0>(cp) == expectedBeginName)
+ {
+ foundBegin = true;
+ }
+ }
+ EXPECT_TRUE(foundOriginal);
+ EXPECT_TRUE(foundBegin);
+}
+
+/*
+ * Verifies that setting multiple durations via SetNodeDuration results in the
+ * correct total count when retrieved via GetNodeAdditionalDurations
+ * immediately after setting.
+ */
+TEST_F(ApiTest, duration_set)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host2NodeConfig("host2",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host2NodeConfig);
+
+ // Test on host2
+ // Set durations for host2
+ for (const auto& dur : inDur)
+ {
+ api->SetNodeDuration(host2NodeConfig, std::get<0>(dur),
+ std::get<1>(dur));
+ }
+ EXPECT_EQ(api->GetNodeAdditionalDurations(host2NodeConfig).size(),
+ inDur.size());
+}
+
+/*
+ * Verifies the state transitions of the IsNodeRebooting flag.
+ * It should be false initially, true after a checkpoint is set, false after
+ * NotifyNodeComplete is called, and true again after another checkpoint is set.
+ */
+TEST_F(ApiTest, rebootcomplete_behavior)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host3NodeConfig("host3",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host3NodeConfig);
+
+ // Test on host3
+ // Initially, no files exist or are empty, so not rebooting
+ EXPECT_FALSE(api->IsNodeRebooting(host3NodeConfig));
+
+ // Setting a checkpoint starts the "reboot" process for the node
+ api->SetNodeCheckpoint(host3NodeConfig, "FirstCheckpoint", 0, 0);
+ EXPECT_TRUE(api->IsNodeRebooting(host3NodeConfig));
+
+ // Marking complete finishes the reboot process
+ api->NotifyNodeComplete(host3NodeConfig);
+ EXPECT_FALSE(api->IsNodeRebooting(host3NodeConfig));
+
+ // Setting another checkpoint starts a new reboot cycle
+ api->SetNodeCheckpoint(host3NodeConfig, "SecondCheckpoint", 0, 0);
+ EXPECT_TRUE(api->IsNodeRebooting(host3NodeConfig));
+}
+
+/*
+ * Verifies that GetNodeCheckpointList retrieves the list of checkpoints
+ * belonging to the *current* boot cycle (i.e., since the last
+ * NotifyNodeComplete). Checkpoints from the previous completed cycle are not
+ * included.
+ */
+TEST_F(ApiTest, checkpoint_get)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host4NodeConfig("host4",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host4NodeConfig);
+
+ // Test on host4
+ // Set the initial checkpoints
+ for (const auto& cp : inCP)
+ {
+ api->SetNodeCheckpoint(host4NodeConfig, std::get<0>(cp),
+ std::get<1>(cp), std::get<2>(cp));
+ }
+ // Calculate expected lines for the initial set
+ std::size_t expectedLines =
+ api->GetNodeCheckpointList(host4NodeConfig).size();
+
+ // Mark the initial set as complete
+ api->NotifyNodeComplete(host4NodeConfig);
+ // GetCheckpointList should return the *completed* list
+ EXPECT_EQ(api->GetNodeCheckpointList(host4NodeConfig).size(),
+ expectedLines);
+
+ // Set a new checkpoint (part of the *next* boot cycle)
+ api->SetNodeCheckpoint(host4NodeConfig, "NewCycleCheckpoint", 0, 0);
+
+ // GetCheckpointList should return the checkpoint amount after previous
+ // RebootComplete
+ EXPECT_EQ(api->GetNodeCheckpointList(host4NodeConfig).size(), 1);
+}
+
+/*
+ * Verifies that when SetNodeCheckpoint is called with wallTime=0, the system
+ * automatically assigns non-zero values for both the wall time (epoch) and
+ * monotonic time in the recorded checkpoint data.
+ */
+TEST_F(ApiTest, checkpoint_monotory)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host5NodeConfig("host5",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host5NodeConfig);
+
+ // Test on host5
+ api->SetNodeCheckpoint(host5NodeConfig, "AutoTimestamp", 0, 0);
+
+ auto checkpoints = api->GetNodeCheckpointList(host5NodeConfig);
+ ASSERT_EQ(checkpoints.size(), 1);
+
+ // Wall time (index 1) and monotonic time (index 2) should be non-zero
+ EXPECT_NE(std::get<1>(checkpoints[0]), 0);
+ EXPECT_NE(std::get<2>(checkpoints[0]), 0);
+}
+
+/*
+ * Verifies that calls to SetPSUCheckpoint and SetPSUDuration are broadcast
+ * and correctly recorded for *all* registered nodes.
+ */
+TEST_F(ApiTest, powercycle_behavior)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ std::vector<btm::NodeConfig> ncs;
+ for (int i = 6; i < 8; i++)
+ {
+ ncs.emplace_back("host6", std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(ncs.back());
+ }
+
+ // This test inherently checks all nodes
+ const std::string_view psuCpName = "PSU_PowerOn";
+ const std::string_view psuDurName = "PSU_Stabilize";
+
+ api->SetPSUCheckpoint(psuCpName, 0, 0);
+ api->SetPSUDuration(psuDurName, 1234);
+
+ for (const auto& nc : ncs) // Iterate through all created nodes
+ {
+ auto checkpoints = api->GetNodeCheckpointList(nc);
+ EXPECT_EQ(checkpoints.size(), 1);
+ EXPECT_EQ(std::get<0>(checkpoints.back()), psuCpName);
+
+ auto durations = api->GetNodeAdditionalDurations(nc);
+ EXPECT_EQ(durations.size(), 1);
+ EXPECT_EQ(std::get<0>(durations.back()), psuDurName);
+ EXPECT_EQ(std::get<1>(durations.back()), 1234);
+ }
+}
+
+/*
+ * Verifies that the NotifyNodeComplete(RebootComplete) should be individule,
+ * not boardcast to all of the nodes.
+ */
+TEST_F(ApiTest, notify_complete_is_individual)
+{
+ std::unique_ptr<btm::api::Api> api = std::make_unique<btm::api::Api>();
+ btm::NodeConfig host9NodeConfig("host9",
+ std::string(btm::kBootTimeTagHost));
+ btm::NodeConfig host10NodeConfig("host10",
+ std::string(btm::kBootTimeTagHost));
+ api->RegisterNode(host9NodeConfig);
+ api->RegisterNode(host10NodeConfig);
+
+ // Set checkpoints on two different nodes to mark them as rebooting
+ api->SetNodeCheckpoint(host9NodeConfig, "Node0_BootStart", 0, 0);
+ api->SetNodeCheckpoint(host10NodeConfig, "Node1_BootStart", 0, 0);
+
+ EXPECT_TRUE(api->IsNodeRebooting(host9NodeConfig));
+ EXPECT_TRUE(api->IsNodeRebooting(host10NodeConfig));
+
+ // Notify only the first node as complete
+ api->NotifyNodeComplete(host9NodeConfig);
+
+ // Verify the first node is no longer rebooting, but the second one still is
+ EXPECT_FALSE(api->IsNodeRebooting(host9NodeConfig));
+ EXPECT_TRUE(api->IsNodeRebooting(host10NodeConfig));
+}
+} // namespace
+} // namespace boot_time_monitor
diff --git a/test/boot_manager_test.cpp b/test/boot_manager_test.cpp
deleted file mode 100644
index bcbfd32..0000000
--- a/test/boot_manager_test.cpp
+++ /dev/null
@@ -1,197 +0,0 @@
-#include "boot_manager.hpp"
-#include "mockups.hpp"
-#include "utils.hpp"
-
-#include <string>
-#include <string_view>
-#include <tuple>
-
-#include <gtest/gtest.h>
-
-namespace boot_time_monitor
-{
-namespace
-{
-
-using ::testing::_;
-using ::testing::Return;
-
-class BootManagerTest : public ::testing::Test
-{
- public:
- // Wall time (second param) equals zero means BMC will get wall time by
- // itself.
- const std::vector<std::tuple<std::string_view, int64_t, int64_t>> inCP = {
- {"reboot_start", 1000, 0},
- {"Shutdown1", 0 /*will be 2000*/, 1000},
- {"thisIs_2_shutdown", 0 /*will be 3000*/, 50},
- {"000S0", 0 /*will be 4000*/, 0},
- {"_PowerBack_", 0 /*will be 5000*/, 100},
- {"start1", 0 /*will be 6000*/, 500},
- {"start2", 0 /*will be 7000*/, 100},
- {"reboot_end", 8000, 0},
- };
-
- const std::vector<std::tuple<std::string_view, int64_t>> inDur = {
- {"nic1", 0},
- {"A_nic", 1000},
- {"_nextNIC", 4},
- {"99NNN", 99},
- };
-
- BootManagerTest() :
- util(std::make_shared<MockUtil>()),
- cpFile(std::make_shared<MockFileUtil>()),
- durFile(std::make_shared<MockFileUtil>())
- {
- utilPtr = util.get();
- cpFilePtr = cpFile.get();
- durFilePtr = durFile.get();
-
- EXPECT_CALL(*cpFile, loadCheckpoints(false))
- .WillOnce(Return(std::make_unique<std::vector<Checkpoint>>()));
- EXPECT_CALL(*durFile, loadDurations(false))
- .WillOnce(Return(std::make_unique<std::vector<Duration>>()));
- EXPECT_CALL(*cpFile, loadCheckpoints(true))
- .WillOnce(Return(std::make_unique<std::vector<Checkpoint>>()));
- EXPECT_CALL(*durFile, loadDurations(true))
- .WillOnce(Return(std::make_unique<std::vector<Duration>>()));
-
- stateMachine = std::make_unique<BootManager>(
- std::move(util), std::move(cpFile), std::move(durFile));
- }
-
- protected:
- std::shared_ptr<MockUtil> util;
- MockUtil* utilPtr;
- std::shared_ptr<MockFileUtil> cpFile;
- MockFileUtil* cpFilePtr;
- std::shared_ptr<MockFileUtil> durFile;
- MockFileUtil* durFilePtr;
-
- std::unique_ptr<BootManager> stateMachine;
-};
-
-TEST_F(BootManagerTest, AddCheckpointTest)
-{
- EXPECT_CALL(*utilPtr, getUpTimeInMs()).WillOnce(Return(10000));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(inCP[0])))
- .WillOnce(Return(true));
- EXPECT_CALL(*cpFilePtr, addCheckpoint(std::get<0>(inCP[0]), _, _));
- stateMachine->setCheckpoint(std::get<0>(inCP[0]), std::get<1>(inCP[0]),
- std::get<2>(inCP[0]));
-
- for (uint32_t i = 1; i <= 6; i++)
- {
- EXPECT_CALL(*utilPtr, getWallTimeInMs())
- .WillOnce(Return(1000 + 1000 * i));
- EXPECT_CALL(*utilPtr, getUpTimeInMs())
- .WillOnce(Return(10000 + 1000 * i));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(inCP[i])))
- .WillOnce(Return(true));
- // Won't enter `addCheckpoint` twice if duration is 0.
- if (std::get<2>(inCP[i]) != 0)
- {
- EXPECT_CALL(
- *cpFilePtr,
- addCheckpoint(std::get<0>(inCP[i]).data() +
- std::string{BootManager::kBeginStageSuffix},
- _, _));
- }
- EXPECT_CALL(*cpFilePtr, addCheckpoint(std::get<0>(inCP[i]), _, _));
- stateMachine->setCheckpoint(std::get<0>(inCP[i]), std::get<1>(inCP[i]),
- std::get<2>(inCP[i]));
- }
-
- EXPECT_CALL(*utilPtr, getUpTimeInMs()).WillOnce(Return(17000));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(inCP[7])))
- .WillOnce(Return(true));
- EXPECT_CALL(*cpFilePtr, addCheckpoint(std::get<0>(inCP[7]), _, _));
- stateMachine->setCheckpoint(std::get<0>(inCP[7]), std::get<1>(inCP[7]),
- std::get<2>(inCP[7]));
-
- auto cps = stateMachine->getCheckpoints();
- int32_t count = 0;
- for (uint32_t i = 0; i < inCP.size(); i++)
- {
- auto name = std::get<0>(inCP[i]);
- auto duration = std::get<2>(inCP[i]);
-
- if (i != 0 && duration != 0)
- {
- EXPECT_EQ(cps[count].name,
- name.data() +
- std::string{BootManager::kBeginStageSuffix});
- EXPECT_EQ(cps[count].wallTime, 1000 * (i + 1) - duration);
- EXPECT_EQ(cps[count].monoTime, 10000 + 1000 * i - duration);
- count++;
- }
- EXPECT_EQ(cps[count].name, name);
- EXPECT_EQ(cps[count].wallTime, 1000 * (i + 1));
- EXPECT_EQ(cps[count].monoTime, 10000 + 1000 * i);
- count++;
- }
- EXPECT_EQ(count, cps.size());
-}
-
-TEST_F(BootManagerTest, AddDurationTest)
-{
- for (auto dur : inDur)
- {
- EXPECT_CALL(*durFilePtr, addDuration(_, _));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(dur)))
- .WillOnce(Return(true));
- stateMachine->setDuration(std::get<0>(dur), std::get<1>(dur));
- }
-
- auto durs = stateMachine->getDurations();
- EXPECT_EQ(inDur.size(), durs.size());
-
- for (uint32_t i = 0; i < inDur.size(); i++)
- {
- auto name = std::get<0>(inDur[i]);
- auto duration = std::get<1>(inDur[i]);
-
- EXPECT_EQ(durs[i].name, name);
- EXPECT_EQ(durs[i].duration, duration);
- }
-}
-
-TEST_F(BootManagerTest, CompleteTest)
-{
- EXPECT_CALL(*utilPtr, getUpTimeInMs()).WillOnce(Return(10000));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(inCP[0])))
- .WillOnce(Return(true));
- EXPECT_CALL(*cpFilePtr, addCheckpoint(std::get<0>(inCP[0]), _, _));
- stateMachine->setCheckpoint(std::get<0>(inCP[0]), std::get<1>(inCP[0]),
- std::get<2>(inCP[0]));
- EXPECT_CALL(*durFilePtr, addDuration(std::get<0>(inDur[0]), _));
- EXPECT_CALL(*utilPtr, isValidName(std::get<0>(inDur[0])))
- .WillOnce(Return(true));
- stateMachine->setDuration(std::get<0>(inDur[0]), std::get<1>(inDur[0]));
-
- // Originally these 2 vectors should be empty.
- EXPECT_TRUE(stateMachine->getPreCheckpoints().empty());
- EXPECT_TRUE(stateMachine->getPreDurations().empty());
-
- EXPECT_CALL(*cpFilePtr, completeCurrent());
- EXPECT_CALL(*durFilePtr, completeCurrent());
- stateMachine->notifyComplete();
-
- auto cps = stateMachine->getPreCheckpoints();
- auto durs = stateMachine->getPreDurations();
- EXPECT_EQ(cps.size(), 1);
- EXPECT_EQ(durs.size(), 1);
- EXPECT_TRUE(stateMachine->getCheckpoints().empty());
- EXPECT_TRUE(stateMachine->getDurations().empty());
-
- EXPECT_EQ(cps[0].name, std::get<0>(inCP[0]));
- EXPECT_EQ(cps[0].wallTime, std::get<1>(inCP[0]));
- EXPECT_EQ(cps[0].monoTime, 10000);
-
- EXPECT_EQ(durs[0].name, std::get<0>(inDur[0]));
- EXPECT_EQ(durs[0].duration, std::get<1>(inDur[0]));
-}
-
-} // namespace
-} // namespace boot_time_monitor
diff --git a/test/include/mockups.hpp b/test/include/mockups.hpp
deleted file mode 100644
index 387e006..0000000
--- a/test/include/mockups.hpp
+++ /dev/null
@@ -1,64 +0,0 @@
-#pragma once
-
-#include "boot_manager.hpp"
-#include "utils.hpp"
-
-#include <cstdint>
-#include <memory>
-#include <optional>
-#include <string_view>
-
-#include <gmock/gmock.h>
-
-namespace boot_time_monitor
-{
-
-class MockUtil : public UtilIface
-{
- public:
- MOCK_METHOD(std::optional<int64_t>, getUpTimeInMs, (), (override));
- MOCK_METHOD(int64_t, getWallTimeInMs, (), (override));
- MOCK_METHOD(bool, isValidName, (std::string_view), (override));
- MOCK_METHOD(std::string, getCPPath, (std::string_view, bool), (override));
- MOCK_METHOD(std::string, getDurPath, (std::string_view, bool), (override));
- MOCK_METHOD(std::optional<uint32_t>, readMem4Bytes, (uint32_t), (override));
-};
-
-class MockFileUtil : public FileUtilIface
-{
- public:
- MOCK_METHOD(void, addCheckpoint,
- (std::string_view name, int64_t wallTimeInMs,
- int64_t monoTimeInMs),
- (override));
- MOCK_METHOD(void, addDuration,
- (std::string_view name, int64_t durationInMs), (override));
- MOCK_METHOD(void, completeCurrent, (), (override));
- MOCK_METHOD(std::unique_ptr<std::vector<Checkpoint>>, loadCheckpoints,
- (bool), (override));
- MOCK_METHOD(std::unique_ptr<std::vector<Duration>>, loadDurations, (bool),
- (override));
- MOCK_METHOD(bool, isEmpty, (), (override));
-};
-
-class MockBootManager : public BootManagerIface
-{
- MOCK_METHOD(void, setCheckpoint,
- (std::string_view cpName, int64_t externalWallTime,
- int64_t duration),
- (override));
- MOCK_METHOD(void, setDuration, (std::string_view durName, int64_t duration),
- (override));
- MOCK_METHOD(void, notifyComplete, (), (override));
- MOCK_METHOD(bool, isRebooting, (), (override));
- MOCK_METHOD((const std::vector<Checkpoint>&), getCheckpoints, (),
- (const, override));
- MOCK_METHOD((const std::vector<Duration>&), getDurations, (),
- (const, override));
- MOCK_METHOD((const std::vector<Checkpoint>&), getPreCheckpoints, (),
- (const, override));
- MOCK_METHOD((const std::vector<Duration>&), getPreDurations, (),
- (const, override));
-};
-
-} // namespace boot_time_monitor
diff --git a/test/meson.build b/test/meson.build
index 61279cf..ed84efc 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -20,17 +20,13 @@
endif
endif
-boot_time_monitor_test_incs = include_directories('include')
-
tests = [
- 'utils_test',
- 'boot_manager_test',
+ 'api_test',
]
foreach t : tests
test(t, executable(t.underscorify(), t + '.cpp',
build_by_default: false,
- include_directories : boot_time_monitor_test_incs,
implicit_include_directories: false,
dependencies: [boot_time_monitor_dep, gtest, gmock]))
endforeach
diff --git a/test/utils_test.cpp b/test/utils_test.cpp
deleted file mode 100644
index 9326069..0000000
--- a/test/utils_test.cpp
+++ /dev/null
@@ -1,190 +0,0 @@
-#include "config.h"
-
-#include "mockups.hpp"
-#include "utils.hpp"
-
-#include <chrono>
-#include <filesystem>
-#include <fstream>
-#include <memory>
-#include <optional>
-#include <string>
-#include <string_view>
-#include <thread>
-
-#include <gtest/gtest.h>
-
-namespace boot_time_monitor
-{
-namespace
-{
-
-namespace fs = std::filesystem;
-
-TEST(DoesUtilWorkTest, Works)
-{
- Util u;
- auto curUpTime = u.getUpTimeInMs();
- auto curWallTime = u.getWallTimeInMs();
- std::this_thread::sleep_for(std::chrono::seconds(2));
-
- ASSERT_NE(curUpTime, std::nullopt);
- ASSERT_GT(curUpTime.value(), 0);
-
- EXPECT_GT(u.getUpTimeInMs().value_or(-1), curUpTime);
- EXPECT_GT(u.getWallTimeInMs(), curWallTime);
-}
-
-TEST(ValidNameTest, Valid)
-{
- Util u;
- EXPECT_TRUE(u.isValidName("0129"));
- EXPECT_TRUE(u.isValidName("ABCZ"));
- EXPECT_TRUE(u.isValidName("abcz"));
- EXPECT_TRUE(u.isValidName("____"));
- EXPECT_TRUE(u.isValidName("A_a_1_"));
- EXPECT_FALSE(u.isValidName("A-_a_1_"));
- EXPECT_FALSE(u.isValidName("A_a*_1_"));
- EXPECT_FALSE(u.isValidName("A_a_1@_"));
- EXPECT_FALSE(u.isValidName("A#_a_1_"));
- EXPECT_FALSE(u.isValidName("A_a _1_"));
-}
-
-TEST(getPathTest, Works)
-{
- Util u;
- EXPECT_EQ(u.getCPPath("host0", false),
- std::string(bootTimeDataDir) + "host0_checkpoints.csv");
- EXPECT_EQ(u.getCPPath("bmc", false),
- std::string(bootTimeDataDir) + "bmc_checkpoints.csv");
- EXPECT_EQ(u.getCPPath("_N_o_d_e_", false),
- std::string(bootTimeDataDir) + "_N_o_d_e__checkpoints.csv");
-
- EXPECT_EQ(u.getCPPath("host0", true),
- std::string(bootTimeDataDir) + "host0_checkpoints.csv.completed");
- EXPECT_EQ(u.getCPPath("bmc", true),
- std::string(bootTimeDataDir) + "bmc_checkpoints.csv.completed");
- EXPECT_EQ(u.getCPPath("_N_o_d_e_", true),
- std::string(bootTimeDataDir) +
- "_N_o_d_e__checkpoints.csv.completed");
-
- EXPECT_EQ(u.getDurPath("host0", false),
- std::string(bootTimeDataDir) + "host0_durations.csv");
- EXPECT_EQ(u.getDurPath("bmc", false),
- std::string(bootTimeDataDir) + "bmc_durations.csv");
- EXPECT_EQ(u.getDurPath("_N_o_d_e_", false),
- std::string(bootTimeDataDir) + "_N_o_d_e__durations.csv");
-
- EXPECT_EQ(u.getDurPath("host0", true),
- std::string(bootTimeDataDir) + "host0_durations.csv.completed");
- EXPECT_EQ(u.getDurPath("bmc", true),
- std::string(bootTimeDataDir) + "bmc_durations.csv.completed");
- EXPECT_EQ(u.getDurPath("_N_o_d_e_", true),
- std::string(bootTimeDataDir) +
- "_N_o_d_e__durations.csv.completed");
-}
-
-TEST(DoesFileUtilWork, CheckpointWorks)
-{
- constexpr std::string_view cpName = "temp_cp";
- const std::string cpCompleted = std::string{cpName} + kCompletedSuffix;
-
- // Clean previous test result
- fs::remove(cpName);
- fs::remove(cpCompleted);
-
- FileUtil cpUtil(cpName);
- EXPECT_TRUE(cpUtil.isEmpty());
- cpUtil.addCheckpoint("checkpoint_10", 10, 20);
- cpUtil.addCheckpoint("checkpoint_30", 30, 50);
- EXPECT_FALSE(cpUtil.isEmpty());
-
- // Completed data should be empty
- EXPECT_TRUE(cpUtil.loadCheckpoints(true)->empty());
- auto vec = cpUtil.loadCheckpoints(false);
- EXPECT_EQ(vec->size(), 2);
- EXPECT_EQ((*vec)[0].name, "checkpoint_10");
- EXPECT_EQ((*vec)[0].wallTime, 10);
- EXPECT_EQ((*vec)[0].monoTime, 20);
- EXPECT_EQ((*vec)[1].name, "checkpoint_30");
- EXPECT_EQ((*vec)[1].wallTime, 30);
- EXPECT_EQ((*vec)[1].monoTime, 50);
-
- cpUtil.completeCurrent();
- // Current data should be empty
- EXPECT_TRUE(cpUtil.loadCheckpoints(false)->empty());
- EXPECT_TRUE(cpUtil.isEmpty());
-
- // Check completed data
- vec = cpUtil.loadCheckpoints(true);
- EXPECT_EQ(vec->size(), 2);
- EXPECT_EQ((*vec)[0].name, "checkpoint_10");
- EXPECT_EQ((*vec)[0].wallTime, 10);
- EXPECT_EQ((*vec)[0].monoTime, 20);
- EXPECT_EQ((*vec)[1].name, "checkpoint_30");
- EXPECT_EQ((*vec)[1].wallTime, 30);
- EXPECT_EQ((*vec)[1].monoTime, 50);
-
- // Check file content
- std::string input;
- std::ifstream ifs(cpCompleted);
- ASSERT_TRUE(ifs.good());
- ifs >> input;
- EXPECT_EQ(input, "checkpoint_10,10,20");
- ifs >> input;
- EXPECT_EQ(input, "checkpoint_30,30,50");
- EXPECT_FALSE(ifs >> input);
- ifs.close();
-}
-
-TEST(DoesFileUtilWork, DurationWorks)
-{
- constexpr std::string_view durName = "temp_dur";
- const std::string durCompleted = std::string{durName} + kCompletedSuffix;
-
- // Clean previous test result
- fs::remove(durName);
- fs::remove(durCompleted);
-
- FileUtil durUtil(durName);
- EXPECT_TRUE(durUtil.isEmpty());
- durUtil.addDuration("duration_1000", 1000);
- durUtil.addDuration("duration_3000", 3000);
- EXPECT_FALSE(durUtil.isEmpty());
-
- // Completed data should be empty
- EXPECT_TRUE(durUtil.loadDurations(true)->empty());
- auto vec = durUtil.loadDurations(false);
- EXPECT_EQ(vec->size(), 2);
- EXPECT_EQ((*vec)[0].name, "duration_1000");
- EXPECT_EQ((*vec)[0].duration, 1000);
- EXPECT_EQ((*vec)[1].name, "duration_3000");
- EXPECT_EQ((*vec)[1].duration, 3000);
-
- durUtil.completeCurrent();
- // Current data should be empty
- EXPECT_TRUE(durUtil.loadDurations(false)->empty());
- EXPECT_TRUE(durUtil.isEmpty());
-
- // Check completed data
- vec = durUtil.loadDurations(true);
- EXPECT_EQ(vec->size(), 2);
- EXPECT_EQ((*vec)[0].name, "duration_1000");
- EXPECT_EQ((*vec)[0].duration, 1000);
- EXPECT_EQ((*vec)[1].name, "duration_3000");
- EXPECT_EQ((*vec)[1].duration, 3000);
-
- // Check file content
- std::string input;
- std::ifstream ifs(durCompleted);
- ASSERT_TRUE(ifs.good());
- ifs >> input;
- EXPECT_EQ(input, "duration_1000,1000");
- ifs >> input;
- EXPECT_EQ(input, "duration_3000,3000");
- EXPECT_FALSE(ifs >> input);
- ifs.close();
-}
-
-} // namespace
-} // namespace boot_time_monitor