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