1 : // Copyright 2014 Google Inc. All Rights Reserved.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 : //
15 : // -----------------
16 : // Repository Format
17 : // -----------------
18 : //
19 : // This file implements a repository for crash reports that are pending upload.
20 : // The repository has a single root directory and creates several subdirectories
21 : // beneath it:
22 : //
23 : // <root>/Incoming
24 : // <root>/Retry
25 : // <root>/Retry 2
26 : //
27 : // Reports are stored in the repository by creating a minidump file and passing
28 : // its path, along with a dictionary of crash keys to StoreReport. The minidump
29 : // will be moved into Incoming and its crash keys serialized alongside it. The
30 : // minidump will be given a .dmp extension (if it doesn't already have one) and
31 : // the crash keys will be in a file having the same basename and a '.kys'
32 : // extension.
33 : //
34 : // After a successful upload, the minidump and crash keys files are deleted.
35 : // After a failed upload, a report in "Incoming" will be moved to "Retry", a
36 : // report in "Retry" to "Retry 2", and a report from "Retry 2" will be processed
37 : // using the configured PermanentFailureHandler.
38 : //
39 : // When the repository receives or attempts to upload a report the report file
40 : // timestamps are updated. While files in "Incoming" are always eligible for
41 : // upload, those in "Retry" and "Retry 2" are eligible when their last-modified
42 : // date is older than the configured retry interval.
43 : //
44 : // Orphaned report files (minidumps without crash keys and vice-versa) may be
45 : // detected during upload attempts. When receiving new minidumps, we first write
46 : // the crash keys to "Incoming" before moving the minidump file in. As a result,
47 : // an orphaned minidump file is always an error condition and will be deleted
48 : // immediately upon detection. An orphaned crash keys file may occur normally in
49 : // the interval before the minidump file is moved. These files are only deleted
50 : // when their timestamp is more than a day in the past.
51 :
52 : #include "syzygy/kasko/report_repository.h"
53 :
54 : #include "base/file_util.h"
55 : #include "base/logging.h"
56 : #include "base/files/file_enumerator.h"
57 : #include "syzygy/kasko/crash_keys_serialization.h"
58 :
59 : namespace kasko {
60 :
61 : namespace {
62 :
63 : // The extension used when serializing crash keys.
64 : const base::char16 kCrashKeysFileExtension[] = L".kys";
65 : // The extension used to identify minidump files.
66 : const base::char16 kDumpFileExtension[] = L".dmp";
67 : // The subdirectory where new reports (minidumps and crash keys) are initially
68 : // stored.
69 : const base::char16 kIncomingReportsSubdir[] = L"Incoming";
70 : // The subdirectory where reports that have failed once are stored.
71 : const base::char16 kFailedOnceSubdir[] = L"Retry";
72 : // The subdirectory where reports that have failed twice are stored.
73 : const base::char16 kFailedTwiceSubdir[] = L"Retry 2";
74 :
75 : // Deletes a path non-recursively and logs an error in case of failure.
76 : // @param path The path to delete.
77 : // @returns true if the operation succeeds.
78 E : bool LoggedDeleteFile(const base::FilePath& path) {
79 E : if (base::DeleteFile(path, false))
80 E : return true;
81 i : LOG(ERROR) << "Failed to delete " << path.value();
82 i : return false;
83 E : }
84 :
85 : // Takes ownership of a FilePath. The owned path will be deleted when the
86 : // ScopedReportFile is destroyed.
87 : class ScopedReportFile {
88 : public:
89 E : explicit ScopedReportFile(const base::FilePath& path) : path_(path) {}
90 :
91 E : ~ScopedReportFile() {
92 E : if (!path_.empty())
93 E : LoggedDeleteFile(path_);
94 E : }
95 :
96 : // Provides access to the owned value.
97 : // @returns the owned path.
98 E : base::FilePath Get() const { return path_; }
99 :
100 : // Releases ownership of the owned path.
101 : // @returns the owned path.
102 E : base::FilePath Take() {
103 E : base::FilePath temp = path_;
104 E : path_ = base::FilePath();
105 E : return temp;
106 E : }
107 :
108 : // Moves the file pointed to by the owned path, and updates the owned path
109 : // to the new path.
110 : // @param new_path The full destination path.
111 : // @returns true if the operation succeeds.
112 E : bool Move(const base::FilePath& new_path) {
113 E : if (base::Move(path_, new_path)) {
114 E : path_ = new_path;
115 E : return true;
116 : }
117 i : LOG(ERROR) << "Failed to move " << path_.value() << " to "
118 : << new_path.value();
119 i : return false;
120 E : }
121 :
122 : // Sets the last-modified timestamp of the file pointed to by the owned path.
123 : // @param value The desired timestamp.
124 : // @returns true if the operation succeeds.
125 E : bool UpdateTimestamp(const base::Time& value) {
126 E : if (path_.empty())
127 i : return false;
128 E : if (base::TouchFile(path_, value, value))
129 E : return true;
130 i : LOG(ERROR) << "Failed to update timestamp for " << path_.value();
131 i : return false;
132 E : }
133 :
134 : private:
135 : base::FilePath path_;
136 :
137 : DISALLOW_COPY_AND_ASSIGN(ScopedReportFile);
138 : };
139 :
140 : // Returns the crash keys file path corresponding to the supplied minidump file
141 : // path.
142 : // @param minidump_path The path to a minidump file.
143 : // @returns The path where the corresponding crash keys file should be stored.
144 : base::FilePath GetCrashKeysFileForDumpFile(
145 E : const base::FilePath& minidump_path) {
146 E : return minidump_path.ReplaceExtension(kCrashKeysFileExtension);
147 E : }
148 :
149 : // Returns the minidump file path corresponding to the supplied crash keys file
150 : // path.
151 : // @param crash_keys_path The path to a crash keys file.
152 : // @returns The path where the corresponding minidump file should be stored.
153 : base::FilePath GetDumpFileForCrashKeysFile(
154 E : const base::FilePath& crash_keys_path) {
155 E : return crash_keys_path.ReplaceExtension(kDumpFileExtension);
156 E : }
157 :
158 : // Returns a minidump that is eligible for upload from the given directory, if
159 : // any are.
160 : // @param directory The directory to scan.
161 : // @param maximum_timestamp_for_retries The cutoff for the most most recent
162 : // upload attempt of eligible minidumps. If null, there is no cutoff.
163 : // @returns The path to a minidump that is eligible for upload, if any.
164 : base::FilePath GetPendingReportFromDirectory(
165 : const base::FilePath& directory,
166 E : const base::Time& maximum_timestamp_for_retries) {
167 : base::FileEnumerator file_enumerator(
168 : directory, false, base::FileEnumerator::FILES,
169 E : base::string16(L"*") + kDumpFileExtension);
170 : // Visit all files in this directory until we find an eligible one.
171 E : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
172 E : candidate = file_enumerator.Next()) {
173 : // Skip dumps with missing crash keys.
174 E : if (!base::PathExists(GetCrashKeysFileForDumpFile(candidate))) {
175 E : LOG(ERROR) << "Deleting a minidump file with missing crash keys: "
176 : << candidate.value();
177 E : LoggedDeleteFile(candidate);
178 E : continue;
179 : }
180 E : if (maximum_timestamp_for_retries.is_null())
181 E : return candidate;
182 :
183 : // Check if this file is eligible for retry.
184 E : base::FileEnumerator::FileInfo file_info = file_enumerator.GetInfo();
185 E : if (file_info.GetLastModifiedTime() <= maximum_timestamp_for_retries)
186 E : return candidate;
187 E : }
188 E : return base::FilePath();
189 E : }
190 :
191 : void CleanOrphanedCrashKeysFiles(
192 : const base::FilePath& repository_path,
193 E : const base::Time& now) {
194 E : base::Time one_day_ago(now - base::TimeDelta::FromDays(1));
195 : const base::char16* subdirs[] = {
196 E : kIncomingReportsSubdir, kFailedOnceSubdir, kFailedTwiceSubdir};
197 :
198 E : for (size_t i = 0; i < arraysize(subdirs); ++i) {
199 : base::FileEnumerator file_enumerator(
200 : repository_path.Append(subdirs[i]), false, base::FileEnumerator::FILES,
201 E : base::string16(L"*") + kCrashKeysFileExtension);
202 E : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
203 E : candidate = file_enumerator.Next()) {
204 E : if (base::PathExists(GetDumpFileForCrashKeysFile(candidate)))
205 E : continue;
206 :
207 : // We write crash keys files before moving dump files, so there is a brief
208 : // period where an orphan might be expected. Only delete orphans that are
209 : // more than a day old.
210 E : if (file_enumerator.GetInfo().GetLastModifiedTime() >= one_day_ago)
211 E : continue;
212 :
213 E : LOG(ERROR) << "Deleting a crash keys file with missing minidump: "
214 : << candidate.value();
215 E : LoggedDeleteFile(candidate);
216 E : }
217 E : }
218 E : }
219 :
220 : // Returns a minidump that is eligible for upload, if any are.
221 : // @param repository_path The directory where this repository stores reports.
222 : // @param now The current time.
223 : // @param retry_interval The minimum interval between upload attempts for a
224 : // given report.
225 : // @returns A pair of mindump path (empty if none) and failure destination
226 : // (empty if the next failure is permanent).
227 : std::pair<base::FilePath, base::FilePath> GetPendingReport(
228 : const base::FilePath& repository_path,
229 : const base::Time& now,
230 E : const base::TimeDelta& retry_interval) {
231 : struct {
232 : const base::char16* subdir;
233 : const base::char16* failure_subdir;
234 : base::Time retry_cutoff;
235 : } directories[] = {
236 : {kIncomingReportsSubdir, kFailedOnceSubdir, base::Time()},
237 : {kFailedOnceSubdir, kFailedTwiceSubdir, now - retry_interval},
238 E : {kFailedTwiceSubdir, nullptr, now - retry_interval}};
239 :
240 E : for (size_t i = 0; i < arraysize(directories); ++i) {
241 : base::FilePath result = GetPendingReportFromDirectory(
242 : repository_path.Append(directories[i].subdir),
243 E : directories[i].retry_cutoff);
244 E : if (result.empty())
245 E : continue;
246 E : if (!directories[i].failure_subdir)
247 E : return std::make_pair(result, base::FilePath());
248 : return std::make_pair(
249 E : result, repository_path.Append(directories[i].failure_subdir));
250 i : }
251 E : return std::pair<base::FilePath, base::FilePath>();
252 E : }
253 :
254 : // Handles a non-permanent failure by moving the report files to a new queue.
255 : // @param minidump_file The minidump file. This method calls Take() on success.
256 : // @param crash_keys_file The crash keys file. This method calls Take() on
257 : // success.
258 : // @param destination_directory The directory where the files should be moved
259 : // to.
260 : void HandleNonpermanentFailure(ScopedReportFile* minidump_file,
261 : ScopedReportFile* crash_keys_file,
262 E : const base::FilePath& destination_directory) {
263 E : if (!base::CreateDirectory(destination_directory)) {
264 i : LOG(ERROR) << "Failed to create destination directory "
265 : << destination_directory.value();
266 i : return;
267 : }
268 : if (!minidump_file->Move(
269 E : destination_directory.Append(minidump_file->Get().BaseName()))) {
270 i : return;
271 : }
272 : if (!crash_keys_file->Move(
273 E : destination_directory.Append(crash_keys_file->Get().BaseName()))) {
274 i : return;
275 : }
276 E : minidump_file->Take();
277 E : crash_keys_file->Take();
278 E : }
279 :
280 : // Handles a permanent failure by invoking the PermanentFailureHandler. Ensures
281 : // that the report files are removed from the repository.
282 : // @param minidump_path The path to the minidump file.
283 : // @param crash_keys_path The path to the crash keys file.
284 : // @param permanent_failure_handler The PermanentFailureHandler to invoke.
285 : void HandlePermanentFailure(const base::FilePath& minidump_path,
286 : const base::FilePath& crash_keys_path,
287 : const ReportRepository::PermanentFailureHandler&
288 E : permanent_failure_handler) {
289 E : permanent_failure_handler.Run(minidump_path, crash_keys_path);
290 :
291 : // In case the handler didn't delete the files, we will.
292 E : if (base::PathExists(minidump_path))
293 E : LoggedDeleteFile(minidump_path);
294 E : if (base::PathExists(crash_keys_path))
295 E : LoggedDeleteFile(crash_keys_path);
296 E : }
297 :
298 : } // namespace
299 :
300 : ReportRepository::ReportRepository(
301 : const base::FilePath& repository_path,
302 : const base::TimeDelta& retry_interval,
303 : const TimeSource& time_source,
304 : const Uploader& uploader,
305 : const PermanentFailureHandler& permanent_failure_handler)
306 : : repository_path_(repository_path),
307 : retry_interval_(retry_interval),
308 : time_source_(time_source),
309 : uploader_(uploader),
310 E : permanent_failure_handler_(permanent_failure_handler) {
311 E : }
312 :
313 E : ReportRepository::~ReportRepository() {
314 E : }
315 :
316 : void ReportRepository::StoreReport(
317 : const base::FilePath& minidump_path,
318 E : const std::map<base::string16, base::string16>& crash_keys) {
319 E : ScopedReportFile minidump_file(minidump_path);
320 :
321 : base::FilePath destination_directory(
322 E : repository_path_.Append(kIncomingReportsSubdir));
323 E : if (!base::CreateDirectory(destination_directory)) {
324 i : LOG(ERROR) << "Failed to create target directory "
325 : << destination_directory.value();
326 i : return;
327 : }
328 :
329 : // Choose the location and extension where the minidump will be stored.
330 : base::FilePath minidump_target_path = destination_directory.Append(
331 E : minidump_path.BaseName().ReplaceExtension(kDumpFileExtension));
332 : base::FilePath crash_keys_path =
333 E : GetCrashKeysFileForDumpFile(minidump_target_path);
334 :
335 E : if (!WriteCrashKeysToFile(crash_keys_path, crash_keys))
336 i : return;
337 E : ScopedReportFile crash_keys_file(crash_keys_path);
338 :
339 E : if (!minidump_file.Move(minidump_target_path))
340 i : return;
341 :
342 E : base::Time now = time_source_.Run();
343 E : if (!minidump_file.UpdateTimestamp(now))
344 i : return;
345 E : if (!crash_keys_file.UpdateTimestamp(now))
346 i : return;
347 :
348 : // Prevent the files from being deleted.
349 E : minidump_file.Take();
350 E : crash_keys_file.Take();
351 E : }
352 :
353 E : bool ReportRepository::UploadPendingReport() {
354 E : base::Time now = time_source_.Run();
355 :
356 : // Do a bit of opportunistic cleanup.
357 E : CleanOrphanedCrashKeysFiles(repository_path_, now);
358 :
359 : std::pair<base::FilePath, base::FilePath> entry =
360 E : GetPendingReport(repository_path_, now, retry_interval_);
361 E : ScopedReportFile minidump_file(entry.first);
362 E : base::FilePath failure_destination = entry.second;
363 :
364 E : if (minidump_file.Get().empty())
365 E : return true; // Successful no-op.
366 :
367 : ScopedReportFile crash_keys_file(
368 E : GetCrashKeysFileForDumpFile(minidump_file.Get()));
369 :
370 : // Renew the file timestamps before attempting upload. If we are unable to do
371 : // this, make no upload attempt (since that would potentially lead to a hot
372 : // loop of upload attempts).
373 E : if (!minidump_file.UpdateTimestamp(now))
374 i : return false;
375 E : if (!crash_keys_file.UpdateTimestamp(now))
376 i : return false;
377 :
378 : // Attempt the upload.
379 E : std::map<base::string16, base::string16> crash_keys;
380 E : if (ReadCrashKeysFromFile(crash_keys_file.Get(), &crash_keys)) {
381 E : if (uploader_.Run(minidump_file.Get(), crash_keys))
382 E : return true;
383 : }
384 :
385 : // We failed.
386 E : if (!failure_destination.empty()) {
387 : HandleNonpermanentFailure(&minidump_file, &crash_keys_file,
388 E : failure_destination);
389 E : } else {
390 : HandlePermanentFailure(minidump_file.Take(), crash_keys_file.Take(),
391 E : permanent_failure_handler_);
392 : }
393 E : return false;
394 E : }
395 :
396 E : bool ReportRepository::HasPendingReports() {
397 : return !GetPendingReport(repository_path_, time_source_.Run(),
398 E : retry_interval_).first.empty();
399 E : }
400 :
401 : } // namespace kasko
|