mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-21 22:22:28 -05:00
Add automatic removal of unavailable games
This commit is contained in:
1 parent
73b26ba3fb
commit
c4e9839fe5
13 files changed
+368
-73
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in new issue
Block a user