diff --git a/base/cvd/cuttlefish/common/libs/utils/files.cpp b/base/cvd/cuttlefish/common/libs/utils/files.cpp index ad9f4282d7..cbb2755570 100644 --- a/base/cvd/cuttlefish/common/libs/utils/files.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/files.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,8 @@ namespace cuttlefish { +static constexpr char kWhitespaceCharacters[] = " \n\t\r\v\f"; + bool FileExists(const std::string& path, bool follow_symlinks) { struct stat st {}; return (follow_symlinks ? stat : lstat)(path.c_str(), &st) == 0; @@ -226,6 +229,25 @@ Result> DirectoryContents(const std::string& path) { return ret; } +Result> DirectoryContentsByModTimeDesc( + const std::string& path) { + std::vector contents = + CF_EXPECTF(DirectoryContents(path), + "Failure retrieving contents of directory at \"{}\"", path); + + auto not_self_or_parent_directory = [](std::string filename) { + return filename != "." && filename != ".."; + }; + std::vector filtered; + std::copy_if(contents.begin(), contents.end(), std::back_inserter(filtered), + not_self_or_parent_directory); + + std::sort(filtered.begin(), filtered.end(), [](std::string a, std::string b) { + return FileModificationTime(a) > FileModificationTime(b); + }); + return filtered; +} + bool DirectoryExists(const std::string& path, bool follow_symlinks) { struct stat st {}; if ((follow_symlinks ? stat : lstat)(path.c_str(), &st) == -1) { @@ -613,35 +635,40 @@ bool FileIsSocket(const std::string& path) { } // return unit determined by the `--block-size` argument -Result GetDiskUsage(const std::string& path, - const std::string& size_arg) { +Result GetDiskUsage(const std::string& path, + const std::string& size_arg) { Command du_cmd("du"); du_cmd.AddParameter("-s"); // summarize, only output total du_cmd.AddParameter( "--apparent-size"); // apparent size rather than device usage du_cmd.AddParameter("--block-size=" + size_arg); du_cmd.AddParameter(path); - SharedFD read_fd; - SharedFD write_fd; - SharedFD::Pipe(&read_fd, &write_fd); - du_cmd.RedirectStdIO(Subprocess::StdIOChannel::kStdOut, write_fd); - auto subprocess = du_cmd.Start(); - std::array text_output{}; - const auto bytes_read = read_fd->Read(text_output.data(), text_output.size()); - CF_EXPECTF(bytes_read > 0, "Failed to read from pipe: {}", strerror(errno)); - std::move(subprocess).Wait(); - int result; - CF_EXPECTF(android::base::ParseInt(text_output.data(), &result), - "Failure parsing \"{}\" to integer.", text_output.data()); + + std::string out; + std::string err; + int return_code = RunWithManagedStdio(std::move(du_cmd), nullptr, &out, &err); + CF_EXPECT(return_code == 0, "Failed to run `du` command"); + CF_EXPECT(out.empty() == false, "No output read from `du` command"); + std::vector split_out = + android::base::Tokenize(out, kWhitespaceCharacters); + CF_EXPECTF(split_out.empty() == false, + "No valid output read from `du` command in \"{}\"", out); + std::string total = split_out.front(); + + std::size_t result; + CF_EXPECTF(android::base::ParseUint(total, &result), + "Failure parsing \"{}\" to integer.", total); return result; } -Result GetDiskUsageBytes(const std::string& path) { - return GetDiskUsage(path, "1"); +Result GetDiskUsageBytes(const std::string& path) { + return CF_EXPECTF(GetDiskUsage(path, "1"), + "Unable to determine disk usage of file \"{}\"", path); } -Result GetDiskUsageGigabytes(const std::string& path) { - return GetDiskUsage(path, "1G"); +Result GetDiskUsageGigabytes(const std::string& path) { + return CF_EXPECTF(GetDiskUsage(path, "1G"), + "Unable to determine disk usage of file \"{}\"", path); } /** diff --git a/base/cvd/cuttlefish/common/libs/utils/files.h b/base/cvd/cuttlefish/common/libs/utils/files.h index 504297a69f..7e96d6e28d 100644 --- a/base/cvd/cuttlefish/common/libs/utils/files.h +++ b/base/cvd/cuttlefish/common/libs/utils/files.h @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,9 @@ Result MoveDirectoryContents(const std::string& source, const std::string& destination); bool FileHasContent(const std::string& path); Result> DirectoryContents(const std::string& path); +// does not include self-reference or parent reference ("." or "..") +Result> DirectoryContentsByModTimeDesc( + const std::string& path); bool DirectoryExists(const std::string& path, bool follow_symlinks = true); inline bool IsDirectory(const std::string& path) { return DirectoryExists(path); @@ -78,8 +82,8 @@ std::string cpp_basename(const std::string& str); bool FileIsSocket(const std::string& path); // Get disk usage of a path. If this path is a directory, disk usage will // account for all files under this folder(recursively). -Result GetDiskUsageBytes(const std::string& path); -Result GetDiskUsageGigabytes(const std::string& path); +Result GetDiskUsageBytes(const std::string& path); +Result GetDiskUsageGigabytes(const std::string& path); // acloud related API std::string FindImage(const std::string& search_path, diff --git a/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp b/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp index c549a5f07e..56d6f68293 100644 --- a/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/flag_parser.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include +#include #include #include #include @@ -566,6 +568,25 @@ Flag GflagsCompatFlag(const std::string& name, int32_t& value) { return GflagsCompatNumericFlagGeneric(name, value); } +template +static Flag GflagsCompatUnsignedNumericFlagGeneric(const std::string& name, + T& value) { + return GflagsCompatFlag(name) + .Getter([&value]() { return std::to_string(value); }) + .Setter([&value](const FlagMatch& match) -> Result { + T result; + CF_EXPECTF(android::base::ParseUint(match.value, &result), + "Failed to parse \"{}\" as an unsigned integer", + match.value); + value = result; + return {}; + }); +} + +Flag GflagsCompatFlag(const std::string& name, std::size_t& value) { + return GflagsCompatUnsignedNumericFlagGeneric(name, value); +} + Flag GflagsCompatFlag(const std::string& name, bool& value) { return GflagsCompatBoolFlagBase(name) .Getter([&value]() { return fmt::format("{}", value); }) diff --git a/base/cvd/cuttlefish/common/libs/utils/flag_parser.h b/base/cvd/cuttlefish/common/libs/utils/flag_parser.h index 4b33063469..51328c876e 100644 --- a/base/cvd/cuttlefish/common/libs/utils/flag_parser.h +++ b/base/cvd/cuttlefish/common/libs/utils/flag_parser.h @@ -173,6 +173,7 @@ Flag UnexpectedArgumentGuard(); Flag GflagsCompatFlag(const std::string& name); Flag GflagsCompatFlag(const std::string& name, std::string& value); Flag GflagsCompatFlag(const std::string& name, std::int32_t& value); +Flag GflagsCompatFlag(const std::string& name, std::size_t& value); Flag GflagsCompatFlag(const std::string& name, bool& value); Flag GflagsCompatFlag(const std::string& name, std::vector& value); Flag GflagsCompatFlag(const std::string& name, std::vector& value, diff --git a/base/cvd/cuttlefish/common/libs/utils/tee_logging.cpp b/base/cvd/cuttlefish/common/libs/utils/tee_logging.cpp index 42b3c75e57..3010711283 100644 --- a/base/cvd/cuttlefish/common/libs/utils/tee_logging.cpp +++ b/base/cvd/cuttlefish/common/libs/utils/tee_logging.cpp @@ -133,6 +133,18 @@ TeeLogger::TeeLogger(const std::vector& destinations, const std::string& prefix) : destinations_(destinations), prefix_(prefix) {} +ScopedTeeLogger::ScopedTeeLogger(TeeLogger tee_logger) + // Set the android logger to full verbosity, the tee_logger will choose + // whether to write each line. + : scoped_severity_(android::base::VERBOSE) { + old_logger_ = android::base::SetLogger(tee_logger); +} + +ScopedTeeLogger::~ScopedTeeLogger() { + // restore the previous logger + android::base::SetLogger(std::move(old_logger_)); +} + // Copied from system/libbase/logging_splitters.h static std::pair CountSizeAndNewLines(const char* message) { int size = 0; @@ -273,6 +285,15 @@ static std::vector SeverityTargetsForFiles( return log_severities; } +TeeLogger LogToStderr( + const std::string& log_prefix, MetadataLevel stderr_level, + std::optional stderr_severity) { + std::vector log_severities{ + SeverityTarget{stderr_severity ? *stderr_severity : ConsoleSeverity(), + SharedFD::Dup(/* stderr */ 2), stderr_level}}; + return TeeLogger(log_severities, log_prefix); +} + TeeLogger LogToFiles(const std::vector& files, const std::string& log_prefix) { return TeeLogger(SeverityTargetsForFiles(files), log_prefix); diff --git a/base/cvd/cuttlefish/common/libs/utils/tee_logging.h b/base/cvd/cuttlefish/common/libs/utils/tee_logging.h index c9dd4b039f..f8c78bcc1c 100644 --- a/base/cvd/cuttlefish/common/libs/utils/tee_logging.h +++ b/base/cvd/cuttlefish/common/libs/utils/tee_logging.h @@ -62,6 +62,20 @@ class TeeLogger { std::string prefix_; }; +class ScopedTeeLogger { + public: + ScopedTeeLogger(TeeLogger tee_logger); + ~ScopedTeeLogger(); + + private: + android::base::LogFunction old_logger_; + android::base::ScopedLogSeverity scoped_severity_; +}; + +TeeLogger LogToStderr( + const std::string& log_prefix = "", + MetadataLevel stderr_level = MetadataLevel::ONLY_MESSAGE, + std::optional stderr_severity = std::nullopt); TeeLogger LogToFiles(const std::vector& files, const std::string& log_prefix = ""); TeeLogger LogToStderrAndFiles( diff --git a/base/cvd/cuttlefish/host/commands/cvd/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/BUILD.bazel index b3715ac647..47287ea0a2 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/BUILD.bazel @@ -62,6 +62,7 @@ cc_library( "cli/commands/acloud_mixsuperimage.cpp", "cli/commands/acloud_translator.cpp", "cli/commands/bugreport.cpp", + "cli/commands/cache.cpp", "cli/commands/clear.cpp", "cli/commands/cmd_list.cpp", "cli/commands/create.cpp", @@ -154,6 +155,7 @@ cc_library( "cli/commands/acloud_mixsuperimage.h", "cli/commands/acloud_translator.h", "cli/commands/bugreport.h", + "cli/commands/cache.h", "cli/commands/clear.h", "cli/commands/cmd_list.h", "cli/commands/create.h", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.cpp new file mode 100644 index 0000000000..6021469c92 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 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 "host/commands/cvd/cli/commands/cache.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "common/libs/utils/files.h" +#include "common/libs/utils/flag_parser.h" +#include "common/libs/utils/result.h" +#include "common/libs/utils/tee_logging.h" +#include "host/commands/cvd/cli/commands/server_handler.h" +#include "host/commands/cvd/cli/types.h" +#include "host/commands/cvd/utils/common.h" + +namespace cuttlefish { + +namespace { +constexpr int kDefaultCacheSizeGigabytes = 25; + +constexpr char kDetailedHelpText[] = + R"(usage: cvd cache [...] + +Example usage: + cvd cache where - display the filepath of the cache + cvd cache size - display the approximate size of the cache + cvd cache cleanup - caps the cache at the default size + cvd cache cleanup --allowed_size_GB= - caps the cache at the given size + cvd cache empty - wipes out all files in the cache directory + +**Notes**: + - size and cleanup round the cache size up to the nearest gigabyte for calculating + - cleanup uses last modification time to remove oldest files first +)"; + +constexpr char kSummaryHelpText[] = "Used to manage the files cached by cvd"; + +enum class Action { + Cleanup, + Empty, + Size, + Where, +}; + +struct CacheArguments { + Action action = Action::Where; + std::size_t allowed_size_GB = kDefaultCacheSizeGigabytes; +}; + +Result ToAction(const std::string& key) { + const std::unordered_map kMapping{ + {"cleanup", Action::Cleanup}, + {"empty", Action::Empty}, + {"size", Action::Size}, + {"where", Action::Where}, + }; + auto lookup = kMapping.find(key); + CF_EXPECTF(lookup != kMapping.end(), "Unable to find action \"{}\"", key); + return lookup->second; +} + +Result ProcessArguments( + const std::vector& subcommand_arguments) { + CF_EXPECT(!subcommand_arguments.empty(), + "cvd cache requires at least an action argument. Run `cvd help " + "cache` for details."); + std::vector cache_arguments = subcommand_arguments; + std::string action = cache_arguments.front(); + cache_arguments.erase(cache_arguments.begin()); + CacheArguments result{ + .action = CF_EXPECTF(ToAction(action), + "Provided \"{}\" is not a valid cache action. (Is " + "there a non-selector flag before the action?)", + action), + }; + + std::vector flags; + flags.emplace_back(GflagsCompatFlag("allowed_size_GB", result.allowed_size_GB) + .Help("Allowed size of the cache during cleanup " + "operation, in gigabytes.")); + flags.emplace_back(UnexpectedArgumentGuard()); + CF_EXPECTF(ConsumeFlags(flags, cache_arguments), + "Failure processing arguments and flags: cvd cache {} {}", action, + fmt::join(cache_arguments, " ")); + + return result; +} + +Result RunCleanup(const std::string& cache_directory, + const std::size_t allowed_size_GB) { + std::size_t cache_size = CF_EXPECT(GetDiskUsageGigabytes(cache_directory)); + // Descending because elements are removed from the back + std::vector cache_files = + CF_EXPECT(DirectoryContentsByModTimeDesc(cache_directory)); + std::string next; + while (cache_size > allowed_size_GB) { + CF_EXPECTF(!cache_files.empty(), + "No more files found for deletion, but approximate cache size " + "of {}GB is still larger than allowed {}GB", + cache_size, allowed_size_GB); + next = cache_files.back(); + cache_files.pop_back(); + LOG(DEBUG) << fmt::format("Deleting \"{}\" for cleanup", next); + // handles removal of non-directory top-level files as well + CF_EXPECT(RecursivelyRemoveDirectory( + fmt::format("{}/{}", cache_directory, next))); + cache_size = CF_EXPECT(GetDiskUsageGigabytes(cache_directory)); + } + LOG(INFO) << fmt::format("Cache at \"{}\": ~{}GB of allowed {}GB", + cache_directory, cache_size, allowed_size_GB); + return {}; +} + +Result RunEmpty(const std::string& cache_directory) { + CF_EXPECT(RecursivelyRemoveDirectory(cache_directory)); + CF_EXPECT(EnsureDirectoryExists(cache_directory)); + LOG(INFO) << fmt::format("Cache at \"{}\" has been emptied", cache_directory); + return {}; +} + +Result RunSize(const std::string& cache_directory) { + std::size_t cache_size = CF_EXPECT(GetDiskUsageGigabytes(cache_directory)); + LOG(INFO) << fmt::format("Cache at \"{}\": ~{}GB", cache_directory, + cache_size); + return {}; +} + +void RunWhere(std::string_view cache_directory) { + LOG(INFO) << fmt::format("Cache located at: {}", cache_directory); +} + +class CvdCacheCommandHandler : public CvdServerHandler { + public: + Result Handle(const CommandRequest& request) override; + cvd_common::Args CmdList() const override { return {"cache"}; } + Result SummaryHelp() const override; + bool ShouldInterceptHelp() const override { return true; } + Result DetailedHelp(std::vector&) const override; +}; + +Result CvdCacheCommandHandler::Handle(const CommandRequest& request) { + CF_EXPECT(CanHandle(request)); + auto logger = ScopedTeeLogger(LogToStderr()); + + CacheArguments arguments = + CF_EXPECT(ProcessArguments(request.SubcommandArguments())); + std::string cache_directory = PerUserCacheDir(); + CF_EXPECT(EnsureDirectoryExists(cache_directory)); + switch (arguments.action) { + case Action::Cleanup: + CF_EXPECTF(RunCleanup(cache_directory, arguments.allowed_size_GB), + "Error capping cache at {} to {}GB", cache_directory, + arguments.allowed_size_GB); + break; + case Action::Empty: + CF_EXPECTF(RunEmpty(cache_directory), "Error emptying cache at {}", + cache_directory); + break; + case Action::Size: + CF_EXPECTF(RunSize(cache_directory), + "Error determining size of cache at {}", cache_directory); + break; + case Action::Where: + RunWhere(cache_directory); + break; + } + + return {}; +} + +Result CvdCacheCommandHandler::SummaryHelp() const { + return kSummaryHelpText; +} + +Result CvdCacheCommandHandler::DetailedHelp( + std::vector&) const { + return kDetailedHelpText; +} + +} // namespace + +std::unique_ptr NewCvdCacheCommandHandler() { + return std::unique_ptr(new CvdCacheCommandHandler()); +} + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.h new file mode 100644 index 0000000000..053aa1841e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/cache.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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. + */ + +#pragma once + +#include + +#include "host/commands/cvd/cli/commands/server_handler.h" + +namespace cuttlefish { + +std::unique_ptr NewCvdCacheCommandHandler(); + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp index 935fd84cc8..f612c5ea9a 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp @@ -23,12 +23,11 @@ #include "common/libs/utils/result.h" #include "host/commands/cvd/cli/command_request.h" #include "host/commands/cvd/cli/command_sequence.h" -#include "host/commands/cvd/instances/instance_lock.h" -#include "host/commands/cvd/instances/instance_manager.h" #include "host/commands/cvd/cli/commands/acloud_command.h" #include "host/commands/cvd/cli/commands/acloud_mixsuperimage.h" #include "host/commands/cvd/cli/commands/acloud_translator.h" #include "host/commands/cvd/cli/commands/bugreport.h" +#include "host/commands/cvd/cli/commands/cache.h" #include "host/commands/cvd/cli/commands/clear.h" #include "host/commands/cvd/cli/commands/cmd_list.h" #include "host/commands/cvd/cli/commands/create.h" @@ -51,6 +50,8 @@ #include "host/commands/cvd/cli/commands/stop.h" #include "host/commands/cvd/cli/commands/try_acloud.h" #include "host/commands/cvd/cli/commands/version.h" +#include "host/commands/cvd/instances/instance_lock.h" +#include "host/commands/cvd/instances/instance_manager.h" namespace cuttlefish { @@ -63,6 +64,7 @@ RequestContext::RequestContext( request_handlers_.emplace_back(NewAcloudCommand(command_sequence_executor_)); request_handlers_.emplace_back(NewAcloudMixSuperImageCommand()); request_handlers_.emplace_back(NewAcloudTranslatorCommand(instance_manager_)); + request_handlers_.emplace_back(NewCvdCacheCommandHandler()); request_handlers_.emplace_back( NewCvdCmdlistHandler(command_sequence_executor_)); request_handlers_.emplace_back(NewCvdCreateCommandHandler( diff --git a/base/cvd/cuttlefish/host/commands/cvd/fetch/fetch_cvd.cc b/base/cvd/cuttlefish/host/commands/cvd/fetch/fetch_cvd.cc index 1debc30d38..e9991f0079 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/fetch/fetch_cvd.cc +++ b/base/cvd/cuttlefish/host/commands/cvd/fetch/fetch_cvd.cc @@ -369,7 +369,7 @@ Result> GetBuildApi(const BuildApiFlags& flags) { : CF_EXPECT(GetCredentialSourceFromFlags(*retrying_http_client, flags, oauth_filepath)); - const auto cache_base_path = PerUserDir() + "/cache"; + const auto cache_base_path = PerUserCacheDir(); std::unique_ptr cas_downloader = nullptr; Result> result = @@ -981,22 +981,10 @@ Result FetchCvdMain(int argc, char** argv) { MetadataLevel metadata_level = isatty(0) ? MetadataLevel::ONLY_MESSAGE : MetadataLevel::FULL; - auto old_logger = android::base::SetLogger( + auto logger = ScopedTeeLogger( LogToStderrAndFiles({log_file}, "", metadata_level, flags.verbosity)); - // Set the android logger to full verbosity, the tee logger will choose - // whether to write each line. - auto old_severity = - android::base::SetMinimumLogSeverity(android::base::VERBOSE); + CF_EXPECT(Fetch(flags, host_target, targets)); - auto fetch_res = Fetch(flags, host_target, targets); - - // This function is no longer only called direcly from a main function, so the - // previous logger must be restored. This also ensures logs from other - // components don't land in fetch.log. - android::base::SetLogger(std::move(old_logger)); - android::base::SetMinimumLogSeverity(old_severity); - - CF_EXPECT(std::move(fetch_res)); return {}; } diff --git a/base/cvd/cuttlefish/host/commands/cvd/utils/common.cpp b/base/cvd/cuttlefish/host/commands/cvd/utils/common.cpp index 3775dc216a..3cccdfc5ef 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/utils/common.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/utils/common.cpp @@ -141,8 +141,10 @@ std::string CvdDir() { return "/tmp/cvd"; } -std::string PerUserDir() { - return fmt::format("/tmp/cvd/{}", getuid()); +std::string PerUserDir() { return fmt::format("{}/{}", CvdDir(), getuid()); } + +std::string PerUserCacheDir() { + return fmt::format("{}/{}/cache", CvdDir(), getuid()); } std::string InstanceDatabasePath() { diff --git a/base/cvd/cuttlefish/host/commands/cvd/utils/common.h b/base/cvd/cuttlefish/host/commands/cvd/utils/common.h index 9f4d2956de..a5d8b3f0f2 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/utils/common.h +++ b/base/cvd/cuttlefish/host/commands/cvd/utils/common.h @@ -77,6 +77,8 @@ std::string CvdDir(); std::string PerUserDir(); +std::string PerUserCacheDir(); + std::string InstanceDatabasePath(); std::string DefaultBaseDir();