xref: /openbmc/phosphor-fan-presence/control/fanctl.cpp (revision 7d07cb1ef3c2347e6355aabfa7e8a48bfdac6dbf)
1  /**
2   * Copyright © 2022 IBM Corporation
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   *     http://www.apache.org/licenses/LICENSE-2.0
9   *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  #include "config.h"
18  
19  #include "sdbusplus.hpp"
20  
21  #include <CLI/CLI.hpp>
22  #include <nlohmann/json.hpp>
23  #include <sdbusplus/bus.hpp>
24  
25  #include <filesystem>
26  #include <iomanip>
27  #include <iostream>
28  
29  using SDBusPlus = phosphor::fan::util::SDBusPlus;
30  
31  constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
32  constexpr auto systemdPath = "/org/freedesktop/systemd1";
33  constexpr auto systemdService = "org.freedesktop.systemd1";
34  constexpr auto phosphorServiceName = "phosphor-fan-control@0.service";
35  constexpr auto dumpFile = "/tmp/fan_control_dump.json";
36  
37  enum
38  {
39      FAN_NAMES = 0,
40      PATH_MAP = 1,
41      IFACES = 2,
42      METHOD = 3
43  };
44  
45  struct DumpQuery
46  {
47      std::string section;
48      std::string name;
49      std::vector<std::string> properties;
50      bool dump{false};
51  };
52  
53  struct SensorOpts
54  {
55      std::string type;
56      std::string name;
57      bool verbose{false};
58  };
59  
60  struct SensorOutput
61  {
62      std::string name;
63      double value;
64      bool functional;
65      bool available;
66  };
67  
68  /**
69   * @function extracts fan name from dbus path string (last token where
70   * delimiter is the / character), with proper bounds checking.
71   * @param[in] path - D-Bus path
72   * @return just the fan name.
73   */
74  
justFanName(const std::string & path)75  std::string justFanName(const std::string& path)
76  {
77      std::string fanName;
78  
79      auto itr = path.rfind("/");
80      if (itr != std::string::npos && itr < path.size())
81      {
82          fanName = path.substr(1 + itr);
83      }
84  
85      return fanName;
86  }
87  
88  /**
89   * @function produces subtree paths whose names match fan token names.
90   * @param[in] path - D-Bus path to obtain subtree from
91   * @param[in] iface - interface to obtain subTreePaths from
92   * @param[in] fans - label matching tokens to filter by
93   * @param[in] shortPath - flag to shorten fan token
94   * @return map of paths by fan name
95   */
96  
getPathsFromIface(const std::string & path,const std::string & iface,const std::vector<std::string> & fans,bool shortPath=false)97  std::map<std::string, std::vector<std::string>> getPathsFromIface(
98      const std::string& path, const std::string& iface,
99      const std::vector<std::string>& fans, bool shortPath = false)
100  {
101      std::map<std::string, std::vector<std::string>> dest;
102  
103      for (auto& path :
104           SDBusPlus::getSubTreePathsRaw(SDBusPlus::getBus(), path, iface, 0))
105      {
106          for (auto& fan : fans)
107          {
108              if (shortPath)
109              {
110                  if (fan == justFanName(path))
111                  {
112                      dest[fan].push_back(path);
113                  }
114              }
115              else if (std::string::npos != path.find(fan + "_"))
116              {
117                  dest[fan].push_back(path);
118              }
119          }
120      }
121  
122      return dest;
123  }
124  
125  /**
126   * @function consolidated function to load dbus paths and fan names
127   */
loadDBusData()128  auto loadDBusData()
129  {
130      auto& bus{SDBusPlus::getBus()};
131  
132      std::vector<std::string> fanNames;
133  
134      // paths by D-bus interface,fan name
135      std::map<std::string, std::map<std::string, std::vector<std::string>>>
136          pathMap;
137  
138      std::string method("RPM");
139  
140      std::map<const std::string, const std::string> interfaces{
141          {"FanSpeed", "xyz.openbmc_project.Control.FanSpeed"},
142          {"FanPwm", "xyz.openbmc_project.Control.FanPwm"},
143          {"SensorValue", "xyz.openbmc_project.Sensor.Value"},
144          {"Item", "xyz.openbmc_project.Inventory.Item"},
145          {"OpStatus", "xyz.openbmc_project.State.Decorator.OperationalStatus"}};
146  
147      std::map<const std::string, const std::string> paths{
148          {"motherboard",
149           "/xyz/openbmc_project/inventory/system/chassis/motherboard"},
150          {"tach", "/xyz/openbmc_project/sensors/fan_tach"}};
151  
152      // build a list of all fans
153      for (auto& path : SDBusPlus::getSubTreePathsRaw(bus, paths["tach"],
154                                                      interfaces["FanSpeed"], 0))
155      {
156          // special case where we build the list of fans
157          auto fan = justFanName(path);
158          fan = fan.substr(0, fan.rfind("_"));
159          fanNames.push_back(fan);
160      }
161  
162      // retry with PWM mode if none found
163      if (0 == fanNames.size())
164      {
165          method = "PWM";
166  
167          for (auto& path : SDBusPlus::getSubTreePathsRaw(
168                   bus, paths["tach"], interfaces["FanPwm"], 0))
169          {
170              // special case where we build the list of fans
171              auto fan = justFanName(path);
172              fan = fan.substr(0, fan.rfind("_"));
173              fanNames.push_back(fan);
174          }
175      }
176  
177      // load tach sensor paths for each fan
178      pathMap["tach"] =
179          getPathsFromIface(paths["tach"], interfaces["SensorValue"], fanNames);
180  
181      // load inventory Item data for each fan
182      pathMap["inventory"] = getPathsFromIface(
183          paths["motherboard"], interfaces["Item"], fanNames, true);
184  
185      // load operational status data for each fan
186      pathMap["opstatus"] = getPathsFromIface(
187          paths["motherboard"], interfaces["OpStatus"], fanNames, true);
188  
189      return std::make_tuple(fanNames, pathMap, interfaces, method);
190  }
191  
192  /**
193   * @function gets the states of phosphor-fanctl. equivalent to
194   * "systemctl status phosphor-fan-control@0"
195   * @return a list of several (sub)states of fanctl (loaded,
196   * active, running) as well as D-Bus properties representing
197   * BMC states (bmc state,chassis power state, host state)
198   */
199  
getStates()200  std::array<std::string, 6> getStates()
201  {
202      using DBusTuple =
203          std::tuple<std::string, std::string, std::string, std::string,
204                     std::string, std::string, sdbusplus::message::object_path,
205                     uint32_t, std::string, sdbusplus::message::object_path>;
206  
207      std::array<std::string, 6> ret;
208  
209      std::vector<std::string> services{phosphorServiceName};
210  
211      try
212      {
213          auto fields{SDBusPlus::callMethodAndRead<std::vector<DBusTuple>>(
214              systemdService, systemdPath, systemdMgrIface, "ListUnitsByNames",
215              services)};
216  
217          if (fields.size() > 0)
218          {
219              ret[0] = std::get<2>(fields[0]);
220              ret[1] = std::get<3>(fields[0]);
221              ret[2] = std::get<4>(fields[0]);
222          }
223          else
224          {
225              std::cout << "No units found for systemd service: " << services[0]
226                        << std::endl;
227          }
228      }
229      catch (const std::exception& e)
230      {
231          std::cerr << "Failure retrieving phosphor-fan-control states: "
232                    << e.what() << std::endl;
233      }
234  
235      std::string path("/xyz/openbmc_project/state/bmc0");
236      std::string iface("xyz.openbmc_project.State.BMC");
237      ret[3] =
238          SDBusPlus::getProperty<std::string>(path, iface, "CurrentBMCState");
239  
240      path = "/xyz/openbmc_project/state/chassis0";
241      iface = "xyz.openbmc_project.State.Chassis";
242      ret[4] =
243          SDBusPlus::getProperty<std::string>(path, iface, "CurrentPowerState");
244  
245      path = "/xyz/openbmc_project/state/host0";
246      iface = "xyz.openbmc_project.State.Host";
247      ret[5] =
248          SDBusPlus::getProperty<std::string>(path, iface, "CurrentHostState");
249  
250      return ret;
251  }
252  
253  /**
254   * @function helper to determine interface type from a given control method
255   */
ifaceTypeFromMethod(const std::string & method)256  std::string ifaceTypeFromMethod(const std::string& method)
257  {
258      return (method == "RPM" ? "FanSpeed" : "FanPwm");
259  }
260  
261  /**
262   * @function performs the "status" command from the cmdline.
263   * get states and sensor data and output to the console
264   */
status()265  void status()
266  {
267      using std::cout;
268      using std::endl;
269      using std::setw;
270  
271      auto busData = loadDBusData();
272      auto& method = std::get<METHOD>(busData);
273  
274      std::string property;
275  
276      // get the state,substate of fan-control and obmc
277      auto states = getStates();
278  
279      // print the header
280      cout << "Fan Control Service State   : " << states[0] << ", " << states[1]
281           << "(" << states[2] << ")" << endl;
282      cout << endl;
283      cout << "CurrentBMCState     : " << states[3] << endl;
284      cout << "CurrentPowerState   : " << states[4] << endl;
285      cout << "CurrentHostState    : " << states[5] << endl;
286      cout << endl;
287      cout << "FAN       "
288           << "TARGET(" << method << ")     FEEDBACKS(RPM)   PRESENT"
289           << "   FUNCTIONAL" << endl;
290      cout << "==============================================================="
291           << endl;
292  
293      auto& fanNames{std::get<FAN_NAMES>(busData)};
294      auto& pathMap{std::get<PATH_MAP>(busData)};
295      auto& interfaces{std::get<IFACES>(busData)};
296  
297      for (auto& fan : fanNames)
298      {
299          cout << setw(8) << std::left << fan << std::right << setw(13);
300  
301          // get the target RPM
302          property = "Target";
303          cout << SDBusPlus::getProperty<uint64_t>(
304                      pathMap["tach"][fan][0],
305                      interfaces[ifaceTypeFromMethod(method)], property)
306               << setw(19);
307  
308          // get the sensor RPM
309          property = "Value";
310  
311          std::ostringstream output;
312          int numRotors = pathMap["tach"][fan].size();
313          // print tach readings for each rotor
314          for (auto& path : pathMap["tach"][fan])
315          {
316              output << SDBusPlus::getProperty<double>(
317                  path, interfaces["SensorValue"], property);
318  
319              // dont print slash on last rotor
320              if (--numRotors)
321                  output << "/";
322          }
323          cout << output.str() << setw(10);
324  
325          // print the Present property
326          property = "Present";
327          auto itFan = pathMap["inventory"].find(fan);
328          if (itFan != pathMap["inventory"].end())
329          {
330              for (auto& path : itFan->second)
331              {
332                  try
333                  {
334                      cout << std::boolalpha
335                           << SDBusPlus::getProperty<bool>(
336                                  path, interfaces["Item"], property);
337                  }
338                  catch (const phosphor::fan::util::DBusError&)
339                  {
340                      cout << "Unknown";
341                  }
342              }
343          }
344          else
345          {
346              cout << "Unknown";
347          }
348  
349          cout << setw(13);
350  
351          // and the functional property
352          property = "Functional";
353          itFan = pathMap["opstatus"].find(fan);
354          if (itFan != pathMap["opstatus"].end())
355          {
356              for (auto& path : itFan->second)
357              {
358                  try
359                  {
360                      cout << std::boolalpha
361                           << SDBusPlus::getProperty<bool>(
362                                  path, interfaces["OpStatus"], property);
363                  }
364                  catch (const phosphor::fan::util::DBusError&)
365                  {
366                      cout << "Unknown";
367                  }
368              }
369          }
370          else
371          {
372              cout << "Unknown";
373          }
374  
375          cout << endl;
376      }
377  }
378  
379  /**
380   * @function print target RPM/PWM and tach readings from each fan
381   */
get()382  void get()
383  {
384      using std::cout;
385      using std::endl;
386      using std::setw;
387  
388      auto busData = loadDBusData();
389  
390      auto& fanNames{std::get<FAN_NAMES>(busData)};
391      auto& pathMap{std::get<PATH_MAP>(busData)};
392      auto& interfaces{std::get<IFACES>(busData)};
393      auto& method = std::get<METHOD>(busData);
394  
395      std::string property;
396  
397      // print the header
398      cout << "TARGET SENSOR" << setw(11) << "TARGET(" << method
399           << ")   FEEDBACK SENSOR    FEEDBACK(RPM)" << endl;
400      cout << "==============================================================="
401           << endl;
402  
403      for (auto& fan : fanNames)
404      {
405          if (pathMap["tach"][fan].size() == 0)
406              continue;
407          // print just the sensor name
408          auto shortPath = pathMap["tach"][fan][0];
409          shortPath = justFanName(shortPath);
410          cout << setw(13) << std::left << shortPath << std::right << setw(15);
411  
412          // print its target RPM/PWM
413          property = "Target";
414          cout << SDBusPlus::getProperty<uint64_t>(
415              pathMap["tach"][fan][0], interfaces[ifaceTypeFromMethod(method)],
416              property);
417  
418          // print readings for each rotor
419          property = "Value";
420  
421          auto indent = 0;
422          for (auto& path : pathMap["tach"][fan])
423          {
424              cout << setw(18 + indent) << justFanName(path) << setw(17)
425                   << SDBusPlus::getProperty<double>(
426                          path, interfaces["SensorValue"], property)
427                   << endl;
428  
429              if (0 == indent)
430                  indent = 28;
431          }
432      }
433  }
434  
435  /**
436   * @function set fan[s] to a target RPM
437   */
set(uint64_t target,std::vector<std::string> & fanList)438  void set(uint64_t target, std::vector<std::string>& fanList)
439  {
440      auto busData = loadDBusData();
441      auto& bus{SDBusPlus::getBus()};
442      auto& pathMap{std::get<PATH_MAP>(busData)};
443      auto& interfaces{std::get<IFACES>(busData)};
444      auto& method = std::get<METHOD>(busData);
445  
446      std::string ifaceType(method == "RPM" ? "FanSpeed" : "FanPwm");
447  
448      // stop the fan-control service
449      SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
450          systemdService, systemdPath, systemdMgrIface, "StopUnit",
451          phosphorServiceName, "replace");
452  
453      if (fanList.size() == 0)
454      {
455          fanList = std::get<FAN_NAMES>(busData);
456      }
457  
458      for (auto& fan : fanList)
459      {
460          try
461          {
462              auto paths(pathMap["tach"].find(fan));
463  
464              if (pathMap["tach"].end() == paths)
465              {
466                  // try again, maybe it was a sensor name instead of a fan name
467                  for (const auto& [fanName, sensors] : pathMap["tach"])
468                  {
469                      for (const auto& path : sensors)
470                      {
471                          std::string sensor(path.substr(path.rfind("/")));
472  
473                          if (sensor.size() > 0)
474                          {
475                              sensor = sensor.substr(1);
476  
477                              if (sensor == fan)
478                              {
479                                  paths = pathMap["tach"].find(fanName);
480  
481                                  break;
482                              }
483                          }
484                      }
485                  }
486              }
487  
488              if (pathMap["tach"].end() == paths)
489              {
490                  std::cout << "Could not find tach path for fan: " << fan
491                            << std::endl;
492                  continue;
493              }
494  
495              // set the target RPM
496              SDBusPlus::setProperty<uint64_t>(bus, paths->second[0],
497                                               interfaces[ifaceType], "Target",
498                                               std::move(target));
499          }
500          catch (const phosphor::fan::util::DBusPropertyError& e)
501          {
502              std::cerr << "Cannot set target rpm for " << fan
503                        << " caught D-Bus exception: " << e.what() << std::endl;
504          }
505      }
506  }
507  
508  /**
509   * @function restart fan-control to allow it to manage fan speeds
510   */
resume()511  void resume()
512  {
513      try
514      {
515          auto retval =
516              SDBusPlus::callMethodAndRead<sdbusplus::message::object_path>(
517                  systemdService, systemdPath, systemdMgrIface, "StartUnit",
518                  phosphorServiceName, "replace");
519      }
520      catch (const phosphor::fan::util::DBusMethodError& e)
521      {
522          std::cerr << "Unable to start fan control: " << e.what() << std::endl;
523      }
524  }
525  
526  /**
527   * @function force reload of control files by sending HUP signal
528   */
reload()529  void reload()
530  {
531      try
532      {
533          SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface,
534                                "KillUnit", phosphorServiceName, "main", SIGHUP);
535      }
536      catch (const phosphor::fan::util::DBusPropertyError& e)
537      {
538          std::cerr << "Unable to reload configuration files: " << e.what()
539                    << std::endl;
540      }
541  }
542  
543  /**
544   * @function dump debug data
545   */
dumpFanControl()546  void dumpFanControl()
547  {
548      namespace fs = std::filesystem;
549  
550      try
551      {
552          // delete existing file
553          if (fs::exists(dumpFile))
554          {
555              std::filesystem::remove(dumpFile);
556          }
557  
558          SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface,
559                                "KillUnit", phosphorServiceName, "main", SIGUSR1);
560  
561          bool done = false;
562          size_t tries = 0;
563          const size_t maxTries = 30;
564  
565          do
566          {
567              // wait for file to be detected
568              sleep(1);
569  
570              if (fs::exists(dumpFile))
571              {
572                  try
573                  {
574                      auto unused{nlohmann::json::parse(std::ifstream{dumpFile})};
575                      done = true;
576                  }
577                  catch (...)
578                  {}
579              }
580  
581              if (++tries > maxTries)
582              {
583                  std::cerr << "Timed out waiting for fan control dump.\n";
584                  return;
585              }
586          } while (!done);
587  
588          std::cout << "Fan control dump written to: " << dumpFile << std::endl;
589      }
590      catch (const phosphor::fan::util::DBusPropertyError& e)
591      {
592          std::cerr << "Unable to dump fan control: " << e.what() << std::endl;
593      }
594  }
595  
596  /**
597   * @function Query items in the dump file
598   */
queryDumpFile(const DumpQuery & dq)599  void queryDumpFile(const DumpQuery& dq)
600  {
601      nlohmann::json output;
602      std::ifstream file{dumpFile};
603  
604      if (!file.good())
605      {
606          std::cerr << "Unable to open dump file, please run 'fanctl dump'.\n";
607          return;
608      }
609  
610      auto dumpData = nlohmann::json::parse(file);
611  
612      if (!dumpData.contains(dq.section))
613      {
614          std::cerr << "Error: Dump file does not contain " << dq.section
615                    << " section"
616                    << "\n";
617          return;
618      }
619  
620      const auto& section = dumpData.at(dq.section);
621  
622      if (section.is_array())
623      {
624          for (const auto& entry : section)
625          {
626              if (!entry.is_string() || dq.name.empty() ||
627                  (entry.get<std::string>().find(dq.name) != std::string::npos))
628              {
629                  output[dq.section].push_back(entry);
630              }
631          }
632          std::cout << std::setw(4) << output << "\n";
633          return;
634      }
635  
636      for (const auto& [key1, values1] : section.items())
637      {
638          if (dq.name.empty() || (key1.find(dq.name) != std::string::npos))
639          {
640              // If no properties specified, print the whole JSON value
641              if (dq.properties.empty())
642              {
643                  output[key1] = values1;
644                  continue;
645              }
646  
647              // Look for properties both one and two levels down.
648              // Future improvement: Use recursion.
649              for (const auto& [key2, values2] : values1.items())
650              {
651                  for (const auto& prop : dq.properties)
652                  {
653                      if (prop == key2)
654                      {
655                          output[key1][prop] = values2;
656                      }
657                  }
658  
659                  for (const auto& [key3, values3] : values2.items())
660                  {
661                      for (const auto& prop : dq.properties)
662                      {
663                          if (prop == key3)
664                          {
665                              output[key1][prop] = values3;
666                          }
667                      }
668                  }
669              }
670          }
671      }
672  
673      if (!output.empty())
674      {
675          std::cout << std::setw(4) << output << "\n";
676      }
677  }
678  
679  /**
680   * @function Get the sensor type based on the sensor name
681   *
682   * @param sensor The sensor object path
683   */
getSensorType(const std::string & sensor)684  std::string getSensorType(const std::string& sensor)
685  {
686      // Get type from /xyz/openbmc_project/sensors/<type>/<name>
687      try
688      {
689          auto type =
690              sensor.substr(std::string{"/xyz/openbmc_project/sensors/"}.size());
691          return type.substr(0, type.find_first_of('/'));
692      }
693      catch (const std::exception& e)
694      {
695          std::cerr << "Failed extracting type from sensor " << sensor << ": "
696                    << e.what() << "\n";
697      }
698      return std::string{};
699  }
700  
701  /**
702   * @function Print the sensors passed in
703   *
704   * @param sensors The sensors to print
705   */
printSensors(const std::vector<SensorOutput> & sensors)706  void printSensors(const std::vector<SensorOutput>& sensors)
707  {
708      size_t maxNameSize = 0;
709  
710      std::ranges::for_each(sensors, [&maxNameSize](const auto& s) {
711          maxNameSize = std::max(maxNameSize, s.name.size());
712      });
713  
714      std::ranges::for_each(sensors, [maxNameSize](const auto& sensor) {
715          auto nameField = sensor.name + ':';
716          std::cout << std::left << std::setw(maxNameSize + 2) << nameField
717                    << sensor.value;
718          if (!sensor.functional)
719          {
720              std::cout << " (Functional=false)";
721          }
722  
723          if (!sensor.available)
724          {
725              std::cout << " (Available=false)";
726          }
727          std::cout << "\n";
728      });
729  }
730  
731  /**
732   * @function Extracts the sensor out of the GetManagedObjects output
733   *           for the one object path passed in.
734   *
735   * @param object The GetManagedObjects output for a single object path
736   * @param opts The sensor options
737   * @param[out] sensors Filled in with the sensor data
738   */
extractSensorData(const auto & object,const SensorOpts & opts,std::vector<SensorOutput> & sensors)739  void extractSensorData(const auto& object, const SensorOpts& opts,
740                         std::vector<SensorOutput>& sensors)
741  {
742      auto it = object.second.find("xyz.openbmc_project.Sensor.Value");
743      if (it == object.second.end())
744      {
745          return;
746      }
747      auto value = std::get<double>(it->second.at("Value"));
748  
749      // Use the full D-Bus path of the sensor for the name if verbose
750      std::string name = object.first.str;
751      name = name.substr(name.find_last_of('/') + 1);
752      std::string printName = name;
753      if (opts.verbose)
754      {
755          printName = object.first.str;
756      }
757  
758      // Apply the name filter
759      if (!opts.name.empty())
760      {
761          if (!name.contains(opts.name))
762          {
763              return;
764          }
765      }
766  
767      // Apply the type filter
768      if (!opts.type.empty())
769      {
770          if (opts.type != getSensorType(object.first.str))
771          {
772              return;
773          }
774      }
775  
776      bool functional = true;
777      it = object.second.find(
778          "xyz.openbmc_project.State.Decorator.OperationalStatus");
779      if (it != object.second.end())
780      {
781          functional = std::get<bool>(it->second.at("Functional"));
782      }
783  
784      bool available = true;
785      it = object.second.find("xyz.openbmc_project.State.Decorator.Availability");
786      if (it != object.second.end())
787      {
788          available = std::get<bool>(it->second.at("Available"));
789      }
790  
791      sensors.emplace_back(printName, value, functional, available);
792  }
793  
794  /**
795   * @function Call GetManagedObjects on all sensor object managers and then
796   *           print the sensor values.
797   *
798   * @param sensorManagers map<service, path> of sensor ObjectManagers
799   * @param opts The sensor options
800   */
readSensorsAndPrint(std::map<std::string,std::string> & sensorManagers,const SensorOpts & opts)801  void readSensorsAndPrint(std::map<std::string, std::string>& sensorManagers,
802                           const SensorOpts& opts)
803  {
804      std::vector<SensorOutput> sensors;
805  
806      using PropertyVariantType =
807          std::variant<bool, int32_t, int64_t, double, std::string>;
808  
809      std::ranges::for_each(sensorManagers, [&opts, &sensors](const auto& entry) {
810          auto values = SDBusPlus::getManagedObjects<PropertyVariantType>(
811              SDBusPlus::getBus(), entry.first, entry.second);
812  
813          // Pull out the sensor details
814          std::ranges::for_each(values, [&opts, &sensors](const auto& sensor) {
815              extractSensorData(sensor, opts, sensors);
816          });
817      });
818  
819      std::ranges::sort(sensors, [](const auto& left, const auto& right) {
820          return left.name < right.name;
821      });
822  
823      printSensors(sensors);
824  }
825  
826  /**
827   * @function Prints sensor values
828   *
829   * @param opts The sensor options
830   */
displaySensors(const SensorOpts & opts)831  void displaySensors(const SensorOpts& opts)
832  {
833      // Find the services that provide sensors
834      auto sensorObjects = SDBusPlus::getSubTreeRaw(
835          SDBusPlus::getBus(), "/", "xyz.openbmc_project.Sensor.Value", 0);
836  
837      std::set<std::string> sensorServices;
838  
839      std::ranges::for_each(sensorObjects, [&sensorServices](const auto& object) {
840          sensorServices.insert(object.second.begin()->first);
841      });
842  
843      // Find the ObjectManagers for those services
844      auto objectManagers = SDBusPlus::getSubTreeRaw(
845          SDBusPlus::getBus(), "/", "org.freedesktop.DBus.ObjectManager", 0);
846  
847      std::map<std::string, std::string> managers;
848  
849      std::ranges::for_each(
850          objectManagers, [&sensorServices, &managers](const auto& object) {
851              // Check every service on this path
852              std::ranges::for_each(
853                  object.second, [&managers, path = object.first,
854                                  &sensorServices](const auto& entry) {
855                      // Check if this service provides sensors
856                      if (std::ranges::contains(sensorServices, entry.first))
857                      {
858                          managers[entry.first] = path;
859                      }
860                  });
861          });
862  
863      readSensorsAndPrint(managers, opts);
864  }
865  
866  /**
867   * @function setup the CLI object to accept all options
868   */
initCLI(CLI::App & app,uint64_t & target,std::vector<std::string> & fanList,DumpQuery & dq,SensorOpts & sensorOpts)869  void initCLI(CLI::App& app, uint64_t& target, std::vector<std::string>& fanList,
870               [[maybe_unused]] DumpQuery& dq, SensorOpts& sensorOpts)
871  {
872      app.set_help_flag("-h,--help", "Print this help page and exit.");
873  
874      // App requires only 1 subcommand to be given
875      app.require_subcommand(1);
876  
877      // This represents the command given
878      auto commands = app.add_option_group("Commands");
879  
880      // status method
881      std::string strHelp("Prints fan target/tach readings, present/functional "
882                          "states, and fan-monitor/BMC/Power service status");
883  
884      auto cmdStatus = commands->add_subcommand("status", strHelp);
885      cmdStatus->set_help_flag("-h, --help", strHelp);
886      cmdStatus->require_option(0);
887  
888      // get method
889      strHelp = "Get the current fan target and feedback speeds for all rotors";
890      auto cmdGet = commands->add_subcommand("get", strHelp);
891      cmdGet->set_help_flag("-h, --help", strHelp);
892      cmdGet->require_option(0);
893  
894      // set method
895      strHelp = "Set target (all rotors) for one-or-more fans";
896      auto cmdSet = commands->add_subcommand("set", strHelp);
897      strHelp = R"(set <TARGET> [TARGET SENSOR(S)]
898    <TARGET>
899        - RPM/PWM target to set the fans
900  [TARGET SENSOR LIST]
901  - list of target sensors to set)";
902      cmdSet->set_help_flag("-h, --help", strHelp);
903      cmdSet->add_option("target", target, "RPM/PWM target to set the fans");
904      cmdSet->add_option(
905          "fan list", fanList,
906          "[optional] list of 1+ fans to set target RPM/PWM (default: all)");
907      cmdSet->require_option();
908  
909  #ifdef CONTROL_USE_JSON
910      strHelp = "Reload phosphor-fan configuration files";
911      auto cmdReload = commands->add_subcommand("reload", strHelp);
912      cmdReload->set_help_flag("-h, --help", strHelp);
913      cmdReload->require_option(0);
914  #endif
915  
916      strHelp = "Resume running phosphor-fan-control";
917      auto cmdResume = commands->add_subcommand("resume", strHelp);
918      cmdResume->set_help_flag("-h, --help", strHelp);
919      cmdResume->require_option(0);
920  
921      // Dump method
922      auto cmdDump = commands->add_subcommand("dump", "Dump debug data");
923      cmdDump->set_help_flag("-h, --help", "Dump debug data");
924      cmdDump->require_option(0);
925  
926  #ifdef CONTROL_USE_JSON
927      // Query dump
928      auto cmdDumpQuery =
929          commands->add_subcommand("query_dump", "Query the dump file");
930  
931      cmdDumpQuery->set_help_flag("-h, --help", "Query the dump file");
932      cmdDumpQuery
933          ->add_option("-s, --section", dq.section, "Dump file section name")
934          ->required();
935      cmdDumpQuery->add_option("-n, --name", dq.name,
936                               "Optional dump file entry name (or substring)");
937      cmdDumpQuery->add_option("-p, --properties", dq.properties,
938                               "Optional list of dump file property names");
939      cmdDumpQuery->add_flag("-d, --dump", dq.dump,
940                             "Force a dump before the query");
941  #endif
942  
943      auto cmdSensors =
944          commands->add_subcommand("sensors", "Retrieve sensor values");
945      cmdSensors->set_help_flag("-h, --help", "Retrieve sensor values");
946      cmdSensors->add_option(
947          "-t, --type", sensorOpts.type,
948          "Only show sensors of this type (i.e. 'temperature'). Optional");
949      cmdSensors->add_option(
950          "-n, --name", sensorOpts.name,
951          "Only show sensors with this string in the name. Optional");
952      cmdSensors->add_flag("-v, --verbose", sensorOpts.verbose,
953                           "Verbose: Use sensor object path for the name");
954  }
955  
956  /**
957   * @function main entry point for the application
958   */
main(int argc,char * argv[])959  int main(int argc, char* argv[])
960  {
961      auto rc = 0;
962      uint64_t target{0U};
963      std::vector<std::string> fanList;
964      DumpQuery dq;
965      SensorOpts sensorOpts;
966  
967      try
968      {
969          CLI::App app{"Manually control, get fan tachs, view status, and resume "
970                       "automatic control of all fans within a chassis. Full "
971                       "documentation can be found at the readme:\n"
972                       "https://github.com/openbmc/phosphor-fan-presence/tree/"
973                       "master/docs/control/fanctl"};
974  
975          initCLI(app, target, fanList, dq, sensorOpts);
976  
977          CLI11_PARSE(app, argc, argv);
978  
979          if (app.got_subcommand("get"))
980          {
981              get();
982          }
983          else if (app.got_subcommand("set"))
984          {
985              set(target, fanList);
986          }
987  #ifdef CONTROL_USE_JSON
988          else if (app.got_subcommand("reload"))
989          {
990              reload();
991          }
992  #endif
993          else if (app.got_subcommand("resume"))
994          {
995              resume();
996          }
997          else if (app.got_subcommand("status"))
998          {
999              status();
1000          }
1001          else if (app.got_subcommand("dump"))
1002          {
1003  #ifdef CONTROL_USE_JSON
1004              dumpFanControl();
1005  #else
1006              std::ofstream(dumpFile)
1007                  << "{\n\"msg\":   \"Unable to create dump on "
1008                     "non-JSON config based system\"\n}";
1009  #endif
1010          }
1011  #ifdef CONTROL_USE_JSON
1012          else if (app.got_subcommand("query_dump"))
1013          {
1014              if (dq.dump)
1015              {
1016                  dumpFanControl();
1017              }
1018              queryDumpFile(dq);
1019          }
1020  #endif
1021          else if (app.got_subcommand("sensors"))
1022          {
1023              displaySensors(sensorOpts);
1024          }
1025      }
1026      catch (const std::exception& e)
1027      {
1028          rc = -1;
1029          std::cerr << argv[0] << " failed: " << e.what() << std::endl;
1030      }
1031  
1032      return rc;
1033  }
1034