// Copyright 2015 The Crashpad Authors // // 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 #include #include #include #include #include #include #include #include #include #include #include #include #include "base/check_op.h" #include "base/files/file_path.h" #include "base/numerics/safe_conversions.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "client/crash_report_database.h" #include "client/settings.h" #include "tools/tool_support.h" #include "util/file/file_io.h" #include "util/file/file_reader.h" #include "util/misc/uuid.h" #include "util/stdlib/string_number_conversion.h" namespace crashpad { namespace { void Usage(const base::FilePath& me) { // clang-format off fprintf(stderr, "Usage: %" PRFilePath " [OPTION]... PID\n" "Operate on Crashpad crash report databases.\n" "\n" " --create allow database at PATH to be created\n" " -d, --database=PATH operate on the crash report database at PATH\n" " --show-client-id show the client ID\n" " --show-uploads-enabled show whether uploads are enabled\n" " --show-last-upload-attempt-time\n" " show the last-upload-attempt time\n" " --show-pending-reports show reports eligible for upload\n" " --show-completed-reports show reports not eligible for upload\n" " --show-all-report-info with --show-*-reports, show more information\n" " --show-report=UUID show report stored under UUID\n" " --set-uploads-enabled=BOOL enable or disable uploads\n" " --set-last-upload-attempt-time=TIME\n" " set the last-upload-attempt time to TIME\n" " --new-report=PATH submit a new report at PATH, or - for stdin\n" " --utc show and set UTC times instead of local\n" " --help display this help and exit\n" " --version output version information and exit\n", me.value().c_str()); // clang-format on ToolSupport::UsageTail(me); } struct Options { std::vector show_reports; std::vector new_report_paths; const char* database; const char* set_last_upload_attempt_time_string; time_t set_last_upload_attempt_time; bool create; bool show_client_id; bool show_uploads_enabled; bool show_last_upload_attempt_time; bool show_pending_reports; bool show_completed_reports; bool show_all_report_info; bool set_uploads_enabled; bool has_set_uploads_enabled; bool utc; }; // Converts |string| to |boolean|, returning true if a conversion could be // performed, and false without setting |boolean| if no conversion could be // performed. Various string representations of a boolean are recognized // case-insensitively. bool StringToBool(const char* string, bool* boolean) { static constexpr const char* kFalseWords[] = { "0", "false", "no", "off", "disabled", "clear", }; static constexpr const char* kTrueWords[] = { "1", "true", "yes", "on", "enabled", "set", }; for (size_t index = 0; index < std::size(kFalseWords); ++index) { if (strcasecmp(string, kFalseWords[index]) == 0) { *boolean = false; return true; } } for (size_t index = 0; index < std::size(kTrueWords); ++index) { if (strcasecmp(string, kTrueWords[index]) == 0) { *boolean = true; return true; } } return false; } // Converts |boolean| to a string, either "true" or "false". std::string BoolToString(bool boolean) { return std::string(boolean ? "true" : "false"); } // Converts |string| to |out_time|, returning true if a conversion could be // performed, and false without setting |boolean| if no conversion could be // performed. Various time formats are recognized, including several string // representations and a numeric time_t representation. The special |string| // "never" is recognized as converted to a |out_time| value of 0; "now" is // converted to the current time. |utc|, when true, causes |string| to be // interpreted as a UTC time rather than a local time when the time zone is // ambiguous. bool StringToTime(const char* string, time_t* out_time, bool utc) { if (strcasecmp(string, "never") == 0) { *out_time = 0; return true; } if (strcasecmp(string, "now") == 0) { errno = 0; PCHECK(time(out_time) != -1 || errno == 0); return true; } const char* end = string + strlen(string); static constexpr const char* kFormats[] = { "%Y-%m-%d %H:%M:%S %Z", "%Y-%m-%d %H:%M:%S", "%+", }; for (size_t index = 0; index < std::size(kFormats); ++index) { tm time_tm; const char* strptime_result = strptime(string, kFormats[index], &time_tm); if (strptime_result == end) { time_t test_out_time; if (utc) { test_out_time = timegm(&time_tm); } else { test_out_time = mktime(&time_tm); } // mktime() is supposed to set errno in the event of an error, but support // for this is spotty, so there’s no way to distinguish between a true // time_t of -1 (1969-12-31 23:59:59 UTC) and an error. Assume error. // // See 10.11.5 Libc-1082.50.1/stdtime/FreeBSD/localtime.c and // glibc-2.24/time/mktime.c, which don’t set errno or save and restore // errno. Post-Android 7.1.0 Bionic is even more hopeless, setting errno // whenever the time conversion returns -1, even for valid input. See // libc/tzcode/localtime.c mktime(). Windows seems to get it right: see // 10.0.14393 SDK Source/ucrt/time/mktime.cpp. if (test_out_time != -1) { *out_time = test_out_time; return true; } } } int64_t int64_result; if (StringToNumber(string, &int64_result) && base::IsValueInRangeForNumericType(int64_result)) { *out_time = int64_result; return true; } return false; } // Converts |out_time| to a string, and returns it. |utc| determines whether the // converted time will reference local time or UTC. If |out_time| is 0, the // string "never" will be returned as a special case. std::string TimeToString(time_t out_time, bool utc) { if (out_time == 0) { return std::string("never"); } tm time_tm; if (utc) { PCHECK(gmtime_r(&out_time, &time_tm)); } else { PCHECK(localtime_r(&out_time, &time_tm)); } char string[64]; CHECK_NE( strftime(string, std::size(string), "%Y-%m-%d %H:%M:%S %Z", &time_tm), 0u); return std::string(string); } // Shows information about a single |report|. |space_count| is the number of // spaces to print before each line that is printed. |utc| determines whether // times should be shown in UTC or the local time zone. void ShowReport(const CrashReportDatabase::Report& report, size_t space_count, bool utc) { std::string spaces(space_count, ' '); printf("%sPath: %" PRFilePath "\n", spaces.c_str(), report.file_path.value().c_str()); if (!report.id.empty()) { printf("%sRemote ID: %s\n", spaces.c_str(), report.id.c_str()); } printf("%sCreation time: %s\n", spaces.c_str(), TimeToString(report.creation_time, utc).c_str()); printf("%sUploaded: %s\n", spaces.c_str(), BoolToString(report.uploaded).c_str()); printf("%sLast upload attempt time: %s\n", spaces.c_str(), TimeToString(report.last_upload_attempt_time, utc).c_str()); printf("%sUpload attempts: %d\n", spaces.c_str(), report.upload_attempts); } // Shows information about a vector of |reports|. |space_count| is the number of // spaces to print before each line that is printed. |options| will be consulted // to determine whether to show expanded information // (options.show_all_report_info) and what time zone to use when showing // expanded information (options.utc). void ShowReports(const std::vector& reports, size_t space_count, const Options& options) { std::string spaces(space_count, ' '); const char* colon = options.show_all_report_info ? ":" : ""; for (const CrashReportDatabase::Report& report : reports) { printf("%s%s%s\n", spaces.c_str(), report.uuid.ToString().c_str(), colon); if (options.show_all_report_info) { ShowReport(report, space_count + 2, options.utc); } } } int DatabaseUtilMain(int argc, char* argv[]) { const base::FilePath argv0( ToolSupport::CommandLineArgumentToFilePathStringType(argv[0])); const base::FilePath me(argv0.BaseName()); enum OptionFlags { // “Short” (single-character) options. kOptionDatabase = 'd', // Long options without short equivalents. kOptionLastChar = 255, kOptionCreate, kOptionShowClientID, kOptionShowUploadsEnabled, kOptionShowLastUploadAttemptTime, kOptionShowPendingReports, kOptionShowCompletedReports, kOptionShowAllReportInfo, kOptionShowReport, kOptionSetUploadsEnabled, kOptionSetLastUploadAttemptTime, kOptionNewReport, kOptionUTC, // Standard options. kOptionHelp = -2, kOptionVersion = -3, }; static constexpr option long_options[] = { {"create", no_argument, nullptr, kOptionCreate}, {"database", required_argument, nullptr, kOptionDatabase}, {"show-client-id", no_argument, nullptr, kOptionShowClientID}, {"show-uploads-enabled", no_argument, nullptr, kOptionShowUploadsEnabled}, {"show-last-upload-attempt-time", no_argument, nullptr, kOptionShowLastUploadAttemptTime}, {"show-pending-reports", no_argument, nullptr, kOptionShowPendingReports}, {"show-completed-reports", no_argument, nullptr, kOptionShowCompletedReports}, {"show-all-report-info", no_argument, nullptr, kOptionShowAllReportInfo}, {"show-report", required_argument, nullptr, kOptionShowReport}, {"set-uploads-enabled", required_argument, nullptr, kOptionSetUploadsEnabled}, {"set-last-upload-attempt-time", required_argument, nullptr, kOptionSetLastUploadAttemptTime}, {"new-report", required_argument, nullptr, kOptionNewReport}, {"utc", no_argument, nullptr, kOptionUTC}, {"help", no_argument, nullptr, kOptionHelp}, {"version", no_argument, nullptr, kOptionVersion}, {nullptr, 0, nullptr, 0}, }; Options options = {}; int opt; while ((opt = getopt_long(argc, argv, "d:", long_options, nullptr)) != -1) { switch (opt) { case kOptionCreate: { options.create = true; break; } case kOptionDatabase: { options.database = optarg; break; } case kOptionShowClientID: { options.show_client_id = true; break; } case kOptionShowUploadsEnabled: { options.show_uploads_enabled = true; break; } case kOptionShowLastUploadAttemptTime: { options.show_last_upload_attempt_time = true; break; } case kOptionShowPendingReports: { options.show_pending_reports = true; break; } case kOptionShowCompletedReports: { options.show_completed_reports = true; break; } case kOptionShowAllReportInfo: { options.show_all_report_info = true; break; } case kOptionShowReport: { UUID uuid; if (!uuid.InitializeFromString(optarg)) { ToolSupport::UsageHint(me, "--show-report requires a UUID"); return EXIT_FAILURE; } options.show_reports.push_back(uuid); break; } case kOptionSetUploadsEnabled: { if (!StringToBool(optarg, &options.set_uploads_enabled)) { ToolSupport::UsageHint(me, "--set-uploads-enabled requires a BOOL"); return EXIT_FAILURE; } options.has_set_uploads_enabled = true; break; } case kOptionSetLastUploadAttemptTime: { options.set_last_upload_attempt_time_string = optarg; break; } case kOptionNewReport: { options.new_report_paths.push_back(base::FilePath( ToolSupport::CommandLineArgumentToFilePathStringType(optarg))); break; } case kOptionUTC: { options.utc = true; break; } case kOptionHelp: { Usage(me); return EXIT_SUCCESS; } case kOptionVersion: { ToolSupport::Version(me); return EXIT_SUCCESS; } default: { ToolSupport::UsageHint(me, nullptr); return EXIT_FAILURE; } } } argc -= optind; argv += optind; if (!options.database) { ToolSupport::UsageHint(me, "--database is required"); return EXIT_FAILURE; } // This conversion couldn’t happen in the option-processing loop above because // it depends on options.utc, which may have been set after // options.set_last_upload_attempt_time_string. if (options.set_last_upload_attempt_time_string) { if (!StringToTime(options.set_last_upload_attempt_time_string, &options.set_last_upload_attempt_time, options.utc)) { ToolSupport::UsageHint(me, "--set-last-upload-attempt-time requires a TIME"); return EXIT_FAILURE; } } // --new-report is treated as a show operation because it produces output. const size_t show_operations = options.show_client_id + options.show_uploads_enabled + options.show_last_upload_attempt_time + options.show_pending_reports + options.show_completed_reports + options.show_reports.size() + options.new_report_paths.size(); const size_t set_operations = options.has_set_uploads_enabled + (options.set_last_upload_attempt_time_string != nullptr); if ((options.create ? 1 : 0) + show_operations + set_operations == 0) { ToolSupport::UsageHint(me, "nothing to do"); return EXIT_FAILURE; } std::unique_ptr database; base::FilePath database_path = base::FilePath( ToolSupport::CommandLineArgumentToFilePathStringType(options.database)); if (options.create) { database = CrashReportDatabase::Initialize(database_path); } else { database = CrashReportDatabase::InitializeWithoutCreating(database_path); } if (!database) { return EXIT_FAILURE; } Settings* settings = database->GetSettings(); // Handle the “show” options before the “set” options so that when they’re // specified together, the “show” option reflects the initial state. if (options.show_client_id) { UUID client_id; if (!settings->GetClientID(&client_id)) { return EXIT_FAILURE; } const char* prefix = (show_operations > 1) ? "Client ID: " : ""; printf("%s%s\n", prefix, client_id.ToString().c_str()); } if (options.show_uploads_enabled) { bool uploads_enabled; if (!settings->GetUploadsEnabled(&uploads_enabled)) { return EXIT_FAILURE; } const char* prefix = (show_operations > 1) ? "Uploads enabled: " : ""; printf("%s%s\n", prefix, BoolToString(uploads_enabled).c_str()); } if (options.show_last_upload_attempt_time) { time_t last_upload_attempt_time; if (!settings->GetLastUploadAttemptTime(&last_upload_attempt_time)) { return EXIT_FAILURE; } const char* prefix = (show_operations > 1) ? "Last upload attempt time: " : ""; printf("%s%s (%ld)\n", prefix, TimeToString(last_upload_attempt_time, options.utc).c_str(), static_cast(last_upload_attempt_time)); } if (options.show_pending_reports) { std::vector pending_reports; if (database->GetPendingReports(&pending_reports) != CrashReportDatabase::kNoError) { return EXIT_FAILURE; } if (show_operations > 1) { printf("Pending reports:\n"); } ShowReports(pending_reports, show_operations > 1 ? 2 : 0, options); } if (options.show_completed_reports) { std::vector completed_reports; if (database->GetCompletedReports(&completed_reports) != CrashReportDatabase::kNoError) { return EXIT_FAILURE; } if (show_operations > 1) { printf("Completed reports:\n"); } ShowReports(completed_reports, show_operations > 1 ? 2 : 0, options); } for (const UUID& uuid : options.show_reports) { CrashReportDatabase::Report report; CrashReportDatabase::OperationStatus status = database->LookUpCrashReport(uuid, &report); if (status == CrashReportDatabase::kNoError) { if (show_operations > 1) { printf("Report %s:\n", uuid.ToString().c_str()); } ShowReport(report, show_operations > 1 ? 2 : 0, options.utc); } else if (status == CrashReportDatabase::kReportNotFound) { // If only asked to do one thing, a failure to find the single requested // report should result in a failure exit status. if (show_operations + set_operations == 1) { fprintf( stderr, "%" PRFilePath ": Report not found\n", me.value().c_str()); return EXIT_FAILURE; } printf("Report %s not found\n", uuid.ToString().c_str()); } else { return EXIT_FAILURE; } } if (options.has_set_uploads_enabled && !settings->SetUploadsEnabled(options.set_uploads_enabled)) { return EXIT_FAILURE; } if (options.set_last_upload_attempt_time_string && !settings->SetLastUploadAttemptTime( options.set_last_upload_attempt_time)) { return EXIT_FAILURE; } bool used_stdin = false; for (const base::FilePath& new_report_path : options.new_report_paths) { std::unique_ptr file_reader; if (new_report_path.value() == FILE_PATH_LITERAL("-")) { if (used_stdin) { fprintf(stderr, "%" PRFilePath ": Only one --new-report may be read from standard input\n", me.value().c_str()); return EXIT_FAILURE; } used_stdin = true; file_reader.reset(new WeakFileHandleFileReader( StdioFileHandle(StdioStream::kStandardInput))); } else { std::unique_ptr file_path_reader(new FileReader()); if (!file_path_reader->Open(new_report_path)) { return EXIT_FAILURE; } file_reader = std::move(file_path_reader); } std::unique_ptr new_report; CrashReportDatabase::OperationStatus status = database->PrepareNewCrashReport(&new_report); if (status != CrashReportDatabase::kNoError) { return EXIT_FAILURE; } char buf[4096]; FileOperationResult read_result; do { read_result = file_reader->Read(buf, sizeof(buf)); if (read_result < 0) { return EXIT_FAILURE; } if (read_result > 0 && !new_report->Writer()->Write(buf, read_result)) { return EXIT_FAILURE; } } while (read_result > 0); UUID uuid; status = database->FinishedWritingCrashReport(std::move(new_report), &uuid); if (status != CrashReportDatabase::kNoError) { return EXIT_FAILURE; } const char* prefix = (show_operations > 1) ? "New report ID: " : ""; printf("%s%s\n", prefix, uuid.ToString().c_str()); } return EXIT_SUCCESS; } } // namespace } // namespace crashpad #if BUILDFLAG(IS_POSIX) int main(int argc, char* argv[]) { return crashpad::DatabaseUtilMain(argc, argv); } #elif BUILDFLAG(IS_WIN) int wmain(int argc, wchar_t* argv[]) { return crashpad::ToolSupport::Wmain(argc, argv, crashpad::DatabaseUtilMain); } #endif // BUILDFLAG(IS_POSIX)