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 : 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 : bool result = base::DeleteFile(path, false);
80 E : LOG_IF(ERROR, !result) << "Failed to delete " << path.value();
81 E : return result;
82 E : }
83 :
84 : // Takes ownership of a FilePath. The owned path will be deleted when the
85 : // ScopedReportFile is destroyed.
86 : class ScopedReportFile {
87 : public:
88 E : explicit ScopedReportFile(const base::FilePath& path) : path_(path) {}
89 :
90 E : ~ScopedReportFile() {
91 E : if (!path_.empty())
92 E : LoggedDeleteFile(path_);
93 E : }
94 :
95 : // Provides access to the owned value.
96 : // @returns the owned path.
97 E : base::FilePath Get() const { return path_; }
98 :
99 : // Releases ownership of the owned path.
100 : // @returns the owned path.
101 E : base::FilePath Take() {
102 E : base::FilePath temp = path_;
103 E : path_ = base::FilePath();
104 E : return temp;
105 E : }
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 E : bool Move(const base::FilePath& new_path) {
112 E : bool result = base::Move(path_, new_path);
113 E : LOG_IF(ERROR, !result) << "Failed to move " << path_.value() << " to "
114 : << new_path.value();
115 E : if (result)
116 E : path_ = new_path;
117 E : return result;
118 E : }
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 E : bool UpdateTimestamp(const base::Time& value) {
124 E : bool result = false;
125 E : if (!path_.empty()) {
126 E : result = base::TouchFile(path_, value, value);
127 E : LOG_IF(ERROR, !result) << "Failed to update timestamp for "
128 : << path_.value();
129 : }
130 E : return result;
131 E : }
132 :
133 : private:
134 : base::FilePath path_;
135 :
136 : DISALLOW_COPY_AND_ASSIGN(ScopedReportFile);
137 : };
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 : base::FilePath GetCrashKeysFileForDumpFile(
144 E : const base::FilePath& minidump_path) {
145 E : return minidump_path.ReplaceExtension(kCrashKeysFileExtension);
146 E : }
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 : base::FilePath GetDumpFileForCrashKeysFile(
153 E : const base::FilePath& crash_keys_path) {
154 E : return crash_keys_path.ReplaceExtension(kDumpFileExtension);
155 E : }
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 : base::FilePath GetPendingReportFromDirectory(
164 : const base::FilePath& directory,
165 E : const base::Time& maximum_timestamp_for_retries) {
166 : base::FileEnumerator file_enumerator(
167 : directory, false, base::FileEnumerator::FILES,
168 E : base::string16(L"*") + kDumpFileExtension);
169 : // Visit all files in this directory until we find an eligible one.
170 E : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
171 E : candidate = file_enumerator.Next()) {
172 : // Skip dumps with missing crash keys.
173 E : if (!base::PathExists(GetCrashKeysFileForDumpFile(candidate))) {
174 E : LOG(ERROR) << "Deleting a minidump file with missing crash keys: "
175 : << candidate.value();
176 E : LoggedDeleteFile(candidate);
177 E : continue;
178 : }
179 E : if (maximum_timestamp_for_retries.is_null())
180 E : return candidate;
181 :
182 : // Check if this file is eligible for retry.
183 E : base::FileEnumerator::FileInfo file_info = file_enumerator.GetInfo();
184 E : if (file_info.GetLastModifiedTime() <= maximum_timestamp_for_retries)
185 E : return candidate;
186 E : }
187 E : return base::FilePath();
188 E : }
189 :
190 : void CleanOrphanedCrashKeysFiles(
191 : const base::FilePath& repository_path,
192 E : const base::Time& now) {
193 E : base::Time one_day_ago(now - base::TimeDelta::FromDays(1));
194 : const base::char16* subdirs[] = {
195 E : kIncomingReportsSubdir, kFailedOnceSubdir, kFailedTwiceSubdir};
196 :
197 E : for (size_t i = 0; i < arraysize(subdirs); ++i) {
198 : base::FileEnumerator file_enumerator(
199 : repository_path.Append(subdirs[i]), false, base::FileEnumerator::FILES,
200 E : base::string16(L"*") + kCrashKeysFileExtension);
201 E : for (base::FilePath candidate = file_enumerator.Next(); !candidate.empty();
202 E : candidate = file_enumerator.Next()) {
203 E : if (base::PathExists(GetDumpFileForCrashKeysFile(candidate)))
204 E : 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 E : if (file_enumerator.GetInfo().GetLastModifiedTime() >= one_day_ago)
210 E : continue;
211 :
212 E : LOG(ERROR) << "Deleting a crash keys file with missing minidump: "
213 : << candidate.value();
214 E : LoggedDeleteFile(candidate);
215 E : }
216 E : }
217 E : }
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 : std::pair<base::FilePath, base::FilePath> GetPendingReport(
227 : const base::FilePath& repository_path,
228 : const base::Time& now,
229 E : const base::TimeDelta& retry_interval) {
230 : struct {
231 : const base::char16* subdir;
232 : const base::char16* failure_subdir;
233 : base::Time retry_cutoff;
234 : } directories[] = {
235 : {kIncomingReportsSubdir, kFailedOnceSubdir, base::Time()},
236 : {kFailedOnceSubdir, kFailedTwiceSubdir, now - retry_interval},
237 E : {kFailedTwiceSubdir, nullptr, now - retry_interval}};
238 :
239 E : for (size_t i = 0; i < arraysize(directories); ++i) {
240 : base::FilePath result = GetPendingReportFromDirectory(
241 : repository_path.Append(directories[i].subdir),
242 E : directories[i].retry_cutoff);
243 E : if (!result.empty()) {
244 E : if (!directories[i].failure_subdir)
245 E : return std::make_pair(result, base::FilePath());
246 : return std::make_pair(
247 E : result, repository_path.Append(directories[i].failure_subdir));
248 : }
249 E : }
250 E : return std::pair<base::FilePath, base::FilePath>();
251 E : }
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 : void HandleNonpermanentFailure(ScopedReportFile* minidump_file,
260 : ScopedReportFile* crash_keys_file,
261 E : const base::FilePath& destination_directory) {
262 E : bool result = base::CreateDirectory(destination_directory);
263 E : LOG_IF(ERROR, !result) << "Failed to create destination directory "
264 : << destination_directory.value();
265 E : if (result) {
266 : if (minidump_file->Move(
267 E : destination_directory.Append(minidump_file->Get().BaseName()))) {
268 : if (crash_keys_file->Move(destination_directory.Append(
269 E : crash_keys_file->Get().BaseName()))) {
270 E : minidump_file->Take();
271 E : crash_keys_file->Take();
272 : }
273 : }
274 : }
275 E : }
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 : void HandlePermanentFailure(const base::FilePath& minidump_path,
283 : const base::FilePath& crash_keys_path,
284 : const ReportRepository::PermanentFailureHandler&
285 E : permanent_failure_handler) {
286 E : permanent_failure_handler.Run(minidump_path, crash_keys_path);
287 :
288 : // In case the handler didn't delete the files, we will.
289 E : if (base::PathExists(minidump_path))
290 E : LoggedDeleteFile(minidump_path);
291 E : if (base::PathExists(crash_keys_path))
292 E : LoggedDeleteFile(crash_keys_path);
293 E : }
294 :
295 : } // namespace
296 :
297 : ReportRepository::ReportRepository(
298 : const base::FilePath& repository_path,
299 : const base::TimeDelta& retry_interval,
300 : const TimeSource& time_source,
301 : const Uploader& uploader,
302 : const PermanentFailureHandler& permanent_failure_handler)
303 : : repository_path_(repository_path),
304 : retry_interval_(retry_interval),
305 : time_source_(time_source),
306 : uploader_(uploader),
307 E : permanent_failure_handler_(permanent_failure_handler) {
308 E : }
309 :
310 E : ReportRepository::~ReportRepository() {
311 E : }
312 :
313 : void ReportRepository::StoreReport(
314 : const base::FilePath& minidump_path,
315 E : const std::map<base::string16, base::string16>& crash_keys) {
316 E : ScopedReportFile minidump_file(minidump_path);
317 :
318 : base::FilePath destination_directory(
319 E : repository_path_.Append(kIncomingReportsSubdir));
320 E : bool result = base::CreateDirectory(destination_directory);
321 E : LOG_IF(ERROR, !result) << "Failed to create destination directory "
322 : << destination_directory.value();
323 E : if (result) {
324 : // Choose the location and extension where the minidump will be stored.
325 : base::FilePath minidump_target_path = destination_directory.Append(
326 E : minidump_path.BaseName().ReplaceExtension(kDumpFileExtension));
327 : base::FilePath crash_keys_path =
328 E : GetCrashKeysFileForDumpFile(minidump_target_path);
329 :
330 E : if (WriteCrashKeysToFile(crash_keys_path, crash_keys)) {
331 E : ScopedReportFile crash_keys_file(crash_keys_path);
332 :
333 E : if (minidump_file.Move(minidump_target_path)) {
334 E : base::Time now = time_source_.Run();
335 E : if (minidump_file.UpdateTimestamp(now)) {
336 E : if (crash_keys_file.UpdateTimestamp(now)) {
337 : // Prevent the files from being deleted.
338 E : minidump_file.Take();
339 E : crash_keys_file.Take();
340 : }
341 : }
342 : }
343 E : }
344 E : }
345 E : }
346 :
347 E : bool ReportRepository::UploadPendingReport() {
348 E : base::Time now = time_source_.Run();
349 :
350 : // Do a bit of opportunistic cleanup.
351 E : CleanOrphanedCrashKeysFiles(repository_path_, now);
352 :
353 : std::pair<base::FilePath, base::FilePath> entry =
354 E : GetPendingReport(repository_path_, now, retry_interval_);
355 E : ScopedReportFile minidump_file(entry.first);
356 E : base::FilePath failure_destination = entry.second;
357 :
358 E : if (minidump_file.Get().empty())
359 E : return true; // Successful no-op.
360 :
361 : ScopedReportFile crash_keys_file(
362 E : 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 E : if (minidump_file.UpdateTimestamp(now)) {
368 E : if (crash_keys_file.UpdateTimestamp(now)) {
369 : // Attempt the upload.
370 E : std::map<base::string16, base::string16> crash_keys;
371 E : if (ReadCrashKeysFromFile(crash_keys_file.Get(), &crash_keys)) {
372 E : if (uploader_.Run(minidump_file.Get(), crash_keys))
373 E : return true;
374 : }
375 :
376 : // We failed.
377 E : if (!failure_destination.empty()) {
378 : HandleNonpermanentFailure(&minidump_file, &crash_keys_file,
379 E : failure_destination);
380 E : } else {
381 : HandlePermanentFailure(minidump_file.Take(), crash_keys_file.Take(),
382 E : permanent_failure_handler_);
383 : }
384 E : }
385 : }
386 :
387 E : return false;
388 E : }
389 :
390 E : bool ReportRepository::HasPendingReports() {
391 : return !GetPendingReport(repository_path_, time_source_.Run(),
392 E : retry_interval_).first.empty();
393 E : }
394 :
395 : } // namespace kasko
|