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