diff --git a/README.md b/README.md index 0641a7c..95c1f48 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Supported keys (all optional): - `mount_read_only=1|0` (default: `1`) - `force_mount=1|0` (mounting even damaged file systems; default: `0`) - `app_install_all=1|0` (`1` stages new titles and submits them through the stock batch `sceAppInstUtilAppInstallAll`; default: `0` on every firmware. On FW `12.00+`, the default per-title path is provided by the SceShellCore TitleDir bridge.) +- `auto_remove_missing_games=1|0` (`1` automatically removes games from the system library when their source is no longer available; default: `0`) +- `auto_remove_missing_delay_seconds=<1..86400>` (how long a source must remain unavailable before removal; default: `300`) - `image_ro=` (repeatable; force read-only mode for this image filename) - `image_rw=` (repeatable; force read-write mode for this image filename) - `image_sector=:` (repeatable; force sector size for this image filename) @@ -121,6 +123,7 @@ Scan path behavior: - If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`. - Full scan loop runs every `scan_interval_seconds` (default: `15`). - Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`). +- With `auto_remove_missing_games=1`, a game is removed from the system library only after its source remains unavailable for `auto_remove_missing_delay_seconds`. If the source becomes available again before removal, the pending removal is cancelled. - Direct folder installs use `/sce_sys` for this check; image and backport sources use the target path itself. Backport overlay behavior: diff --git a/config.ini.example b/config.ini.example index 868bfb0..40a3ef7 100644 --- a/config.ini.example +++ b/config.ini.example @@ -46,6 +46,18 @@ # Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer # app_install_all=0 +# Automatically remove games from the system library when their source +# is no longer available: +# 1/true/yes/on -> remove after the configured missing delay +# 0/false/no/off -> keep the registration (default) +# Default: 0 +# auto_remove_missing_games=0 + +# How long a source must remain unavailable before removal (seconds), +# range: 1..86400 +# Default: 300 +# auto_remove_missing_delay_seconds=300 + # Scan depth relative to each scan root: # 1 -> scan only first-level subdirectories # 2 -> also scan one additional nested level diff --git a/include/sm_appdb.h b/include/sm_appdb.h index 586a9f6..30a54e5 100644 --- a/include/sm_appdb.h +++ b/include/sm_appdb.h @@ -22,6 +22,8 @@ void free_app_db_title_list(struct AppDbTitleList *list); void invalidate_app_db_title_cache(void); // Copy the cached app.db title list into caller-owned storage, loading it if needed. bool get_app_db_title_list_cached(struct AppDbTitleList *list_out); +// Copy titles whose metadata path matches /user/app/PPSA*/sce_sys. +bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out); // Copy the cached PPSA titles that app.db marks as not uninstallable. bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out); diff --git a/include/sm_game_cache.h b/include/sm_game_cache.h index 8e41a15..e76380c 100644 --- a/include/sm_game_cache.h +++ b/include/sm_game_cache.h @@ -23,6 +23,13 @@ void cache_game_entry(const char *path, const char *title_id, void prune_game_cache(void); // Drop invalid or stale entries that belong to a specific scan root. void prune_game_cache_for_root(const char *root); +// Cancel pending removal when the same title is discovered at a valid source. +void note_game_cache_source_seen(const char *path, const char *title_id, + const char *title_name); +// Reconcile app.db auto-remove candidates and uninstall expired missing entries. +void reconcile_missing_app_db_games(void); +// Reset pending missing timers after relevant runtime config changes. +void reset_missing_game_cache_timers(void); // Look up a cached game entry by path or title ID. bool find_cached_game(const char *path, const char *title_id, const char **existing_path_out); diff --git a/include/sm_image_index.h b/include/sm_image_index.h index f32c928..957a7f2 100644 --- a/include/sm_image_index.h +++ b/include/sm_image_index.h @@ -22,6 +22,8 @@ typedef struct { bool sm_image_index_needs_scan(const char *path, const struct stat *st, const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready); +// Return true when a title is mapped to an existing backing image. +bool sm_image_index_has_source_for_title(const char *title_id); // Start a new fingerprint generation before mounting an image. void sm_image_index_begin_scan(const char *path, const struct stat *st); // Associate a discovered mounted game with its outer backing image. Returns diff --git a/include/sm_limits.h b/include/sm_limits.h index 92f0122..a234856 100644 --- a/include/sm_limits.h +++ b/include/sm_limits.h @@ -4,6 +4,7 @@ #define DEFAULT_SCAN_INTERVAL_US 15000000u #define DEFAULT_SCAN_DEPTH 1u #define DEFAULT_STABILITY_WAIT_SECONDS 10u +#define DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS 300u #define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u #define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u @@ -37,6 +38,8 @@ #define MIN_SCAN_INTERVAL_SECONDS 1u #define MAX_SCAN_INTERVAL_SECONDS 3600u #define MAX_STABILITY_WAIT_SECONDS 3600u +#define MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS 1u +#define MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS 86400u #define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u #define APP_DB_QUERY_BUSY_RETRIES 3 diff --git a/include/sm_types.h b/include/sm_types.h index 0ae512a..fd4cd60 100644 --- a/include/sm_types.h +++ b/include/sm_types.h @@ -109,6 +109,7 @@ typedef struct runtime_config { bool mount_read_only; bool force_mount; bool app_install_all_enabled; + bool auto_remove_missing_games; bool backport_fakelib_enabled; bool global_fakelib_enabled; bool global_fakelib_mount_first; @@ -123,6 +124,7 @@ typedef struct runtime_config { uint32_t scan_depth; uint32_t scan_interval_us; uint32_t stability_wait_seconds; + uint32_t auto_remove_missing_delay_seconds; uint32_t kstuff_pause_delay_image_seconds; uint32_t kstuff_pause_delay_direct_seconds; attach_backend_t exfat_backend; diff --git a/src/sm_appdb.c b/src/sm_appdb.c index 29f3a09..171f5cc 100644 --- a/src/sm_appdb.c +++ b/src/sm_appdb.c @@ -14,10 +14,13 @@ static sqlite3 *g_app_db; static sqlite3_stmt *g_app_db_stmt_update_snd0; static sqlite3_stmt *g_app_db_stmt_normalize_snd0; static struct AppDbTitleList g_app_db_title_cache; +static struct AppDbTitleList g_app_db_auto_remove_cache; static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache; static bool g_app_db_title_cache_ready = false; +static bool g_app_db_auto_remove_cache_ready = false; static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false; static time_t g_app_db_title_cache_mtime = 0; +static time_t g_app_db_auto_remove_cache_mtime = 0; static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0; static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -49,10 +52,13 @@ void free_app_db_title_list(struct AppDbTitleList *list) { void shutdown_app_db(void) { pthread_mutex_lock(&g_app_db_mutex); free_app_db_title_list(&g_app_db_title_cache); + free_app_db_title_list(&g_app_db_auto_remove_cache); free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache); g_app_db_title_cache_ready = false; + g_app_db_auto_remove_cache_ready = false; g_app_db_blocked_uninstall_ppsa_cache_ready = false; g_app_db_title_cache_mtime = 0; + g_app_db_auto_remove_cache_mtime = 0; g_app_db_blocked_uninstall_ppsa_cache_mtime = 0; close_app_db(); pthread_mutex_unlock(&g_app_db_mutex); @@ -418,6 +424,28 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) { return loaded; } +static bool load_app_db_auto_remove_title_list(struct AppDbTitleList *list) { + free_app_db_title_list(list); + + const char *sql = + "SELECT DISTINCT titleId " + "FROM tbl_contentinfo " + "WHERE metaDataPath GLOB '/user/app/PPSA*/sce_sys' " + "ORDER BY titleId;"; + sqlite3_stmt *stmt = NULL; + int prep_rc = app_db_prepare_with_retry(sql, &stmt, + APP_DB_PREPARE_BUSY_RETRIES, + "auto-remove title query"); + if (prep_rc != SQLITE_OK) + return false; + + bool loaded = step_app_db_title_query(list, stmt, "auto-remove title list"); + if (loaded) + log_debug(" [DB] loaded app.db auto-remove title list: %d entries", + list->count); + return loaded; +} + static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) { free_app_db_title_list(list); @@ -444,8 +472,10 @@ static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) void invalidate_app_db_title_cache(void) { pthread_mutex_lock(&g_app_db_mutex); g_app_db_title_cache_ready = false; + g_app_db_auto_remove_cache_ready = false; g_app_db_blocked_uninstall_ppsa_cache_ready = false; g_app_db_title_cache_mtime = 0; + g_app_db_auto_remove_cache_mtime = 0; g_app_db_blocked_uninstall_ppsa_cache_mtime = 0; pthread_mutex_unlock(&g_app_db_mutex); } @@ -470,34 +500,54 @@ static bool copy_app_db_title_list(struct AppDbTitleList *dst, return true; } +typedef bool (*load_app_db_title_list_fn)(struct AppDbTitleList *list); + +static bool get_cached_app_db_title_list( + struct AppDbTitleList *list_out, struct AppDbTitleList *cache, + bool *cache_ready, time_t *cache_mtime, load_app_db_title_list_fn load_fn) { + free_app_db_title_list(list_out); + + struct stat st; + int app_db_stat_rc = stat(APP_DB_PATH, &st); + if (!*cache_ready || + (app_db_stat_rc == 0 && *cache_mtime != st.st_mtime)) { + struct AppDbTitleList fresh = {0}; + if (app_db_stat_rc == 0 && load_fn(&fresh)) { + free_app_db_title_list(cache); + *cache = fresh; + *cache_mtime = st.st_mtime; + *cache_ready = true; + } else { + free_app_db_title_list(&fresh); + if (!*cache_ready) + return false; + } + } + + return copy_app_db_title_list(list_out, cache); +} + bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) { if (!list_out) return false; - free_app_db_title_list(list_out); pthread_mutex_lock(&g_app_db_mutex); + bool copied = get_cached_app_db_title_list( + list_out, &g_app_db_title_cache, &g_app_db_title_cache_ready, + &g_app_db_title_cache_mtime, load_app_db_title_list); + pthread_mutex_unlock(&g_app_db_mutex); + return copied; +} - struct stat st; - int app_db_stat_rc = stat(APP_DB_PATH, &st); +bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out) { + if (!list_out) + return false; - if (!g_app_db_title_cache_ready || - (app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) { - struct AppDbTitleList fresh = {0}; - if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) { - free_app_db_title_list(&g_app_db_title_cache); - g_app_db_title_cache = fresh; - g_app_db_title_cache_mtime = st.st_mtime; - g_app_db_title_cache_ready = true; - } else { - free_app_db_title_list(&fresh); - if (!g_app_db_title_cache_ready) { - pthread_mutex_unlock(&g_app_db_mutex); - return false; - } - } - } - - bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache); + pthread_mutex_lock(&g_app_db_mutex); + bool copied = get_cached_app_db_title_list( + list_out, &g_app_db_auto_remove_cache, + &g_app_db_auto_remove_cache_ready, &g_app_db_auto_remove_cache_mtime, + load_app_db_auto_remove_title_list); pthread_mutex_unlock(&g_app_db_mutex); return copied; } @@ -506,33 +556,12 @@ bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) { if (!list_out) return false; - free_app_db_title_list(list_out); pthread_mutex_lock(&g_app_db_mutex); - - struct stat st; - int app_db_stat_rc = stat(APP_DB_PATH, &st); - - if (!g_app_db_blocked_uninstall_ppsa_cache_ready || - (app_db_stat_rc == 0 && - g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) { - struct AppDbTitleList fresh = {0}; - if (app_db_stat_rc == 0 && - load_app_db_blocked_uninstall_ppsa_list(&fresh)) { - free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache); - g_app_db_blocked_uninstall_ppsa_cache = fresh; - g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime; - g_app_db_blocked_uninstall_ppsa_cache_ready = true; - } else { - free_app_db_title_list(&fresh); - if (!g_app_db_blocked_uninstall_ppsa_cache_ready) { - pthread_mutex_unlock(&g_app_db_mutex); - return false; - } - } - } - - bool loaded = - copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache); + bool loaded = get_cached_app_db_title_list( + list_out, &g_app_db_blocked_uninstall_ppsa_cache, + &g_app_db_blocked_uninstall_ppsa_cache_ready, + &g_app_db_blocked_uninstall_ppsa_cache_mtime, + load_app_db_blocked_uninstall_ppsa_list); pthread_mutex_unlock(&g_app_db_mutex); return loaded; } diff --git a/src/sm_config_mount.c b/src/sm_config_mount.c index cc50a23..61c79ad 100644 --- a/src/sm_config_mount.c +++ b/src/sm_config_mount.c @@ -241,6 +241,7 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) { state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0); state->cfg.force_mount = false; state->cfg.app_install_all_enabled = false; + state->cfg.auto_remove_missing_games = false; state->cfg.backport_fakelib_enabled = true; state->cfg.global_fakelib_enabled = true; state->cfg.global_fakelib_mount_first = true; @@ -255,6 +256,8 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) { state->cfg.scan_depth = DEFAULT_SCAN_DEPTH; state->cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US; state->cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS; + state->cfg.auto_remove_missing_delay_seconds = + DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS; state->cfg.kstuff_pause_delay_image_seconds = DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS; state->cfg.kstuff_pause_delay_direct_seconds = @@ -1214,6 +1217,15 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st continue; } + if (strcasecmp(key, "auto_remove_missing_games") == 0) { + if (!parse_bool_ini(value, &bval)) { + log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value); + continue; + } + state->cfg.auto_remove_missing_games = bval; + continue; + } + if (strcasecmp(key, "image_ro") == 0 || strcasecmp(key, "image_rw") == 0) { bool rule_read_only = (strcasecmp(key, "image_ro") == 0); @@ -1382,6 +1394,22 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st continue; } + if (strcasecmp(key, "auto_remove_missing_delay_seconds") == 0 || + strcasecmp(key, "auto_remove_missing_delay_sec") == 0) { + if (!parse_u32_ini(value, &u32) || + u32 < MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS || + u32 > MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS) { + log_debug(" [CFG] invalid auto-remove delay at line %d: %s=%s " + "(range: %u..%u)", + line_no, key, value, + (unsigned)MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS, + (unsigned)MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS); + continue; + } + state->cfg.auto_remove_missing_delay_seconds = u32; + continue; + } + if (strcasecmp(key, "kstuff_pause_delay_image_seconds") == 0 || strcasecmp(key, "kstuff_pause_delay_image_sec") == 0) { if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) { @@ -1497,7 +1525,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st } log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d " - "app_install_all=%d api=%s:%u scan_depth=%u " + "app_install_all=%d auto_remove_missing_games=%d " + "auto_remove_missing_delay_s=%u api=%s:%u scan_depth=%u " "legacy_recursive_scan_forced=%d backport_fakelib=%d " "global_fakelib=%d global_fakelib_priority=%s " "global_fakelib_path=%s global_fakelib_exclude=%u " @@ -1511,6 +1540,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st state->cfg.mount_read_only ? 1 : 0, state->cfg.force_mount ? 1 : 0, state->cfg.app_install_all_enabled ? 1 : 0, + state->cfg.auto_remove_missing_games ? 1 : 0, + state->cfg.auto_remove_missing_delay_seconds, state->cfg.api_bind_address, state->cfg.api_port, state->cfg.scan_depth, state->cfg.legacy_recursive_scan_forced ? 1 : 0, diff --git a/src/sm_game_cache.c b/src/sm_game_cache.c index 09060eb..0f3c5f9 100644 --- a/src/sm_game_cache.c +++ b/src/sm_game_cache.c @@ -2,12 +2,15 @@ #include #include "sm_game_cache.h" +#include "sm_appdb.h" #include "sm_config_mount.h" -#include "sm_filesystem.h" #include "sm_image_cache.h" +#include "sm_image_index.h" +#include "sm_install_queue.h" #include "sm_limits.h" #include "sm_log.h" #include "sm_path_utils.h" +#include "sm_time.h" #include "sm_title_state.h" struct GameCache { @@ -15,6 +18,7 @@ struct GameCache { char title_id[MAX_TITLE_ID]; char title_name[MAX_TITLE_NAME]; char owning_scan_root[MAX_PATH]; + uint64_t missing_since_us; bool valid; }; @@ -22,15 +26,7 @@ static struct GameCache g_game_cache[MAX_PENDING]; static pthread_mutex_t g_game_cache_mutex = PTHREAD_MUTEX_INITIALIZER; static bool game_cache_source_exists(const struct GameCache *entry) { - if (path_exists(entry->path)) - return true; - if (!is_under_image_mount_base(entry->path)) - return false; - - char image_path[MAX_PATH]; - return read_mount_image_link(entry->title_id, image_path, - sizeof(image_path)) && - path_exists(image_path); + return entry->path[0] != '\0' && path_exists(entry->path); } static bool resolve_game_cache_owning_scan_root(const char *path, @@ -71,6 +67,7 @@ static void write_game_cache_slot(struct GameCache *entry, const char *path, (void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name)); (void)strlcpy(entry->owning_scan_root, owning_scan_root, sizeof(entry->owning_scan_root)); + entry->missing_since_us = 0; entry->valid = true; } @@ -81,6 +78,27 @@ static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) { return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root); } +static void start_missing_timer(struct GameCache *entry, uint64_t now_us) { + if (entry->missing_since_us != 0 || now_us == 0) + return; + + entry->missing_since_us = now_us; + if (entry->path[0] != '\0') { + log_debug(" [CACHE] source missing, auto-remove timer started: %s (%s)", + entry->title_id, entry->path); + } else { + log_debug(" [CACHE] source missing, auto-remove timer started: %s", + entry->title_id); + } +} + +static bool missing_timer_expired(const struct GameCache *entry, + uint64_t now_us, uint64_t delay_us) { + return entry->valid && entry->missing_since_us != 0 && + now_us >= entry->missing_since_us && + now_us - entry->missing_since_us >= delay_us; +} + static void clear_game_cache_slot(int index, const char *reason) { if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid) return; @@ -129,37 +147,193 @@ void cache_game_entry(const char *path, const char *title_id, pthread_mutex_unlock(&g_game_cache_mutex); } -void prune_game_cache(void) { +static void prune_game_cache_entries(const char *root) { + const bool auto_remove = runtime_config()->auto_remove_missing_games; + pthread_mutex_lock(&g_game_cache_mutex); for (int k = 0; k < MAX_PENDING; k++) { if (!g_game_cache[k].valid) continue; - if (game_cache_source_exists(&g_game_cache[k])) + if (root) { + const char *entry_root = + ensure_game_cache_owning_scan_root(&g_game_cache[k]) + ? g_game_cache[k].owning_scan_root + : g_game_cache[k].path; + if (!path_matches_root_or_child(entry_root, root)) + continue; + } + + struct GameCache *entry = &g_game_cache[k]; + if (game_cache_source_exists(entry)) { + if (entry->missing_since_us != 0) { + entry->missing_since_us = 0; + log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)", + entry->title_id, entry->path); + } + continue; + } + if (auto_remove) continue; clear_game_cache_slot(k, "source removed"); } pthread_mutex_unlock(&g_game_cache_mutex); } +void prune_game_cache(void) { prune_game_cache_entries(NULL); } + void prune_game_cache_for_root(const char *root) { if (!root || root[0] == '\0') { prune_game_cache(); return; } + prune_game_cache_entries(root); +} + +void note_game_cache_source_seen(const char *path, const char *title_id, + const char *title_name) { + if (!path || !title_id || title_id[0] == '\0') + return; + + pthread_mutex_lock(&g_game_cache_mutex); + for (int k = 0; k < MAX_PENDING; k++) { + if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us == 0) + continue; + if (strcmp(g_game_cache[k].title_id, title_id) != 0) + continue; + + char owning_scan_root[MAX_PATH]; + (void)resolve_game_cache_owning_scan_root(path, owning_scan_root); + write_game_cache_slot(&g_game_cache[k], path, title_id, + title_name ? title_name : "", owning_scan_root); + log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)", + title_id, path); + break; + } + pthread_mutex_unlock(&g_game_cache_mutex); +} + +static void reconcile_missing_game_cache( + const struct AppDbTitleList *auto_remove_titles, uint64_t now_us) { + pthread_mutex_lock(&g_game_cache_mutex); + for (int k = 0; k < MAX_PENDING; ++k) { + struct GameCache *entry = &g_game_cache[k]; + if (!entry->valid || game_cache_source_exists(entry) || + app_db_title_list_contains(auto_remove_titles, entry->title_id)) { + continue; + } + clear_game_cache_slot(k, "source removed"); + } + pthread_mutex_unlock(&g_game_cache_mutex); + + bool cache_full_logged = false; + for (int i = 0; i < auto_remove_titles->count; ++i) { + const char *title_id = auto_remove_titles->ids[i]; + bool image_source_exists = sm_image_index_has_source_for_title(title_id); + + pthread_mutex_lock(&g_game_cache_mutex); + int entry_index = -1; + int free_index = -1; + for (int k = 0; k < MAX_PENDING; ++k) { + if (!g_game_cache[k].valid) { + if (free_index < 0) + free_index = k; + continue; + } + if (strcmp(g_game_cache[k].title_id, title_id) == 0) { + entry_index = k; + break; + } + } + + if (entry_index >= 0) { + struct GameCache *entry = &g_game_cache[entry_index]; + if (game_cache_source_exists(entry) || image_source_exists) { + if (entry->missing_since_us != 0) { + log_debug(" [CACHE] source restored, auto-remove cancelled: %s", + title_id); + } + if (entry->path[0] == '\0') + clear_game_cache_slot(entry_index, NULL); + else + entry->missing_since_us = 0; + } else { + start_missing_timer(entry, now_us); + } + } else if (!image_source_exists && free_index >= 0) { + struct GameCache *entry = &g_game_cache[free_index]; + write_game_cache_slot(entry, "", title_id, "", ""); + start_missing_timer(entry, now_us); + } else if (!image_source_exists && !cache_full_logged) { + log_debug(" [CACHE] auto-remove tracking full; remaining titles deferred"); + cache_full_logged = true; + } + pthread_mutex_unlock(&g_game_cache_mutex); + } +} + +void reconcile_missing_app_db_games(void) { + const runtime_config_t *cfg = runtime_config(); + if (!cfg->auto_remove_missing_games) + return; + + struct AppDbTitleList auto_remove_titles = {0}; + if (!get_app_db_auto_remove_title_list(&auto_remove_titles)) { + log_debug(" [REG] app.db auto-remove title list unavailable"); + return; + } + + const uint64_t now_us = monotonic_time_us(); + const uint64_t delay_us = + (uint64_t)cfg->auto_remove_missing_delay_seconds * 1000000ull; + reconcile_missing_game_cache(&auto_remove_titles, now_us); + + for (;;) { + char title_id[MAX_TITLE_ID]; + title_id[0] = '\0'; + + pthread_mutex_lock(&g_game_cache_mutex); + for (int k = 0; k < MAX_PENDING; k++) { + const struct GameCache *entry = &g_game_cache[k]; + if (!missing_timer_expired(entry, now_us, delay_us)) + continue; + + (void)strlcpy(title_id, entry->title_id, sizeof(title_id)); + clear_game_cache_slot(k, NULL); + break; + } + pthread_mutex_unlock(&g_game_cache_mutex); + + if (title_id[0] == '\0') + break; + + if (!app_db_title_list_contains(&auto_remove_titles, title_id)) + continue; + + int res = sceAppInstUtilAppUnInstall(title_id); + if (res == 0) { + sm_install_forget_pending_title(title_id); + invalidate_app_db_title_cache(); + log_debug(" [REG] auto-remove requested: %s", title_id); + continue; + } + + log_debug(" [REG] auto-remove failed (not retried): %s code=0x%08X", + title_id, (uint32_t)res); + } + + free_app_db_title_list(&auto_remove_titles); +} + +void reset_missing_game_cache_timers(void) { pthread_mutex_lock(&g_game_cache_mutex); for (int k = 0; k < MAX_PENDING; k++) { if (!g_game_cache[k].valid) continue; - const char *entry_root = - ensure_game_cache_owning_scan_root(&g_game_cache[k]) - ? g_game_cache[k].owning_scan_root - : g_game_cache[k].path; - if (!path_matches_root_or_child(entry_root, root)) - continue; - if (game_cache_source_exists(&g_game_cache[k])) - continue; - clear_game_cache_slot(k, "source removed"); + if (g_game_cache[k].path[0] == '\0') + clear_game_cache_slot(k, NULL); + else + g_game_cache[k].missing_since_us = 0; } pthread_mutex_unlock(&g_game_cache_mutex); } @@ -173,7 +347,7 @@ void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn, struct GameCache entry; bool valid = false; pthread_mutex_lock(&g_game_cache_mutex); - if (g_game_cache[k].valid) { + if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0) { bool has_owning_scan_root = ensure_game_cache_owning_scan_root(&g_game_cache[k]); const char *entry_root = has_owning_scan_root @@ -213,7 +387,7 @@ bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out, pthread_mutex_lock(&g_game_cache_mutex); size_t count = 0; for (int k = 0; k < MAX_PENDING; ++k) { - if (g_game_cache[k].valid) + if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0) count++; } @@ -228,7 +402,7 @@ bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out, size_t copied = 0; for (int k = 0; k < MAX_PENDING; ++k) { - if (!g_game_cache[k].valid) + if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0) continue; sm_game_cache_snapshot_entry_t *entry = &entries[copied++]; (void)strlcpy(entry->path, g_game_cache[k].path, sizeof(entry->path)); @@ -274,7 +448,7 @@ bool find_cached_game(const char *path, const char *title_id, void clear_cached_game(const char *path) { pthread_mutex_lock(&g_game_cache_mutex); for (int k = 0; k < MAX_PENDING; k++) { - if (!g_game_cache[k].valid) + if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0) continue; if (strcmp(g_game_cache[k].path, path) != 0) continue; diff --git a/src/sm_image_index.c b/src/sm_image_index.c index f6ff767..4bcc647 100644 --- a/src/sm_image_index.c +++ b/src/sm_image_index.c @@ -232,6 +232,26 @@ bool sm_image_index_needs_scan(const char *path, const struct stat *st, return needs_scan; } +bool sm_image_index_has_source_for_title(const char *title_id) { + if (!title_id || title_id[0] == '\0') + return false; + + bool found = false; + pthread_mutex_lock(&g_image_index_mutex); + load_index(); + for (int i = 0; i < MAX_IMAGE_TITLES; ++i) { + const image_index_title_t *title = &g_image_titles[i]; + if (!title->valid || strcmp(title->title_id, title_id) != 0) + continue; + + const image_index_entry_t *entry = &g_image_index[title->image_index]; + found = entry->valid && path_exists(entry->path); + break; + } + pthread_mutex_unlock(&g_image_index_mutex); + return found; +} + void sm_image_index_begin_scan(const char *path, const struct stat *st) { pthread_mutex_lock(&g_image_index_mutex); load_index(); diff --git a/src/sm_scan.c b/src/sm_scan.c index 565c84e..0c5c5d1 100644 --- a/src/sm_scan.c +++ b/src/sm_scan.c @@ -381,6 +381,8 @@ static bool try_collect_candidate_for_directory( if (probe_result == DIRECTORY_CANDIDATE_DESCEND) return false; + note_game_cache_source_seen(full_path, info.title_id, info.title_name); + if (!sm_image_index_record_game(full_path, info.title_id) && unstable_found_out) { *unstable_found_out = true; diff --git a/src/sm_scanner.c b/src/sm_scanner.c index 0424d0c..93becf4 100644 --- a/src/sm_scanner.c +++ b/src/sm_scanner.c @@ -11,6 +11,7 @@ #include "sm_fakelib.h" #include "sm_filesystem.h" #include "sm_game_lifecycle.h" +#include "sm_game_cache.h" #include "sm_gameinfo.h" #include "sm_hash.h" #include "sm_image.h" @@ -914,6 +915,11 @@ static bool apply_runtime_config_reload_effects(int kq, sm_kstuff_on_config_reload(); + if (old_cfg->auto_remove_missing_games != + new_cfg->auto_remove_missing_games) { + reset_missing_game_cache_timers(); + } + if (scan_topology_changed) { clear_scanner_watch_entries(); reset_scanner_root_states(); @@ -971,6 +977,8 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason, if (should_abort_scan_cycle()) return false; + reconcile_missing_app_db_games(); + if (!sm_install_has_pending_work() && !release_scan_runtime_mounts()) log_debug(" [IMG] some discovery mounts remain busy");