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/logging.h"
55 : #include "base/files/file_enumerator.h"
56 : #include "base/files/file_util.h"
57 : #include "syzygy/kasko/crash_keys_serialization.h"
58 :
59 m : namespace kasko {
60 :
61 m : namespace {
62 :
63 : // The extension used when serializing crash keys.
64 m : const base::char16 kCrashKeysFileExtension[] = L".kys";
65 : // The extension used to identify minidump files.
66 m : const base::char16 kDumpFileExtension[] = L".dmp";
67 : // The subdirectory where new reports (minidumps and crash keys) are initially
68 : // stored.
69 m : const base::char16 kIncomingReportsSubdir[] = L"Incoming";
70 : // The subdirectory where reports that have failed once are stored.
71 m : const base::char16 kFailedOnceSubdir[] = L"Retry";
72 : // The subdirectory where reports that have failed twice are stored.
73 m : 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 m : bool LoggedDeleteFile(const base::FilePath& path) {
79 m : bool result = base::DeleteFile(path, false);
80 m : LOG_IF(ERROR, !result) << "Failed to delete " << path.value();
81 m : return result;
82 m : }
83 :
84 : // Takes ownership of a FilePath. The owned path will be deleted when the
85 : // ScopedReportFile is destroyed.
86 m : class ScopedReportFile {
87 m : public:
88 m : explicit ScopedReportFile(const base::FilePath& path) : path_(path) {}
89 :
90 m : ~ScopedReportFile() {
91 m : if (!path_.empty())
92 m : LoggedDeleteFile(path_);
93 m : }
94 :
95 : // Provides access to the owned value.
96 : // @returns the owned path.
97 m : base::FilePath Get() const { return path_; }
98 :
99 : // Releases ownership of the owned path.
100 : // @returns the owned path.
101 m : base::FilePath Take() {
102 m : base::FilePath temp = path_;
103 m : path_ = base::FilePath();
104 m : return temp;
105 m : }
106 :
107 : // Moves the file pointed to by the owned path, and updates the owned path
108 : // to the new path.
109 : // @param new_path The full destination path.
110 : // @returns true if the operation succeeds.
111 m : bool Move(const base::FilePath& new_path) {
112 m : bool result = base::Move(path_, new_path);
113 m : LOG_IF(ERROR, !result) << "Failed to move " << path_.value() << " to "
114 m : << new_path.value();
115 m : if (result)
116 m : path_ = new_path;
117 m : return result;
118 m : }
119 :
120 : // Sets the last-modified timestamp of the file pointed to by the owned path.
121 : // @param value The desired timestamp.
122 : // @returns true if the operation succeeds.
123 m : bool UpdateTimestamp(const base::Time& value) {
124 m : bool result = false;
125 m : if (!path_.empty()) {
126 m : result = base::TouchFile(path_, value, value);
127 m : LOG_IF(ERROR, !result) << "Failed to update timestamp for "
128 m : << path_.value();
129 m : }
130 m : return result;
131 m : }
132 :
133 m : private:
134 m : base::FilePath path_;
135 :
136 m : DISALLOW_COPY_AND_ASSIGN(ScopedReportFile);
137 m : };
138 :
139 : // Returns the crash keys file path corresponding to the supplied minidump file
140 : // path.
141 : // @param minidump_path The path to a minidump file.
142 : // @returns The path where the corresponding crash keys file should be stored.
143 m : base::FilePath GetCrashKeysFileForDumpFile(
144 m : const base::FilePath& minidump_path) {
145 m : return minidump_path.ReplaceExtension(kCrashKeysFileExtension);
146 m : }
147 :
148 : // Returns the minidump file path corresponding to the supplied crash keys file
149 : // path.
150 : // @param crash_keys_path The path to a crash keys file.
151 : // @returns The path where the corresponding minidump file should be stored.
152 m : base::FilePath GetDumpFileForCrashKeysFile(
153 m : const base::FilePath& crash_keys_path) {
154 m : return crash_keys_path.ReplaceExtension(kDumpFileExtension);
155 m : }
156 :
157 : // Returns a minidump that is eligible for upload from the given directory, if
158 : // any are.
159 : // @param directory The directory to scan.
160 : // @param maximum_timestamp_for_retries The cutoff for the most most recent
161 : // upload attempt of eligible minidumps. If null, there is no cutoff.
162 : // @returns The path to a minidump that is eligible for upload, if any.
163 m : base::FilePath GetPendingReportFromDirectory(
164 m : const base::FilePath& directory,
165 m : const base::Time& maximum_timestamp_for_retries) {
166 m : base::FileEnumerator file_enumerator(
167 m : directory, false, base::FileEnumerator::FILES,
168 m : base::string16(L"*") + kDumpFileExtension);
169 : // Visit all files in this directory until we find an eligible one.
170 m : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
171 m : candidate = file_enumerator.Next()) {
172 : // Skip dumps with missing crash keys.
173 m : if (!base::PathExists(GetCrashKeysFileForDumpFile(candidate))) {
174 m : LOG(ERROR) << "Deleting a minidump file with missing crash keys: "
175 m : << candidate.value();
176 m : LoggedDeleteFile(candidate);
177 m : continue;
178 m : }
179 m : if (maximum_timestamp_for_retries.is_null())
180 m : return candidate;
181 :
182 : // Check if this file is eligible for retry.
183 m : base::FileEnumerator::FileInfo file_info = file_enumerator.GetInfo();
184 m : if (file_info.GetLastModifiedTime() <= maximum_timestamp_for_retries)
185 m : return candidate;
186 m : }
187 m : return base::FilePath();
188 m : }
189 :
190 m : void CleanOrphanedCrashKeysFiles(
191 m : const base::FilePath& repository_path,
192 m : const base::Time& now) {
193 m : base::Time one_day_ago(now - base::TimeDelta::FromDays(1));
194 m : const base::char16* subdirs[] = {
195 m : kIncomingReportsSubdir, kFailedOnceSubdir, kFailedTwiceSubdir};
196 :
197 m : for (size_t i = 0; i < arraysize(subdirs); ++i) {
198 m : base::FileEnumerator file_enumerator(
199 m : repository_path.Append(subdirs[i]), false, base::FileEnumerator::FILES,
200 m : base::string16(L"*") + kCrashKeysFileExtension);
201 m : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
202 m : candidate = file_enumerator.Next()) {
203 m : if (base::PathExists(GetDumpFileForCrashKeysFile(candidate)))
204 m : continue;
205 :
206 : // We write crash keys files before moving dump files, so there is a brief
207 : // period where an orphan might be expected. Only delete orphans that are
208 : // more than a day old.
209 m : if (file_enumerator.GetInfo().GetLastModifiedTime() >= one_day_ago)
210 m : continue;
211 :
212 m : LOG(ERROR) << "Deleting a crash keys file with missing minidump: "
213 m : << candidate.value();
214 m : LoggedDeleteFile(candidate);
215 m : }
216 m : }
217 m : }
218 :
219 : // Returns a minidump that is eligible for upload, if any are.
220 : // @param repository_path The directory where this repository stores reports.
221 : // @param now The current time.
222 : // @param retry_interval The minimum interval between upload attempts for a
223 : // given report.
224 : // @returns A pair of mindump path (empty if none) and failure destination
225 : // (empty if the next failure is permanent).
226 m : std::pair<base::FilePath, base::FilePath> GetPendingReport(
227 m : const base::FilePath& repository_path,
228 m : const base::Time& now,
229 m : const base::TimeDelta& retry_interval) {
230 m : struct {
231 m : const base::char16* subdir;
232 m : const base::char16* failure_subdir;
233 m : base::Time retry_cutoff;
234 m : } directories[] = {
235 m : {kIncomingReportsSubdir, kFailedOnceSubdir, base::Time()},
236 m : {kFailedOnceSubdir, kFailedTwiceSubdir, now - retry_interval},
237 m : {kFailedTwiceSubdir, nullptr, now - retry_interval}};
238 :
239 m : for (size_t i = 0; i < arraysize(directories); ++i) {
240 m : base::FilePath result = GetPendingReportFromDirectory(
241 m : repository_path.Append(directories[i].subdir),
242 m : directories[i].retry_cutoff);
243 m : if (!result.empty()) {
244 m : if (!directories[i].failure_subdir)
245 m : return std::make_pair(result, base::FilePath());
246 m : return std::make_pair(
247 m : result, repository_path.Append(directories[i].failure_subdir));
248 m : }
249 m : }
250 m : return std::pair<base::FilePath, base::FilePath>();
251 m : }
252 :
253 : // Handles a non-permanent failure by moving the report files to a new queue.
254 : // @param minidump_file The minidump file. This method calls Take() on success.
255 : // @param crash_keys_file The crash keys file. This method calls Take() on
256 : // success.
257 : // @param destination_directory The directory where the files should be moved
258 : // to.
259 m : void HandleNonpermanentFailure(ScopedReportFile* minidump_file,
260 m : ScopedReportFile* crash_keys_file,
261 m : const base::FilePath& destination_directory) {
262 m : bool result = base::CreateDirectory(destination_directory);
263 m : LOG_IF(ERROR, !result) << "Failed to create destination directory "
264 m : << destination_directory.value();
265 m : if (result) {
266 m : if (minidump_file->Move(
267 m : destination_directory.Append(minidump_file->Get().BaseName()))) {
268 m : if (crash_keys_file->Move(destination_directory.Append(
269 m : crash_keys_file->Get().BaseName()))) {
270 m : minidump_file->Take();
271 m : crash_keys_file->Take();
272 m : }
273 m : }
274 m : }
275 m : }
276 :
277 : // Handles a permanent failure by invoking the PermanentFailureHandler. Ensures
278 : // that the report files are removed from the repository.
279 : // @param minidump_path The path to the minidump file.
280 : // @param crash_keys_path The path to the crash keys file.
281 : // @param permanent_failure_handler The PermanentFailureHandler to invoke.
282 m : void HandlePermanentFailure(const base::FilePath& minidump_path,
283 m : const base::FilePath& crash_keys_path,
284 m : const ReportRepository::PermanentFailureHandler&
285 m : permanent_failure_handler) {
286 m : permanent_failure_handler.Run(minidump_path, crash_keys_path);
287 :
288 : // In case the handler didn't delete the files, we will.
289 m : if (base::PathExists(minidump_path))
290 m : LoggedDeleteFile(minidump_path);
291 m : if (base::PathExists(crash_keys_path))
292 m : LoggedDeleteFile(crash_keys_path);
293 m : }
294 :
295 m : } // namespace
296 :
297 m : ReportRepository::ReportRepository(
298 m : const base::FilePath& repository_path,
299 m : const base::TimeDelta& retry_interval,
300 m : const TimeSource& time_source,
301 m : const Uploader& uploader,
302 m : const PermanentFailureHandler& permanent_failure_handler)
303 m : : repository_path_(repository_path),
304 m : retry_interval_(retry_interval),
305 m : time_source_(time_source),
306 m : uploader_(uploader),
307 m : permanent_failure_handler_(permanent_failure_handler) {
308 m : }
309 :
310 m : ReportRepository::~ReportRepository() {
311 m : }
312 :
313 m : void ReportRepository::StoreReport(
314 m : const base::FilePath& minidump_path,
315 m : const std::map<base::string16, base::string16>& crash_keys) {
316 m : ScopedReportFile minidump_file(minidump_path);
317 :
318 m : base::FilePath destination_directory(
319 m : repository_path_.Append(kIncomingReportsSubdir));
320 m : bool result = base::CreateDirectory(destination_directory);
321 m : LOG_IF(ERROR, !result) << "Failed to create destination directory "
322 m : << destination_directory.value();
323 m : if (result) {
324 : // Choose the location and extension where the minidump will be stored.
325 m : base::FilePath minidump_target_path = destination_directory.Append(
326 m : minidump_path.BaseName().ReplaceExtension(kDumpFileExtension));
327 m : base::FilePath crash_keys_path =
328 m : GetCrashKeysFileForDumpFile(minidump_target_path);
329 :
330 m : if (WriteCrashKeysToFile(crash_keys_path, crash_keys)) {
331 m : ScopedReportFile crash_keys_file(crash_keys_path);
332 :
333 m : if (minidump_file.Move(minidump_target_path)) {
334 m : base::Time now = time_source_.Run();
335 m : if (minidump_file.UpdateTimestamp(now)) {
336 m : if (crash_keys_file.UpdateTimestamp(now)) {
337 : // Prevent the files from being deleted.
338 m : minidump_file.Take();
339 m : crash_keys_file.Take();
340 m : }
341 m : }
342 m : }
343 m : }
344 m : }
345 m : }
346 :
347 m : bool ReportRepository::UploadPendingReport() {
348 m : base::Time now = time_source_.Run();
349 :
350 : // Do a bit of opportunistic cleanup.
351 m : CleanOrphanedCrashKeysFiles(repository_path_, now);
352 :
353 m : std::pair<base::FilePath, base::FilePath> entry =
354 m : GetPendingReport(repository_path_, now, retry_interval_);
355 m : ScopedReportFile minidump_file(entry.first);
356 m : base::FilePath failure_destination = entry.second;
357 :
358 m : if (minidump_file.Get().empty())
359 m : return true; // Successful no-op.
360 :
361 m : ScopedReportFile crash_keys_file(
362 m : GetCrashKeysFileForDumpFile(minidump_file.Get()));
363 :
364 : // Renew the file timestamps before attempting upload. If we are unable to do
365 : // this, make no upload attempt (since that would potentially lead to a hot
366 : // loop of upload attempts).
367 m : if (minidump_file.UpdateTimestamp(now)) {
368 m : if (crash_keys_file.UpdateTimestamp(now)) {
369 : // Attempt the upload.
370 m : std::map<base::string16, base::string16> crash_keys;
371 m : if (ReadCrashKeysFromFile(crash_keys_file.Get(), &crash_keys)) {
372 m : if (uploader_.Run(minidump_file.Get(), crash_keys))
373 m : return true;
374 m : }
375 :
376 : // We failed.
377 m : if (!failure_destination.empty()) {
378 m : HandleNonpermanentFailure(&minidump_file, &crash_keys_file,
379 m : failure_destination);
380 m : } else {
381 m : HandlePermanentFailure(minidump_file.Take(), crash_keys_file.Take(),
382 m : permanent_failure_handler_);
383 m : }
384 m : }
385 m : }
386 :
387 m : return false;
388 m : }
389 :
390 m : bool ReportRepository::HasPendingReports() {
391 m : return !GetPendingReport(repository_path_, time_source_.Run(),
392 m : retry_interval_).first.empty();
393 m : }
394 :
395 m : } // namespace kasko
|