dbus-top: initial commits

This commit covers the basic functionalities of the dbus-top tool.

The UI is divided into 3 windows as follows:

  +--------------------------+  Window list
  |         Window A         |  A: Summary statistics
  +------------+-------------+  B: Sensor list or detail
  |  Window B  |   Window C  |  C: Detailed statistics
  +------------+-------------+

To navigate the UI:
* Use tab to navigate each window

When a window is highlighted:
  In Window B:
  * Press esc key 3 times to leave the current sensor selection

  In Window C:
  * Press [Enter] to show/hide pop-up menu for column selectio
  * Press [Left]  to move highlight cursor to the left
  * Press [Right] to move highlight cursor to the right
  * Press [A] to sort by the highlighted column in ascending order
  * Press [D] to sort by the highlighted column in descending order

To add recipe to Yocto and build the recipe:
1) Copy and paste the content of the .bb file into a folder that can be
   detected by bitbake, such as meta-phosphor/recipes-phosphor/ipmi.
2) run "devtool modify -n dbus-top (path_to_openbmc_tools)/dbus-top/".

Signed-off-by: Adedeji Adebisi <adedejiadebisi01@gmail.com>
Change-Id: Id58ba30b815cfd9d18f54cf477d749dbdbc4545b
diff --git a/dbus-top/analyzer.cpp b/dbus-top/analyzer.cpp
new file mode 100644
index 0000000..f31868c
--- /dev/null
+++ b/dbus-top/analyzer.cpp
@@ -0,0 +1,624 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "analyzer.hpp"
+#include "histogram.hpp"
+#include "main.hpp"
+#include "sensorhelper.hpp"
+#include "views.hpp"
+#include "xmlparse.hpp"
+
+#include <unistd.h>
+#include <atomic>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+extern SensorSnapshot* g_sensor_snapshot;
+extern DBusConnectionSnapshot* g_connection_snapshot;
+extern sd_bus* g_bus;
+extern SensorDetailView* g_sensor_detail_view;
+
+static std::unordered_map<uint64_t, uint64_t>
+    in_flight_methodcalls; // serial => microseconds
+uint64_t Microseconds()
+{
+    long us;  // usec
+    time_t s; // Seconds
+    struct timespec spec;
+    clock_gettime(CLOCK_REALTIME, &spec);
+    s = spec.tv_sec;
+    us = round(spec.tv_nsec / 1000); // Convert nanoseconds to milliseconds
+    if (us > 999999)
+    {
+        s++;
+        us = 0;
+    }
+    return s * 1000000 + us;
+}
+
+int g_update_interval_millises = 2000;
+int GetSummaryIntervalInMillises()
+{
+    return g_update_interval_millises;
+}
+
+bool DBusTopSortFieldIsNumeric(DBusTopSortField field)
+{
+    switch (field)
+    {
+        case kSender:
+        case kDestination:
+        case kInterface:
+        case kPath:
+        case kMember:
+        case kSenderCMD:
+            return false;
+        case kSenderPID:
+        case kMsgPerSec:
+        case kAverageLatency:
+            return true;
+    }
+    return false;
+}
+
+namespace dbus_top_analyzer
+{
+    DBusTopStatistics g_dbus_statistics;
+    Histogram<float> g_mc_time_histogram;
+    std::unordered_map<uint32_t, uint64_t> in_flight_methodcalls;
+    std::atomic<bool> g_program_done = false;
+    std::chrono::time_point<std::chrono::steady_clock> g_last_update;
+    DBusTopStatisticsCallback g_callback;
+    void SetDBusTopStatisticsCallback(DBusTopStatisticsCallback cb)
+    {
+        g_callback = cb;
+    }
+
+    int UserInputThread()
+    {
+        return 0;
+    }
+
+    std::string g_dbus_top_conn = " ";
+    void SetDBusTopConnectionForMonitoring(const std::string& conn)
+    {
+        g_dbus_top_conn = conn;
+    }
+
+    // Performs one step of analysis
+    void Process()
+    {
+        std::chrono::time_point<std::chrono::steady_clock> t =
+            std::chrono::steady_clock::now();
+        std::chrono::time_point<std::chrono::steady_clock> next_update =
+            g_last_update + std::chrono::milliseconds(g_update_interval_millises);
+        if (t >= next_update)
+        {
+            float seconds_since_last_sample =
+                std::chrono::duration_cast<std::chrono::microseconds>(t -
+                                                                    g_last_update)
+                    .count() /
+                1000000.0f;
+            g_dbus_statistics.seconds_since_last_sample_ =
+                seconds_since_last_sample;
+            // Update snapshot
+            if (g_callback)
+            {
+                g_callback(&g_dbus_statistics, &g_mc_time_histogram);
+            }
+            g_dbus_statistics.Reset();
+            g_last_update = t;
+        }
+    }
+
+    void Finish()
+    {
+        g_program_done = true;
+    }
+
+    std::vector<std::string> FindAllObjectPathsForService(
+        const std::string& service,
+        std::function<void(const std::string&, const std::vector<std::string>&)>
+            on_interface_cb)
+    {
+        sd_bus_error err = SD_BUS_ERROR_NULL;
+        sd_bus_message *m, *reply;
+        std::vector<std::string> paths; // Current iteration
+        std::vector<std::string>
+            all_obj_paths; // All object paths under the supervision of ObjectMapper
+        paths.push_back("/");
+        // busctl call xyz.openbmc_project.ObjectMapper        /
+        // org.freedesktop.DBus.Introspectable        Introspect
+        while (!paths.empty())
+        {
+            // printf("%d paths to explore, total %d paths so far\n",
+            // int(paths.size()), int(all_obj_paths.size()));
+            std::vector<std::string> new_paths;
+            for (const std::string& obj_path : paths)
+            {
+                all_obj_paths.push_back(obj_path);
+                int r = sd_bus_message_new_method_call(
+                    g_bus, &m, service.c_str(), obj_path.c_str(),
+                    "org.freedesktop.DBus.Introspectable", "Introspect");
+                if (r < 0)
+                {
+                    printf("Oh! Cannot create new method call. r=%d, strerror=%s\n",
+                        r, strerror(-r));
+                    continue;
+                }
+                r = sd_bus_call(g_bus, m, 0, &err, &reply);
+                if (r < 0)
+                {
+                    printf("Could not execute method call, r=%d, strerror=%s\n", r,
+                        strerror(-r));
+                }
+                const char* sig = sd_bus_message_get_signature(reply, 0);
+                if (!strcmp(sig, "s"))
+                {
+                    const char* s;
+                    int r = sd_bus_message_read(reply, "s", &s);
+                    std::string s1(s);
+                    if (r < 0)
+                    {
+                        printf("Could not read string payload, r=%d, strerror=%s\n",
+                            r, strerror(-r));
+                    }
+                    else
+                    {
+                        XMLNode* t = ParseXML(s1);
+                        std::vector<std::string> ch = t->GetChildNodeNames();
+                        if (on_interface_cb != nullptr)
+                        {
+                            on_interface_cb(obj_path, t->GetInterfaceNames());
+                        }
+                        DeleteTree(t);
+                        for (const std::string& cn : ch)
+                        {
+                            std::string ch_path = obj_path;
+                            if (obj_path.back() == '/')
+                            {}
+                            else
+                                ch_path.push_back('/');
+                            ch_path += cn;
+                            new_paths.push_back(ch_path);
+                        }
+                    }
+                }
+            }
+            paths = new_paths;
+        }
+        return all_obj_paths;
+    }
+
+    void ListAllSensors()
+    {
+        g_connection_snapshot = new DBusConnectionSnapshot();
+        printf("1. Getting names\n");
+        char** names;
+        int r = sd_bus_list_names(g_bus, &names, nullptr);
+        std::vector<std::string> services;
+        std::vector<int> pids;
+        std::vector<std::string> comms;
+        for (char** ptr = names; ptr && *ptr; ++ptr)
+        {
+            services.push_back(*ptr);
+            free(*ptr);
+        }
+        free(names);
+        printf("2. Getting creds of each name\n");
+        for (int i = 0; i < static_cast<int>(services.size()); i++)
+        {
+            const std::string& service = services[i];
+            sd_bus_creds* creds = nullptr;
+            r = sd_bus_get_name_creds(g_bus, services[i].c_str(),
+                                    SD_BUS_CREDS_AUGMENT | SD_BUS_CREDS_EUID |
+                                        SD_BUS_CREDS_PID | SD_BUS_CREDS_COMM |
+                                        SD_BUS_CREDS_UNIQUE_NAME |
+                                        SD_BUS_CREDS_UNIT | SD_BUS_CREDS_SESSION |
+                                        SD_BUS_CREDS_DESCRIPTION,
+                                    &creds);
+            // PID
+            int pid = INVALID;
+            if (r < 0)
+            {
+                printf("Oh! Cannot get creds for %s\n", services[i].c_str());
+            }
+            else
+            {
+                r = sd_bus_creds_get_pid(creds, &pid);
+            }
+            pids.push_back(pid);
+            // comm
+            std::string comm;
+            if (pid != INVALID)
+            {
+                std::ifstream ifs("/proc/" + std::to_string(pid) + "/cmdline");
+                std::string line;
+                std::getline(ifs, line);
+                for (char c : line)
+                {
+                    if (c < 32 || c >= 127)
+                        c = ' ';
+                    comm.push_back(c);
+                }
+            }
+            comms.push_back(comm);
+            // unique name, also known as "Connection"
+            std::string connection;
+            const char* u;
+            r = sd_bus_creds_get_unique_name(creds, &u);
+            if (r >= 0)
+            {
+                connection = u;
+            }
+            else
+            {
+                printf("Oh! Could not get unique name for %s\n", service.c_str());
+            }
+            std::string unit;
+            r = sd_bus_creds_get_unit(creds, &u);
+            if (r >= 0)
+            {
+                unit = u;
+            }
+            else
+            {
+                printf("Oh! Could not get unit name for %s\n", unit.c_str());
+            }
+            printf("AddConnection    %s    %s    %s    %s    %d\n", service.c_str(),
+                connection.c_str(), comm.c_str(), unit.c_str(), pid);
+            g_connection_snapshot->AddConnection(service, connection, comm, unit,
+                                                pid);
+        }
+        printf("There are %d DBus names.\n", int(services.size()));
+        for (int i = 0; i < int(services.size()); i++)
+        {
+            printf("    %d: %s [%s]\n", i, services[i].c_str(), comms[i].c_str());
+        }
+        g_sensor_snapshot = new SensorSnapshot(g_connection_snapshot);
+        // busctl call xyz.openbmc_project.ObjectMapper /
+        // org.freedesktop.DBus.Introspectable Introspect
+        printf("3. See which sensors are visible from Object Mapper\n");
+        printf("3.1. Introspect Object Mapper for object paths\n");
+        std::vector<std::string> all_obj_paths = FindAllObjectPathsForService(
+            "xyz.openbmc_project.ObjectMapper", nullptr);
+        sd_bus_error err = SD_BUS_ERROR_NULL;
+        sd_bus_message *m, *reply;
+        printf("%d paths found while introspecting ObjectMapper.\n",
+            int(all_obj_paths.size()));
+        printf("3.2. Call ObjectMapper's GetObject method against the sensor "
+            "object paths that represent sensors\n");
+        for (const std::string& p : all_obj_paths)
+        {
+            if (IsSensorObjectPath(p))
+            {
+                err = SD_BUS_ERROR_NULL;
+                r = sd_bus_message_new_method_call(
+                    g_bus, &m, "xyz.openbmc_project.ObjectMapper",
+                    "/xyz/openbmc_project/object_mapper",
+                    "xyz.openbmc_project.ObjectMapper", "GetObject");
+                if (r < 0)
+                {
+                    printf("Cannot create new method call. r=%d, strerror=%s\n", r,
+                        strerror(-r));
+                    continue;
+                }
+                r = sd_bus_message_append_basic(m, 's', p.c_str());
+                if (r < 0)
+                {
+                    printf("Could not append a string parameter to m\n");
+                    continue;
+                }
+                // empty array
+                r = sd_bus_message_open_container(m, 'a', "s");
+                if (r < 0)
+                {
+                    printf("Could not open a container for m\n");
+                    continue;
+                }
+                r = sd_bus_message_close_container(m);
+                if (r < 0)
+                {
+                    printf("Could not close container for m\n");
+                    continue;
+                }
+                r = sd_bus_call(g_bus, m, 0, &err, &reply);
+                if (r < 0)
+                {
+                    printf("Error performing dbus method call\n");
+                }
+                const char* sig = sd_bus_message_get_signature(reply, 0);
+                if (!strcmp(sig, "a{sas}"))
+                {
+                    r = sd_bus_message_enter_container(reply, 'a', "{sas}");
+                    if (r < 0)
+                    {
+                        printf("Could not enter the level 0 array container\n");
+                        continue;
+                    }
+                    while (true)
+                    {
+                        r = sd_bus_message_enter_container(
+                            reply, SD_BUS_TYPE_DICT_ENTRY, "sas");
+                        if (r < 0)
+                        {
+                            // printf("Could not enter the level 1 dict
+                            // container\n");
+                            goto DONE;
+                        }
+                        else if (r == 0)
+                        {}
+                        else
+                        {
+                            // The following 2 correspond to `interface_map` in
+                            // phosphor-mapper
+                            const char* interface_map_first;
+                            r = sd_bus_message_read_basic(reply, 's',
+                                                        &interface_map_first);
+                            if (r < 0)
+                            {
+                                printf("Could not read interface_map_first\n");
+                                goto DONE;
+                            }
+                            r = sd_bus_message_enter_container(reply, 'a', "s");
+                            if (r < 0)
+                            {
+                                printf("Could not enter the level 2 array "
+                                    "container\n");
+                                goto DONE;
+                            }
+                            bool has_value_interface = false;
+                            while (true)
+                            {
+                                const char* interface_map_second;
+                                r = sd_bus_message_read_basic(
+                                    reply, 's', &interface_map_second);
+                                if (r < 0)
+                                {
+                                    printf("Could not read interface_map_second\n");
+                                }
+                                else if (r == 0)
+                                    break;
+                                else
+                                {
+                                    // printf("    %s\n", interface_map_second);
+                                    if (!strcmp(interface_map_second,
+                                                "xyz.openbmc_project.Sensor.Value"))
+                                    {
+                                        has_value_interface = true;
+                                    }
+                                }
+                            }
+                            if (has_value_interface)
+                            {
+                                g_sensor_snapshot->SerSensorVisibleFromObjectMapper(
+                                    std::string(interface_map_first), p);
+                            }
+                            r = sd_bus_message_exit_container(reply);
+                        }
+                        r = sd_bus_message_exit_container(reply);
+                    }
+                    r = sd_bus_message_exit_container(reply);
+                }
+            DONE:
+            {}
+            }
+        }
+        printf("4. Check Hwmon's DBus objects\n");
+        for (int i = 0; i < int(comms.size()); i++)
+        {
+            const std::string& comm = comms[i];
+            const std::string& service = services[i];
+            if (comm.find("phosphor-hwmon-readd") != std::string::npos &&
+                !IsUniqueName(service))
+            {
+                // printf("Should introspect %s\n", service.c_str());
+                std::vector<std::string> objpaths =
+                    FindAllObjectPathsForService(service, nullptr);
+                for (const std::string& op : objpaths)
+                {
+                    if (IsSensorObjectPath(op))
+                    {
+                        g_sensor_snapshot->SetSensorVisibleFromHwmon(service, op);
+                    }
+                }
+            }
+        }
+        // Call `ipmitool sdr list` and see which sensors exist.
+        printf("5. Checking ipmitool SDR List\n");
+        std::string out;
+        bool skip_sdr_list = false;
+        if (getenv("SKIP"))
+        {
+            skip_sdr_list = true;
+        }
+        if (!skip_sdr_list)
+        {
+            constexpr int MAX_BUFFER = 255;
+            char buffer[MAX_BUFFER];
+            FILE* stream = popen("ipmitool sdr list", "r");
+            while (fgets(buffer, MAX_BUFFER, stream) != NULL)
+            {
+                out.append(buffer);
+            }
+            pclose(stream);
+        }
+        std::stringstream ss(out);
+        while (true)
+        {
+            std::string sensor_id, reading, status;
+            std::getline(ss, sensor_id, '|');
+            std::getline(ss, reading, '|');
+            std::getline(ss, status);
+            // printf("%s %s %s\n", sensor_id.c_str(), reading.c_str(),
+            // status.c_str());
+            if (sensor_id.size() > 0 && reading.size() > 0 && status.size() > 0)
+            {
+                g_sensor_snapshot->SetSensorVisibleFromIpmitoolSdr(Trim(sensor_id));
+            }
+            else
+                break;
+        }
+        printf("=== Sensors snapshot summary: ===\n");
+        g_sensor_snapshot->PrintSummary();
+    }
+} // namespace dbus_top_analyzer
+
+void DBusTopStatistics::OnNewDBusMessage(const char* sender,
+                                         const char* destination,
+                                         const char* interface,
+                                         const char* path, const char* member,
+                                         const char type, sd_bus_message* m)
+{
+    num_messages_++;
+    std::vector<std::string> keys;
+
+    std::string sender_orig = CheckAndFixNullString(sender);
+    std::string dest_orig = CheckAndFixNullString(destination);
+    // For method return messages, we actually want to show the sender
+    // and destination of the original method call, so we swap the
+    // sender and destination
+    if (type == 2)
+    { // DBUS_MESSAGE_TYPE_METHOD_METHOD_RETURN
+        std::swap(sender_orig, dest_orig);
+    }
+
+    // Special case: when PID == 1 (init), the DBus unit would be systemd.
+    // It seems it was not possible to obtain the connection name of systemd
+    // so we manually set it here.
+    const int sender_orig_pid =
+        g_connection_snapshot->GetConnectionPIDFromNameOrUniqueName(
+            sender_orig);
+
+    if (sender_orig_pid == 1)
+    {
+        sender_orig = "systemd";
+    }
+    const int dest_orig_pid =
+        g_connection_snapshot->GetConnectionPIDFromNameOrUniqueName(dest_orig);
+    if (dest_orig_pid == 1)
+    {
+        dest_orig = "systemd";
+    }
+
+    for (DBusTopSortField field : fields_)
+    {
+        switch (field)
+        {
+            case kSender:
+                keys.push_back(sender_orig);
+                break;
+            case kDestination:
+                keys.push_back(dest_orig);
+                break;
+            case kInterface:
+                keys.push_back(CheckAndFixNullString(interface));
+                break;
+            case kPath:
+                keys.push_back(CheckAndFixNullString(path));
+                break;
+            case kMember:
+                keys.push_back(CheckAndFixNullString(member));
+                break;
+            case kSenderPID:
+            {
+                if (sender_orig_pid != INVALID)
+                {
+                    keys.push_back(std::to_string(sender_orig_pid));
+                }
+                else
+                {
+                    keys.push_back("(unknown)");
+                }
+                break;
+            }
+            case kSenderCMD:
+            {
+                keys.push_back(
+                    g_connection_snapshot->GetConnectionCMDFromNameOrUniqueName(
+                        sender_orig));
+                break;
+            }
+            case kMsgPerSec:
+            case kAverageLatency:
+                break; // Don't populate "keys" using these 2 fields
+        }
+    }
+    // keys = combination of fields of user's choice
+
+    if (stats_.count(keys) == 0)
+    {
+        stats_[keys] = DBusTopComputedMetrics();
+    }
+    // Need to update msg/s regardless
+    switch (type)
+    {
+        case 1: // DBUS_MESSAGE_TYPE_METHOD_CALL
+            stats_[keys].num_method_calls++;
+            break;
+        case 2: // DBUS_MESSAGE_TYPE_METHOD_METHOD_RETURN
+            stats_[keys].num_method_returns++;
+            break;
+        case 3: // DBUS_MESSAGE_TYPE_ERROR
+            stats_[keys].num_errors++;
+            break;
+        case 4: // DBUS_MESSAGE_TYPE_SIGNAL
+            stats_[keys].num_signals++;
+            break;
+    }
+    // Update global latency histogram
+    // For method call latency
+    if (type == 1) // DBUS_MESSAGE_TYPE_METHOD_CALL
+    {
+        uint64_t serial; // serial == cookie
+        sd_bus_message_get_cookie(m, &serial);
+        in_flight_methodcalls[serial] = Microseconds();
+    }
+    else if (type == 2) // DBUS_MESSAGE_TYPE_MEHOTD_RETURN
+    {
+        uint64_t reply_serial = 0; // serial == cookie
+        sd_bus_message_get_reply_cookie(m, &reply_serial);
+        if (in_flight_methodcalls.count(reply_serial) > 0)
+        {
+            float dt_usec =
+                Microseconds() - in_flight_methodcalls[reply_serial];
+            in_flight_methodcalls.erase(reply_serial);
+            dbus_top_analyzer::g_mc_time_histogram.AddSample(dt_usec);
+
+            // Add method call count and total latency to the corresponding key
+            stats_[keys].total_latency_usec += dt_usec;
+        }
+    }
+    // For meaning of type see here
+    // https://dbus.freedesktop.org/doc/api/html/group__DBusProtocol.html#ga4a9012edd7f22342f845e98150aeb858
+    switch (type)
+    {
+        case 1:
+            num_mc_++;
+            break;
+        case 2:
+            num_mr_++;
+            break;
+        case 3:
+            num_error_++;
+            break;
+        case 4:
+            num_sig_++;
+            break;
+    }
+}