xref: /openbmc/phosphor-pid-control/main.cpp (revision f8b6e55147148c3cfb42327ff267197a460b411c)
1 /**
2  * Copyright 2017 Google Inc.
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 "buildjson/buildjson.hpp"
18 #include "conf.hpp"
19 #include "dbus/dbusconfiguration.hpp"
20 #include "failsafeloggers/builder.hpp"
21 #include "pid/builder.hpp"
22 #include "pid/buildjson.hpp"
23 #include "pid/pidloop.hpp"
24 #include "pid/tuning.hpp"
25 #include "sensors/builder.hpp"
26 #include "sensors/buildjson.hpp"
27 #include "sensors/manager.hpp"
28 #include "util.hpp"
29 #include "zone_interface.hpp"
30 
31 #include <signal.h>
32 
33 #include <CLI/CLI.hpp>
34 #include <boost/asio/error.hpp>
35 #include <boost/asio/io_context.hpp>
36 #include <boost/asio/post.hpp>
37 #include <boost/asio/signal_set.hpp>
38 #include <boost/asio/steady_timer.hpp>
39 #include <sdbusplus/asio/connection.hpp>
40 #include <sdbusplus/bus.hpp>
41 #include <sdbusplus/server/manager.hpp>
42 
43 #include <chrono>
44 #include <csignal>
45 #include <cstdint>
46 #include <cstdlib>
47 #include <exception>
48 #include <filesystem>
49 #include <fstream>
50 #include <iostream>
51 #include <map>
52 #include <memory>
53 #include <optional>
54 #include <stdexcept>
55 #include <string>
56 #include <tuple>
57 #include <unordered_map>
58 #include <utility>
59 #include <vector>
60 
61 namespace pid_control
62 {
63 
64 /* The configuration converted sensor list. */
65 std::map<std::string, conf::SensorConfig> sensorConfig = {};
66 /* The configuration converted PID list. */
67 std::map<int64_t, conf::PIDConf> zoneConfig = {};
68 /* The configuration converted Zone configuration. */
69 std::map<int64_t, conf::ZoneConfig> zoneDetailsConfig = {};
70 
71 namespace state
72 {
73 /* Set to true while canceling is in progress */
74 static bool isCanceling = false;
75 /* The zones build from configuration */
76 static std::unordered_map<int64_t, std::shared_ptr<ZoneInterface>> zones;
77 /* The timers used by the PID loop */
78 static std::vector<std::shared_ptr<boost::asio::steady_timer>> timers;
79 /* The sensors build from configuration */
80 static std::optional<SensorManager> mgmr;
81 } // namespace state
82 
83 } // namespace pid_control
84 
85 std::filesystem::path configPath = "";
86 
87 /* async io context for operation */
88 boost::asio::io_context io;
89 /* async signal_set for signal handling */
90 boost::asio::signal_set signals(io, SIGHUP, SIGTERM);
91 
92 /* buses for system control */
93 static sdbusplus::asio::connection modeControlBus(io);
94 static sdbusplus::asio::connection hostBus(io, sdbusplus::bus::new_bus());
95 static sdbusplus::asio::connection passiveBus(io, sdbusplus::bus::new_bus());
96 
97 namespace pid_control
98 {
99 
searchConfigurationPath()100 std::filesystem::path searchConfigurationPath()
101 {
102     static constexpr auto name = "config.json";
103 
104     for (const auto& pathSeg : {std::filesystem::current_path(),
105                                 std::filesystem::path{"/var/lib/swampd"},
106                                 std::filesystem::path{"/usr/share/swampd"}})
107     {
108         auto file = pathSeg / name;
109         if (std::filesystem::exists(file))
110         {
111             return file;
112         }
113     }
114 
115     return name;
116 }
117 
stopControlLoops()118 void stopControlLoops()
119 {
120     for (const auto& timer : state::timers)
121     {
122         timer->cancel();
123     }
124     state::isCanceling = true;
125     state::timers.clear();
126 
127     if (state::zones.size() > 0 && state::zones.begin()->second.use_count() > 1)
128     {
129         throw std::runtime_error("wait for count back to 1");
130     }
131 
132     state::zones.clear();
133     state::isCanceling = false;
134 }
135 
restartControlLoops()136 void restartControlLoops()
137 {
138     stopControlLoops();
139 
140     const std::filesystem::path path =
141         (!configPath.empty()) ? configPath : searchConfigurationPath();
142 
143     if (std::filesystem::exists(path))
144     {
145         /*
146          * When building the sensors, if any of the dbus passive ones aren't on
147          * the bus, it'll fail immediately.
148          */
149         try
150         {
151             auto jsonData = parseValidateJson(path);
152             sensorConfig = buildSensorsFromJson(jsonData);
153             std::tie(zoneConfig, zoneDetailsConfig) =
154                 buildPIDsFromJson(jsonData);
155         }
156         catch (const std::exception& e)
157         {
158             std::cerr << "Failed during building: " << e.what() << "\n";
159             exit(EXIT_FAILURE); /* fatal error. */
160         }
161     }
162     else
163     {
164         static boost::asio::steady_timer reloadTimer(io);
165         if (!dbus_configuration::init(modeControlBus, reloadTimer, sensorConfig,
166                                       zoneConfig, zoneDetailsConfig))
167         {
168             return; // configuration not ready
169         }
170     }
171 
172     state::mgmr = buildSensors(sensorConfig, passiveBus, hostBus);
173     state::zones =
174         buildZones(zoneConfig, zoneDetailsConfig, *state::mgmr, modeControlBus);
175     // Set `logMaxCountPerSecond` to 20 will limit the number of logs output per
176     // second in each zone. Using 20 here would limit the output rate to be no
177     // larger than 100 per sec for most platforms as the number of zones are
178     // usually <=3. This will effectively avoid resource exhaustion.
179     buildFailsafeLoggers(state::zones, /* logMaxCountPerSecond = */ 20);
180 
181     if (0 == state::zones.size())
182     {
183         std::cerr << "No zones defined, exiting.\n";
184         std::exit(EXIT_FAILURE);
185     }
186 
187     for (const auto& i : state::zones)
188     {
189         std::shared_ptr<boost::asio::steady_timer> timer =
190             state::timers.emplace_back(
191                 std::make_shared<boost::asio::steady_timer>(io));
192         std::cerr << "pushing zone " << i.first << "\n";
193         pidControlLoop(i.second, timer, &state::isCanceling);
194     }
195 }
196 
tryRestartControlLoops(bool first)197 void tryRestartControlLoops(bool first)
198 {
199     static const auto delayTime = std::chrono::seconds(10);
200     static boost::asio::steady_timer timer(io);
201 
202     auto restartLbd = [](const boost::system::error_code& error) {
203         if (error == boost::asio::error::operation_aborted)
204         {
205             return;
206         }
207 
208         // retry when restartControlLoops() has some failure.
209         try
210         {
211             restartControlLoops();
212         }
213         catch (const std::exception& e)
214         {
215             std::cerr << "Failed during restartControlLoops, try again: "
216                       << e.what() << "\n";
217             tryRestartControlLoops(false);
218         }
219     };
220 
221     // first time of trying to restart the control loop without a delay
222     if (first)
223     {
224         boost::asio::post(io, [restartLbd] {
225             restartLbd(boost::system::error_code());
226         });
227     }
228     // re-try control loop, set up a delay.
229     else
230     {
231         timer.expires_after(delayTime);
232         timer.async_wait(restartLbd);
233     }
234 
235     return;
236 }
237 
tryTerminateControlLoops(bool first)238 void tryTerminateControlLoops(bool first)
239 {
240     static const auto delayTime = std::chrono::milliseconds(50);
241     static boost::asio::steady_timer timer(io);
242 
243     auto stopLbd = [](const boost::system::error_code& error) {
244         if (error == boost::asio::error::operation_aborted)
245         {
246             return;
247         }
248 
249         // retry when stopControlLoops() has some failure.
250         try
251         {
252             stopControlLoops();
253         }
254         catch (const std::exception& e)
255         {
256             std::cerr << "Failed during stopControlLoops, try again: "
257                       << e.what() << "\n";
258             tryTerminateControlLoops(false);
259             return;
260         }
261         io.stop();
262     };
263 
264     // first time of trying to stop the control loop without a delay
265     if (first)
266     {
267         boost::asio::post(io,
268                           [stopLbd] { stopLbd(boost::system::error_code()); });
269     }
270     // re-try control loop, set up a delay.
271     else
272     {
273         timer.expires_after(delayTime);
274         timer.async_wait(stopLbd);
275     }
276 
277     return;
278 }
279 
280 } // namespace pid_control
281 
signalHandler(const boost::system::error_code & error,int signal_number)282 void signalHandler(const boost::system::error_code& error, int signal_number)
283 {
284     static boost::asio::steady_timer timer(io);
285 
286     if (error)
287     {
288         std::cout << "Signal " << signal_number
289                   << " handler error: " << error.message() << "\n";
290         return;
291     }
292     if (signal_number == SIGTERM)
293     {
294         pid_control::tryTerminateControlLoops(true);
295     }
296     else
297     {
298         timer.expires_after(std::chrono::seconds(1));
299         timer.async_wait([](const boost::system::error_code ec) {
300             if (ec)
301             {
302                 std::cout << "Signal timer error: " << ec.message() << "\n";
303                 return;
304             }
305 
306             std::cout << "reloading configuration\n";
307             pid_control::tryRestartControlLoops();
308         });
309     }
310 
311     signals.async_wait(signalHandler);
312 }
313 
main(int argc,char * argv[])314 int main(int argc, char* argv[])
315 {
316     loggingPath = "";
317     loggingEnabled = false;
318     tuningEnabled = false;
319     debugEnabled = false;
320     coreLoggingEnabled = false;
321 
322     CLI::App app{"OpenBMC Fan Control Daemon"};
323 
324     app.add_option("-c,--conf", configPath,
325                    "Optional parameter to specify configuration at run-time")
326         ->check(CLI::ExistingFile);
327     app.add_option("-l,--log", loggingPath,
328                    "Optional parameter to specify logging folder")
329         ->check(CLI::ExistingDirectory);
330     app.add_flag("-t,--tuning", tuningEnabled, "Enable or disable tuning");
331     app.add_flag("-d,--debug", debugEnabled, "Enable or disable debug mode");
332     app.add_flag("-g,--corelogging", coreLoggingEnabled,
333                  "Enable or disable logging of core PID loop computations");
334 
335     CLI11_PARSE(app, argc, argv);
336 
337     static constexpr auto loggingEnablePath = "/etc/thermal.d/logging";
338     static constexpr auto tuningEnablePath = "/etc/thermal.d/tuning";
339     static constexpr auto debugEnablePath = "/etc/thermal.d/debugging";
340     static constexpr auto coreLoggingEnablePath = "/etc/thermal.d/corelogging";
341 
342     // Set up default logging path, preferring command line if it was given
343     std::string defLoggingPath(loggingPath);
344     if (defLoggingPath.empty())
345     {
346         defLoggingPath = std::filesystem::temp_directory_path();
347     }
348     else
349     {
350         // Enable logging, if user explicitly gave path on command line
351         loggingEnabled = true;
352     }
353 
354     // If this file exists, enable logging at runtime
355     std::ifstream fsLogging(loggingEnablePath);
356     if (fsLogging)
357     {
358         // Allow logging path to be changed by file content
359         std::string altPath;
360         std::getline(fsLogging, altPath);
361         fsLogging.close();
362 
363         if (std::filesystem::exists(altPath))
364         {
365             loggingPath = altPath;
366         }
367         else
368         {
369             loggingPath = defLoggingPath;
370         }
371 
372         loggingEnabled = true;
373     }
374     if (loggingEnabled)
375     {
376         std::cerr << "Logging enabled: " << loggingPath << "\n";
377     }
378 
379     // If this file exists, enable tuning at runtime
380     if (std::filesystem::exists(tuningEnablePath))
381     {
382         tuningEnabled = true;
383     }
384     if (tuningEnabled)
385     {
386         std::cerr << "Tuning enabled\n";
387     }
388 
389     // If this file exists, enable debug mode at runtime
390     if (std::filesystem::exists(debugEnablePath))
391     {
392         debugEnabled = true;
393     }
394 
395     if (debugEnabled)
396     {
397         std::cerr << "Debug mode enabled\n";
398     }
399 
400     // If this file exists, enable core logging at runtime
401     if (std::filesystem::exists(coreLoggingEnablePath))
402     {
403         coreLoggingEnabled = true;
404     }
405     if (coreLoggingEnabled)
406     {
407         std::cerr << "Core logging enabled\n";
408     }
409 
410     static constexpr auto modeRoot = "/xyz/openbmc_project/settings/fanctrl";
411     // Create a manager for the ModeBus because we own it.
412     sdbusplus::server::manager_t manager(
413         static_cast<sdbusplus::bus_t&>(modeControlBus), modeRoot);
414     hostBus.request_name("xyz.openbmc_project.Hwmon.external");
415     modeControlBus.request_name("xyz.openbmc_project.State.FanCtrl");
416     sdbusplus::server::manager_t objManager(modeControlBus, modeRoot);
417 
418     // Enable SIGHUP handling to reload JSON config
419     signals.async_wait(signalHandler);
420 
421     /*
422      * All sensors are managed by one manager, but each zone has a pointer to
423      * it.
424      */
425 
426     pid_control::tryRestartControlLoops();
427 
428     io.run();
429     return 0;
430 }
431