Index: client/crash_report_database_mac.mm |
diff --git a/client/crash_report_database_mac.mm b/client/crash_report_database_mac.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..fe3e13a360a465531ac08d0182ff6532d0da32ec |
--- /dev/null |
+++ b/client/crash_report_database_mac.mm |
@@ -0,0 +1,541 @@ |
+// Copyright 2015 The Crashpad Authors. All rights reserved. |
+// |
+// Licensed under the Apache License, Version 2.0 (the "License"); |
+// you may not use this file except in compliance with the License. |
+// You may obtain a copy of the License at |
+// |
+// http://www.apache.org/licenses/LICENSE-2.0 |
+// |
+// Unless required by applicable law or agreed to in writing, software |
+// distributed under the License is distributed on an "AS IS" BASIS, |
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+// See the License for the specific language governing permissions and |
+// limitations under the License. |
+ |
+#include "client/crash_report_database.h" |
+ |
+#include <errno.h> |
+#include <fcntl.h> |
+#import <Foundation/Foundation.h> |
+#include <stdio.h> |
+#include <sys/stat.h> |
+#include <sys/types.h> |
+#include <unistd.h> |
+#include <uuid/uuid.h> |
+ |
+#include "base/logging.h" |
+#include "base/posix/eintr_wrapper.h" |
+#include "base/scoped_generic.h" |
+#include "base/strings/string_piece.h" |
+#include "base/strings/stringprintf.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "util/file/file_io.h" |
+#include "util/mac/xattr.h" |
+ |
+namespace crashpad { |
+ |
+namespace { |
+ |
+const char kDatabaseDirectoryName[] = "Crashpad"; |
+ |
+const char kWriteDirectory[] = "new"; |
+const char kUploadPendingDirectory[] = "pending"; |
+const char kCompletedDirectory[] = "completed"; |
+ |
+const char* const kReportDirectories[] = { |
+ kWriteDirectory, |
+ kUploadPendingDirectory, |
+ kCompletedDirectory, |
+}; |
+ |
+const char kCrashReportFileExtension[] = "dmp"; |
+ |
+const char kXattrUUID[] = "uuid"; |
+const char kXattrCollectorID[] = "id"; |
+const char kXattrCreationTime[] = "creation_time"; |
+const char kXattrIsUploaded[] = "uploaded"; |
+const char kXattrLastUploadTime[] = "last_upload_time"; |
+const char kXattrUploadAttemptCount[] = "upload_count"; |
+ |
+const char kXattrDatabaseInitialized[] = "initialized"; |
+ |
+// Ensures that the node at |path| is a directory, and creates it if it does |
+// not exist. If the |path| points to a file, rather than a directory, or the |
+// directory could not be created, returns false. Otherwise, returns true, |
+// indicating that |path| already was or now is a directory. |
+bool CreateOrEnsureDirectoryExists(const base::FilePath& path) { |
+ if (mkdir(path.value().c_str(), 0755) == 0) { |
+ return true; |
+ } else if (errno == EEXIST) { |
+ struct stat st; |
+ if (stat(path.value().c_str(), &st) != 0) { |
+ PLOG(ERROR) << "stat"; |
+ return false; |
+ } |
+ if (S_ISDIR(st.st_mode)) { |
+ return true; |
+ } else { |
+ LOG(ERROR) << "not a directory"; |
+ return false; |
+ } |
+ } else { |
+ PLOG(ERROR) << "mkdir"; |
+ return false; |
+ } |
+} |
+ |
+//! \brief A CrashReportDatabase that uses HFS+ extended attributes to store |
+//! report metadata. |
+//! |
+//! The database maintains three directories of reports: `"new"` to hold crash |
+//! reports that are in the process of being written, `"completed"` to hold |
+//! reports that have been written and are awaing upload, and `"uploaded"` to |
+//! hold reports successfully uploaded to a collection server. If the user has |
+//! opted out of report collection, reports will still be written and moved |
+//! to the completed directory, but they just will not be uploaded. |
+//! |
+//! The database stores its metadata in extended filesystem attributes. To |
+//! ensure safe access, the report file is locked using `O_EXLOCK` during all |
+//! extended attribute operations. The lock should be obtained using |
+//! ObtainReportLock(). |
+class CrashReportDatabaseMac : public CrashReportDatabase { |
+ public: |
+ explicit CrashReportDatabaseMac(const base::FilePath& path); |
+ virtual ~CrashReportDatabaseMac(); |
+ |
+ bool Initialize(); |
+ |
+ // CrashReportDatabase: |
+ OperationStatus PrepareNewCrashReport(NewReport** report) override; |
+ OperationStatus FinishedWritingCrashReport(NewReport* report, |
+ UUID* uuid) override; |
+ OperationStatus LookUpCrashReport(const UUID& uuid, |
+ Report* report) override; |
+ OperationStatus GetPendingReports( |
+ std::vector<const Report>* reports) override; |
+ OperationStatus GetCompletedReports( |
+ std::vector<const Report>* reports) override; |
+ OperationStatus GetReportForUploading(const UUID& uuid, |
+ const Report** report) override; |
+ OperationStatus RecordUploadAttempt(const Report* report, |
+ bool successful, |
+ const std::string& id) override; |
+ OperationStatus SkipReportUpload(const UUID& uuid) override; |
+ |
+ private: |
+ //! \brief A private extension of the Report class that maintains bookkeeping |
+ //! information of the database. |
+ struct UploadReport : public Report { |
+ //! \brief Stores the flock of the file for the duration of |
+ //! GetReportForUploading() and RecordUploadAttempt(). |
+ int lock_fd; |
+ }; |
+ |
+ //! \brief Locates a crash report in the database by UUID. |
+ //! |
+ //! \param[in] uuid The UUID of the crash report to locate. |
+ //! |
+ //! \return The full path to the report file, or an empty path if it cannot be |
+ //! found. |
+ base::FilePath LocateCrashReport(const UUID& uuid); |
+ |
+ //! \brief Obtains an exclusive advisory lock on a file. |
+ //! |
+ //! The flock is used to prevent cross-process concurrent metadata reads or |
+ //! writes. While xattrs do not observe the lock, if the lock-then-mutate |
+ //! protocol is observed by all clients of the database, it still enforces |
+ //! synchronization. |
+ //! |
+ //! This does not block, and so callers must ensure that the lock is valid |
+ //! after calling. |
+ //! |
+ //! \param[in] path The path of the file to lcok. |
+ //! |
+ //! \return A scoped lock object. If the result is not valid, an error is |
+ //! logged. |
+ static base::ScopedFD ObtainReportLock(const base::FilePath& path); |
+ |
+ //! \brief Reads all the database xattrs from a file into a Report. The file |
+ //! must be locked with ObtainReportLock. |
+ //! |
+ //! \param[in] path The path of the report. |
+ //! \param[out] report The object into which data will be read. |
+ //! |
+ //! \return `true` if all the metadata was read successfully, `false` |
+ //! otherwise. |
+ static bool ReadReportMetadataLocked(const base::FilePath& path, |
+ Report* report); |
+ |
+ //! \brief Reads the metadata from all the reports in a database subdirectory. |
+ //! Invalid reports are skipped. |
+ //! |
+ //! \param[in] path The database subdirectory path. |
+ //! \param[out] reports An empty vector of reports, which will be filled. |
+ //! |
+ //! \return The operation status code. |
+ static OperationStatus ReportsInDirectory(const base::FilePath& path, |
+ std::vector<const Report>* reports); |
+ |
+ |
+ //! \brief Creates a database xattr name from the short constant name. |
+ //! |
+ //! \param[in] name The short name of the extended attribute. |
+ //! |
+ //! \return The long name of the extended attribute. |
+ static std::string XattrName(const base::StringPiece& name); |
+ |
+ base::FilePath base_dir_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseMac); |
+}; |
+ |
+CrashReportDatabaseMac::CrashReportDatabaseMac(const base::FilePath& path) |
+ : CrashReportDatabase(), base_dir_(path) { |
+} |
+ |
+CrashReportDatabaseMac::~CrashReportDatabaseMac() {} |
+ |
+bool CrashReportDatabaseMac::Initialize() { |
+ // Check if the database already exists. |
+ if (!CreateOrEnsureDirectoryExists(base_dir_)) |
+ return false; |
+ |
+ // Create the three processing directories for the database. |
+ for (size_t i = 0; i < arraysize(kReportDirectories); ++i) { |
+ if (!CreateOrEnsureDirectoryExists(base_dir_.Append(kReportDirectories[i]))) |
+ return false; |
+ } |
+ |
+ // Write an xattr as the last step, to ensure the filesystem has support for |
+ // them. This attribute will never be read. |
+ return WriteXattrBool(base_dir_, XattrName(kXattrDatabaseInitialized), true); |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::PrepareNewCrashReport(NewReport** out_report) { |
+ uuid_t uuid_gen; |
+ uuid_generate(uuid_gen); |
+ UUID uuid(uuid_gen); |
+ |
+ scoped_ptr<NewReport> report(new NewReport()); |
+ |
+ report->path = |
+ base_dir_.Append(kWriteDirectory) |
+ .Append(uuid.ToString() + "." + kCrashReportFileExtension); |
+ |
+ report->handle = HANDLE_EINTR(open(report->path.value().c_str(), |
+ O_CREAT | O_WRONLY | O_EXCL | O_EXLOCK, |
+ 0600)); |
+ if (report->handle < 0) { |
+ PLOG(ERROR) << "open " << report->path.value(); |
+ return kFileSystemError; |
+ } |
+ |
+ // TODO(rsesek): Potentially use an fsetxattr() here instead. |
+ if (!WriteXattr(report->path, XattrName(kXattrUUID), uuid.ToString())) { |
+ PLOG_IF(ERROR, IGNORE_EINTR(close(report->handle)) != 0) << "close"; |
+ return kDatabaseError; |
+ } |
+ |
+ *out_report = report.release(); |
+ |
+ return kNoError; |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::FinishedWritingCrashReport(NewReport* report, |
+ UUID* uuid) { |
+ // Takes ownership of the |handle| and the O_EXLOCK. |
+ base::ScopedFD lock(report->handle); |
+ |
+ // Take ownership of the report. |
+ scoped_ptr<NewReport> scoped_report(report); |
+ |
+ // Get the report's UUID to return. |
+ std::string uuid_string; |
+ if (ReadXattr(report->path, XattrName(kXattrUUID), |
+ &uuid_string) != XattrStatus::kOK || |
+ !uuid->InitializeFromString(uuid_string)) { |
+ LOG(ERROR) << "Failed to read UUID for crash report " |
+ << report->path.value(); |
+ return kDatabaseError; |
+ } |
+ |
+ // Record the creation time of this report. |
+ if (!WriteXattrTimeT(report->path, XattrName(kXattrCreationTime), |
+ time(nullptr))) { |
+ return kDatabaseError; |
+ } |
+ |
+ // Move the report to its new location for uploading. |
+ base::FilePath new_path = |
+ base_dir_.Append(kUploadPendingDirectory).Append(report->path.BaseName()); |
+ if (rename(report->path.value().c_str(), new_path.value().c_str()) != 0) { |
+ PLOG(ERROR) << "rename " << report->path.value() << " to " |
+ << new_path.value(); |
+ return kFileSystemError; |
+ } |
+ |
+ return kNoError; |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::LookUpCrashReport(const UUID& uuid, |
+ CrashReportDatabase::Report* report) { |
+ base::FilePath path = LocateCrashReport(uuid); |
+ if (path.empty()) |
+ return kReportNotFound; |
+ |
+ base::ScopedFD lock(ObtainReportLock(path)); |
+ if (!lock.is_valid()) |
+ return kBusyError; |
+ |
+ *report = Report(); |
+ report->file_path = path; |
+ if (!ReadReportMetadataLocked(path, report)) |
+ return kDatabaseError; |
+ |
+ return kNoError; |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::GetPendingReports( |
+ std::vector<const CrashReportDatabase::Report>* reports) { |
+ return ReportsInDirectory(base_dir_.Append(kUploadPendingDirectory), reports); |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::GetCompletedReports( |
+ std::vector<const CrashReportDatabase::Report>* reports) { |
+ return ReportsInDirectory(base_dir_.Append(kCompletedDirectory), reports); |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::GetReportForUploading(const UUID& uuid, |
+ const Report** report) { |
+ base::FilePath report_path = LocateCrashReport(uuid); |
+ if (report_path.empty()) |
+ return kReportNotFound; |
+ |
+ scoped_ptr<UploadReport> upload_report(new UploadReport()); |
+ upload_report->file_path = report_path; |
+ |
+ base::ScopedFD lock(ObtainReportLock(report_path)); |
+ if (!lock.is_valid()) |
+ return kBusyError; |
+ |
+ if (!ReadReportMetadataLocked(report_path, upload_report.get())) |
+ return kDatabaseError; |
+ |
+ upload_report->lock_fd = lock.release(); |
+ *report = upload_report.release(); |
+ return kNoError; |
+} |
+ |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::RecordUploadAttempt(const Report* report, |
+ bool successful, |
+ const std::string& id) { |
+ DCHECK(report); |
+ DCHECK(successful || id.empty()); |
+ |
+ base::FilePath report_path = LocateCrashReport(report->uuid); |
+ if (report_path.empty()) |
+ return kReportNotFound; |
+ |
+ scoped_ptr<const UploadReport> upload_report( |
+ static_cast<const UploadReport*>(report)); |
+ |
+ base::ScopedFD lock(upload_report->lock_fd); |
+ if (!lock.is_valid()) |
+ return kBusyError; |
+ |
+ if (successful) { |
+ base::FilePath new_path = |
+ base_dir_.Append(kCompletedDirectory).Append(report_path.BaseName()); |
+ if (rename(report_path.value().c_str(), new_path.value().c_str()) != 0) { |
+ PLOG(ERROR) << "rename " << report_path.value() << " to " |
+ << new_path.value(); |
+ return kFileSystemError; |
+ } |
+ report_path = new_path; |
+ } |
+ |
+ if (!WriteXattrBool(report_path, XattrName(kXattrIsUploaded), successful)) { |
+ return kDatabaseError; |
+ } |
+ if (!WriteXattr(report_path, XattrName(kXattrCollectorID), id)) { |
+ return kDatabaseError; |
+ } |
+ if (!WriteXattrTimeT(report_path, |
+ XattrName(kXattrLastUploadTime), |
+ time(nullptr))) { |
+ return kDatabaseError; |
+ } |
+ |
+ int upload_attempts = 0; |
+ std::string name = XattrName(kXattrUploadAttemptCount); |
+ if (ReadXattrInt(report_path, name, &upload_attempts) == |
+ XattrStatus::kOtherError) { |
+ return kDatabaseError; |
+ } |
+ if (!WriteXattrInt(report_path, name, ++upload_attempts)) { |
+ return kDatabaseError; |
+ } |
+ |
+ return kNoError; |
+} |
+ |
+CrashReportDatabase::OperationStatus CrashReportDatabaseMac::SkipReportUpload( |
+ const UUID& uuid) { |
+ base::FilePath report_path = LocateCrashReport(uuid); |
+ if (report_path.empty()) |
+ return kReportNotFound; |
+ |
+ base::ScopedFD lock(ObtainReportLock(report_path)); |
+ if (!lock.is_valid()) |
+ return kBusyError; |
+ |
+ base::FilePath new_path = |
+ base_dir_.Append(kCompletedDirectory).Append(report_path.BaseName()); |
+ if (rename(report_path.value().c_str(), new_path.value().c_str()) != 0) { |
+ PLOG(ERROR) << "rename " << report_path.value() << " to " |
+ << new_path.value(); |
+ return kFileSystemError; |
+ } |
+ |
+ return kNoError; |
+} |
+ |
+base::FilePath CrashReportDatabaseMac::LocateCrashReport(const UUID& uuid) { |
+ const std::string target_uuid = uuid.ToString(); |
+ for (size_t i = 0; i < arraysize(kReportDirectories); ++i) { |
+ base::FilePath path = |
+ base_dir_.Append(kReportDirectories[i]) |
+ .Append(target_uuid + "." + kCrashReportFileExtension); |
+ |
+ // Test if the path exists. |
+ struct stat st; |
+ if (lstat(path.value().c_str(), &st)) { |
+ continue; |
+ } |
+ |
+ // Check that the UUID of the report matches. |
+ std::string uuid_string; |
+ if (ReadXattr(path, XattrName(kXattrUUID), |
+ &uuid_string) == XattrStatus::kOK && |
+ uuid_string == target_uuid) { |
+ return path; |
+ } |
+ } |
+ |
+ return base::FilePath(); |
+} |
+ |
+// static |
+base::ScopedFD CrashReportDatabaseMac::ObtainReportLock( |
+ const base::FilePath& path) { |
+ int fd = HANDLE_EINTR(open(path.value().c_str(), |
+ O_RDONLY | O_EXLOCK | O_CLOEXEC | O_NONBLOCK)); |
+ PLOG_IF(ERROR, fd < 0) << "open lock " << path.value(); |
+ return base::ScopedFD(fd); |
+} |
+ |
+// static |
+bool CrashReportDatabaseMac::ReadReportMetadataLocked( |
+ const base::FilePath& path, Report* report) { |
+ std::string uuid_string; |
+ if (ReadXattr(path, XattrName(kXattrUUID), |
+ &uuid_string) != XattrStatus::kOK || |
+ !report->uuid.InitializeFromString(uuid_string)) { |
+ return false; |
+ } |
+ |
+ if (ReadXattrTimeT(path, XattrName(kXattrCreationTime), |
+ &report->creation_time) != XattrStatus::kOK) { |
+ return false; |
+ } |
+ |
+ report->id = std::string(); |
+ if (ReadXattr(path, XattrName(kXattrCollectorID), |
+ &report->id) == XattrStatus::kOtherError) { |
+ return false; |
+ } |
+ |
+ report->uploaded = false; |
+ if (ReadXattrBool(path, XattrName(kXattrIsUploaded), |
+ &report->uploaded) == XattrStatus::kOtherError) { |
+ return false; |
+ } |
+ |
+ report->last_upload_attempt_time = 0; |
+ if (ReadXattrTimeT(path, XattrName(kXattrLastUploadTime), |
+ &report->last_upload_attempt_time) == |
+ XattrStatus::kOtherError) { |
+ return false; |
+ } |
+ |
+ report->upload_attempts = 0; |
+ if (ReadXattrInt(path, XattrName(kXattrUploadAttemptCount), |
+ &report->upload_attempts) == XattrStatus::kOtherError) { |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+// static |
+CrashReportDatabase::OperationStatus |
+CrashReportDatabaseMac::ReportsInDirectory( |
+ const base::FilePath& path, |
+ std::vector<const CrashReportDatabase::Report>* reports) { |
+ DCHECK(reports->empty()); |
+ |
+ NSError* error = nil; |
+ NSArray* paths = [[NSFileManager defaultManager] |
+ contentsOfDirectoryAtPath:base::SysUTF8ToNSString(path.value()) |
+ error:&error]; |
+ if (error) { |
+ LOG(ERROR) << "Failed to enumerate reports in directory " << path.value() |
+ << ": " << [[error description] UTF8String]; |
+ return kFileSystemError; |
+ } |
+ |
+ reports->reserve([paths count]); |
+ for (NSString* entry in paths) { |
+ base::FilePath report_path = path.Append([entry fileSystemRepresentation]); |
+ base::ScopedFD lock(ObtainReportLock(report_path)); |
+ if (!lock.is_valid()) |
+ continue; |
+ |
+ Report report; |
+ if (!ReadReportMetadataLocked(report_path, &report)) { |
+ LOG(WARNING) << "Failed to read report metadata for " |
+ << report_path.value(); |
+ continue; |
+ } |
+ reports->push_back(report); |
+ } |
+ |
+ return kNoError; |
+} |
+ |
+// static |
+std::string CrashReportDatabaseMac::XattrName(const base::StringPiece& name) { |
+ return base::StringPrintf("com.googlecode.crashpad.%s", name.data()); |
+} |
+ |
+} // namespace |
+ |
+// static |
+scoped_ptr<CrashReportDatabase> CrashReportDatabase::Initialize( |
+ const base::FilePath& path) { |
+ scoped_ptr<CrashReportDatabaseMac> database_mac( |
+ new CrashReportDatabaseMac(path.Append(kDatabaseDirectoryName))); |
+ if (!database_mac->Initialize()) |
+ database_mac.reset(); |
+ |
+ return scoped_ptr<CrashReportDatabase>(database_mac.release()); |
+} |
+ |
+} // namespace crashpad |