| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * 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. |
| */ |
| #define DEBUG false |
| #include "Log.h" |
| |
| #include "Reporter.h" |
| |
| #include "Privacy.h" |
| #include "report_directory.h" |
| #include "section_list.h" |
| |
| #include <android-base/properties.h> |
| #include <android/os/DropBoxManager.h> |
| #include <private/android_filesystem_config.h> |
| #include <utils/SystemClock.h> |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <string> |
| |
| /** |
| * The directory where the incident reports are stored. |
| */ |
| static const char* INCIDENT_DIRECTORY = "/data/misc/incidents/"; |
| |
| namespace android { |
| namespace os { |
| namespace incidentd { |
| |
| // ================================================================================ |
| ReportRequest::ReportRequest(const IncidentReportArgs& a, |
| const sp<IIncidentReportStatusListener>& l, int f) |
| : args(a), listener(l), fd(f), err(NO_ERROR) {} |
| |
| ReportRequest::~ReportRequest() { |
| if (fd >= 0) { |
| // clean up the opened file descriptor |
| close(fd); |
| } |
| } |
| |
| bool ReportRequest::ok() { return fd >= 0 && err == NO_ERROR; } |
| |
| // ================================================================================ |
| ReportRequestSet::ReportRequestSet() |
| : mRequests(), mSections(), mMainFd(-1), mMainDest(-1), mMetadata(), mSectionStats() {} |
| |
| ReportRequestSet::~ReportRequestSet() {} |
| |
| // TODO: dedup on exact same args and fd, report the status back to listener! |
| void ReportRequestSet::add(const sp<ReportRequest>& request) { |
| mRequests.push_back(request); |
| mSections.merge(request->args); |
| mMetadata.set_request_size(mMetadata.request_size() + 1); |
| } |
| |
| void ReportRequestSet::setMainFd(int fd) { |
| mMainFd = fd; |
| mMetadata.set_use_dropbox(fd > 0); |
| } |
| |
| void ReportRequestSet::setMainDest(int dest) { |
| mMainDest = dest; |
| PrivacySpec spec = PrivacySpec::new_spec(dest); |
| switch (spec.dest) { |
| case android::os::DEST_AUTOMATIC: |
| mMetadata.set_dest(IncidentMetadata_Destination_AUTOMATIC); |
| break; |
| case android::os::DEST_EXPLICIT: |
| mMetadata.set_dest(IncidentMetadata_Destination_EXPLICIT); |
| break; |
| case android::os::DEST_LOCAL: |
| mMetadata.set_dest(IncidentMetadata_Destination_LOCAL); |
| break; |
| } |
| } |
| |
| bool ReportRequestSet::containsSection(int id) { return mSections.containsSection(id); } |
| |
| IncidentMetadata::SectionStats* ReportRequestSet::sectionStats(int id) { |
| if (mSectionStats.find(id) == mSectionStats.end()) { |
| IncidentMetadata::SectionStats stats; |
| stats.set_id(id); |
| mSectionStats[id] = stats; |
| } |
| return &mSectionStats[id]; |
| } |
| |
| // ================================================================================ |
| Reporter::Reporter() : Reporter(INCIDENT_DIRECTORY) { isTest = false; }; |
| |
| Reporter::Reporter(const char* directory) : batch() { |
| char buf[100]; |
| |
| mMaxSize = 30 * 1024 * 1024; // incident reports can take up to 30MB on disk |
| mMaxCount = 100; |
| |
| // string ends up with '/' is a directory |
| String8 dir = String8(directory); |
| if (directory[dir.size() - 1] != '/') dir += "/"; |
| mIncidentDirectory = dir.string(); |
| |
| // There can't be two at the same time because it's on one thread. |
| mStartTime = time(NULL); |
| strftime(buf, sizeof(buf), "incident-%Y%m%d-%H%M%S", localtime(&mStartTime)); |
| mFilename = mIncidentDirectory + buf; |
| } |
| |
| Reporter::~Reporter() {} |
| |
| Reporter::run_report_status_t Reporter::runReport(size_t* reportByteSize) { |
| status_t err = NO_ERROR; |
| bool needMainFd = false; |
| int mainFd = -1; |
| int mainDest = -1; |
| HeaderSection headers; |
| MetadataSection metadataSection; |
| std::string buildType = android::base::GetProperty("ro.build.type", ""); |
| const bool isUserdebugOrEng = buildType == "userdebug" || buildType == "eng"; |
| |
| // See if we need the main file |
| for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) { |
| if ((*it)->fd < 0 && mainFd < 0) { |
| needMainFd = true; |
| mainDest = (*it)->args.dest(); |
| break; |
| } |
| } |
| if (needMainFd) { |
| // Create the directory |
| if (!isTest) err = create_directory(mIncidentDirectory); |
| if (err != NO_ERROR) { |
| goto DONE; |
| } |
| |
| // If there are too many files in the directory (for whatever reason), |
| // delete the oldest ones until it's under the limit. Doing this first |
| // does mean that we can go over, so the max size is not a hard limit. |
| if (!isTest) clean_directory(mIncidentDirectory, mMaxSize, mMaxCount); |
| |
| // Open the file. |
| err = create_file(&mainFd); |
| if (err != NO_ERROR) { |
| goto DONE; |
| } |
| |
| // Add to the set |
| batch.setMainFd(mainFd); |
| batch.setMainDest(mainDest); |
| } |
| |
| // Tell everyone that we're starting. |
| for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) { |
| if ((*it)->listener != NULL) { |
| (*it)->listener->onReportStarted(); |
| } |
| } |
| |
| // Write the incident headers |
| headers.Execute(&batch); |
| |
| // For each of the report fields, see if we need it, and if so, execute the command |
| // and report to those that care that we're doing it. |
| for (const Section** section = SECTION_LIST; *section; section++) { |
| const int id = (*section)->id; |
| if ((*section)->userdebugAndEngOnly && !isUserdebugOrEng) { |
| ALOGD("Skipping incident report section %d '%s' because it's limited to userdebug/eng", |
| id, (*section)->name.string()); |
| continue; |
| } |
| if (this->batch.containsSection(id)) { |
| ALOGD("Taking incident report section %d '%s'", id, (*section)->name.string()); |
| for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) { |
| if ((*it)->listener != NULL && (*it)->args.containsSection(id)) { |
| (*it)->listener->onReportSectionStatus( |
| id, IIncidentReportStatusListener::STATUS_STARTING); |
| } |
| } |
| |
| // Execute - go get the data and write it into the file descriptors. |
| IncidentMetadata::SectionStats* stats = batch.sectionStats(id); |
| int64_t startTime = uptimeMillis(); |
| err = (*section)->Execute(&batch); |
| int64_t endTime = uptimeMillis(); |
| stats->set_success(err == NO_ERROR); |
| stats->set_exec_duration_ms(endTime - startTime); |
| if (err != NO_ERROR) { |
| ALOGW("Incident section %s (%d) failed: %s. Stopping report.", |
| (*section)->name.string(), id, strerror(-err)); |
| goto DONE; |
| } |
| (*reportByteSize) += stats->report_size_bytes(); |
| |
| // Notify listener of starting |
| for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) { |
| if ((*it)->listener != NULL && (*it)->args.containsSection(id)) { |
| (*it)->listener->onReportSectionStatus( |
| id, IIncidentReportStatusListener::STATUS_FINISHED); |
| } |
| } |
| ALOGD("Finish incident report section %d '%s'", id, (*section)->name.string()); |
| } |
| } |
| |
| DONE: |
| // Reports the metdadata when taking the incident report. |
| if (!isTest) metadataSection.Execute(&batch); |
| |
| // Close the file. |
| if (mainFd >= 0) { |
| close(mainFd); |
| } |
| |
| // Tell everyone that we're done. |
| for (ReportRequestSet::iterator it = batch.begin(); it != batch.end(); it++) { |
| if ((*it)->listener != NULL) { |
| if (err == NO_ERROR) { |
| (*it)->listener->onReportFinished(); |
| } else { |
| (*it)->listener->onReportFailed(); |
| } |
| } |
| } |
| |
| // Put the report into dropbox. |
| if (needMainFd && err == NO_ERROR) { |
| sp<DropBoxManager> dropbox = new DropBoxManager(); |
| Status status = dropbox->addFile(String16("incident"), mFilename, 0); |
| ALOGD("Incident report done. dropbox status=%s\n", status.toString8().string()); |
| if (!status.isOk()) { |
| return REPORT_NEEDS_DROPBOX; |
| } |
| |
| // If the status was ok, delete the file. If not, leave it around until the next |
| // boot or the next checkin. If the directory gets too big older files will |
| // be rotated out. |
| if (!isTest) unlink(mFilename.c_str()); |
| } |
| |
| return REPORT_FINISHED; |
| } |
| |
| /** |
| * Create our output file and set the access permissions to -rw-rw---- |
| */ |
| status_t Reporter::create_file(int* fd) { |
| const char* filename = mFilename.c_str(); |
| |
| *fd = open(filename, O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0660); |
| if (*fd < 0) { |
| ALOGE("Couldn't open incident file: %s (%s)", filename, strerror(errno)); |
| return -errno; |
| } |
| |
| // Override umask. Not super critical. If it fails go on with life. |
| chmod(filename, 0660); |
| |
| if (chown(filename, AID_INCIDENTD, AID_INCIDENTD)) { |
| ALOGE("Unable to change ownership of incident file %s: %s\n", filename, strerror(errno)); |
| status_t err = -errno; |
| unlink(mFilename.c_str()); |
| return err; |
| } |
| |
| return NO_ERROR; |
| } |
| |
| Reporter::run_report_status_t Reporter::upload_backlog() { |
| DIR* dir; |
| struct dirent* entry; |
| struct stat st; |
| status_t err; |
| |
| ALOGD("Start uploading backlogs in %s", INCIDENT_DIRECTORY); |
| if ((err = create_directory(INCIDENT_DIRECTORY)) != NO_ERROR) { |
| ALOGE("directory doesn't exist: %s", strerror(-err)); |
| return REPORT_FINISHED; |
| } |
| |
| if ((dir = opendir(INCIDENT_DIRECTORY)) == NULL) { |
| ALOGE("Couldn't open incident directory: %s", INCIDENT_DIRECTORY); |
| return REPORT_NEEDS_DROPBOX; |
| } |
| |
| sp<DropBoxManager> dropbox = new DropBoxManager(); |
| |
| // Enumerate, count and add up size |
| int count = 0; |
| while ((entry = readdir(dir)) != NULL) { |
| if (entry->d_name[0] == '.') { |
| continue; |
| } |
| String8 filename = String8(INCIDENT_DIRECTORY) + entry->d_name; |
| if (stat(filename.string(), &st) != 0) { |
| ALOGE("Unable to stat file %s", filename.string()); |
| continue; |
| } |
| if (!S_ISREG(st.st_mode)) { |
| continue; |
| } |
| |
| Status status = dropbox->addFile(String16("incident"), filename.string(), 0); |
| ALOGD("Incident report done. dropbox status=%s\n", status.toString8().string()); |
| if (!status.isOk()) { |
| return REPORT_NEEDS_DROPBOX; |
| } |
| |
| // If the status was ok, delete the file. If not, leave it around until the next |
| // boot or the next checkin. If the directory gets too big older files will |
| // be rotated out. |
| unlink(filename.string()); |
| count++; |
| } |
| ALOGD("Successfully uploaded %d files to Dropbox.", count); |
| closedir(dir); |
| |
| return REPORT_FINISHED; |
| } |
| |
| } // namespace incidentd |
| } // namespace os |
| } // namespace android |