/* * Copyright 2024 Google LLC * * 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 "app_cache.h" #include "kernel/events.h" #include "kernel/pbl_malloc.h" #include "kernel/pebble_tasks.h" #include "process_management/app_install_manager.h" #include "process_management/app_storage.h" #include "services/common/system_task.h" #include "services/normal/blob_db/pin_db.h" #include "services/normal/filesystem/app_file.h" #include "services/normal/filesystem/pfs.h" #include "services/normal/settings/settings_file.h" #include "services/normal/settings/settings_file.h" #include "shell/normal/quick_launch.h" #include "shell/normal/watchface.h" #include "shell/prefs.h" #include "system/logging.h" #include "system/passert.h" #include "util/attributes.h" #include "util/list.h" #include "util/math.h" #include "util/time/time.h" #include "util/units.h" //! @file app_cache.c //! App Cache //! The App Cache keeps track of the install date, last launch, launch count, and size of an //! application. //! //! A priority can also be calculated for each entry. It is calculated by a simple last used //! algorithm (TODO Improve: PBL-13209) which will help determine which application //! needs to be evicted in order to free up more space for other application binaries. //! //! When an entry is added into the app cache, it means the binaries now reside on the watch. On //! this function call, a callback is initiated to check if we need to free space for a possible //! future application. If so, the applications with the lowest priority that add up to or are //! greater than the space needed will be removed. //! //! It is assumed that there will ALWAYS be space for a single application of maximum size based //! on the platform. The only time when this isn't true is the time between "add_entry" and the //! callback to clean up the cache. #define APP_CACHE_FILE_NAME "appcache" //! each cache entry is ~16 bytes, 4000 / 16 = 250 apps #define APP_CACHE_MAX_SIZE 4000 //! Keep enough room for the maximum sized application based on platform, plus a little more room. //! Source: https://pebbletechnology.atlassian.net/wiki/display/DEV/PBW+3.0 #if PLATFORM_TINTIN || PLATFORM_SILK || UNITTEST #define APP_SPACE_BUFFER KiBYTES(300) #else #define APP_SPACE_BUFFER MiBYTES(4) #endif #define MAX_PRIORITY ((uint32_t)~0) // 4 quick launch apps, 1 default watchface, 1 default worker #define DO_NOT_EVICT_LIST_SIZE (NUM_BUTTONS + 2) static PebbleRecursiveMutex *s_app_cache_mutex = NULL; //! Actual data structure stored in flash about an app cache entry typedef struct PACKED { time_t install_date; time_t last_launch; uint32_t total_size; uint16_t launch_count; } AppCacheEntry; typedef struct { ListNode node; AppInstallId id; uint32_t size; uint32_t priority; } EvictListNode; typedef struct { EvictListNode *list; uint32_t bytes_needed; uint32_t bytes_in_list; const AppInstallId do_not_evict[DO_NOT_EVICT_LIST_SIZE]; } EachEvictData; //! Takes the information given in entry and calculates a new priority for the app. //! //! Policy rules: //! 1. App that has least recently launched or been installed app is evicted. static uint32_t prv_calculate_priority(AppCacheEntry *entry) { return (uint32_t) MAX(entry->last_launch, entry->install_date); } //! Comparator for EvictListNode static int evict_node_comparator(void *a, void *b) { EvictListNode *a_node = (EvictListNode *)a; EvictListNode *b_node = (EvictListNode *)b; if (b_node->priority > a_node->priority) { return 1; } else if (b_node->priority < a_node->priority) { return -1; } else { // bigger applications to have a lower priority if (b_node->size < a_node->size) { return 1; } else if (b_node->size > a_node->size) { return -1; } else { return 0; } } } //! Trim the applications with highest priority while still keeping (bytes_in_list > bytes_needed) static void prv_trim_top_priorities(EvictListNode **list_node, uint32_t *bytes_in_list, uint32_t bytes_needed) { EvictListNode *node = *list_node; while (node) { EvictListNode *temp = node; if (node->size <= (*bytes_in_list - bytes_needed)) { *bytes_in_list -= node->size; node = (EvictListNode *)list_pop_head((ListNode *)node); kernel_free(temp); } else { break; } } *list_node = node; } //! Check if we need to free up some space in the cache. If so, do it. static void prv_cleanup_app_cache_if_needed(void *data) { uint32_t pfs_space = get_available_pfs_space(); if (pfs_space < APP_SPACE_BUFFER) { const uint32_t to_free = (APP_SPACE_BUFFER - pfs_space); PBL_LOG(LOG_LEVEL_DEBUG, "Cache OOS: Need to free %"PRIu32" bytes, PFS avail space: %"PRIu32"", to_free, pfs_space); app_cache_free_up_space(to_free); } } static void prv_delete_cache_callback(void *data) { app_cache_flush(); } static void prv_delete_cached_files(void) { pfs_remove_files(is_app_file_name); } static bool prv_is_in_list(AppInstallId id, const AppInstallId list[], uint8_t len) { for (unsigned int i = 0; i < len; i++) { if (list[i] == id) { return true; } } return false; } ////////////////////// // Settings Helpers ////////////////////// //! Settings iterator function that finds the entry with the lowest calculated priority static bool prv_each_free_up_space(SettingsFile *file, SettingsRecordInfo *info, void *context) { // check entry is valid if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) { PBL_LOG(LOG_LEVEL_WARNING, "Invalid cache entry with key_len: %u and val_len: %u, flushing", info->key_len, info->val_len); system_task_add_callback(prv_delete_cache_callback, NULL); return false; // stop iterating, delete the file and binaries } EachEvictData *data = (EachEvictData *)context; AppInstallId id; AppCacheEntry entry; info->get_key(file, (uint8_t *)&id, info->key_len); info->get_val(file, (uint8_t *)&entry, info->val_len); // create node EvictListNode *node = kernel_malloc_check(sizeof(EvictListNode)); list_init((ListNode *)node); // give them an extremely high priority so that we only remove them if we really NEED to // This list contains defaults that we shouldn't be removing. uint32_t priority = 0; if (prv_is_in_list(id, data->do_not_evict, DO_NOT_EVICT_LIST_SIZE)) { priority = MAX_PRIORITY; } *node = (EvictListNode) { .id = id, .size = entry.total_size, .priority = MAX(priority, prv_calculate_priority(&entry)), }; data->list = (EvictListNode *)list_sorted_add((ListNode *)data->list, (ListNode *)node, evict_node_comparator, false); data->bytes_in_list += node->size; if (data->bytes_in_list > data->bytes_needed) { prv_trim_top_priorities(&data->list, &data->bytes_in_list, data->bytes_needed); } return true; // continue iterating } ////////////////////////// // AppCache API's ////////////////////////// //! Updates metadata within the cache entry for the given AppInstallId. Will update such fields as //! launch count, last launch, and priority status_t app_cache_app_launched(AppInstallId app_id) { status_t rv; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } AppCacheEntry entry = { 0 }; rv = settings_file_get(&file, (uint8_t *)&app_id, sizeof(AppInstallId), (uint8_t *)&entry, sizeof(AppCacheEntry)); if (rv == S_SUCCESS) { entry.last_launch = rtc_get_time(); entry.launch_count += 1; rv = settings_file_set(&file, (uint8_t *)&app_id, sizeof(AppInstallId), (uint8_t *)&entry, sizeof(AppCacheEntry)); } else { app_storage_delete_app(app_id); settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId)); } settings_file_close(&file); } unlock: mutex_unlock_recursive(s_app_cache_mutex); return rv; } //! Asks the app cache to remove 'bytes_needed' bytes of application binaries to free up space //! for other things. status_t app_cache_free_up_space(uint32_t bytes_needed) { if (bytes_needed == 0) { return E_INVALID_ARGUMENT; } status_t rv; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } // we don't want to remove any default apps or quick launch apps, so keep them in a list. EachEvictData evict_data = (EachEvictData) { .bytes_needed = bytes_needed, .do_not_evict = { #if !SHELL_SDK quick_launch_get_app(BUTTON_ID_UP), quick_launch_get_app(BUTTON_ID_SELECT), quick_launch_get_app(BUTTON_ID_DOWN), quick_launch_get_app(BUTTON_ID_BACK), #endif watchface_get_default_install_id(), worker_preferences_get_default_worker(), }, }; settings_file_each(&file, prv_each_free_up_space, &evict_data); settings_file_close(&file); // remove all nodes found EvictListNode *node = evict_data.list; while (node) { EvictListNode *temp = node; PBL_LOG(LOG_LEVEL_DEBUG, "Deleting application binaries for app id: %"PRIu32", size: %"PRIu32, node->id, node->size); app_cache_remove_entry(node->id); node = (EvictListNode *)list_pop_head((ListNode *)node); kernel_free(temp); } } unlock: mutex_unlock_recursive(s_app_cache_mutex); return rv; } ////////////////////// // AppCache Helpers ////////////////////// // Remove the filename entry in the PFSFileList (via context) that corresponds to the // app install id passed in via info static bool prv_remove_matching_resource_file_callback(SettingsFile *file, SettingsRecordInfo *info, void *context) { AppInstallId id; // examine the SettingsRecordInfo and extract the AppInstallId from it info->get_key(file, (uint8_t *)&id, info->key_len); // the context passed in is really a pointer to the resource_list PFSFileListEntry **resource_list = context; PFSFileListEntry *iter = *resource_list; while (iter) { // grab the next entry right now since we may delete the node we're looking at PFSFileListEntry *next = (PFSFileListEntry *)iter->list_node.next; if (app_file_parse_app_id(iter->name) == id) { // the AppInstallId of the file matches the one in the cache so we can remove this // entry from the resource_list (since we don't want to delete it) // note: resource_list may be updated if we happen to remove the first entry in the list list_remove(&(iter->list_node), (ListNode**)resource_list, NULL); kernel_free(iter); // free up the memory for the node we just removed break; // we can quit now that we've found a match for this id } iter = next; } return true; } // Delete files from resource_list that don't correspond to entries in the app cache static void prv_app_cache_find_and_delete_orphans(PFSFileListEntry **resource_list) { mutex_lock_recursive(s_app_cache_mutex); SettingsFile file; status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { mutex_unlock_recursive(s_app_cache_mutex); return; } // resource_list contains all of the resource files we found. We only // want to delete orphans so we can remove any entries from the list that correspond // to items in the app cache... // prv_remove_matching_resource_file_callback scans resource_list and removes the entry // corresponding to the passed-in application's id settings_file_each(&file, prv_remove_matching_resource_file_callback, resource_list); settings_file_close(&file); mutex_unlock_recursive(s_app_cache_mutex); // resource_list now only contains filenames of resource files that don't have corresponding // entries in the app cache. We can safely delete these files. PFSFileListEntry *iter = *resource_list; while (iter) { PBL_LOG(LOG_LEVEL_INFO, "Orphaned resource file removed: %s", iter->name); pfs_remove(iter->name); iter = (PFSFileListEntry *)iter->list_node.next; } } // The bug addressed in PBL-34010 caused resource files to remain in the filesystem even // after the associated application had been deleted. This function attempts to find such // orphaned files and remove them. Note: further to the bug in PBL-34010, this function will // remove any resource files that are not related to apps currently in the cache. static void prv_purge_orphaned_resource_files(void) { // create a list of all app resource files in the filesystem PFSFileListEntry *resource_files = pfs_create_file_list(is_app_resource_file_name); // delete app resource files that don't correspond to entries in the app cache prv_app_cache_find_and_delete_orphans(&resource_files); pfs_delete_file_list(resource_files); } ////////////////////////// // AppCache Settings API's ////////////////////////// //! Set up the app cache void app_cache_init(void) { s_app_cache_mutex = mutex_create_recursive(); mutex_lock_recursive(s_app_cache_mutex); { // if no cache file exists, then we should go ahead and clean up any files that are left over int fd = pfs_open(APP_CACHE_FILE_NAME, OP_FLAG_READ, FILE_TYPE_STATIC, 0); if (fd < 0) { prv_delete_cached_files(); goto unlock; } pfs_close(fd); } unlock: mutex_unlock_recursive(s_app_cache_mutex); prv_purge_orphaned_resource_files(); } //! Adds an entry with the given AppInstallId to the cache status_t app_cache_add_entry(AppInstallId app_id, uint32_t total_size) { status_t rv; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } AppCacheEntry entry = { .install_date = rtc_get_time(), .last_launch = 0, .launch_count = 0, .total_size = total_size, }; rv = settings_file_set(&file, (uint8_t *)&app_id, sizeof(AppInstallId), (uint8_t *)&entry, sizeof(AppCacheEntry)); settings_file_close(&file); // cleanup the cache if we need to system_task_add_callback(prv_cleanup_app_cache_if_needed, NULL); } unlock: mutex_unlock_recursive(s_app_cache_mutex); return rv; } //! Tests if an entry with the given AppInstallId is in the cache bool app_cache_entry_exists(AppInstallId app_id) { bool exists = false; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } exists = settings_file_exists(&file, (uint8_t *)&app_id, sizeof(AppInstallId)); if (exists && !app_storage_app_exists(app_id)) { settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId)); exists = false; } settings_file_close(&file); } unlock: mutex_unlock_recursive(s_app_cache_mutex); return exists; } //! Removes an entry with the given AppInstallId from the cache status_t app_cache_remove_entry(AppInstallId app_id) { status_t rv; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } rv = settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId)); if (rv == S_SUCCESS) { // Will delete an app from the filesystem. app_storage_delete_app(app_id); } settings_file_close(&file); } if (rv == S_SUCCESS) { PebbleEvent e = { .type = PEBBLE_APP_CACHE_EVENT, .app_cache_event = { .cache_event_type = PebbleAppCacheEvent_Removed, .install_id = app_id, }, }; event_put(&e); } unlock: mutex_unlock_recursive(s_app_cache_mutex); return rv; } void app_cache_flush(void) { PBL_ASSERT_TASK(PebbleTask_KernelBackground); mutex_lock_recursive(s_app_cache_mutex); { pfs_remove(APP_CACHE_FILE_NAME); prv_delete_cached_files(); } mutex_unlock_recursive(s_app_cache_mutex); } //////////////////////////////// // Testing only //////////////////////////////// static bool prv_each_get_size(SettingsFile *file, SettingsRecordInfo *info, void *context) { if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) { return true; // continue iterating } uint32_t *cache_size = (uint32_t *)context; AppCacheEntry entry; info->get_val(file, (uint8_t *)&entry, info->val_len); *cache_size += entry.total_size; return true; // continue iterating } uint32_t app_cache_get_size(void) { uint32_t cache_size = 0; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } settings_file_each(&file, prv_each_get_size, &cache_size); settings_file_close(&file); } unlock: mutex_unlock_recursive(s_app_cache_mutex); return cache_size; } typedef struct { AppInstallId id; uint32_t priority; } AppCacheEachData; //! Settings iterator function that finds the entry with the lowest calculated priority static bool prv_each_min_priority(SettingsFile *file, SettingsRecordInfo *info, void *context) { // check entry is valid if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) { return true; // continue iterating } AppCacheEachData *to_evict = (AppCacheEachData *)context; AppInstallId id; AppCacheEntry entry; info->get_key(file, (uint8_t *)&id, info->key_len); info->get_val(file, (uint8_t *)&entry, info->val_len); uint32_t entry_priority = prv_calculate_priority(&entry); if (entry_priority < to_evict->priority) { to_evict->id = id; to_evict->priority = entry_priority; } return true; // continue iterating } //! Find the entry in the app cache with the lowest calculated priority AppInstallId app_cache_get_next_eviction(void) { AppInstallId ret_value = INSTALL_ID_INVALID; mutex_lock_recursive(s_app_cache_mutex); { SettingsFile file; status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE); if (rv != S_SUCCESS) { goto unlock; } // set max so that any application will have a lower priority. AppCacheEachData to_evict = { .id = INSTALL_ID_INVALID, .priority = MAX_PRIORITY, }; settings_file_each(&file, prv_each_min_priority, (void *)&to_evict); settings_file_close(&file); ret_value = to_evict.id; } unlock: mutex_unlock_recursive(s_app_cache_mutex); return ret_value; }