diff --git a/client/client.gyp b/client/client.gyp index 445f92b4..9361a49d 100644 --- a/client/client.gyp +++ b/client/client.gyp @@ -31,6 +31,9 @@ 'crashpad_client_mac.cc', 'crashpad_info.cc', 'crashpad_info.h', + 'crash_report_database_mac.mm', + 'crash_report_database.cc', + 'crash_report_database.h', 'simple_string_dictionary.cc', 'simple_string_dictionary.h', 'simulate_crash.h', @@ -54,6 +57,7 @@ ], 'sources': [ 'capture_context_mac_test.cc', + 'crash_report_database_test.cc', 'simple_string_dictionary_test.cc', 'simulate_crash_mac_test.cc', ], diff --git a/client/crash_report_database.cc b/client/crash_report_database.cc new file mode 100644 index 00000000..0a090b5c --- /dev/null +++ b/client/crash_report_database.cc @@ -0,0 +1,24 @@ +// 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" + +namespace crashpad { + +CrashReportDatabase::Report::Report() + : uuid(), file_path(), id(), creation_time(0), uploaded(false), + last_upload_attempt_time(0), upload_attempts(0) { +} + +} // namespace crashpad diff --git a/client/crash_report_database.h b/client/crash_report_database.h new file mode 100644 index 00000000..3db2f583 --- /dev/null +++ b/client/crash_report_database.h @@ -0,0 +1,255 @@ +// 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. + +#ifndef CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_ +#define CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_ + +#include + +#include +#include + +#include "base/basictypes.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "util/file/file_io.h" +#include "util/misc/uuid.h" + +namespace crashpad { + +//! \brief An interface for managing a collection of crash report files and +//! metadata associated with the crash reports. +//! +//! All Report objects that are returned by this class are logically const. +//! They are snapshots of the database at the time the query was run, and the +//! data returned is liable to change after the query is executed. +//! +//! The lifecycle of a crash report has three stages: +//! +//! 1. New: A crash report is created with PrepareNewCrashReport(), the +//! the client then writes the report, and then calls +//! FinishedWritingCrashReport() to make the report Pending. +//! 2. Pending: The report has been written but has not been locally +//! processed. +//! 3. Completed: The report has been locally processed, either by uploading +//! it to a collection server and calling RecordUploadAttempt(), or by +//! calling SkipReportUpload(). +class CrashReportDatabase { + public: + //! \brief A crash report record. + //! + //! This represents the metadata for a crash report, as well as the location + //! of the report itself. A CrashReportDatabase maintains at least this + //! information. + struct Report { + Report(); + + //! A unique identifier by which this report will always be known to the + //! database. + UUID uuid; + + //! The current location of the crash report on the client’s filesystem. + //! The location of a crash report may change over time, so the UUID should + //! be used as the canonical identifier. + base::FilePath file_path; + + //! An identifier issued to this crash report by a collection server. + std::string id; + + //! The time at which the report was generated. + time_t creation_time; + + //! Whether this crash report was successfully uploaded to a collection + //! server. + bool uploaded; + + //! The last timestamp at which an attempt was made to submit this crash + //! report to a collection server. If this is zero, then the report has + //! never been uploaded. If #uploaded is true, then this timestamp is the + //! time at which the report was uploaded, and no other attempts to upload + //! this report will be made. + time_t last_upload_attempt_time; + + //! The number of times an attempt was made to submit this report to + //! a collection server. If this is more than zero, then + //! #last_upload_attempt_time will be set to the timestamp of the most + //! recent attempt. + int upload_attempts; + }; + + //! \brief A crash report that is in the process of being written. + //! + //! An instance of this struct should be created via PrepareNewCrashReport() + //! and destroyed with FinishedWritingCrashReport(). + struct NewReport { + //! The file handle to which the report should be written. + FileHandle handle; + + //! The path to the crash report being written. + base::FilePath path; + }; + + //! \brief The result code for operations performed on a database. + enum OperationStatus { + //! \brief No error occurred. + kNoError = 0, + + //! \brief The report that was requested could not be located. + kReportNotFound, + + //! \brief An error occured while performing a file operation on a crash + //! report. + //! + //! A database is responsible for managing both the metadata about a report + //! and the actual crash report itself. This error is returned when an + //! error occurred when managing the report file. Additional information + //! will be logged. + kFileSystemError, + + //! \brief An error occured while recording metadata for a crash report. + //! + //! A database is responsible for managing both the metadata about a report + //! and the actual crash report itself. This error is returned when an + //! error occurred when managing the metadata about a crash report. + //! Additional information will be logged. + kDatabaseError, + + //! \brief The operation could not be completed because a concurrent + //! operation affecting the report is occurring. + kBusyError, + }; + + virtual ~CrashReportDatabase() {} + + //! \brief Initializes a database of crash reports. + //! + //! \param[in] path A path to a writable directory, where the database can + //! be created or opened. + //! + //! \return A database object on success, `nullptr` on failure with an error + //! logged. + static scoped_ptr Initialize(const base::FilePath& path); + + //! \brief Creates a record of a new crash report. + //! + //! Callers can then write the crash report using the file handle provided. + //! The caller does not own this handle, and it must be explicitly closed with + //! FinishedWritingCrashReport(). + //! + //! \param[out] report A file handle to which the crash report data should be + //! written. Only valid if this returns #kNoError. The caller must not + //! close this handle. + //! + //! \return The operation status code. + virtual OperationStatus PrepareNewCrashReport(NewReport** report) = 0; + + //! \brief Informs the database that a crash report has been written. + //! + //! After calling this method, the database is permitted to move and rename + //! the file at Report::file_path. + //! + //! \param[in] report A handle obtained with PrepareNewCrashReport(). The + //! handle will be invalidated as part of this call. + //! \param[out] uuid The UUID of this crash report. + //! + //! \return The operation status code. + virtual OperationStatus FinishedWritingCrashReport(NewReport* report, + UUID* uuid) = 0; + + //! \brief Returns the crash report record for the unique identifier. + //! + //! \param[in] uuid The crash report record unique identifier. + //! \param[out] report A crash report record. Only valid if this returns + //! #kNoError. + //! + //! \return The operation status code. + virtual OperationStatus LookUpCrashReport(const UUID& uuid, + Report* report) = 0; + + //! \brief Returns a list of crash report records that have not been uploaded. + //! + //! \param[out] reports A list of crash report record objects. This must be + //! empty on entry. Only valid if this returns #kNoError. + //! + //! \return The operation status code. + virtual OperationStatus GetPendingReports( + std::vector* reports) = 0; + + //! \brief Returns a list of crash report records that have been completed, + //! either by being uploaded or by skipping upload. + //! + //! \param[out] reports A list of crash report record objects. This must be + //! empty on entry. Only valid if this returns #kNoError. + //! + //! \return The operation status code. + virtual OperationStatus GetCompletedReports( + std::vector* reports) = 0; + + //! \brief Obtains a report object for uploading to a collection server. + //! + //! The file at Report::file_path should be uploaded by the caller, and then + //! the returned Report object must be disposed of via a call to + //! RecordUploadAttempt(). + //! + //! A subsequent call to this method with the same \a uuid is illegal until + //! RecordUploadAttempt() has been called. + //! + //! \param[in] uuid The unique identifier for the crash report record. + //! \param[out] report A crash report record for the report to be uploaded. + //! The caller does not own this object. Only valid if this returns + //! #kNoError. + //! + //! \return The operation status code. + virtual OperationStatus GetReportForUploading(const UUID& uuid, + const Report** report) = 0; + + //! \brief Adjusts a crash report record’s metadata to account for an upload + //! attempt. + //! + //! After calling this method, the database is permitted to move and rename + //! the file at Report::file_path. + //! + //! \param[in] report The report object obtained from + //! GetReportForUploading(). This object is invalidated after this call. + //! \param[in] successful Whether the upload attempt was successful. + //! \param[in] id The identifier assigned to this crash report by the + //! collection server. Must be empty if \a successful is `false`; may be + //! empty if it is `true`. + //! + //! \return The operation status code. + virtual OperationStatus RecordUploadAttempt(const Report* report, + bool successful, + const std::string& id) = 0; + + //! \brief Moves a report from the pending state to the completed state, but + //! without the report being uploaded. + //! + //! This can be used if the user has disabled crash report collection, but + //! crash generation is still enabled in the product. + //! + //! \param[in] uuid The unique identifier for the crash report record. + //! + //! \return The operation status code. + virtual OperationStatus SkipReportUpload(const UUID& uuid) = 0; + + protected: + CrashReportDatabase() {} + + private: + DISALLOW_COPY_AND_ASSIGN(CrashReportDatabase); +}; + +} // namespace crashpad + +#endif // CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_ diff --git a/client/crash_report_database_mac.mm b/client/crash_report_database_mac.mm new file mode 100644 index 00000000..fe3e13a3 --- /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 +#include +#import +#include +#include +#include +#include +#include + +#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* reports) override; + OperationStatus GetCompletedReports( + std::vector* 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* 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 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 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* reports) { + return ReportsInDirectory(base_dir_.Append(kUploadPendingDirectory), reports); +} + +CrashReportDatabase::OperationStatus +CrashReportDatabaseMac::GetCompletedReports( + std::vector* 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 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 upload_report( + static_cast(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* 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::Initialize( + const base::FilePath& path) { + scoped_ptr database_mac( + new CrashReportDatabaseMac(path.Append(kDatabaseDirectoryName))); + if (!database_mac->Initialize()) + database_mac.reset(); + + return scoped_ptr(database_mac.release()); +} + +} // namespace crashpad diff --git a/client/crash_report_database_test.cc b/client/crash_report_database_test.cc new file mode 100644 index 00000000..93967b9c --- /dev/null +++ b/client/crash_report_database_test.cc @@ -0,0 +1,420 @@ +// 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 + +#include "gtest/gtest.h" +#include "util/file/file_io.h" +#include "util/test/scoped_temp_dir.h" + +namespace crashpad { +namespace test { +namespace { + +bool FileExistsAtPath(const base::FilePath& path) { +#if defined(OS_POSIX) + struct stat st; + return lstat(path.value().c_str(), &st) == 0; +#else +#error "Not implemented" +#endif +} + +void CreateFile(const base::FilePath& path) { + FileHandle handle = LoggingOpenFileForWrite(path, + FileWriteMode::kCreateOrFail, + FilePermissions::kWorldReadable); + ASSERT_GE(handle, 0); + ASSERT_TRUE( + LoggingWriteFile(handle, path.value().c_str(), path.value().length())); + ASSERT_TRUE(LoggingCloseFile(handle)); +} + +class CrashReportDatabaseTest : public testing::Test { + protected: + // testing::Test: + void SetUp() override { + db_ = CrashReportDatabase::Initialize(path()); + ASSERT_TRUE(db_.get()); + } + + void ResetDatabase() { + db_.reset(); + } + + CrashReportDatabase* db() const { return db_.get(); } + const base::FilePath& path() const { return temp_dir_.path(); } + + void CreateCrashReport(CrashReportDatabase::Report* report) { + CrashReportDatabase::NewReport* new_report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db_->PrepareNewCrashReport(&new_report)); + const char kTest[] = "test"; + ASSERT_TRUE(LoggingWriteFile(new_report->handle, kTest, sizeof(kTest))); + + UUID uuid; + EXPECT_EQ(CrashReportDatabase::kNoError, + db_->FinishedWritingCrashReport(new_report, &uuid)); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db_->LookUpCrashReport(uuid, report)); + ExpectPreparedCrashReport(*report); + ASSERT_TRUE(FileExistsAtPath(report->file_path)); + } + + void UploadReport(const UUID& uuid, bool successful, const std::string& id) { + const CrashReportDatabase::Report* report = nullptr; + EXPECT_EQ(CrashReportDatabase::kNoError, + db_->GetReportForUploading(uuid, &report)); + EXPECT_TRUE(report); + EXPECT_NE(UUID(), report->uuid); + EXPECT_FALSE(report->file_path.empty()); + EXPECT_TRUE(FileExistsAtPath(report->file_path)) + << report->file_path.value(); + EXPECT_GT(report->creation_time, 0); + EXPECT_EQ(CrashReportDatabase::kNoError, + db_->RecordUploadAttempt(report, successful, id)); + } + + void ExpectPreparedCrashReport(const CrashReportDatabase::Report& report) { + EXPECT_NE(UUID(), report.uuid); + EXPECT_FALSE(report.file_path.empty()); + EXPECT_TRUE(FileExistsAtPath(report.file_path)) << report.file_path.value(); + EXPECT_TRUE(report.id.empty()); + EXPECT_GT(report.creation_time, 0); + EXPECT_FALSE(report.uploaded); + EXPECT_EQ(0, report.last_upload_attempt_time); + EXPECT_EQ(0, report.upload_attempts); + } + + private: + ScopedTempDir temp_dir_; + scoped_ptr db_; +}; + +TEST_F(CrashReportDatabaseTest, Initialize) { + // Initialize the database for the first time, creating it. + EXPECT_TRUE(db()); + + // Close and reopen the database at the same path. + ResetDatabase(); + EXPECT_FALSE(db()); + auto db = CrashReportDatabase::Initialize(path()); + EXPECT_TRUE(db.get()); + + std::vector reports; + EXPECT_EQ(CrashReportDatabase::kNoError, db->GetPendingReports(&reports)); + EXPECT_TRUE(reports.empty()); + EXPECT_EQ(CrashReportDatabase::kNoError, db->GetCompletedReports(&reports)); + EXPECT_TRUE(reports.empty()); +} + +TEST_F(CrashReportDatabaseTest, NewCrashReport) { + CrashReportDatabase::NewReport* new_report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->PrepareNewCrashReport(&new_report)); + UUID uuid; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->FinishedWritingCrashReport(new_report, &uuid)); + + CrashReportDatabase::Report report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(uuid, &report)); + ExpectPreparedCrashReport(report); + + std::vector reports; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&reports)); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(report.uuid, reports[0].uuid); + + reports.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&reports)); + EXPECT_TRUE(reports.empty()); +} + +TEST_F(CrashReportDatabaseTest, LookUpCrashReport) { + UUID uuid; + + { + CrashReportDatabase::Report report; + CreateCrashReport(&report); + uuid = report.uuid; + } + + { + CrashReportDatabase::Report report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(uuid, &report)); + EXPECT_EQ(uuid, report.uuid); + EXPECT_NE(std::string::npos, report.file_path.value().find(path().value())); + EXPECT_EQ("", report.id); + EXPECT_FALSE(report.uploaded); + EXPECT_EQ(0, report.last_upload_attempt_time); + EXPECT_EQ(0, report.upload_attempts); + } + + UploadReport(uuid, true, "test"); + + { + CrashReportDatabase::Report report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(uuid, &report)); + EXPECT_EQ(uuid, report.uuid); + EXPECT_NE(std::string::npos, report.file_path.value().find(path().value())); + EXPECT_EQ("test", report.id); + EXPECT_TRUE(report.uploaded); + EXPECT_NE(0, report.last_upload_attempt_time); + EXPECT_EQ(1, report.upload_attempts); + } +} + +TEST_F(CrashReportDatabaseTest, RecordUploadAttempt) { + std::vector reports(3); + CreateCrashReport(&reports[0]); + CreateCrashReport(&reports[1]); + CreateCrashReport(&reports[2]); + + // Record two attempts: one successful, one not. + UploadReport(reports[1].uuid, false, ""); + UploadReport(reports[2].uuid, true, "abc123"); + + std::vector query(3); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[0].uuid, &query[0])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[1].uuid, &query[1])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[2].uuid, &query[2])); + + EXPECT_EQ("", query[0].id); + EXPECT_EQ("", query[1].id); + EXPECT_EQ("abc123", query[2].id); + + EXPECT_FALSE(query[0].uploaded); + EXPECT_FALSE(query[1].uploaded); + EXPECT_TRUE(query[2].uploaded); + + EXPECT_EQ(0, query[0].last_upload_attempt_time); + EXPECT_NE(0, query[1].last_upload_attempt_time); + EXPECT_NE(0, query[2].last_upload_attempt_time); + + EXPECT_EQ(0, query[0].upload_attempts); + EXPECT_EQ(1, query[1].upload_attempts); + EXPECT_EQ(1, query[2].upload_attempts); + + // Attempt to upload and fail again. + UploadReport(reports[1].uuid, false, ""); + + time_t report_2_upload_time = query[2].last_upload_attempt_time; + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[0].uuid, &query[0])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[1].uuid, &query[1])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[2].uuid, &query[2])); + + EXPECT_FALSE(query[0].uploaded); + EXPECT_FALSE(query[1].uploaded); + EXPECT_TRUE(query[2].uploaded); + + EXPECT_EQ(0, query[0].last_upload_attempt_time); + EXPECT_GE(query[1].last_upload_attempt_time, report_2_upload_time); + EXPECT_EQ(report_2_upload_time, query[2].last_upload_attempt_time); + + EXPECT_EQ(0, query[0].upload_attempts); + EXPECT_EQ(2, query[1].upload_attempts); + EXPECT_EQ(1, query[2].upload_attempts); + + // Third time's the charm: upload and succeed. + UploadReport(reports[1].uuid, true, "666hahaha"); + + time_t report_1_upload_time = query[1].last_upload_attempt_time; + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[0].uuid, &query[0])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[1].uuid, &query[1])); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->LookUpCrashReport(reports[2].uuid, &query[2])); + + EXPECT_FALSE(query[0].uploaded); + EXPECT_TRUE(query[1].uploaded); + EXPECT_TRUE(query[2].uploaded); + + EXPECT_EQ(0, query[0].last_upload_attempt_time); + EXPECT_GE(query[1].last_upload_attempt_time, report_1_upload_time); + EXPECT_EQ(report_2_upload_time, query[2].last_upload_attempt_time); + + EXPECT_EQ(0, query[0].upload_attempts); + EXPECT_EQ(3, query[1].upload_attempts); + EXPECT_EQ(1, query[2].upload_attempts); +} + +// This test covers both query functions since they are related. +TEST_F(CrashReportDatabaseTest, GetCompletedAndNotUploadedReports) { + std::vector reports(5); + CreateCrashReport(&reports[0]); + CreateCrashReport(&reports[1]); + CreateCrashReport(&reports[2]); + CreateCrashReport(&reports[3]); + CreateCrashReport(&reports[4]); + + const UUID& report_0_uuid = reports[0].uuid; + const UUID& report_1_uuid = reports[1].uuid; + const UUID& report_2_uuid = reports[2].uuid; + const UUID& report_3_uuid = reports[3].uuid; + const UUID& report_4_uuid = reports[4].uuid; + + std::vector pending; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + + std::vector completed; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + EXPECT_EQ(reports.size(), pending.size()); + EXPECT_EQ(0u, completed.size()); + + // Upload one report successfully. + UploadReport(report_1_uuid, true, "report1"); + + pending.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + completed.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + EXPECT_EQ(4u, pending.size()); + ASSERT_EQ(1u, completed.size()); + + for (const auto& report : pending) + EXPECT_NE(report_1_uuid, report.uuid); + EXPECT_EQ(report_1_uuid, completed[0].uuid); + EXPECT_EQ("report1", completed[0].id); + EXPECT_EQ(true, completed[0].uploaded); + EXPECT_GT(completed[0].last_upload_attempt_time, 0); + EXPECT_EQ(1, completed[0].upload_attempts); + + const CrashReportDatabase::Report completed_report_1 = completed[0]; + + // Fail to upload one report. + UploadReport(report_2_uuid, false, ""); + + pending.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + completed.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + EXPECT_EQ(4u, pending.size()); + ASSERT_EQ(1u, completed.size()); + + for (const auto& report : pending) { + if (report.upload_attempts != 0) { + EXPECT_EQ(report_2_uuid, report.uuid); + EXPECT_GT(report.last_upload_attempt_time, 0); + EXPECT_FALSE(report.uploaded); + EXPECT_TRUE(report.id.empty()); + } + } + + // Upload a second report. + UploadReport(report_4_uuid, true, "report_4"); + + pending.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + completed.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + EXPECT_EQ(3u, pending.size()); + ASSERT_EQ(2u, completed.size()); + + // Succeed the failed report. + UploadReport(report_2_uuid, true, "report 2"); + + pending.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + completed.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + EXPECT_EQ(2u, pending.size()); + ASSERT_EQ(3u, completed.size()); + + for (const auto& report : pending) { + EXPECT_TRUE(report.uuid == report_0_uuid || + report.uuid == report_3_uuid); + } + + // Skip upload for one report. + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->SkipReportUpload(report_3_uuid)); + + pending.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetPendingReports(&pending)); + completed.clear(); + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetCompletedReports(&completed)); + + ASSERT_EQ(1u, pending.size()); + ASSERT_EQ(4u, completed.size()); + + EXPECT_EQ(report_0_uuid, pending[0].uuid); + + for (const auto& report : completed) { + if (report.uuid == report_3_uuid) { + EXPECT_FALSE(report.uploaded); + EXPECT_EQ(0, report.upload_attempts); + EXPECT_EQ(0, report.last_upload_attempt_time); + } else { + EXPECT_TRUE(report.uploaded); + EXPECT_GT(report.upload_attempts, 0); + EXPECT_GT(report.last_upload_attempt_time, 0); + } + } +} + +TEST_F(CrashReportDatabaseTest, DuelingUploads) { + CrashReportDatabase::Report report; + CreateCrashReport(&report); + + const CrashReportDatabase::Report* upload_report; + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->GetReportForUploading(report.uuid, &upload_report)); + + const CrashReportDatabase::Report* upload_report_2 = nullptr; + EXPECT_EQ(CrashReportDatabase::kBusyError, + db()->GetReportForUploading(report.uuid, &upload_report_2)); + EXPECT_FALSE(upload_report_2); + + EXPECT_EQ(CrashReportDatabase::kNoError, + db()->RecordUploadAttempt(upload_report, true, "")); +} + +} // namespace +} // namespace test +} // namespace crashpad