Add automatic removal of unavailable games

This commit is contained in:
Roman Tarasov committed 2026-07-15 23:31:30 +03:00
1 parent 73b26ba3fb
commit c4e9839fe5
13 files changed
+368 -73

No files matched your search

+3
View File
@@ -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=<image_filename>` (repeatable; force read-only mode for this image filename)
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `image_sector=<image_filename>:<sector_size>` (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 `<game>/sce_sys` for this check; image and backport sources use the target path itself.
Backport overlay behavior:
+12
View File
@@ -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
+2
View File
@@ -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);
+7
View File
@@ -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);
+2
View File
@@ -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
+3
View File
@@ -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
+2
View File
@@ -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;
+76 -47
View File
@@ -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;
}
+32 -1
View File
@@ -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,
+199 -25
View File
@@ -2,12 +2,15 @@
#include <pthread.h>
#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;
+20
View File
@@ -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();
+2
View File
@@ -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;
+8
View File
@@ -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");