Home | History | Annotate | Download | only in storage
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 #define DEBUG false  // STOPSHIP if true
     18 #include "Log.h"
     19 
     20 #include "android-base/stringprintf.h"
     21 #include "guardrail/StatsdStats.h"
     22 #include "storage/StorageManager.h"
     23 #include "stats_log_util.h"
     24 
     25 #include <android-base/file.h>
     26 #include <dirent.h>
     27 #include <private/android_filesystem_config.h>
     28 #include <fstream>
     29 #include <iostream>
     30 
     31 namespace android {
     32 namespace os {
     33 namespace statsd {
     34 
     35 using android::util::FIELD_COUNT_REPEATED;
     36 using android::util::FIELD_TYPE_MESSAGE;
     37 using std::map;
     38 
     39 /**
     40  * NOTE: these directories are protected by SELinux, any changes here must also update
     41  * the SELinux policies.
     42  */
     43 #define STATS_DATA_DIR "/data/misc/stats-data"
     44 #define STATS_SERVICE_DIR "/data/misc/stats-service"
     45 #define TRAIN_INFO_DIR "/data/misc/train-info"
     46 #define TRAIN_INFO_PATH "/data/misc/train-info/train-info.bin"
     47 
     48 // Magic word at the start of the train info file, change this if changing the file format
     49 const uint32_t TRAIN_INFO_FILE_MAGIC = 0xff7447ff;
     50 
     51 // for ConfigMetricsReportList
     52 const int FIELD_ID_REPORTS = 2;
     53 
     54 std::mutex StorageManager::sTrainInfoMutex;
     55 
     56 using android::base::StringPrintf;
     57 using std::unique_ptr;
     58 
     59 struct FileName {
     60     int64_t mTimestampSec;
     61     int mUid;
     62     int64_t mConfigId;
     63     bool mIsHistory;
     64     string getFullFileName(const char* path) {
     65         return StringPrintf("%s/%lld_%d_%lld%s", path, (long long)mTimestampSec, (int)mUid,
     66                             (long long)mConfigId, (mIsHistory ? "_history" : ""));
     67     };
     68 };
     69 
     70 string StorageManager::getDataFileName(long wallClockSec, int uid, int64_t id) {
     71     return StringPrintf("%s/%ld_%d_%lld", STATS_DATA_DIR, wallClockSec, uid,
     72                         (long long)id);
     73 }
     74 
     75 string StorageManager::getDataHistoryFileName(long wallClockSec, int uid, int64_t id) {
     76     return StringPrintf("%s/%ld_%d_%lld_history", STATS_DATA_DIR, wallClockSec, uid,
     77                         (long long)id);
     78 }
     79 
     80 // Returns array of int64_t which contains timestamp in seconds, uid,
     81 // configID and whether the file is a local history file.
     82 static void parseFileName(char* name, FileName* output) {
     83     int64_t result[3];
     84     int index = 0;
     85     char* substr = strtok(name, "_");
     86     while (substr != nullptr && index < 3) {
     87         result[index] = StrToInt64(substr);
     88         index++;
     89         substr = strtok(nullptr, "_");
     90     }
     91     // When index ends before hitting 3, file name is corrupted. We
     92     // intentionally put -1 at index 0 to indicate the error to caller.
     93     // TODO(b/110563137): consider removing files with unexpected name format.
     94     if (index < 3) {
     95         result[0] = -1;
     96     }
     97 
     98     output->mTimestampSec = result[0];
     99     output->mUid = result[1];
    100     output->mConfigId = result[2];
    101     // check if the file is a local history.
    102     output->mIsHistory = (substr != nullptr && strcmp("history", substr) == 0);
    103 }
    104 
    105 void StorageManager::writeFile(const char* file, const void* buffer, int numBytes) {
    106     int fd = open(file, O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR);
    107     if (fd == -1) {
    108         VLOG("Attempt to access %s but failed", file);
    109         return;
    110     }
    111     trimToFit(STATS_SERVICE_DIR);
    112     trimToFit(STATS_DATA_DIR);
    113 
    114     if (android::base::WriteFully(fd, buffer, numBytes)) {
    115         VLOG("Successfully wrote %s", file);
    116     } else {
    117         ALOGE("Failed to write %s", file);
    118     }
    119 
    120     int result = fchown(fd, AID_STATSD, AID_STATSD);
    121     if (result) {
    122         VLOG("Failed to chown %s to statsd", file);
    123     }
    124 
    125     close(fd);
    126 }
    127 
    128 bool StorageManager::writeTrainInfo(int64_t trainVersionCode, const std::string& trainName,
    129                                     int32_t status, const std::vector<int64_t>& experimentIds) {
    130     std::lock_guard<std::mutex> lock(sTrainInfoMutex);
    131 
    132     deleteAllFiles(TRAIN_INFO_DIR);
    133 
    134     int fd = open(TRAIN_INFO_PATH, O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR);
    135     if (fd == -1) {
    136         VLOG("Attempt to access %s but failed", TRAIN_INFO_PATH);
    137         return false;
    138     }
    139 
    140     size_t result;
    141 
    142     // Write the magic word
    143     result = write(fd, &TRAIN_INFO_FILE_MAGIC, sizeof(TRAIN_INFO_FILE_MAGIC));
    144     if (result != sizeof(TRAIN_INFO_FILE_MAGIC)) {
    145         VLOG("Failed to wrtie train info magic");
    146         close(fd);
    147         return false;
    148     }
    149 
    150     // Write the train version
    151     const size_t trainVersionCodeByteCount = sizeof(trainVersionCode);
    152     result = write(fd, &trainVersionCode, trainVersionCodeByteCount);
    153     if (result != trainVersionCodeByteCount) {
    154         VLOG("Failed to wrtie train version code");
    155         close(fd);
    156         return false;
    157     }
    158 
    159     // Write # of bytes in trainName to file
    160     const size_t trainNameSize = trainName.size();
    161     const size_t trainNameSizeByteCount = sizeof(trainNameSize);
    162     result = write(fd, (uint8_t*)&trainNameSize, trainNameSizeByteCount);
    163     if (result != trainNameSizeByteCount) {
    164         VLOG("Failed to write train name size");
    165         close(fd);
    166         return false;
    167     }
    168 
    169     // Write trainName to file
    170     result = write(fd, trainName.c_str(), trainNameSize);
    171     if (result != trainNameSize) {
    172         VLOG("Failed to write train name");
    173         close(fd);
    174         return false;
    175     }
    176 
    177     // Write status to file
    178     const size_t statusByteCount = sizeof(status);
    179     result = write(fd, (uint8_t*)&status, statusByteCount);
    180     if (result != statusByteCount) {
    181         VLOG("Failed to write status");
    182         close(fd);
    183         return false;
    184     }
    185 
    186     // Write experiment id count to file.
    187     const size_t experimentIdsCount = experimentIds.size();
    188     const size_t experimentIdsCountByteCount = sizeof(experimentIdsCount);
    189     result = write(fd, (uint8_t*) &experimentIdsCount, experimentIdsCountByteCount);
    190     if (result != experimentIdsCountByteCount) {
    191         VLOG("Failed to write experiment id count");
    192         close(fd);
    193         return false;
    194     }
    195 
    196     // Write experimentIds to file
    197     for (size_t i = 0; i < experimentIdsCount; i++) {
    198         const int64_t experimentId = experimentIds[i];
    199         const size_t experimentIdByteCount = sizeof(experimentId);
    200         result = write(fd, &experimentId, experimentIdByteCount);
    201         if (result == experimentIdByteCount) {
    202             VLOG("Successfully wrote experiment IDs");
    203         } else {
    204             VLOG("Failed to write experiment ids");
    205             close(fd);
    206             return false;
    207         }
    208     }
    209 
    210     result = fchown(fd, AID_STATSD, AID_STATSD);
    211     if (result) {
    212         VLOG("Failed to chown train info file to statsd");
    213         close(fd);
    214         return false;
    215     }
    216 
    217     close(fd);
    218     return true;
    219 }
    220 
    221 bool StorageManager::readTrainInfo(InstallTrainInfo& trainInfo) {
    222     std::lock_guard<std::mutex> lock(sTrainInfoMutex);
    223 
    224     int fd = open(TRAIN_INFO_PATH, O_RDONLY | O_CLOEXEC);
    225     if (fd == -1) {
    226         VLOG("Failed to open train-info.bin");
    227         return false;
    228     }
    229 
    230     // Read the magic word
    231     uint32_t magic;
    232     size_t result = read(fd, &magic, sizeof(magic));
    233     if (result != sizeof(magic)) {
    234         VLOG("Failed to read train info magic");
    235         close(fd);
    236         return false;
    237     }
    238 
    239     if (magic != TRAIN_INFO_FILE_MAGIC) {
    240         VLOG("Train info magic was 0x%08x, expected 0x%08x", magic, TRAIN_INFO_FILE_MAGIC);
    241         close(fd);
    242         return false;
    243     }
    244 
    245     // Read the train version code
    246     const size_t trainVersionCodeByteCount(sizeof(trainInfo.trainVersionCode));
    247     result = read(fd, &trainInfo.trainVersionCode, trainVersionCodeByteCount);
    248     if (result != trainVersionCodeByteCount) {
    249         VLOG("Failed to read train version code from train info file");
    250         close(fd);
    251         return false;
    252     }
    253 
    254     // Read # of bytes taken by trainName in the file.
    255     size_t trainNameSize;
    256     result = read(fd, &trainNameSize, sizeof(size_t));
    257     if (result != sizeof(size_t)) {
    258         VLOG("Failed to read train name size from train info file");
    259         close(fd);
    260         return false;
    261     }
    262 
    263     // Read trainName
    264     trainInfo.trainName.resize(trainNameSize);
    265     result = read(fd, trainInfo.trainName.data(), trainNameSize);
    266     if (result != trainNameSize) {
    267         VLOG("Failed to read train name from train info file");
    268         close(fd);
    269         return false;
    270     }
    271 
    272     // Read status
    273     const size_t statusByteCount = sizeof(trainInfo.status);
    274     result = read(fd, &trainInfo.status, statusByteCount);
    275     if (result != statusByteCount) {
    276         VLOG("Failed to read train status from train info file");
    277         close(fd);
    278         return false;
    279     }
    280 
    281     // Read experiment ids count.
    282     size_t experimentIdsCount;
    283     result = read(fd, &experimentIdsCount, sizeof(size_t));
    284     if (result != sizeof(size_t)) {
    285         VLOG("Failed to read train experiment id count from train info file");
    286         close(fd);
    287         return false;
    288     }
    289 
    290     // Read experimentIds
    291     for (size_t i = 0; i < experimentIdsCount; i++) {
    292         int64_t experimentId;
    293         result = read(fd, &experimentId, sizeof(experimentId));
    294         if (result != sizeof(experimentId)) {
    295             VLOG("Failed to read train experiment id from train info file");
    296             close(fd);
    297             return false;
    298         }
    299         trainInfo.experimentIds.push_back(experimentId);
    300     }
    301 
    302     // Expect to be at EOF.
    303     char c;
    304     result = read(fd, &c, 1);
    305     if (result != 0) {
    306         VLOG("Failed to read train info from file. Did not get expected EOF.");
    307         close(fd);
    308         return false;
    309     }
    310 
    311     VLOG("Read train info file successful");
    312     close(fd);
    313     return true;
    314 }
    315 
    316 void StorageManager::deleteFile(const char* file) {
    317     if (remove(file) != 0) {
    318         VLOG("Attempt to delete %s but is not found", file);
    319     } else {
    320         VLOG("Successfully deleted %s", file);
    321     }
    322 }
    323 
    324 void StorageManager::deleteAllFiles(const char* path) {
    325     unique_ptr<DIR, decltype(&closedir)> dir(opendir(path), closedir);
    326     if (dir == NULL) {
    327         VLOG("Directory does not exist: %s", path);
    328         return;
    329     }
    330 
    331     dirent* de;
    332     while ((de = readdir(dir.get()))) {
    333         char* name = de->d_name;
    334         if (name[0] == '.') continue;
    335         deleteFile(StringPrintf("%s/%s", path, name).c_str());
    336     }
    337 }
    338 
    339 void StorageManager::deleteSuffixedFiles(const char* path, const char* suffix) {
    340     unique_ptr<DIR, decltype(&closedir)> dir(opendir(path), closedir);
    341     if (dir == NULL) {
    342         VLOG("Directory does not exist: %s", path);
    343         return;
    344     }
    345 
    346     dirent* de;
    347     while ((de = readdir(dir.get()))) {
    348         char* name = de->d_name;
    349         if (name[0] == '.') {
    350             continue;
    351         }
    352         size_t nameLen = strlen(name);
    353         size_t suffixLen = strlen(suffix);
    354         if (suffixLen <= nameLen && strncmp(name + nameLen - suffixLen, suffix, suffixLen) == 0) {
    355             deleteFile(StringPrintf("%s/%s", path, name).c_str());
    356         }
    357     }
    358 }
    359 
    360 void StorageManager::sendBroadcast(const char* path,
    361                                    const std::function<void(const ConfigKey&)>& sendBroadcast) {
    362     unique_ptr<DIR, decltype(&closedir)> dir(opendir(path), closedir);
    363     if (dir == NULL) {
    364         VLOG("no stats-data directory on disk");
    365         return;
    366     }
    367 
    368     dirent* de;
    369     while ((de = readdir(dir.get()))) {
    370         char* name = de->d_name;
    371         if (name[0] == '.') continue;
    372         VLOG("file %s", name);
    373 
    374         FileName output;
    375         parseFileName(name, &output);
    376         if (output.mTimestampSec == -1 || output.mIsHistory) continue;
    377         sendBroadcast(ConfigKey((int)output.mUid, output.mConfigId));
    378     }
    379 }
    380 
    381 bool StorageManager::hasConfigMetricsReport(const ConfigKey& key) {
    382     unique_ptr<DIR, decltype(&closedir)> dir(opendir(STATS_DATA_DIR), closedir);
    383     if (dir == NULL) {
    384         VLOG("Path %s does not exist", STATS_DATA_DIR);
    385         return false;
    386     }
    387 
    388     string suffix = StringPrintf("%d_%lld", key.GetUid(), (long long)key.GetId());
    389 
    390     dirent* de;
    391     while ((de = readdir(dir.get()))) {
    392         char* name = de->d_name;
    393         if (name[0] == '.') continue;
    394 
    395         size_t nameLen = strlen(name);
    396         size_t suffixLen = suffix.length();
    397         if (suffixLen <= nameLen &&
    398             strncmp(name + nameLen - suffixLen, suffix.c_str(), suffixLen) == 0) {
    399             // Check again that the file name is parseable.
    400             FileName output;
    401             parseFileName(name, &output);
    402             if (output.mTimestampSec == -1 || output.mIsHistory) continue;
    403             return true;
    404         }
    405     }
    406     return false;
    407 }
    408 
    409 void StorageManager::appendConfigMetricsReport(const ConfigKey& key, ProtoOutputStream* proto,
    410                                                bool erase_data, bool isAdb) {
    411     unique_ptr<DIR, decltype(&closedir)> dir(opendir(STATS_DATA_DIR), closedir);
    412     if (dir == NULL) {
    413         VLOG("Path %s does not exist", STATS_DATA_DIR);
    414         return;
    415     }
    416 
    417     dirent* de;
    418     while ((de = readdir(dir.get()))) {
    419         char* name = de->d_name;
    420         string fileName(name);
    421         if (name[0] == '.') continue;
    422         FileName output;
    423         parseFileName(name, &output);
    424 
    425         if (output.mTimestampSec == -1 || (output.mIsHistory && !isAdb) ||
    426             output.mUid != key.GetUid() || output.mConfigId != key.GetId()) {
    427             continue;
    428         }
    429 
    430         auto fullPathName = StringPrintf("%s/%s", STATS_DATA_DIR, fileName.c_str());
    431         int fd = open(fullPathName.c_str(), O_RDONLY | O_CLOEXEC);
    432         if (fd != -1) {
    433             string content;
    434             if (android::base::ReadFdToString(fd, &content)) {
    435                 proto->write(FIELD_TYPE_MESSAGE | FIELD_COUNT_REPEATED | FIELD_ID_REPORTS,
    436                              content.c_str(), content.size());
    437             }
    438             close(fd);
    439         } else {
    440             ALOGE("file cannot be opened");
    441         }
    442 
    443         if (erase_data) {
    444             remove(fullPathName.c_str());
    445         } else if (!output.mIsHistory && !isAdb) {
    446             // This means a real data owner has called to get this data. But the config says it
    447             // wants to keep a local history. So now this file must be renamed as a history file.
    448             // So that next time, when owner calls getData() again, this data won't be uploaded
    449             // again. rename returns 0 on success
    450             if (rename(fullPathName.c_str(), (fullPathName + "_history").c_str())) {
    451                 ALOGE("Failed to rename file %s", fullPathName.c_str());
    452             }
    453         }
    454     }
    455 }
    456 
    457 bool StorageManager::readFileToString(const char* file, string* content) {
    458     int fd = open(file, O_RDONLY | O_CLOEXEC);
    459     bool res = false;
    460     if (fd != -1) {
    461         if (android::base::ReadFdToString(fd, content)) {
    462             res = true;
    463         } else {
    464             VLOG("Failed to read file %s\n", file);
    465         }
    466         close(fd);
    467     }
    468     return res;
    469 }
    470 
    471 void StorageManager::readConfigFromDisk(map<ConfigKey, StatsdConfig>& configsMap) {
    472     unique_ptr<DIR, decltype(&closedir)> dir(opendir(STATS_SERVICE_DIR), closedir);
    473     if (dir == NULL) {
    474         VLOG("no default config on disk");
    475         return;
    476     }
    477     trimToFit(STATS_SERVICE_DIR);
    478 
    479     dirent* de;
    480     while ((de = readdir(dir.get()))) {
    481         char* name = de->d_name;
    482         if (name[0] == '.') continue;
    483 
    484         FileName output;
    485         parseFileName(name, &output);
    486         if (output.mTimestampSec == -1) continue;
    487         string file_name = output.getFullFileName(STATS_SERVICE_DIR);
    488         int fd = open(file_name.c_str(), O_RDONLY | O_CLOEXEC);
    489         if (fd != -1) {
    490             string content;
    491             if (android::base::ReadFdToString(fd, &content)) {
    492                 StatsdConfig config;
    493                 if (config.ParseFromString(content)) {
    494                     configsMap[ConfigKey(output.mUid, output.mConfigId)] = config;
    495                     VLOG("map key uid=%lld|configID=%lld", (long long)output.mUid,
    496                          (long long)output.mConfigId);
    497                 }
    498             }
    499             close(fd);
    500         }
    501     }
    502 }
    503 
    504 bool StorageManager::readConfigFromDisk(const ConfigKey& key, StatsdConfig* config) {
    505     string content;
    506     return config != nullptr &&
    507         StorageManager::readConfigFromDisk(key, &content) && config->ParseFromString(content);
    508 }
    509 
    510 bool StorageManager::readConfigFromDisk(const ConfigKey& key, string* content) {
    511     unique_ptr<DIR, decltype(&closedir)> dir(opendir(STATS_SERVICE_DIR),
    512                                              closedir);
    513     if (dir == NULL) {
    514         VLOG("Directory does not exist: %s", STATS_SERVICE_DIR);
    515         return false;
    516     }
    517 
    518     string suffix = StringPrintf("%d_%lld", key.GetUid(), (long long)key.GetId());
    519     dirent* de;
    520     while ((de = readdir(dir.get()))) {
    521         char* name = de->d_name;
    522         if (name[0] == '.') {
    523             continue;
    524         }
    525         size_t nameLen = strlen(name);
    526         size_t suffixLen = suffix.length();
    527         // There can be at most one file that matches this suffix (config key).
    528         if (suffixLen <= nameLen &&
    529             strncmp(name + nameLen - suffixLen, suffix.c_str(), suffixLen) == 0) {
    530             int fd = open(StringPrintf("%s/%s", STATS_SERVICE_DIR, name).c_str(),
    531                                   O_RDONLY | O_CLOEXEC);
    532             if (fd != -1) {
    533                 if (android::base::ReadFdToString(fd, content)) {
    534                     return true;
    535                 }
    536                 close(fd);
    537             }
    538         }
    539     }
    540     return false;
    541 }
    542 
    543 bool StorageManager::hasIdenticalConfig(const ConfigKey& key,
    544                                         const vector<uint8_t>& config) {
    545     string content;
    546     if (StorageManager::readConfigFromDisk(key, &content)) {
    547         vector<uint8_t> vec(content.begin(), content.end());
    548         if (vec == config) {
    549             return true;
    550         }
    551     }
    552     return false;
    553 }
    554 
    555 void StorageManager::sortFiles(vector<FileInfo>* fileNames) {
    556     // Reverse sort to effectively remove from the back (oldest entries).
    557     // This will sort files in reverse-chronological order. Local history files have lower
    558     // priority than regular data files.
    559     sort(fileNames->begin(), fileNames->end(), [](FileInfo& lhs, FileInfo& rhs) {
    560         // first consider if the file is a local history
    561         if (lhs.mIsHistory && !rhs.mIsHistory) {
    562             return false;
    563         } else if (rhs.mIsHistory && !lhs.mIsHistory) {
    564             return true;
    565         }
    566 
    567         // then consider the age.
    568         if (lhs.mFileAgeSec < rhs.mFileAgeSec) {
    569             return true;
    570         } else if (lhs.mFileAgeSec > rhs.mFileAgeSec) {
    571             return false;
    572         }
    573 
    574         // then good luck.... use string::compare
    575         return lhs.mFileName.compare(rhs.mFileName) > 0;
    576     });
    577 }
    578 
    579 void StorageManager::trimToFit(const char* path) {
    580     unique_ptr<DIR, decltype(&closedir)> dir(opendir(path), closedir);
    581     if (dir == NULL) {
    582         VLOG("Path %s does not exist", path);
    583         return;
    584     }
    585     dirent* de;
    586     int totalFileSize = 0;
    587     vector<FileInfo> fileNames;
    588     auto nowSec = getWallClockSec();
    589     while ((de = readdir(dir.get()))) {
    590         char* name = de->d_name;
    591         if (name[0] == '.') continue;
    592 
    593         FileName output;
    594         parseFileName(name, &output);
    595         if (output.mTimestampSec == -1) continue;
    596         string file_name = output.getFullFileName(path);
    597 
    598         // Check for timestamp and delete if it's too old.
    599         long fileAge = nowSec - output.mTimestampSec;
    600         if (fileAge > StatsdStats::kMaxAgeSecond ||
    601             (output.mIsHistory && fileAge > StatsdStats::kMaxLocalHistoryAgeSecond)) {
    602             deleteFile(file_name.c_str());
    603             continue;
    604         }
    605 
    606         ifstream file(file_name.c_str(), ifstream::in | ifstream::binary);
    607         int fileSize = 0;
    608         if (file.is_open()) {
    609             file.seekg(0, ios::end);
    610             fileSize = file.tellg();
    611             file.close();
    612             totalFileSize += fileSize;
    613         }
    614         fileNames.emplace_back(file_name, output.mIsHistory, fileSize, fileAge);
    615     }
    616 
    617     if (fileNames.size() > StatsdStats::kMaxFileNumber ||
    618         totalFileSize > StatsdStats::kMaxFileSize) {
    619         sortFiles(&fileNames);
    620     }
    621 
    622     // Start removing files from oldest to be under the limit.
    623     while (fileNames.size() > 0 && (fileNames.size() > StatsdStats::kMaxFileNumber ||
    624                                     totalFileSize > StatsdStats::kMaxFileSize)) {
    625         totalFileSize -= fileNames.at(fileNames.size() - 1).mFileSizeBytes;
    626         deleteFile(fileNames.at(fileNames.size() - 1).mFileName.c_str());
    627         fileNames.pop_back();
    628     }
    629 }
    630 
    631 void StorageManager::printStats(int outFd) {
    632     printDirStats(outFd, STATS_SERVICE_DIR);
    633     printDirStats(outFd, STATS_DATA_DIR);
    634 }
    635 
    636 void StorageManager::printDirStats(int outFd, const char* path) {
    637     dprintf(outFd, "Printing stats of %s\n", path);
    638     unique_ptr<DIR, decltype(&closedir)> dir(opendir(path), closedir);
    639     if (dir == NULL) {
    640         VLOG("Path %s does not exist", path);
    641         return;
    642     }
    643     dirent* de;
    644     int fileCount = 0;
    645     int totalFileSize = 0;
    646     while ((de = readdir(dir.get()))) {
    647         char* name = de->d_name;
    648         if (name[0] == '.') {
    649             continue;
    650         }
    651         FileName output;
    652         parseFileName(name, &output);
    653         if (output.mTimestampSec == -1) continue;
    654         dprintf(outFd, "\t #%d, Last updated: %lld, UID: %d, Config ID: %lld, %s", fileCount + 1,
    655                 (long long)output.mTimestampSec, output.mUid, (long long)output.mConfigId,
    656                 (output.mIsHistory ? "local history" : ""));
    657         string file_name = output.getFullFileName(path);
    658         ifstream file(file_name.c_str(), ifstream::in | ifstream::binary);
    659         if (file.is_open()) {
    660             file.seekg(0, ios::end);
    661             int fileSize = file.tellg();
    662             file.close();
    663             dprintf(outFd, ", File Size: %d bytes", fileSize);
    664             totalFileSize += fileSize;
    665         }
    666         dprintf(outFd, "\n");
    667         fileCount++;
    668     }
    669     dprintf(outFd, "\tTotal number of files: %d, Total size of files: %d bytes.\n", fileCount,
    670             totalFileSize);
    671 }
    672 
    673 }  // namespace statsd
    674 }  // namespace os
    675 }  // namespace android
    676