blob: f7c917c0e289d7851375f035e42c521587f8cf60 [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 "arc/apk-cache/cache_cleaner_db.h"
#include <stdint.h>
#include <array>
#include <cinttypes>
#include <iomanip>
#include <set>
#include <tuple>
#include <unordered_set>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <sqlite3.h>
#include "arc/apk-cache/apk_cache_database.h"
#include "arc/apk-cache/apk_cache_utils.h"
#include "arc/apk-cache/cache_cleaner_utils.h"
namespace apk_cache {
// Cache cleaner session source.
constexpr char kCacheCleanerSessionSource[] = "cache_cleaner";
// Maximum age of sessions.
constexpr base::TimeDelta kSessionMaxAge = base::TimeDelta::FromMinutes(10);
// Maximum age of cached files. If a file expires, the whole package will be
// removed.
constexpr base::TimeDelta kValidityPeriod = base::TimeDelta::FromDays(30);
namespace {
// A package is represented by name and version. All file entries with the same
// package name and version code belongs to the same package.
struct Package {
const std::string name;
const int64_t version;
Package(const std::string& name, int64_t version)
: name(name), version(version) {}
};
inline bool operator<(const Package& lhs, const Package& rhs) {
return std::tie(lhs.name, lhs.version) < std::tie(rhs.name, rhs.version);
}
} // namespace
OpaqueFilesCleaner::OpaqueFilesCleaner(const base::FilePath& cache_root)
: cache_root_(cache_root),
db_path_(cache_root.Append(kDatabaseFile)),
files_path_(cache_root.Append(kFilesBase)) {}
OpaqueFilesCleaner::~OpaqueFilesCleaner() = default;
bool OpaqueFilesCleaner::Clean() {
if (!base::DirectoryExists(cache_root_)) {
LOG(ERROR) << "APK cache directory " << cache_root_.value()
<< " does not exist";
return false;
}
// Delete files directory if database file does not exist.
if (!base::PathExists(db_path_)) {
LOG(INFO) << "Database file does not exist";
return DeleteFiles();
}
ApkCacheDatabase db(db_path_);
if (db.Init() != SQLITE_OK) {
LOG(ERROR) << "Cannot connect to database " << db_path_.MaybeAsASCII();
return DeleteCache();
}
// Delete the whole cache if database fails integrity check.
if (!db.CheckIntegrity()) {
LOG(ERROR) << "Database integrity check failed";
return DeleteCache();
}
// Delete files directory if database is an empty file, i.e. desired tables
// do not exist.
if (!db.SessionsTableExists()) {
LOG(INFO) << "Database is empty";
return DeleteFiles();
}
// Clean stale sessions
if (!CleanStaleSessions(db)) {
LOG(ERROR) << "Failed to clean stale sessions";
DeleteCache();
return false;
}
// Exit normally if any other session is active.
if (IsOtherSessionActive(db))
return true;
// Open cache cleaner session.
int64_t session_id = OpenSession(db);
if (session_id == 0) {
LOG(ERROR) << "Failed to create session";
DeleteCache();
return false;
}
bool success = true;
if (!CleanOutdatedFiles(db))
success = false;
if (!CleanSessionsWithoutFile(db, session_id))
success = false;
if (!CleanFiles(db))
success = false;
// Close cache cleaner session.
if (!CloseSession(db, session_id))
success = false;
int result = db.Close();
if (result != SQLITE_OK) {
LOG(ERROR) << "Failed to close database: " << result;
return false;
}
return success;
}
bool OpaqueFilesCleaner::DeleteCache() const {
if (RemoveUnexpectedItemsFromDir(
cache_root_,
base::FileEnumerator::FileType::FILES |
base::FileEnumerator::FileType::DIRECTORIES |
base::FileEnumerator::FileType::SHOW_SYM_LINKS,
{})) {
LOG(INFO) << "Cleared cache";
return true;
}
LOG(ERROR) << "Failed to delete cache";
return false;
}
bool OpaqueFilesCleaner::DeleteFiles() const {
if (base::PathExists(files_path_) && base::DeletePathRecursively(files_path_))
return true;
LOG(ERROR) << "Failed to delete files directory";
return false;
}
bool OpaqueFilesCleaner::CleanStaleSessions(const ApkCacheDatabase& db) const {
auto sessions = db.GetSessions();
if (!sessions)
return false;
base::Time current_time = base::Time::Now();
for (const Session& session : *sessions) {
if (session.status == kSessionStatusOpen) {
// Check if the session is expired. A session will expire if the process
// that created it exited abnormally. For example, Play Store might be
// killed during streaming files because of system shutdown. In this
// situation the dead session will never be closed normally and will block
// other sessions from being created.
base::TimeDelta age = current_time - session.timestamp;
if (age.InSeconds() < 0)
LOG(WARNING) << "Session " << session.id << " is in the future";
else if (age > kSessionMaxAge)
LOG(WARNING) << "Session " << session.id << " expired";
else
continue;
if (!db.DeleteSession(session.id))
return false;
}
}
return true;
}
bool OpaqueFilesCleaner::IsOtherSessionActive(
const ApkCacheDatabase& db) const {
auto sessions = db.GetSessions();
if (!sessions)
return true;
for (const Session& session : *sessions) {
if (session.status == kSessionStatusOpen) {
LOG(INFO) << "Session " << session.id << " from " << session.source
<< " is active";
return true;
}
}
return false;
}
int64_t OpaqueFilesCleaner::OpenSession(const ApkCacheDatabase& db) const {
Session session;
session.source = kCacheCleanerSessionSource;
session.timestamp = base::Time::Now();
session.status = kSessionStatusOpen;
return db.InsertSession(session);
}
bool OpaqueFilesCleaner::CloseSession(const ApkCacheDatabase& db,
uint64_t id) const {
return db.UpdateSessionStatus(id, kSessionStatusClosed);
}
bool OpaqueFilesCleaner::CleanOutdatedFiles(const ApkCacheDatabase& db) const {
auto file_entries = db.GetFileEntries();
if (!file_entries)
return false;
std::set<Package> packages_to_delete;
base::Time current_time = base::Time::Now();
for (const FileEntry& file_entry : *file_entries) {
// Check timestamp.
base::TimeDelta age = current_time - file_entry.access_time;
if (age > kValidityPeriod) {
LOG(INFO) << "Found outdated file " << file_entry.id;
packages_to_delete.emplace(file_entry.package_name,
file_entry.version_code);
}
}
// Delete all invalid packages.
for (const Package& package : packages_to_delete) {
int deleted_rows = db.DeletePackage(package.name, package.version);
if (deleted_rows > 0)
LOG(INFO) << "Deleted " << deleted_rows << " files in package "
<< package.name << " version " << package.version;
}
return true;
}
bool OpaqueFilesCleaner::CleanSessionsWithoutFile(
const ApkCacheDatabase& db, int64_t cleaner_session_id) const {
int result = db.DeleteSessionsWithoutFileEntries(cleaner_session_id);
if (result > 0)
LOG(INFO) << "Deleted " << result << " sessions";
return result != -1;
}
bool OpaqueFilesCleaner::CleanFiles(const ApkCacheDatabase& db) const {
// Get all recorded file entries.
auto file_entries = db.GetFileEntries();
if (!file_entries)
return false;
// Convert ID to file name
std::unordered_set<std::string> known_file_names;
for (const FileEntry& file_entry : *file_entries)
known_file_names.insert(GetFileNameById(file_entry.id));
return RemoveUnexpectedItemsFromDir(
files_path_,
base::FileEnumerator::FileType::FILES |
base::FileEnumerator::FileType::DIRECTORIES |
base::FileEnumerator::FileType::SHOW_SYM_LINKS,
known_file_names);
}
} // namespace apk_cache