| // 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 |