1 /**
2  * Copyright © 2021 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 #pragma once
17 #include "config.h"
18 
19 #include "types.hpp"
20 
21 #include <fmt/format.h>
22 
23 #include <cereal/archives/json.hpp>
24 #include <cereal/types/string.hpp>
25 #include <cereal/types/tuple.hpp>
26 #include <cereal/types/vector.hpp>
27 #include <phosphor-logging/log.hpp>
28 #include <sdeventplus/clock.hpp>
29 #include <sdeventplus/utility/timer.hpp>
30 
31 #include <filesystem>
32 #include <fstream>
33 #include <map>
34 #include <tuple>
35 
36 namespace sensor::monitor
37 {
38 
39 /**
40  * @class AlarmTimestamps
41  *
42  * This class keeps track of the timestamps at which the shutdown
43  * timers are started in case the process or whole BMC restarts
44  * while a timer is running.  In the case where the process starts
45  * when a timer was previously running and an alarm is still active,
46  * a new timer can be started with just the remaining time.
47  */
48 class AlarmTimestamps
49 {
50   public:
51     ~AlarmTimestamps() = default;
52     AlarmTimestamps(const AlarmTimestamps&) = delete;
53     AlarmTimestamps& operator=(const AlarmTimestamps&) = delete;
54     AlarmTimestamps(AlarmTimestamps&&) = delete;
55     AlarmTimestamps& operator=(AlarmTimestamps&&) = delete;
56 
57     /**
58      * @brief Constructor
59      *
60      * Loads any saved timestamps
61      */
62     AlarmTimestamps()
63     {
64         load();
65     }
66 
67     /**
68      * @brief Adds an entry to the timestamps map and persists it.
69      *
70      * @param[in] key - The AlarmKey value
71      * @param[in] timestamp - The start timestamp to save
72      */
73     void add(const AlarmKey& key, uint64_t timestamp)
74     {
75         // Emplace won't do anything if an entry with that
76         // key was already present, so only save if an actual
77         // entry was added.
78         auto result = timestamps.emplace(key, timestamp);
79         if (result.second)
80         {
81             save();
82         }
83     }
84 
85     /**
86      * @brief Erase an entry using the passed in alarm key.
87      *
88      * @param[in] key - The AlarmKey value
89      */
90     void erase(const AlarmKey& key)
91     {
92         size_t removed = timestamps.erase(key);
93         if (removed)
94         {
95             save();
96         }
97     }
98 
99     /**
100      * @brief Erase an entry using an iterator.
101      */
102     void erase(std::map<AlarmKey, uint64_t>::const_iterator& entry)
103     {
104         timestamps.erase(entry);
105         save();
106     }
107 
108     /**
109      * @brief Clear all entries.
110      */
111     void clear()
112     {
113         if (!timestamps.empty())
114         {
115             timestamps.clear();
116             save();
117         }
118     }
119 
120     /**
121      * @brief Remove any entries for which there is not a running timer
122      *        for.  This is used on startup when an alarm could have cleared
123      *        during a restart to get rid of the old entries.
124      *
125      * @param[in] alarms - The current alarms map.
126      */
127     void prune(
128         const std::map<AlarmKey, std::unique_ptr<sdeventplus::utility::Timer<
129                                      sdeventplus::ClockId::Monotonic>>>& alarms)
130     {
131         auto size = timestamps.size();
132 
133         auto isTimerStopped = [&alarms](const AlarmKey& key) {
134             auto alarm = alarms.find(key);
135             if (alarm != alarms.end())
136             {
137                 auto& timer = alarm->second;
138                 if (timer && timer->isEnabled())
139                 {
140                     return false;
141                 }
142             }
143             return true;
144         };
145 
146         auto it = timestamps.begin();
147 
148         while (it != timestamps.end())
149         {
150             if (isTimerStopped(it->first))
151             {
152                 it = timestamps.erase(it);
153             }
154             else
155             {
156                 ++it;
157             }
158         }
159 
160         if (size != timestamps.size())
161         {
162             save();
163         }
164     }
165 
166     /**
167      * @brief Returns the timestamps map
168      */
169     const std::map<AlarmKey, uint64_t>& get() const
170     {
171         return timestamps;
172     }
173 
174     /**
175      * @brief Saves the timestamps map in the filesystem using cereal.
176      *
177      * Since cereal doesn't understand the AlarmType or ShutdownType
178      * enums, they are converted to ints before being written.
179      */
180     void save()
181     {
182         std::filesystem::path path =
183             std::filesystem::path{SENSOR_MONITOR_PERSIST_ROOT_PATH} /
184             timestampsFilename;
185 
186         if (!std::filesystem::exists(path.parent_path()))
187         {
188             std::filesystem::create_directory(path.parent_path());
189         }
190 
191         std::vector<std::tuple<std::string, int, int, uint64_t>> times;
192 
193         for (const auto& [key, time] : timestamps)
194         {
195             times.emplace_back(std::get<std::string>(key),
196                                static_cast<int>(std::get<ShutdownType>(key)),
197                                static_cast<int>(std::get<AlarmType>(key)),
198                                time);
199         }
200 
201         std::ofstream stream{path.c_str()};
202         cereal::JSONOutputArchive oarchive{stream};
203 
204         oarchive(times);
205     }
206 
207   private:
208     static constexpr auto timestampsFilename = "shutdownAlarmStartTimes";
209 
210     /**
211      * @brief Loads the saved timestamps from the filesystem.
212      *
213      * As with save(), cereal doesn't understand the ShutdownType or AlarmType
214      * enums so they have to have been saved as ints and converted.
215      */
216     void load()
217     {
218         std::vector<std::tuple<std::string, int, int, uint64_t>> times;
219 
220         std::filesystem::path path =
221             std::filesystem::path{SENSOR_MONITOR_PERSIST_ROOT_PATH} /
222             timestampsFilename;
223 
224         if (!std::filesystem::exists(path))
225         {
226             return;
227         }
228 
229         try
230         {
231             std::ifstream stream{path.c_str()};
232             cereal::JSONInputArchive iarchive{stream};
233             iarchive(times);
234 
235             for (const auto& [path, shutdownType, alarmType, timestamp] : times)
236             {
237                 timestamps.emplace(
238                     AlarmKey{path, static_cast<ShutdownType>(shutdownType),
239                              static_cast<AlarmType>(alarmType)},
240                     timestamp);
241             }
242         }
243         catch (const std::exception& e)
244         {
245             // Include possible exception when removing file, otherwise ec = 0
246             using namespace phosphor::logging;
247             std::error_code ec;
248             std::filesystem::remove(path, ec);
249             log<level::ERR>(
250                 fmt::format("Unable to restore persisted times ({}, ec: {})",
251                             e.what(), ec.value())
252                     .c_str());
253         }
254     }
255 
256     /**
257      * @brief The map of AlarmKeys and time start times.
258      */
259     std::map<AlarmKey, uint64_t> timestamps;
260 };
261 
262 } // namespace sensor::monitor
263