1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chromecast/crash/linux/synchronized_minidump_manager.h"
6
7 #include <errno.h>
8 #include <fcntl.h>
9 #include <stdint.h>
10 #include <string.h>
11 #include <sys/file.h>
12 #include <unistd.h>
13
14 #include <memory>
15 #include <string>
16 #include <utility>
17
18 #include "base/files/dir_reader_posix.h"
19 #include "base/files/file_util.h"
20 #include "base/logging.h"
21 #include "base/posix/eintr_wrapper.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/string_split.h"
24 #include "base/strings/stringprintf.h"
25 #include "chromecast/base/path_utils.h"
26 #include "chromecast/base/serializers.h"
27 #include "chromecast/crash/linux/dump_info.h"
28
29 // if |cond| is false, returns |retval|.
30 #define RCHECK(cond, retval) \
31 do { \
32 if (!(cond)) { \
33 return (retval); \
34 } \
35 } while (0)
36
37 namespace chromecast {
38
39 namespace {
40
41 const char kLockfileName[] = "lockfile";
42 const char kMetadataName[] = "metadata";
43 const char kMinidumpsDir[] = "minidumps";
44
45 const char kLockfileRatelimitKey[] = "ratelimit";
46 const char kLockfileRatelimitPeriodStartKey[] = "period_start";
47 const char kLockfileRatelimitPeriodDumpsKey[] = "period_dumps";
48 const uint64_t kLockfileNumRatelimitParams = 2;
49
50 // Gets the ratelimit parameter dictionary given a deserialized |metadata|.
51 // Returns nullptr if invalid.
GetRatelimitParams(base::Value * metadata)52 base::DictionaryValue* GetRatelimitParams(base::Value* metadata) {
53 base::DictionaryValue* dict;
54 base::DictionaryValue* ratelimit_params;
55 if (!metadata || !metadata->GetAsDictionary(&dict) ||
56 !dict->GetDictionary(kLockfileRatelimitKey, &ratelimit_params)) {
57 return nullptr;
58 }
59
60 return ratelimit_params;
61 }
62
63 // Returns the time of the current ratelimit period's start in |metadata|.
64 // Returns base::Time() if an error occurs.
GetRatelimitPeriodStart(base::Value * metadata)65 base::Time GetRatelimitPeriodStart(base::Value* metadata) {
66 base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
67 RCHECK(ratelimit_params, base::Time());
68
69 double seconds = 0.0;
70 RCHECK(
71 ratelimit_params->GetDouble(kLockfileRatelimitPeriodStartKey, &seconds),
72 base::Time());
73
74 // Return value of 0 indicates "not initialized", so we need to explicitly
75 // check for it and return time_t = 0 equivalent.
76 return seconds ? base::Time::FromDoubleT(seconds) : base::Time::UnixEpoch();
77 }
78
79 // Sets the time of the current ratelimit period's start in |metadata| to
80 // |period_start|. Returns true on success, false on error.
SetRatelimitPeriodStart(base::Value * metadata,base::Time period_start)81 bool SetRatelimitPeriodStart(base::Value* metadata, base::Time period_start) {
82 DCHECK(!period_start.is_null());
83
84 base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
85 RCHECK(ratelimit_params, false);
86
87 ratelimit_params->SetDouble(kLockfileRatelimitPeriodStartKey,
88 period_start.ToDoubleT());
89 return true;
90 }
91
92 // Gets the number of dumps added to |metadata| in the current ratelimit
93 // period. Returns < 0 on error.
GetRatelimitPeriodDumps(base::Value * metadata)94 int GetRatelimitPeriodDumps(base::Value* metadata) {
95 int period_dumps = -1;
96
97 base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
98 if (!ratelimit_params ||
99 !ratelimit_params->GetInteger(kLockfileRatelimitPeriodDumpsKey,
100 &period_dumps)) {
101 return -1;
102 }
103
104 return period_dumps;
105 }
106
107 // Sets the current ratelimit period's number of dumps in |metadata| to
108 // |period_dumps|. Returns true on success, false on error.
SetRatelimitPeriodDumps(base::Value * metadata,int period_dumps)109 bool SetRatelimitPeriodDumps(base::Value* metadata, int period_dumps) {
110 DCHECK_GE(period_dumps, 0);
111
112 base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
113 RCHECK(ratelimit_params, false);
114
115 ratelimit_params->SetInteger(kLockfileRatelimitPeriodDumpsKey, period_dumps);
116
117 return true;
118 }
119
120 // Returns true if |metadata| contains valid metadata, false otherwise.
ValidateMetadata(base::Value * metadata)121 bool ValidateMetadata(base::Value* metadata) {
122 RCHECK(metadata, false);
123
124 // Validate ratelimit params
125 base::DictionaryValue* ratelimit_params = GetRatelimitParams(metadata);
126
127 return ratelimit_params &&
128 ratelimit_params->size() == kLockfileNumRatelimitParams &&
129 !GetRatelimitPeriodStart(metadata).is_null() &&
130 GetRatelimitPeriodDumps(metadata) >= 0;
131 }
132
133 // Calls flock on valid file descriptor |fd| with flag |flag|. Returns true
134 // on success, false on failure.
CallFlockOnFileWithFlag(const int fd,int flag)135 bool CallFlockOnFileWithFlag(const int fd, int flag) {
136 int ret = -1;
137 if ((ret = HANDLE_EINTR(flock(fd, flag))) < 0)
138 PLOG(ERROR) << "Error locking " << fd;
139
140 return !ret;
141 }
142
OpenAndLockFile(const base::FilePath & path,bool write)143 int OpenAndLockFile(const base::FilePath& path, bool write) {
144 int fd = -1;
145 const char* file = path.value().c_str();
146
147 if ((fd = open(file, write ? O_RDWR : O_RDONLY)) < 0) {
148 PLOG(ERROR) << "Error opening " << file;
149 } else if (!CallFlockOnFileWithFlag(fd, LOCK_EX)) {
150 close(fd);
151 fd = -1;
152 }
153
154 return fd;
155 }
156
UnlockAndCloseFile(const int fd)157 bool UnlockAndCloseFile(const int fd) {
158 if (!CallFlockOnFileWithFlag(fd, LOCK_UN))
159 return false;
160 return !close(fd);
161 }
162
163 } // namespace
164
165 // One day
166 const int SynchronizedMinidumpManager::kRatelimitPeriodSeconds = 24 * 3600;
167 const int SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps = 100;
168
SynchronizedMinidumpManager()169 SynchronizedMinidumpManager::SynchronizedMinidumpManager()
170 : dump_path_(GetHomePathASCII(kMinidumpsDir)),
171 lockfile_path_(dump_path_.Append(kLockfileName).value()),
172 metadata_path_(dump_path_.Append(kMetadataName).value()),
173 lockfile_fd_(-1) {}
174
~SynchronizedMinidumpManager()175 SynchronizedMinidumpManager::~SynchronizedMinidumpManager() {
176 // Release the lock if held.
177 ReleaseLockFile();
178 }
179
180 // TODO(slan): Move some of this pruning logic to ReleaseLockFile?
GetNumDumps(bool delete_all_dumps)181 int SynchronizedMinidumpManager::GetNumDumps(bool delete_all_dumps) {
182 int num_dumps = 0;
183
184 base::DirReaderPosix reader(dump_path_.value().c_str());
185 if (!reader.IsValid()) {
186 LOG(ERROR) << "Unable to open directory " << dump_path_.value();
187 return 0;
188 }
189
190 while (reader.Next()) {
191 if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
192 continue;
193
194 const base::FilePath dump_file(dump_path_.Append(reader.name()));
195 // If file cannot be found, skip.
196 if (!base::PathExists(dump_file))
197 continue;
198
199 // Do not count |lockfile_path_| and |metadata_path_|.
200 if (lockfile_path_ != dump_file && metadata_path_ != dump_file) {
201 ++num_dumps;
202 if (delete_all_dumps) {
203 LOG(INFO) << "Removing " << reader.name()
204 << "which was not in the lockfile";
205 if (!base::DeleteFile(dump_file))
206 PLOG(INFO) << "Removing " << dump_file.value() << " failed";
207 }
208 }
209 }
210
211 return num_dumps;
212 }
213
AcquireLockAndDoWork()214 bool SynchronizedMinidumpManager::AcquireLockAndDoWork() {
215 bool success = false;
216 if (AcquireLockFile()) {
217 success = DoWork();
218 ReleaseLockFile();
219 }
220 return success;
221 }
222
AcquireLockFile()223 bool SynchronizedMinidumpManager::AcquireLockFile() {
224 DCHECK_LT(lockfile_fd_, 0);
225 // Make the directory for the minidumps if it does not exist.
226 base::File::Error error;
227 if (!CreateDirectoryAndGetError(dump_path_, &error)) {
228 LOG(ERROR) << "Failed to create directory " << dump_path_.value()
229 << ". error = " << error;
230 return false;
231 }
232
233 // Open the lockfile. Create it if it does not exist.
234 base::File lockfile(lockfile_path_, base::File::FLAG_OPEN_ALWAYS);
235
236 // If opening or creating the lockfile failed, we don't want to proceed
237 // with dump writing for fear of exhausting up system resources.
238 if (!lockfile.IsValid()) {
239 LOG(ERROR) << "open lockfile failed " << lockfile_path_.value();
240 return false;
241 }
242
243 if ((lockfile_fd_ = OpenAndLockFile(lockfile_path_, false)) < 0) {
244 ReleaseLockFile();
245 return false;
246 }
247
248 // The lockfile is open and locked. Parse it to provide subclasses with a
249 // record of all the current dumps.
250 if (!ParseFiles()) {
251 LOG(ERROR) << "Lockfile did not parse correctly. ";
252 if (!InitializeFiles() || !ParseFiles()) {
253 LOG(ERROR) << "Failed to create a new lock file!";
254 ReleaseLockFile();
255 return false;
256 }
257 }
258
259 DCHECK(dumps_);
260 DCHECK(metadata_);
261
262 // We successfully have acquired the lock.
263 return true;
264 }
265
ParseFiles()266 bool SynchronizedMinidumpManager::ParseFiles() {
267 DCHECK_GE(lockfile_fd_, 0);
268 DCHECK(!dumps_);
269 DCHECK(!metadata_);
270
271 std::string lockfile;
272 RCHECK(ReadFileToString(lockfile_path_, &lockfile), false);
273
274 std::vector<std::string> lines = base::SplitString(
275 lockfile, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
276
277 std::unique_ptr<base::ListValue> dumps = std::make_unique<base::ListValue>();
278
279 // Validate dumps
280 for (const std::string& line : lines) {
281 if (line.size() == 0)
282 continue;
283 std::unique_ptr<base::Value> dump_info = DeserializeFromJson(line);
284 DumpInfo info(dump_info.get());
285 RCHECK(info.valid(), false);
286 dumps->Append(std::move(dump_info));
287 }
288
289 std::unique_ptr<base::Value> metadata =
290 DeserializeJsonFromFile(metadata_path_);
291 RCHECK(ValidateMetadata(metadata.get()), false);
292
293 dumps_ = std::move(dumps);
294 metadata_ = std::move(metadata);
295 return true;
296 }
297
WriteFiles(const base::ListValue * dumps,const base::Value * metadata)298 bool SynchronizedMinidumpManager::WriteFiles(const base::ListValue* dumps,
299 const base::Value* metadata) {
300 DCHECK(dumps);
301 DCHECK(metadata);
302 std::string lockfile;
303
304 for (const auto& elem : *dumps) {
305 base::Optional<std::string> dump_info = SerializeToJson(elem);
306 RCHECK(dump_info, false);
307 lockfile += *dump_info;
308 lockfile += "\n"; // Add line seperatators
309 }
310
311 if (WriteFile(lockfile_path_, lockfile.c_str(), lockfile.size()) < 0) {
312 return false;
313 }
314
315 return SerializeJsonToFile(metadata_path_, *metadata);
316 }
317
InitializeFiles()318 bool SynchronizedMinidumpManager::InitializeFiles() {
319 std::unique_ptr<base::DictionaryValue> metadata =
320 std::make_unique<base::DictionaryValue>();
321
322 auto ratelimit_fields = std::make_unique<base::DictionaryValue>();
323 ratelimit_fields->SetDouble(kLockfileRatelimitPeriodStartKey, 0.0);
324 ratelimit_fields->SetInteger(kLockfileRatelimitPeriodDumpsKey, 0);
325 metadata->Set(kLockfileRatelimitKey, std::move(ratelimit_fields));
326
327 std::unique_ptr<base::ListValue> dumps = std::make_unique<base::ListValue>();
328
329 return WriteFiles(dumps.get(), metadata.get());
330 }
331
AddEntryToLockFile(const DumpInfo & dump_info)332 bool SynchronizedMinidumpManager::AddEntryToLockFile(
333 const DumpInfo& dump_info) {
334 DCHECK_GE(lockfile_fd_, 0);
335 DCHECK(dumps_);
336
337 // Make sure dump_info is valid.
338 if (!dump_info.valid()) {
339 LOG(ERROR) << "Entry to be added is invalid";
340 return false;
341 }
342
343 dumps_->Append(dump_info.GetAsValue());
344 return true;
345 }
346
RemoveEntryFromLockFile(int index)347 bool SynchronizedMinidumpManager::RemoveEntryFromLockFile(int index) {
348 return dumps_->Remove(static_cast<uint64_t>(index), nullptr);
349 }
350
ReleaseLockFile()351 void SynchronizedMinidumpManager::ReleaseLockFile() {
352 // flock is associated with the fd entry in the open fd table, so closing
353 // all fd's will release the lock. To be safe, we explicitly unlock.
354 if (lockfile_fd_ >= 0) {
355 if (dumps_ && metadata_)
356 WriteFiles(dumps_.get(), metadata_.get());
357
358 UnlockAndCloseFile(lockfile_fd_);
359 lockfile_fd_ = -1;
360 }
361
362 dumps_.reset();
363 metadata_.reset();
364 }
365
GetDumps()366 std::vector<std::unique_ptr<DumpInfo>> SynchronizedMinidumpManager::GetDumps() {
367 std::vector<std::unique_ptr<DumpInfo>> dumps;
368
369 for (const auto& elem : *dumps_) {
370 dumps.push_back(std::unique_ptr<DumpInfo>(new DumpInfo(&elem)));
371 }
372
373 return dumps;
374 }
375
SetCurrentDumps(const std::vector<std::unique_ptr<DumpInfo>> & dumps)376 bool SynchronizedMinidumpManager::SetCurrentDumps(
377 const std::vector<std::unique_ptr<DumpInfo>>& dumps) {
378 dumps_->Clear();
379
380 for (auto& dump : dumps)
381 dumps_->Append(dump->GetAsValue());
382
383 return true;
384 }
385
IncrementNumDumpsInCurrentPeriod()386 bool SynchronizedMinidumpManager::IncrementNumDumpsInCurrentPeriod() {
387 DCHECK(metadata_);
388 int last_dumps = GetRatelimitPeriodDumps(metadata_.get());
389 RCHECK(last_dumps >= 0, false);
390
391 return SetRatelimitPeriodDumps(metadata_.get(), last_dumps + 1);
392 }
393
DecrementNumDumpsInCurrentPeriod()394 bool SynchronizedMinidumpManager::DecrementNumDumpsInCurrentPeriod() {
395 DCHECK(metadata_);
396 int last_dumps = GetRatelimitPeriodDumps(metadata_.get());
397 if (last_dumps > 0) {
398 return SetRatelimitPeriodDumps(metadata_.get(), last_dumps - 1);
399 }
400 return true;
401 }
402
ResetRateLimitPeriod()403 void SynchronizedMinidumpManager::ResetRateLimitPeriod() {
404 SetRatelimitPeriodStart(metadata_.get(), base::Time::Now());
405 SetRatelimitPeriodDumps(metadata_.get(), 0);
406 }
407
CanUploadDump()408 bool SynchronizedMinidumpManager::CanUploadDump() {
409 base::Time cur_time = base::Time::Now();
410 base::Time period_start = GetRatelimitPeriodStart(metadata_.get());
411 int period_dumps_count = GetRatelimitPeriodDumps(metadata_.get());
412
413 // If we're in invalid state, or we passed the period, reset the ratelimit.
414 // When the device reboots, |cur_time| may be incorrectly reported to be a
415 // very small number for a short period of time. So only consider
416 // |period_start| invalid when |cur_time| is less if |cur_time| is not very
417 // close to 0.
418 if (period_dumps_count < 0 ||
419 (cur_time < period_start &&
420 cur_time.ToDoubleT() > kRatelimitPeriodSeconds) ||
421 (cur_time - period_start).InSeconds() >= kRatelimitPeriodSeconds) {
422 ResetRateLimitPeriod();
423 return true;
424 }
425
426 return period_dumps_count < kRatelimitPeriodMaxDumps;
427 }
428
HasDumps()429 bool SynchronizedMinidumpManager::HasDumps() {
430 // Check if lockfile has entries.
431 int64_t size = 0;
432 if (base::GetFileSize(lockfile_path_, &size) && size > 0)
433 return true;
434
435 // Check if any files are in minidump directory
436 base::DirReaderPosix reader(dump_path_.value().c_str());
437 if (!reader.IsValid()) {
438 DLOG(ERROR) << "Could not open minidump dir: " << dump_path_.value();
439 return false;
440 }
441
442 while (reader.Next()) {
443 if (strcmp(reader.name(), ".") == 0 || strcmp(reader.name(), "..") == 0)
444 continue;
445
446 const base::FilePath file_path = dump_path_.Append(reader.name());
447 if (file_path != lockfile_path_ && file_path != metadata_path_)
448 return true;
449 }
450
451 return false;
452 }
453
InitializeFileState()454 bool SynchronizedMinidumpManager::InitializeFileState() {
455 if (!AcquireLockFile())
456 return false; // Error logged
457
458 ReleaseLockFile();
459 return true;
460 }
461
462 } // namespace chromecast
463