From 896e5af0fdb5d18b08a8fa13975c7802a17c41f2 Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Sun, 22 Feb 2026 02:35:36 +0300 Subject: [PATCH] Fix RO/RW mount flag mapping and kstuff loader mount-link handling --- New-OsfExfatImage.ps1 | 4 +- README.md | 8 +- config.ini.example | 7 + src/main.c | 571 ++++++++++++++++-------------------------- 4 files changed, 224 insertions(+), 366 deletions(-) diff --git a/New-OsfExfatImage.ps1 b/New-OsfExfatImage.ps1 index 01c1f8e..ccce53a 100644 --- a/New-OsfExfatImage.ps1 +++ b/New-OsfExfatImage.ps1 @@ -88,9 +88,9 @@ function Find-OSFMountCom { if ($cmd) { return $cmd.Source } $candidates = @( - "$env:ProgramFiles\OSFMount\osfmount.com", + "${env:ProgramFiles}\OSFMount\osfmount.com", "${env:ProgramFiles(x86)}\OSFMount\osfmount.com", - "$env:ProgramFiles\PassMark\OSFMount\osfmount.com", + "${env:ProgramFiles}\PassMark\OSFMount\osfmount.com", "${env:ProgramFiles(x86)}\PassMark\OSFMount\osfmount.com" ) | Where-Object { $_ -and (Test-Path $_) } diff --git a/README.md b/README.md index c30a436..4fc11af 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ShadowMountPlus (PS5) -**Version:** `1.5beta3` +**Version:** `1.5beta4` **Repository:** https://github.com/drakmor/shadowMountPlus @@ -39,6 +39,8 @@ Supported keys (all optional): - `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`) - `mount_read_only=1|0` (default: `1`) - `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`) +- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`) +- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`) - `exfat_backend=lvd|md` (default: `lvd`) - `ufs_backend=lvd|md` (default: `lvd`) - `scanpath=` (can be repeated on multiple lines; default: built-in scan path list below) @@ -53,7 +55,8 @@ Scan path behavior: - `/data/ufsmnt` is always added automatically, even with custom paths. - With `recursive_scan=0` (default), only first-level subfolders are checked. - With `recursive_scan=1`, subfolders are scanned recursively. -- Full scan loop runs every 10 seconds. +- Full scan loop runs every `scan_interval_seconds` (default: `10`). +- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`). Validation: - See `config.ini.example` for a ready-to-use template. @@ -189,6 +192,7 @@ If a game is not mounted: - Verify scan depth: - `recursive_scan=0` scans only first-level subfolders; - `recursive_scan=1` scans recursively. +- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish). - Verify game structure: - folder game: `/sce_sys/param.json`; - image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder). diff --git a/config.ini.example b/config.ini.example index 90d1510..e8ea8ac 100644 --- a/config.ini.example +++ b/config.ini.example @@ -19,6 +19,13 @@ mount_read_only=1 # 0/false/no/off -> scan only first-level subdirectories (default) recursive_scan=0 +# Full scan loop interval (seconds), range: 1..3600 +scan_interval_seconds=10 + +# Source stabilization wait window (seconds), range: 0..3600 +# If source mtime is newer than this window, it will be deferred to next scan. +stability_wait_seconds=10 + # Backend selection per filesystem: # lvd -> /dev/lvdctl -> /dev/lvdN # md -> /dev/mdctl -> /dev/mdN diff --git a/src/main.c b/src/main.c index 1362eb8..ebf5a6d 100644 --- a/src/main.c +++ b/src/main.c @@ -27,10 +27,10 @@ #include // --- Configuration --- -#define SCAN_INTERVAL_US 10000000 +#define DEFAULT_SCAN_INTERVAL_US 10000000u +#define DEFAULT_STABILITY_WAIT_SECONDS 10u #define MAX_PENDING 512 #define MAX_UFS_MOUNTS 64 -#define MAX_NULLFS_MOUNTS MAX_PENDING #define PATH_STATE_CAPACITY MAX_PENDING #define TITLE_STATE_CAPACITY MAX_PENDING #define STATE_HASH_SIZE 1024u @@ -39,6 +39,9 @@ #define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3 #define MAX_IMAGE_MOUNT_ATTEMPTS 3 #define IMAGE_MOUNT_READ_ONLY 1 +#define MIN_SCAN_INTERVAL_SECONDS 1u +#define MAX_SCAN_INTERVAL_SECONDS 3600u +#define MAX_STABILITY_WAIT_SECONDS 3600u #define APP_DB_QUERY_BUSY_RETRIES 3 #define APP_DB_UPDATE_BUSY_RETRIES 25 #define APP_DB_PREPARE_BUSY_RETRIES 25 @@ -78,10 +81,10 @@ #define SCE_LVD_IOC_ATTACH 0xC0286D00 #define SCE_LVD_IOC_DETACH 0xC0286D01 #define LVD_ATTACH_IO_VERSION 1 -#define LVD_ATTACH_OPTION_FLAGS_DEFAULT 0x8 -#define LVD_ATTACH_OPTION_FLAGS_RW 0x9 -#define LVD_ATTACH_OPTION_NORM_DD_RO 0x16 -#define LVD_ATTACH_OPTION_NORM_DD_RW 0x1E +#define LVD_ATTACH_OPTION_FLAGS_DEFAULT 0x9 +#define LVD_ATTACH_OPTION_FLAGS_RW 0x8 +#define LVD_ATTACH_OPTION_NORM_DD_RO 0x1E +#define LVD_ATTACH_OPTION_NORM_DD_RW 0x16 #define LVD_SECTOR_SIZE_EXFAT 512u #define LVD_SECTOR_SIZE_UFS 4096u #define LVD_SECTOR_SIZE_PFS 32768u @@ -224,7 +227,7 @@ void notify_system(const char *fmt, ...); void log_debug(const char *fmt, ...); static void close_app_db(void); static void shutdown_ufs_mounts(void); -static void shutdown_nullfs_mounts(void); +static void ensure_runtime_config_ready(void); // Standard Notification typedef struct notify_request { @@ -300,10 +303,10 @@ struct TitleStateEntry { struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY]; static uint16_t g_title_state_hash[STATE_HASH_SIZE]; -struct AppDbLookupCache { - char title_id[MAX_TITLE_ID]; - bool in_app_db; - bool valid; +struct AppDbTitleList { + char(*ids)[MAX_TITLE_ID]; + int count; + int capacity; }; typedef struct { @@ -338,6 +341,8 @@ typedef struct { bool debug_enabled; bool mount_read_only; bool recursive_scan; + uint32_t scan_interval_us; + uint32_t stability_wait_seconds; attach_backend_t exfat_backend; attach_backend_t ufs_backend; uint32_t lvd_sector_exfat; @@ -350,10 +355,8 @@ typedef struct { static runtime_config_t g_runtime_cfg; static bool g_runtime_cfg_ready = false; static sqlite3 *g_app_db = NULL; -static sqlite3_stmt *g_app_db_stmt_has_title = NULL; static sqlite3_stmt *g_app_db_stmt_update_snd0 = NULL; static scan_candidate_t g_scan_candidates[MAX_PENDING]; -static struct AppDbLookupCache g_scan_db_lookup_cache[MAX_PENDING]; static char g_scan_discovered_param_roots[MAX_PENDING][MAX_PATH]; struct UfsCache { @@ -368,14 +371,6 @@ struct UfsCache { }; struct UfsCache ufs_cache[MAX_UFS_MOUNTS]; -struct NullfsCache { - // Mounted target path in /system_ex/app/... - char mount_point[MAX_PATH]; - // Slot occupancy flag. - bool valid; -}; -struct NullfsCache nullfs_cache[MAX_NULLFS_MOUNTS]; - static volatile sig_atomic_t g_stop_requested = 0; typedef enum { @@ -790,7 +785,7 @@ static void clear_image_mount_attempts(const char *path) { entry->image_mount_limit_logged = false; } -// --- Active Mount and NullFS Session Caches --- +// --- Active Mount Session Caches --- static void cache_ufs_mount(const char *path, int unit_id, attach_backend_t backend) { for (int k = 0; k < MAX_UFS_MOUNTS; k++) { @@ -812,35 +807,6 @@ static void cache_ufs_mount(const char *path, int unit_id, } } -static void cache_nullfs_mount(const char *mount_point) { - for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) { - if (nullfs_cache[k].valid && - strcmp(nullfs_cache[k].mount_point, mount_point) == 0) { - return; - } - } - for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) { - if (!nullfs_cache[k].valid) { - (void)strlcpy(nullfs_cache[k].mount_point, mount_point, - sizeof(nullfs_cache[k].mount_point)); - nullfs_cache[k].valid = true; - return; - } - } -} - -static void invalidate_nullfs_mount(const char *mount_point) { - for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) { - if (!nullfs_cache[k].valid) - continue; - if (strcmp(nullfs_cache[k].mount_point, mount_point) == 0) { - nullfs_cache[k].valid = false; - nullfs_cache[k].mount_point[0] = '\0'; - return; - } - } -} - // --- Shutdown and Stop Signal Handling --- static void on_signal(int sig) { (void)sig; @@ -977,28 +943,19 @@ static bool read_mount_link(const char *title_id, char *out, size_t out_size) { return out[0] != '\0'; } -static void build_system_ex_app_path(const char *title_id, char *out, - size_t out_size) { - if (out_size == 0) - return; - snprintf(out, out_size, "/system_ex/app/%s", title_id); -} +static bool mount_link_matches_source(const char *title_id, + const char *source_path) { + if (!source_path || source_path[0] == '\0') + return false; -static bool mount_link_matches_system_ex(const char *title_id) { char tracked_path[MAX_PATH]; - char expected_path[MAX_PATH]; if (!read_mount_link(title_id, tracked_path, sizeof(tracked_path))) return false; - build_system_ex_app_path(title_id, expected_path, sizeof(expected_path)); - return strcmp(tracked_path, expected_path) == 0; + return strcmp(tracked_path, source_path) == 0; } // --- app.db Access Layer --- static void close_app_db(void) { - if (g_app_db_stmt_has_title) { - sqlite3_finalize(g_app_db_stmt_has_title); - g_app_db_stmt_has_title = NULL; - } if (g_app_db_stmt_update_snd0) { sqlite3_finalize(g_app_db_stmt_update_snd0); g_app_db_stmt_update_snd0 = NULL; @@ -1057,147 +1014,6 @@ static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out, return -1; } -static bool init_app_triggers(void) { - static const char *find_sql = - "SELECT substr(name, 14) FROM sqlite_master " - "WHERE type='table' " - "AND name GLOB 'tbl_iconinfo_[0-9]*' " - "LIMIT 1;"; - - for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) { - if (!ensure_app_db_open()) - return false; - - char suffix[21]; - suffix[0] = '\0'; - - sqlite3_stmt *stmt = NULL; - int rc = sqlite3_prepare_v2(g_app_db, find_sql, -1, &stmt, NULL); - if (rc == SQLITE_OK) { - int step_rc = sqlite3_step(stmt); - if (step_rc == SQLITE_ROW) { - const char *val = (const char *)sqlite3_column_text(stmt, 0); - if (val) - (void)strlcpy(suffix, val, sizeof(suffix)); - rc = SQLITE_OK; - } else { - rc = step_rc; - } - } - if (stmt) - sqlite3_finalize(stmt); - - if (rc != SQLITE_OK && rc != SQLITE_DONE) { - log_debug(" [DB] query tbl_iconinfo failed: rc=%d err=%s", rc, - (g_app_db ? sqlite3_errmsg(g_app_db) : "unknown")); - if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, true)) - continue; - close_app_db(); - return false; - } - - if (suffix[0] == '\0') { - log_debug(" [DB] no tbl_iconinfo table found, skipping triggers"); - close_app_db(); - return true; - } - - char table_name[34]; - snprintf(table_name, sizeof(table_name), "tbl_iconinfo_%s", suffix); - - char sql[2048]; - snprintf(sql, sizeof(sql), - "CREATE TRIGGER IF NOT EXISTS trig_update_drm_tbl_iconinfo_%s " - "AFTER UPDATE OF appDrmType ON %s " - "WHEN new.appDrmType = 1 " - "BEGIN " - "UPDATE %s SET appDrmType = 5 WHERE titleId = old.titleId; " - "END;" - "CREATE TRIGGER IF NOT EXISTS trig_insert_drm_tbl_iconinfo_%s " - "AFTER INSERT ON %s " - "WHEN new.appDrmType = 1 " - "BEGIN " - "UPDATE %s SET appDrmType = 5 WHERE titleId = new.titleId; " - "END;" - "UPDATE %s SET appDrmType=5 WHERE appDrmType=1;", - suffix, table_name, table_name, suffix, table_name, table_name, - table_name); - - char *err = NULL; - rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err); - if (rc == SQLITE_OK) { - int changes = sqlite3_changes(g_app_db); - log_debug(" [DB] triggers updated for %s, fixed rows=%d", - table_name, changes); - close_app_db(); - return true; - } - - log_debug(" [DB] update triggers failed: rc=%d err=%s", rc, - err ? err : (g_app_db ? sqlite3_errmsg(g_app_db) : "unknown")); - if (err) - sqlite3_free(err); - - if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, true)) - continue; - - close_app_db(); - return false; - } - - close_app_db(); - return false; -} - -static int query_title_registered_in_app_db(const char *title_id) { - if (!title_id || title_id[0] == '\0') - return 0; - - if (!g_app_db_stmt_has_title) { - const char *sql = - "SELECT 1 FROM tbl_contentinfo WHERE titleId = ?1 LIMIT 1;"; - int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_has_title, - APP_DB_PREPARE_BUSY_RETRIES, - "titleId check"); - if (prep_rc != SQLITE_OK) - return -1; - } - - for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) { - sqlite3_reset(g_app_db_stmt_has_title); - sqlite3_clear_bindings(g_app_db_stmt_has_title); - if (sqlite3_bind_text(g_app_db_stmt_has_title, 1, title_id, -1, - SQLITE_TRANSIENT) != SQLITE_OK) { - log_debug(" [DB] bind failed for titleId check: %s", - sqlite3_errmsg(g_app_db)); - close_app_db(); - return -1; - } - - int rc = sqlite3_step(g_app_db_stmt_has_title); - if (rc == SQLITE_ROW || rc == SQLITE_DONE) { - int ret = (rc == SQLITE_ROW) ? 1 : 0; - // Always reset after stepping, to avoid keeping a cursor/lock open. - sqlite3_reset(g_app_db_stmt_has_title); - sqlite3_clear_bindings(g_app_db_stmt_has_title); - return ret; - } - - if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false)) - continue; - - if (g_app_db) { - log_debug(" [DB] step failed for titleId check: rc=%d err=%s", rc, - sqlite3_errmsg(g_app_db)); - } - close_app_db(); - return -1; - } - - close_app_db(); - return -1; -} - static int update_snd0info(const char *title_id) { if (!title_id || title_id[0] == '\0') return -1; @@ -1249,54 +1065,143 @@ static int update_snd0info(const char *title_id) { return -1; } -static int lookup_title_in_app_db_cached(const char *title_id, - struct AppDbLookupCache *lookup_cache, - int *cache_count) { - if (!title_id || title_id[0] == '\0') - return 0; +static void free_app_db_title_list(struct AppDbTitleList *list) { + if (!list) + return; + free(list->ids); + list->ids = NULL; + list->count = 0; + list->capacity = 0; +} - int count = (cache_count ? *cache_count : 0); - for (int i = 0; i < count; i++) { - if (lookup_cache[i].valid && strcmp(lookup_cache[i].title_id, title_id) == 0) - return lookup_cache[i].in_app_db ? 1 : 0; +static bool append_app_db_title(struct AppDbTitleList *list, + const char *title_id) { + if (!list || !title_id || title_id[0] == '\0') + return true; + + if (list->count >= list->capacity) { + int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024; + char(*new_ids)[MAX_TITLE_ID] = + realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids)); + if (!new_ids) + return false; + list->ids = new_ids; + list->capacity = new_capacity; } - int q = query_title_registered_in_app_db(title_id); - if (q < 0) - return -1; + (void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID); + list->count++; + return true; +} - bool in_app_db = (q != 0); - if (cache_count && count < MAX_PENDING) { - (void)strlcpy(lookup_cache[count].title_id, title_id, - sizeof(lookup_cache[count].title_id)); - lookup_cache[count].in_app_db = in_app_db; - lookup_cache[count].valid = true; - *cache_count = count + 1; +static int compare_title_id_str(const void *a, const void *b) { + return strcmp((const char *)a, (const char *)b); +} + +static bool app_db_title_list_contains(const struct AppDbTitleList *list, + const char *title_id) { + if (!list || !title_id || title_id[0] == '\0') + return false; + return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids), + compare_title_id_str) != NULL; +} + +static bool load_app_db_title_list(struct AppDbTitleList *list) { + if (!list) + return false; + free_app_db_title_list(list); + + const char *sql = + "SELECT DISTINCT titleId " + "FROM tbl_contentinfo " + "WHERE titleId != '' " + "ORDER BY titleId;"; + sqlite3_stmt *stmt = NULL; + int prep_rc = app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES, + "title list query"); + if (prep_rc != SQLITE_OK) { + close_app_db(); + return false; } - return in_app_db ? 1 : 0; + + int busy_attempts = 0; + while (!should_stop_requested()) { + int rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + const char *title_id = (const char *)sqlite3_column_text(stmt, 0); + if (title_id && title_id[0] != '\0') { + if (!append_app_db_title(list, title_id)) { + log_debug(" [DB] title list allocation failed"); + sqlite3_finalize(stmt); + close_app_db(); + free_app_db_title_list(list); + return false; + } + } + continue; + } + if (rc == SQLITE_DONE) { + sqlite3_finalize(stmt); + close_app_db(); + log_debug(" [DB] loaded app.db title list: %d entries", list->count); + return true; + } + if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) && + busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) { + busy_attempts++; + sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US); + continue; + } + + log_debug(" [DB] title list query failed: rc=%d err=%s", rc, + (g_app_db ? sqlite3_errmsg(g_app_db) : "unknown")); + sqlite3_finalize(stmt); + close_app_db(); + free_app_db_title_list(list); + return false; + } + + sqlite3_finalize(stmt); + close_app_db(); + free_app_db_title_list(list); + return false; } // --- FAST STABILITY CHECK --- -static bool is_path_stable_now(const char *path, double *root_diff_out) { +static bool is_path_stable_now(const char *path, double *root_diff_out, + int *stat_errno_out) { struct stat st; time_t now = time(NULL); - if (stat(path, &st) != 0) + ensure_runtime_config_ready(); + if (stat_errno_out) + *stat_errno_out = 0; + if (stat(path, &st) != 0) { + if (root_diff_out) + *root_diff_out = -1.0; + if (stat_errno_out) + *stat_errno_out = errno; return false; + } double root_diff = difftime(now, st.st_mtime); if (root_diff_out) *root_diff_out = root_diff; if (root_diff < 0.0) return true; - return root_diff > 10.0; + return root_diff > (double)g_runtime_cfg.stability_wait_seconds; } bool wait_for_stability_fast(const char *path, const char *name) { double diff = 0.0; - if (is_path_stable_now(path, &diff)) + int st_err = 0; + if (is_path_stable_now(path, &diff, &st_err)) return true; - log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff); + if (st_err != 0) { + log_debug(" [WAIT] %s stat failed for %s: %s", name, path, strerror(st_err)); + } else { + log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff); + } sceKernelUsleep(2000000); // Wait 2s return false; // Force re-scan next cycle } @@ -1304,50 +1209,6 @@ bool wait_for_stability_fast(const char *path, const char *name) { // --- Mount/Copy Helpers for Install Action --- static int copy_file(const char *src, const char *dst); -static int remount_system_ex(void) { - struct iovec iov[] = { - IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"), - IOVEC_ENTRY("fspath"), IOVEC_ENTRY("/system_ex"), - IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"), - IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"), - IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"), - IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL), - IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL)}; - return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE); -} - -static int mount_nullfs(const char *src, const char *dst) { - struct iovec iov[] = {IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"), - IOVEC_ENTRY("from"), IOVEC_ENTRY(src), - IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst)}; - return nmount(iov, IOVEC_SIZE(iov), MNT_RDONLY); -} - -static bool unmount_nullfs_mount(const char *mount_point) { - if (unmount(mount_point, 0) == 0) { - invalidate_nullfs_mount(mount_point); - return true; - } - - int err = errno; - if (err == ENOENT || err == EINVAL) { - invalidate_nullfs_mount(mount_point); - return true; - } - - log_debug(" [MOUNT] unmount failed for %s: %s, trying force...", - mount_point, strerror(err)); - if (unmount(mount_point, MNT_FORCE) == 0 || errno == ENOENT || - errno == EINVAL) { - invalidate_nullfs_mount(mount_point); - return true; - } - - log_debug(" [MOUNT] force unmount failed for %s: %s", mount_point, - strerror(errno)); - return false; -} - static int copy_dir(const char *src, const char *dst) { if (mkdir(dst, 0777) != 0 && errno != EEXIST) return -1; @@ -1507,13 +1368,14 @@ static bool wait_for_dev_node_state(const char *devname, bool should_exist) { static bool is_source_stable_for_mount(const char *path, const char *name, const char *tag) { struct stat st; + ensure_runtime_config_ready(); if (stat(path, &st) != 0) return false; double age = difftime(time(NULL), st.st_mtime); if (age < 0.0) return true; - if (age < 10.0) { + if (age < (double)g_runtime_cfg.stability_wait_seconds) { log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age); return false; } @@ -1525,6 +1387,8 @@ static void init_runtime_config_defaults(void) { g_runtime_cfg.debug_enabled = true; g_runtime_cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0); g_runtime_cfg.recursive_scan = false; + g_runtime_cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US; + g_runtime_cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS; g_runtime_cfg.exfat_backend = DEFAULT_EXFAT_BACKEND; g_runtime_cfg.ufs_backend = DEFAULT_UFS_BACKEND; g_runtime_cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT; @@ -1683,6 +1547,30 @@ static bool load_runtime_config(void) { continue; } + if (strcasecmp(key, "scan_interval_seconds") == 0 || + strcasecmp(key, "scan_interval_sec") == 0) { + if (!parse_u32_ini(value, &u32) || u32 < MIN_SCAN_INTERVAL_SECONDS || + u32 > MAX_SCAN_INTERVAL_SECONDS) { + log_debug(" [CFG] invalid scan interval at line %d: %s=%s (range: %u..%u)", + line_no, key, value, (unsigned)MIN_SCAN_INTERVAL_SECONDS, + (unsigned)MAX_SCAN_INTERVAL_SECONDS); + continue; + } + g_runtime_cfg.scan_interval_us = u32 * 1000000u; + continue; + } + + if (strcasecmp(key, "stability_wait_seconds") == 0 || + strcasecmp(key, "stability_wait_sec") == 0) { + if (!parse_u32_ini(value, &u32) || u32 > MAX_STABILITY_WAIT_SECONDS) { + log_debug(" [CFG] invalid stability wait at line %d: %s=%s (max: %u)", + line_no, key, value, (unsigned)MAX_STABILITY_WAIT_SECONDS); + continue; + } + g_runtime_cfg.stability_wait_seconds = u32; + continue; + } + if (strcasecmp(key, "exfat_backend") == 0) { if (!parse_backend_ini(value, &backend)) { log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key, @@ -1758,7 +1646,7 @@ static bool load_runtime_config(void) { log_debug(" [CFG] loaded: debug=%d ro=%d recursive_scan=%d " "exfat_backend=%s ufs_backend=%s " "lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) " - "scan_paths=%d", + "scan_interval_s=%u stability_wait_s=%u scan_paths=%d", g_runtime_cfg.debug_enabled ? 1 : 0, g_runtime_cfg.mount_read_only ? 1 : 0, g_runtime_cfg.recursive_scan ? 1 : 0, @@ -1766,7 +1654,8 @@ static bool load_runtime_config(void) { backend_name(g_runtime_cfg.ufs_backend), g_runtime_cfg.lvd_sector_exfat, g_runtime_cfg.lvd_sector_ufs, g_runtime_cfg.lvd_sector_pfs, g_runtime_cfg.md_sector_exfat, - g_runtime_cfg.md_sector_ufs, g_scan_path_count); + g_runtime_cfg.md_sector_ufs, g_runtime_cfg.scan_interval_us / 1000000u, + g_runtime_cfg.stability_wait_seconds, g_scan_path_count); return true; } @@ -1807,13 +1696,13 @@ static uint16_t get_lvd_attach_option(image_fs_type_t fs_type, bool mount_read_only) { if (fs_type == IMAGE_FS_UFS) { // UFS runtime mapping: RO -> 0x1E, RW -> 0x16. - return mount_read_only ? LVD_ATTACH_OPTION_NORM_DD_RW - : LVD_ATTACH_OPTION_NORM_DD_RO; + return mount_read_only ? LVD_ATTACH_OPTION_NORM_DD_RO + : LVD_ATTACH_OPTION_NORM_DD_RW; } // Generic/PFS runtime mapping: RO -> 0x9, RW -> 0x8. - return mount_read_only ? LVD_ATTACH_OPTION_FLAGS_RW - : LVD_ATTACH_OPTION_FLAGS_DEFAULT; + return mount_read_only ? LVD_ATTACH_OPTION_FLAGS_DEFAULT + : LVD_ATTACH_OPTION_FLAGS_RW; } static unsigned int get_nmount_flags(image_fs_type_t fs_type, @@ -2522,15 +2411,6 @@ static void shutdown_ufs_mounts(void) { } } -static void shutdown_nullfs_mounts(void) { - for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) { - if (!nullfs_cache[k].valid) - continue; - unmount_nullfs_mount(nullfs_cache[k].mount_point); - nullfs_cache[k].valid = false; - } -} - // --- Game Metadata Parsing (param.json) --- static int extract_json_string(const char *json, const char *key, char *out, size_t out_size) { @@ -2662,14 +2542,11 @@ static void prune_game_cache(void) { if (access(cache[k].path, F_OK) == 0) continue; - if (cache[k].title_id[0] != '\0') { - char mount_point[MAX_PATH]; - build_system_ex_app_path(cache[k].title_id, mount_point, sizeof(mount_point)); - if (unmount_nullfs_mount(mount_point)) { - log_debug(" [MOUNT] source removed, unmounted: %s (%s)", - cache[k].title_id, cache[k].path); - } - } + if (cache[k].title_id[0] != '\0') + log_debug(" [CACHE] source removed: %s (%s)", cache[k].title_id, + cache[k].path); + else + log_debug(" [CACHE] source removed: %s", cache[k].path); cache[k].valid = false; cache[k].path[0] = '\0'; @@ -2725,8 +2602,8 @@ static bool is_under_discovered_param_root( // --- Candidate Discovery --- static bool try_collect_candidate_for_directory( const char *full_path, scan_candidate_t *candidates, int max_candidates, - int *candidate_count, struct AppDbLookupCache *db_cache, - int *db_cache_count, char discovered_param_roots[][MAX_PATH], + int *candidate_count, const struct AppDbTitleList *app_db_titles, + bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count) { struct stat param_st; memset(¶m_st, 0, sizeof(param_st)); @@ -2782,14 +2659,12 @@ static bool try_collect_candidate_for_directory( } } - int in_app_db_q = lookup_title_in_app_db_cached(title_id, db_cache, db_cache_count); - if (in_app_db_q < 0) { - // app.db is busy/locked right now; defer install/remount decisions to a later scan. + if (!app_db_titles_ready) { log_debug(" [SKIP] app.db unavailable (locked/busy), deferring: %s (%s)", title_name, title_id); return true; } - bool in_app_db = (in_app_db_q != 0); + bool in_app_db = app_db_title_list_contains(app_db_titles, title_id); if (in_app_db) { for (int k = 0; k < MAX_PENDING; k++) { @@ -2817,8 +2692,13 @@ static bool try_collect_candidate_for_directory( // Installed status requires both app files and app.db presence. bool installed = is_installed(title_id) && in_app_db; bool mounted = is_data_mounted(title_id); - if (installed && mounted && mount_link_matches_system_ex(title_id)) { - log_debug(" [SKIP] already installed+mounted: %s (%s)", title_name, title_id); + if (installed && mount_link_matches_source(title_id, full_path)) { + if (mounted) + log_debug(" [SKIP] already installed+mounted+linked: %s (%s)", title_name, + title_id); + else + log_debug(" [SKIP] already installed+linked (waiting kstuff mount): %s (%s)", + title_name, title_id); return true; } @@ -2855,8 +2735,8 @@ static bool try_collect_candidate_for_directory( static void collect_candidates_recursively( const char *dir_path, scan_candidate_t *candidates, int max_candidates, - int *candidate_count, struct AppDbLookupCache *db_cache, - int *db_cache_count, char discovered_param_roots[][MAX_PATH], + int *candidate_count, const struct AppDbTitleList *app_db_titles, + bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count) { if (should_stop_requested() || !dir_path || dir_path[0] == '\0') return; @@ -2864,8 +2744,9 @@ static void collect_candidates_recursively( // Once a directory has sce_sys/param.json (valid or not), // it is treated as a terminal game root and descendants are skipped. if (try_collect_candidate_for_directory( - dir_path, candidates, max_candidates, candidate_count, db_cache, - db_cache_count, discovered_param_roots, discovered_param_root_count)) { + dir_path, candidates, max_candidates, candidate_count, app_db_titles, + app_db_titles_ready, discovered_param_roots, + discovered_param_root_count)) { return; } @@ -2896,8 +2777,8 @@ static void collect_candidates_recursively( continue; collect_candidates_recursively( - full_path, candidates, max_candidates, candidate_count, db_cache, - db_cache_count, discovered_param_roots, discovered_param_root_count); + full_path, candidates, max_candidates, candidate_count, app_db_titles, + app_db_titles_ready, discovered_param_roots, discovered_param_root_count); } closedir(d); @@ -2905,9 +2786,9 @@ static void collect_candidates_recursively( // --- Unified Scan Pass (images + game candidates) --- static void cleanup_lost_sources_before_scan(void) { - // 1) Remove stale nullfs mounts for deleted game sources. + // 1) Drop stale game cache entries for deleted sources. prune_game_cache(); - // 2) After nullfs cleanup, stale image unmounts are less likely to be busy. + // 2) Unmount stale image mounts for deleted image files. cleanup_stale_image_mounts(); // 3) Drop stale path-state entries. prune_path_state(); @@ -2916,13 +2797,16 @@ static void cleanup_lost_sources_before_scan(void) { static int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates) { int candidate_count = 0; - struct AppDbLookupCache *db_cache = g_scan_db_lookup_cache; - int db_cache_count = 0; + struct AppDbTitleList app_db_titles = {0}; + bool app_db_titles_ready = load_app_db_title_list(&app_db_titles); char (*discovered_param_roots)[MAX_PATH] = g_scan_discovered_param_roots; int discovered_param_root_count = 0; - memset(db_cache, 0, sizeof(g_scan_db_lookup_cache)); memset(discovered_param_roots, 0, sizeof(g_scan_discovered_param_roots)); + if (!app_db_titles_ready) { + log_debug(" [DB] app.db title list unavailable for this scan cycle"); + } + for (int i = 0; SCAN_PATHS[i] != NULL; i++) { if (should_stop_requested()) goto done; @@ -2966,20 +2850,20 @@ static int collect_scan_candidates(scan_candidate_t *candidates, if (g_runtime_cfg.recursive_scan) { collect_candidates_recursively( - full_path, candidates, max_candidates, &candidate_count, db_cache, - &db_cache_count, discovered_param_roots, + full_path, candidates, max_candidates, &candidate_count, + &app_db_titles, app_db_titles_ready, discovered_param_roots, &discovered_param_root_count); } else { (void)try_collect_candidate_for_directory( - full_path, candidates, max_candidates, &candidate_count, db_cache, - &db_cache_count, discovered_param_roots, + full_path, candidates, max_candidates, &candidate_count, + &app_db_titles, app_db_titles_ready, discovered_param_roots, &discovered_param_root_count); } } closedir(d); } done: - // Keep app.db open only during a single scan cycle. + free_app_db_title_list(&app_db_titles); close_app_db(); return candidate_count; } @@ -2988,31 +2872,9 @@ done: bool mount_and_install(const char *src_path, const char *title_id, const char *title_name, bool is_remount, bool should_register) { - char system_ex_app[MAX_PATH]; char user_app_dir[MAX_PATH]; char user_sce_sys[MAX_PATH]; char src_sce_sys[MAX_PATH]; - bool nullfs_mounted = false; - - // MOUNT - build_system_ex_app_path(title_id, system_ex_app, sizeof(system_ex_app)); - mkdir(system_ex_app, 0777); - if (unmount(system_ex_app, 0) != 0 && errno != ENOENT && errno != EINVAL) { - log_debug(" [MOUNT] pre-unmount failed for %s: %s, trying force...", - system_ex_app, strerror(errno)); - if (unmount(system_ex_app, MNT_FORCE) != 0 && errno != ENOENT && - errno != EINVAL) { - log_debug(" [MOUNT] pre-force-unmount failed for %s: %s", system_ex_app, - strerror(errno)); - return false; - } - } - if (mount_nullfs(src_path, system_ex_app) < 0) { - log_debug(" [MOUNT] FAIL: %s", strerror(errno)); - return false; - } - nullfs_mounted = true; - cache_nullfs_mount(system_ex_app); // COPY FILES if (!is_remount) { @@ -3025,8 +2887,6 @@ bool mount_and_install(const char *src_path, const char *title_id, if (copy_dir(src_sce_sys, user_sce_sys) != 0) { log_debug(" [COPY] Failed to copy sce_sys: %s -> %s", src_sce_sys, user_sce_sys); - if (nullfs_mounted) - unmount_nullfs_mount(system_ex_app); return false; } @@ -3035,8 +2895,6 @@ bool mount_and_install(const char *src_path, const char *title_id, snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id); if (copy_file(icon_src, icon_dst) != 0) { log_debug(" [COPY] Failed to copy icon: %s -> %s", icon_src, icon_dst); - if (nullfs_mounted) - unmount_nullfs_mount(system_ex_app); return false; } } else { @@ -3049,7 +2907,7 @@ bool mount_and_install(const char *src_path, const char *title_id, bool link_ok = false; FILE *flnk = fopen(lnk_path, "w"); if (flnk) { - bool write_ok = (fprintf(flnk, "%s", system_ex_app) >= 0); + bool write_ok = (fprintf(flnk, "%s", src_path) >= 0); bool flush_ok = (fflush(flnk) == 0); bool close_ok = (fclose(flnk) == 0); if (write_ok && flush_ok && close_ok) { @@ -3061,8 +2919,6 @@ bool mount_and_install(const char *src_path, const char *title_id, log_debug(" [LINK] open failed for %s: %s", lnk_path, strerror(errno)); } if (!link_ok) { - if (nullfs_mounted) - unmount_nullfs_mount(system_ex_app); return false; } @@ -3100,8 +2956,6 @@ bool mount_and_install(const char *src_path, const char *title_id, log_debug(" [REG] FAIL: 0x%x", res); notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id, (uint32_t)res); - if (nullfs_mounted) - unmount_nullfs_mount(system_ex_app); return false; } return true; @@ -3110,11 +2964,6 @@ bool mount_and_install(const char *src_path, const char *title_id, // --- Execution (per discovered candidate) --- static void process_scan_candidates(const scan_candidate_t *candidates, int candidate_count) { - if (remount_system_ex() != 0) { - log_debug(" [MOUNT] remount_system_ex failed: %s", strerror(errno)); - return; - } - for (int i = 0; i < candidate_count; i++) { if (should_stop_requested()) return; @@ -3223,7 +3072,6 @@ int main(void) { notify_system("ShadowMount+: Recursive scan enabled."); cleanup_mount_dirs(); - (void)init_app_triggers(); // --- STARTUP LOGIC --- cleanup_lost_sources_before_scan(); @@ -3248,7 +3096,7 @@ int main(void) { // Sleep FIRST since we either just finished scan above, or library was // ready. - if (sleep_with_stop_check(SCAN_INTERVAL_US)) { + if (sleep_with_stop_check(g_runtime_cfg.scan_interval_us)) { log_debug("[SHUTDOWN] stop requested during sleep"); break; } @@ -3256,7 +3104,6 @@ int main(void) { scan_all_paths(); } - shutdown_nullfs_mounts(); shutdown_ufs_mounts(); close_app_db(); if (lock >= 0) {