blob: 455a4d7b09d5d364a242b9cc3b6d4cd8a55165d8 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/sys/appmgr/storage_watchdog.h"
#include <dirent.h>
#include <fcntl.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <lib/async/cpp/task.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <sys/types.h>
#include <string>
#include <re2/re2.h>
#include <src/lib/files/directory.h>
#include <src/lib/files/path.h>
#include "src/lib/fxl/strings/concatenate.h"
namespace {
namespace fio = fuchsia_io;
// Delete the given dirent inside the openend directory. If the dirent is a
// directory itself, it will be recursively deleted.
void DeleteDirentInFd(int dir_fd, struct dirent* ent) {
if (ent->d_type == DT_DIR) {
int child_dir = openat(dir_fd, ent->d_name, O_DIRECTORY);
if (child_dir == -1) {
return;
}
DIR* child_dir_stream = fdopendir(child_dir);
struct dirent* child_ent;
while ((child_ent = readdir(child_dir_stream))) {
if (strncmp(child_ent->d_name, ".", 2) != 0) {
DeleteDirentInFd(child_dir, child_ent);
}
}
closedir(child_dir_stream);
unlinkat(dir_fd, ent->d_name, AT_REMOVEDIR);
} else {
unlinkat(dir_fd, ent->d_name, 0);
}
}
// PurgeCacheIn will remove elements in cache directories inside dir_fd,
// recurse on any nested realms in dir_fd, and close dir_fd when its done.
void PurgeCacheIn(int dir_fd) {
static const re2::RE2* const kV1StorageDirRegex = new re2::RE2("[^:#]*:[^:#]*:[^:#]*#[^#]*");
static const re2::RE2* const kV2StorageDirRegex = new re2::RE2("(data)|([0-9a-f]{64})");
DIR* dir_stream = fdopendir(dir_fd);
// For all children in the path we're looking at, if it's named "r", then
// it's a child realm that we should walk into. If it's not, it's a
// component's cache that should be cleaned. Note that the path naming logic
// implemented in realm.cc:IsolatedPathForPackage() makes it impossible for
// a component to be named "r".
struct dirent* ent = nullptr;
while ((ent = readdir(dir_stream))) {
if (std::string(ent->d_name) == ".") {
// Don't treat `.` as a component directory to be deleted!
continue;
} else if (re2::RE2::FullMatch(std::string(ent->d_name), *kV1StorageDirRegex) ||
re2::RE2::FullMatch(std::string(ent->d_name), *kV2StorageDirRegex)) {
int component_dir = openat(dir_fd, ent->d_name, O_DIRECTORY);
if (component_dir == -1) {
continue;
}
DIR* component_dir_stream = fdopendir(component_dir);
struct dirent* ent = nullptr;
while ((ent = readdir(component_dir_stream))) {
if (std::string(ent->d_name) != ".") {
DeleteDirentInFd(component_dir, ent);
}
}
closedir(component_dir_stream);
} else {
// This is a container directory such as "r", "children", or <v2_moniker>. Open it and
// recurse.
int r_dir = openat(dir_fd, ent->d_name, O_DIRECTORY);
if (r_dir == -1) {
// We failed to open the directory. Keep going, as we want to delete as
// much as we can!
continue;
}
PurgeCacheIn(r_dir);
}
}
closedir(dir_stream);
}
} // namespace
// GetStorageUsage will return the percentage, from 0 to 100, of used bytes on
// the disk located at this.path_to_watch_.
StorageWatchdog::StorageUsage StorageWatchdog::GetStorageUsage() {
TRACE_DURATION("appmgr", "StorageWatchdog::GetStorageUsage");
fbl::unique_fd fd;
fd.reset(open(path_to_watch_.c_str(), O_RDONLY));
if (!fd) {
FX_LOGS(WARNING) << "storage_watchdog: could not open target: " << path_to_watch_;
return StorageUsage();
}
fuchsia_io::wire::FilesystemInfo info;
fdio_cpp::FdioCaller caller(std::move(fd));
zx_status_t status = GetFilesystemInfo(caller.borrow_channel(), &info);
if (status != ZX_OK) {
FX_LOGS(WARNING) << "storage_watchdog: cannot query filesystem: " << status;
return StorageUsage();
}
info.name[fuchsia_io::wire::kMaxFsNameBuffer - 1] = '\0';
// The number of bytes which may be allocated plus the number of bytes which have been allocated.
// |total_bytes| is the amount of data (not counting metadata like inode storage) that minfs has
// currently allocated from the volume manager, while used_bytes is the amount of those actually
// used for current storage.
size_t total = info.free_shared_pool_bytes + info.total_bytes;
if (total == 0) {
FX_LOGS(WARNING) << "storage_watchdog: unable to determine storage "
<< "pressure";
return StorageUsage();
} else if (total < info.used_bytes) {
FX_LOGS(WARNING) << "storage_watchdog: Usage (" << info.used_bytes
<< ") exceeds reported total (" << total << ")";
}
return StorageUsage{
.avail = total,
.used = info.used_bytes,
};
}
void StorageWatchdog::CheckStorage(async_dispatcher_t* dispatcher, size_t threshold_purge_percent) {
StorageUsage usage = this->GetStorageUsage();
this->bytes_used_.Set(usage.used);
this->bytes_avail_.Set(usage.avail);
if (usage.percent() >= threshold_purge_percent) {
FX_LOGS(INFO) << "storage usage has reached threshold of " << threshold_purge_percent
<< "\%, purging the cache now";
this->PurgeCache();
StorageUsage usage_after = this->GetStorageUsage();
FX_LOGS(INFO) << "cache purge is complete, new storage usage is at " << usage.percent()
<< "\% capacity (" << usage.used << " used, " << usage.avail << " avail)";
if (usage_after.percent() >= threshold_purge_percent) {
FX_LOGS(WARNING) << "usage still exceeds threshold after purge (" << usage.used << " used, "
<< usage.avail << " avail)";
}
}
async::PostDelayedTask(
dispatcher, [this, dispatcher] { this->CheckStorage(dispatcher); }, zx::sec(60));
}
void StorageWatchdog::Run(async_dispatcher_t* dispatcher) {
async::PostTask(dispatcher, [this, dispatcher] { this->CheckStorage(dispatcher); });
}
// PurgeCache will remove cache items from this.path_to_clean_.
void StorageWatchdog::PurgeCache() {
TRACE_DURATION("appmgr", "StorageWatchdog::PurgeCache");
// Walk the directory tree from `path_to_clean_`.
int dir_fd = open(path_to_clean_.c_str(), O_DIRECTORY);
if (dir_fd == -1) {
if (errno == ENOENT) {
FX_LOGS(INFO) << "nothing in cache to purge";
} else {
FX_LOGS(ERROR) << "error opening directory: " << errno;
}
return;
}
PurgeCacheIn(dir_fd);
}
zx_status_t StorageWatchdog::GetFilesystemInfo(zx_handle_t directory,
fuchsia_io::wire::FilesystemInfo* out_info) {
auto result =
fidl::WireCall(fidl::UnownedClientEnd<fuchsia_io::Directory>(directory))->QueryFilesystem();
if (result.ok())
*out_info = *result.value().info;
return !result.ok() ? result.status() : result.value().s;
}