| /* |
| * Copyright (C) 2018 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. |
| */ |
| |
| #include <android-base/file.h> |
| #include <android/os/BnDumpstate.h> |
| #include <android/os/BnDumpstateListener.h> |
| #include <binder/IServiceManager.h> |
| #include <binder/ProcessState.h> |
| #include <cutils/properties.h> |
| #include <fcntl.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <libgen.h> |
| #include <ziparchive/zip_archive.h> |
| |
| #include <fstream> |
| #include <regex> |
| |
| #include "dumpstate.h" |
| |
| #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) |
| |
| namespace android { |
| namespace os { |
| namespace dumpstate { |
| |
| using ::testing::Test; |
| using ::std::literals::chrono_literals::operator""s; |
| using android::base::unique_fd; |
| |
| class DumpstateListener; |
| |
| namespace { |
| |
| struct SectionInfo { |
| std::string name; |
| int32_t size_bytes; |
| }; |
| |
| sp<IDumpstate> GetDumpstateService() { |
| return android::interface_cast<IDumpstate>( |
| android::defaultServiceManager()->getService(String16("dumpstate"))); |
| } |
| |
| int OpenForWrite(const std::string& filename) { |
| return TEMP_FAILURE_RETRY(open(filename.c_str(), |
| O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, |
| S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)); |
| } |
| |
| void GetEntry(const ZipArchiveHandle archive, const std::string_view entry_name, ZipEntry* data) { |
| int32_t e = FindEntry(archive, entry_name, data); |
| EXPECT_EQ(e, 0) << ErrorCodeString(e) << " entry name: " << entry_name; |
| } |
| |
| // Extracts the main bugreport txt from the given archive and writes into output_fd. |
| void ExtractBugreport(const ZipArchiveHandle* handle, int output_fd) { |
| // Read contents of main_entry.txt which is a single line indicating the name of the zip entry |
| // that contains the main bugreport txt. |
| ZipEntry main_entry; |
| GetEntry(*handle, "main_entry.txt", &main_entry); |
| std::string bugreport_txt_name; |
| bugreport_txt_name.resize(main_entry.uncompressed_length); |
| ExtractToMemory(*handle, &main_entry, reinterpret_cast<uint8_t*>(bugreport_txt_name.data()), |
| main_entry.uncompressed_length); |
| |
| // Read the main bugreport txt and extract to output_fd. |
| ZipEntry entry; |
| GetEntry(*handle, bugreport_txt_name, &entry); |
| ExtractEntryToFile(*handle, &entry, output_fd); |
| } |
| |
| bool IsSectionStart(const std::string& line, std::string* section_name) { |
| static const std::regex kSectionStart = std::regex{"DUMP OF SERVICE (.*):"}; |
| std::smatch match; |
| if (std::regex_match(line, match, kSectionStart)) { |
| *section_name = match.str(1); |
| return true; |
| } |
| return false; |
| } |
| |
| bool IsSectionEnd(const std::string& line) { |
| // Not all lines that contain "was the duration of" is a section end, but all section ends do |
| // contain "was the duration of". The disambiguation can be done by the caller. |
| return (line.find("was the duration of") != std::string::npos); |
| } |
| |
| // Extracts the zipped bugreport and identifies the sections. |
| void ParseSections(const std::string& zip_path, std::vector<SectionInfo>* sections) { |
| // Open the archive |
| ZipArchiveHandle handle; |
| ASSERT_EQ(OpenArchive(zip_path.c_str(), &handle), 0); |
| |
| // Extract the main entry to a temp file |
| TemporaryFile tmp_binary; |
| ASSERT_NE(-1, tmp_binary.fd); |
| ExtractBugreport(&handle, tmp_binary.fd); |
| |
| // Read line by line and identify sections |
| std::ifstream ifs(tmp_binary.path, std::ifstream::in); |
| std::string line; |
| int section_bytes = 0; |
| std::string current_section_name; |
| while (std::getline(ifs, line)) { |
| std::string section_name; |
| if (IsSectionStart(line, §ion_name)) { |
| section_bytes = 0; |
| current_section_name = section_name; |
| } else if (IsSectionEnd(line)) { |
| if (!current_section_name.empty()) { |
| sections->push_back({current_section_name, section_bytes}); |
| } |
| current_section_name = ""; |
| } else if (!current_section_name.empty()) { |
| section_bytes += line.length(); |
| } |
| } |
| |
| CloseArchive(handle); |
| } |
| |
| } // namespace |
| |
| /** |
| * Listens to bugreport progress and updates the user by writing the progress to STDOUT. All the |
| * section details generated by dumpstate are added to a vector to be used by Tests later. |
| */ |
| class DumpstateListener : public BnDumpstateListener { |
| public: |
| DumpstateListener(int fd, std::shared_ptr<std::vector<SectionInfo>> sections) |
| : out_fd_(fd), sections_(sections) { |
| } |
| |
| DumpstateListener(int fd) : out_fd_(fd) { |
| } |
| |
| binder::Status onProgress(int32_t progress) override { |
| dprintf(out_fd_, "\rIn progress %d", progress); |
| return binder::Status::ok(); |
| } |
| |
| binder::Status onError(int32_t error_code) override { |
| std::lock_guard<std::mutex> lock(lock_); |
| error_code_ = error_code; |
| dprintf(out_fd_, "\rError code %d", error_code); |
| return binder::Status::ok(); |
| } |
| |
| binder::Status onFinished() override { |
| std::lock_guard<std::mutex> lock(lock_); |
| is_finished_ = true; |
| dprintf(out_fd_, "\rFinished"); |
| return binder::Status::ok(); |
| } |
| |
| binder::Status onScreenshotTaken(bool success) override { |
| std::lock_guard<std::mutex> lock(lock_); |
| dprintf(out_fd_, "\rResult of taking screenshot: %s", success ? "success" : "failure"); |
| return binder::Status::ok(); |
| } |
| |
| binder::Status onUiIntensiveBugreportDumpsFinished(const android::String16& callingpackage) |
| override { |
| std::lock_guard <std::mutex> lock(lock_); |
| std::string callingpackageUtf8 = std::string(String8(callingpackage).string()); |
| dprintf(out_fd_, "\rCalling package of ui intensive bugreport dumps finished: %s", |
| callingpackageUtf8.c_str()); |
| return binder::Status::ok(); |
| } |
| |
| bool getIsFinished() { |
| std::lock_guard<std::mutex> lock(lock_); |
| return is_finished_; |
| } |
| |
| int getErrorCode() { |
| std::lock_guard<std::mutex> lock(lock_); |
| return error_code_; |
| } |
| |
| private: |
| int out_fd_; |
| int error_code_ = -1; |
| bool is_finished_ = false; |
| std::shared_ptr<std::vector<SectionInfo>> sections_; |
| std::mutex lock_; |
| }; |
| |
| /** |
| * Generates bug report and provide access to the bug report file and other info for other tests. |
| * Since bug report generation is slow, the bugreport is only generated once. |
| */ |
| class ZippedBugreportGenerationTest : public Test { |
| public: |
| static std::shared_ptr<std::vector<SectionInfo>> sections; |
| static Dumpstate& ds; |
| static std::chrono::milliseconds duration; |
| static void GenerateBugreport() { |
| // clang-format off |
| char* argv[] = { |
| (char*)"dumpstate", |
| (char*)"-d", |
| (char*)"-z" |
| }; |
| // clang-format on |
| sp<DumpstateListener> listener(new DumpstateListener(dup(fileno(stdout)), sections)); |
| ds.listener_ = listener; |
| auto start = std::chrono::steady_clock::now(); |
| ds.ParseCommandlineAndRun(ARRAY_SIZE(argv), argv); |
| auto end = std::chrono::steady_clock::now(); |
| duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); |
| } |
| |
| static const std::string getZipFilePath() { |
| return ds.GetPath(".zip"); |
| } |
| }; |
| std::shared_ptr<std::vector<SectionInfo>> ZippedBugreportGenerationTest::sections = |
| std::make_shared<std::vector<SectionInfo>>(); |
| Dumpstate& ZippedBugreportGenerationTest::ds = Dumpstate::GetInstance(); |
| std::chrono::milliseconds ZippedBugreportGenerationTest::duration = 0s; |
| |
| TEST_F(ZippedBugreportGenerationTest, IsGeneratedWithoutErrors) { |
| GenerateBugreport(); |
| EXPECT_EQ(access(getZipFilePath().c_str(), F_OK), 0); |
| } |
| |
| TEST_F(ZippedBugreportGenerationTest, Is3MBMBinSize) { |
| struct stat st; |
| EXPECT_EQ(stat(getZipFilePath().c_str(), &st), 0); |
| EXPECT_GE(st.st_size, 3000000 /* 3MB */); |
| } |
| |
| TEST_F(ZippedBugreportGenerationTest, TakesBetween30And300Seconds) { |
| EXPECT_GE(duration, 30s) << "Expected completion in more than 30s. Actual time " |
| << duration.count() << " s."; |
| EXPECT_LE(duration, 300s) << "Expected completion in less than 300s. Actual time " |
| << duration.count() << " s."; |
| } |
| |
| /** |
| * Run tests on contents of zipped bug report. |
| */ |
| class ZippedBugReportContentsTest : public Test { |
| public: |
| ZipArchiveHandle handle; |
| void SetUp() { |
| ASSERT_EQ(OpenArchive(ZippedBugreportGenerationTest::getZipFilePath().c_str(), &handle), 0); |
| } |
| void TearDown() { |
| CloseArchive(handle); |
| } |
| |
| void FileExists(const char* filename, uint32_t minsize, |
| uint32_t maxsize = std::numeric_limits<uint32_t>::max()) { |
| ZipEntry entry; |
| GetEntry(handle, filename, &entry); |
| EXPECT_GT(entry.uncompressed_length, minsize); |
| EXPECT_LT(entry.uncompressed_length, maxsize); |
| } |
| }; |
| |
| TEST_F(ZippedBugReportContentsTest, ContainsMainEntry) { |
| ZipEntry main_entry; |
| // contains main entry name file |
| GetEntry(handle, "main_entry.txt", &main_entry); |
| |
| std::string bugreport_txt_name; |
| bugreport_txt_name.resize(main_entry.uncompressed_length); |
| ExtractToMemory(handle, &main_entry, reinterpret_cast<uint8_t*>(bugreport_txt_name.data()), |
| main_entry.uncompressed_length); |
| |
| // contains main entry file |
| FileExists(bugreport_txt_name.c_str(), 1000000U); |
| } |
| |
| TEST_F(ZippedBugReportContentsTest, ContainsVersion) { |
| ZipEntry entry; |
| // contains main entry name file |
| GetEntry(handle, "version.txt", &entry); |
| |
| char* buf = new char[entry.uncompressed_length + 1]; |
| ExtractToMemory(handle, &entry, (uint8_t*)buf, entry.uncompressed_length); |
| buf[entry.uncompressed_length] = 0; |
| EXPECT_STREQ(buf, ZippedBugreportGenerationTest::ds.version_.c_str()); |
| delete[] buf; |
| } |
| |
| TEST_F(ZippedBugReportContentsTest, ContainsBoardSpecificFiles) { |
| // TODO(b/160109027): cf_x86_phone-userdebug does not dump them. |
| // FileExists("dumpstate_board.bin", 1000000U, 80000000U); |
| // FileExists("dumpstate_board.txt", 100000U, 1000000U); |
| } |
| |
| TEST_F(ZippedBugReportContentsTest, ContainsProtoFile) { |
| FileExists("proto/activity.proto", 100000U, 1000000U); |
| } |
| |
| // Spot check on some files pulled from the file system |
| TEST_F(ZippedBugReportContentsTest, ContainsSomeFileSystemFiles) { |
| // FS/proc/*/mountinfo size > 0 |
| FileExists("FS/proc/1/mountinfo", 0U, 100000U); |
| |
| // FS/data/misc/profiles/cur/0/*/primary.prof size > 0 |
| FileExists("FS/data/misc/profiles/cur/0/com.android.phone/primary.prof", 0U, 100000U); |
| } |
| |
| /** |
| * Runs tests on section data generated by dumpstate and captured by DumpstateListener. |
| */ |
| class BugreportSectionTest : public Test { |
| public: |
| static void SetUpTestCase() { |
| ParseSections(ZippedBugreportGenerationTest::getZipFilePath().c_str(), |
| ZippedBugreportGenerationTest::sections.get()); |
| } |
| |
| int numMatches(const std::string& substring) { |
| int matches = 0; |
| for (auto const& section : *ZippedBugreportGenerationTest::sections) { |
| if (section.name.find(substring) != std::string::npos) { |
| matches++; |
| } |
| } |
| return matches; |
| } |
| |
| void SectionExists(const std::string& sectionName, int minsize) { |
| for (auto const& section : *ZippedBugreportGenerationTest::sections) { |
| if (sectionName == section.name) { |
| EXPECT_GE(section.size_bytes, minsize) << " for section:" << sectionName; |
| return; |
| } |
| } |
| FAIL() << sectionName << " not found."; |
| } |
| }; |
| |
| TEST_F(BugreportSectionTest, Atleast3CriticalDumpsysSectionsGenerated) { |
| int numSections = numMatches("CRITICAL"); |
| EXPECT_GE(numSections, 3); |
| } |
| |
| TEST_F(BugreportSectionTest, Atleast2HighDumpsysSectionsGenerated) { |
| int numSections = numMatches("HIGH"); |
| EXPECT_GE(numSections, 2); |
| } |
| |
| TEST_F(BugreportSectionTest, Atleast50NormalDumpsysSectionsGenerated) { |
| int allSections = ZippedBugreportGenerationTest::sections->size(); |
| int criticalSections = numMatches("CRITICAL"); |
| int highSections = numMatches("HIGH"); |
| int normalSections = allSections - criticalSections - highSections; |
| |
| EXPECT_GE(normalSections, 50) << "Total sections less than 50 (Critical:" << criticalSections |
| << "High:" << highSections << "Normal:" << normalSections << ")"; |
| } |
| |
| // Test if some critical sections are being generated. |
| TEST_F(BugreportSectionTest, CriticalSurfaceFlingerSectionGenerated) { |
| SectionExists("CRITICAL SurfaceFlinger", /* bytes= */ 10000); |
| } |
| |
| TEST_F(BugreportSectionTest, ActivitySectionsGenerated) { |
| SectionExists("CRITICAL activity", /* bytes= */ 5000); |
| SectionExists("activity", /* bytes= */ 10000); |
| } |
| |
| TEST_F(BugreportSectionTest, CpuinfoSectionGenerated) { |
| SectionExists("CRITICAL cpuinfo", /* bytes= */ 1000); |
| } |
| |
| TEST_F(BugreportSectionTest, WindowSectionGenerated) { |
| SectionExists("CRITICAL window", /* bytes= */ 20000); |
| } |
| |
| TEST_F(BugreportSectionTest, ConnectivitySectionsGenerated) { |
| SectionExists("HIGH connectivity", /* bytes= */ 3000); |
| SectionExists("connectivity", /* bytes= */ 5000); |
| } |
| |
| TEST_F(BugreportSectionTest, MeminfoSectionGenerated) { |
| SectionExists("HIGH meminfo", /* bytes= */ 100000); |
| } |
| |
| TEST_F(BugreportSectionTest, BatteryStatsSectionGenerated) { |
| SectionExists("batterystats", /* bytes= */ 1000); |
| } |
| |
| TEST_F(BugreportSectionTest, WifiSectionGenerated) { |
| SectionExists("wifi", /* bytes= */ 100000); |
| } |
| |
| class DumpstateBinderTest : public Test { |
| protected: |
| void SetUp() override { |
| // In case there is a stray service, stop it first. |
| property_set("ctl.stop", "bugreportd"); |
| // dry_run results in a faster bugreport. |
| property_set("dumpstate.dry_run", "true"); |
| // We need to receive some async calls later. Ensure we have binder threads. |
| ProcessState::self()->startThreadPool(); |
| } |
| |
| void TearDown() override { |
| property_set("ctl.stop", "bugreportd"); |
| property_set("dumpstate.dry_run", ""); |
| |
| unlink("/data/local/tmp/tmp.zip"); |
| unlink("/data/local/tmp/tmp.png"); |
| } |
| |
| // Waits until listener gets the callbacks. |
| void WaitTillExecutionComplete(DumpstateListener* listener) { |
| // Wait till one of finished, error or timeout. |
| static const int kBugreportTimeoutSeconds = 120; |
| int i = 0; |
| while (!listener->getIsFinished() && listener->getErrorCode() == -1 && |
| i < kBugreportTimeoutSeconds) { |
| sleep(1); |
| i++; |
| } |
| } |
| }; |
| |
| TEST_F(DumpstateBinderTest, Baseline) { |
| // In the beginning dumpstate binder service is not running. |
| sp<android::os::IDumpstate> ds_binder(GetDumpstateService()); |
| EXPECT_EQ(ds_binder, nullptr); |
| |
| // Start bugreportd, which runs dumpstate binary with -w; which starts dumpstate service |
| // and makes it wait. |
| property_set("dumpstate.dry_run", "true"); |
| property_set("ctl.start", "bugreportd"); |
| |
| // Now we are able to retrieve dumpstate binder service. |
| ds_binder = GetDumpstateService(); |
| EXPECT_NE(ds_binder, nullptr); |
| |
| // Prepare arguments |
| unique_fd bugreport_fd(OpenForWrite("/bugreports/tmp.zip")); |
| unique_fd screenshot_fd(OpenForWrite("/bugreports/tmp.png")); |
| |
| EXPECT_NE(bugreport_fd.get(), -1); |
| EXPECT_NE(screenshot_fd.get(), -1); |
| |
| sp<DumpstateListener> listener(new DumpstateListener(dup(fileno(stdout)))); |
| android::binder::Status status = |
| ds_binder->startBugreport(123, "com.dummy.package", std::move(bugreport_fd), std::move(screenshot_fd), |
| Dumpstate::BugreportMode::BUGREPORT_INTERACTIVE, listener, true); |
| // startBugreport is an async call. Verify binder call succeeded first, then wait till listener |
| // gets expected callbacks. |
| EXPECT_TRUE(status.isOk()); |
| WaitTillExecutionComplete(listener.get()); |
| |
| // Bugreport generation requires user consent, which we cannot get in a test set up, |
| // so instead of getting is_finished_, we are more likely to get a consent error. |
| EXPECT_TRUE( |
| listener->getErrorCode() == IDumpstateListener::BUGREPORT_ERROR_USER_DENIED_CONSENT || |
| listener->getErrorCode() == IDumpstateListener::BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); |
| |
| // The service should have died on its own, freeing itself up for a new invocation. |
| sleep(2); |
| ds_binder = GetDumpstateService(); |
| EXPECT_EQ(ds_binder, nullptr); |
| } |
| |
| TEST_F(DumpstateBinderTest, ServiceDies_OnInvalidInput) { |
| // Start bugreportd, which runs dumpstate binary with -w; which starts dumpstate service |
| // and makes it wait. |
| property_set("ctl.start", "bugreportd"); |
| sp<android::os::IDumpstate> ds_binder(GetDumpstateService()); |
| EXPECT_NE(ds_binder, nullptr); |
| |
| // Prepare arguments |
| unique_fd bugreport_fd(OpenForWrite("/data/local/tmp/tmp.zip")); |
| unique_fd screenshot_fd(OpenForWrite("/data/local/tmp/tmp.png")); |
| |
| EXPECT_NE(bugreport_fd.get(), -1); |
| EXPECT_NE(screenshot_fd.get(), -1); |
| |
| // Call startBugreport with bad arguments. |
| sp<DumpstateListener> listener(new DumpstateListener(dup(fileno(stdout)))); |
| android::binder::Status status = |
| ds_binder->startBugreport(123, "com.dummy.package", std::move(bugreport_fd), std::move(screenshot_fd), |
| 2000, // invalid bugreport mode |
| listener, false); |
| EXPECT_EQ(listener->getErrorCode(), IDumpstateListener::BUGREPORT_ERROR_INVALID_INPUT); |
| |
| // The service should have died, freeing itself up for a new invocation. |
| sleep(2); |
| ds_binder = GetDumpstateService(); |
| EXPECT_EQ(ds_binder, nullptr); |
| } |
| |
| TEST_F(DumpstateBinderTest, SimultaneousBugreportsNotAllowed) { |
| // Start bugreportd, which runs dumpstate binary with -w; which starts dumpstate service |
| // and makes it wait. |
| property_set("dumpstate.dry_run", "true"); |
| property_set("ctl.start", "bugreportd"); |
| sp<android::os::IDumpstate> ds_binder(GetDumpstateService()); |
| EXPECT_NE(ds_binder, nullptr); |
| |
| // Prepare arguments |
| unique_fd bugreport_fd(OpenForWrite("/data/local/tmp/tmp.zip")); |
| unique_fd bugreport_fd2(dup(bugreport_fd.get())); |
| unique_fd screenshot_fd(OpenForWrite("/data/local/tmp/tmp.png")); |
| unique_fd screenshot_fd2(dup(screenshot_fd.get())); |
| |
| EXPECT_NE(bugreport_fd.get(), -1); |
| EXPECT_NE(bugreport_fd2.get(), -1); |
| EXPECT_NE(screenshot_fd.get(), -1); |
| EXPECT_NE(screenshot_fd2.get(), -1); |
| |
| sp<DumpstateListener> listener1(new DumpstateListener(dup(fileno(stdout)))); |
| android::binder::Status status = |
| ds_binder->startBugreport(123, "com.dummy.package", std::move(bugreport_fd), std::move(screenshot_fd), |
| Dumpstate::BugreportMode::BUGREPORT_INTERACTIVE, listener1, true); |
| EXPECT_TRUE(status.isOk()); |
| |
| // try to make another call to startBugreport. This should fail. |
| sp<DumpstateListener> listener2(new DumpstateListener(dup(fileno(stdout)))); |
| status = ds_binder->startBugreport(123, "com.dummy.package", std::move(bugreport_fd2), std::move(screenshot_fd2), |
| Dumpstate::BugreportMode::BUGREPORT_INTERACTIVE, listener2, true); |
| EXPECT_FALSE(status.isOk()); |
| WaitTillExecutionComplete(listener2.get()); |
| EXPECT_EQ(listener2->getErrorCode(), |
| IDumpstateListener::BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); |
| |
| // Meanwhile the first call works as expected. Service should not die in this case. |
| WaitTillExecutionComplete(listener1.get()); |
| |
| // Bugreport generation requires user consent, which we cannot get in a test set up, |
| // so instead of getting is_finished_, we are more likely to get a consent error. |
| EXPECT_TRUE( |
| listener1->getErrorCode() == IDumpstateListener::BUGREPORT_ERROR_USER_DENIED_CONSENT || |
| listener1->getErrorCode() == IDumpstateListener::BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); |
| } |
| |
| } // namespace dumpstate |
| } // namespace os |
| } // namespace android |