Restore missing appmeta on remount and replace access checks with stat

This commit is contained in:
Roman Tarasov committed 2026-03-29 15:49:03 +03:00
1 parent 5d0ca6d5d1
commit 47bded61da
12 files changed
+201 -60

No files matched your search

+4 -2
View File
@@ -7,11 +7,13 @@
// Check whether a title is present in the installed app set.
bool is_installed(const char *title_id);
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
bool has_appmeta_data(const char *title_id);
// Check whether a title currently has mounted data.
bool is_data_mounted(const char *title_id);
// Read the mount.lnk source path for an installed title.
// Read the mount.lnk source path for a title tracked under /user/app.
bool read_mount_link(const char *title_id, char *out, size_t out_size);
// Validate a /user/app/<TITLE_ID> entry and optionally build its path.
// Validate a /user/app/<TITLE_ID> tracker entry and optionally build its path.
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
size_t app_dir_size);
// Recover staged mount links and warm image source mappings on startup.
+2
View File
@@ -8,6 +8,8 @@
// Return the last path component without modifying the input string.
const char *get_filename_component(const char *path);
// Return true when stat() can see the path.
bool path_exists(const char *path);
// Return true when a path lives under the image mount root.
bool is_under_image_mount_base(const char *path);
// Build "<scan_path>/backports" for a managed scan root.
+2
View File
@@ -9,6 +9,8 @@
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
#define CONFIG_FILE "/data/shadowmount/config.ini"
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
#define APPMETA_BASE "/user/appmeta"
#define APP_BASE "/user/app"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
#define KILL_FILE "/data/shadowmount/STOP"
#define TOAST_FILE "/data/shadowmount/notify.txt"
+5 -3
View File
@@ -14,6 +14,7 @@
#include "sm_mount_device.h"
#include "sm_filesystem.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_scan.h"
#include "sm_scanner.h"
#include "sm_install.h"
@@ -32,6 +33,7 @@
#define RESTART_WAIT_MAX_US 60000000u
#define KINFO_PID_OFFSET 72
#define KINFO_TDNAME_OFFSET 447
#define AUTHID_BASE 0x4801000000000013L
static volatile sig_atomic_t g_stop_requested = 0;
static atomic_bool g_shutdown_on_going_stop_requested = false;
@@ -68,7 +70,7 @@ void install_signal_handlers(void) {
bool should_stop_requested(void) {
if (g_stop_requested)
return true;
if (access(KILL_FILE, F_OK) == 0) {
if (path_exists(KILL_FILE)) {
remove(KILL_FILE);
return true;
}
@@ -252,7 +254,7 @@ static void log_non_empty_scan_paths(void) {
}
static void ensure_kstuff_noautomount_file(void) {
if (access(KSTUFF_NOAUTOMOUNT_FILE, F_OK) == 0)
if (path_exists(KSTUFF_NOAUTOMOUNT_FILE))
return;
int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_RDONLY | O_CREAT, 0666);
@@ -299,7 +301,7 @@ int main(void) {
sceUserServiceInitialize(0);
sceAppInstUtilInitialize();
kernel_set_ucred_authid(-1, 0x4801000000000013L);
kernel_set_ucred_authid(-1, AUTHID_BASE);
install_signal_handlers();
mkdir(LOG_DIR, 0777);
+23 -15
View File
@@ -12,15 +12,23 @@
// --- FILESYSTEM ---
bool is_installed(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/user/app/%s", title_id);
snprintf(path, sizeof(path), "%s/%s", APP_BASE, title_id);
struct stat st;
return (stat(path, &st) == 0);
}
bool has_appmeta_data(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "%s/%s/param.json", APPMETA_BASE, title_id);
struct stat st;
return (stat(path, &st) == 0 && S_ISREG(st.st_mode));
}
bool is_data_mounted(const char *title_id) {
char path[MAX_PATH];
snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json",
title_id);
return (access(path, F_OK) == 0);
return path_exists(path);
}
static bool read_mount_link_file(const char *lnk_path, char *out,
@@ -47,7 +55,7 @@ static bool read_mount_link_file(const char *lnk_path, char *out,
static void build_title_link_path(const char *title_id, const char *filename,
char out[MAX_PATH]) {
snprintf(out, MAX_PATH, "/user/app/%s/%s", title_id, filename);
snprintf(out, MAX_PATH, "%s/%s/%s", APP_BASE, title_id, filename);
}
typedef struct {
@@ -71,10 +79,10 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id,
static void for_each_title_app_dir(const char *open_error_context,
bool stop_on_signal,
title_app_dir_iter_fn fn, void *ctx) {
DIR *d = opendir("/user/app");
DIR *d = opendir(APP_BASE);
if (!d) {
if (errno != ENOENT)
log_debug(" [LINK] open /user/app failed%s: %s",
log_debug(" [LINK] open %s failed%s: %s", APP_BASE,
open_error_context ? open_error_context : "", strerror(errno));
return;
}
@@ -122,7 +130,7 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
path_size = app_dir_size;
}
int written = snprintf(path, path_size, "/user/app/%s", entry->d_name);
int written = snprintf(path, path_size, "%s/%s", APP_BASE, entry->d_name);
if (written < 0 || (size_t)written >= path_size) {
if (app_dir && app_dir_size > 0)
app_dir[0] = '\0';
@@ -146,18 +154,18 @@ static bool source_path_needs_cleanup(const char *source_path,
char image_source_path[MAX_PATH];
bool has_image_source = resolve_image_source_from_mount_cache(
source_path, image_source_path, sizeof(image_source_path));
if (has_image_source && access(image_source_path, F_OK) != 0)
if (has_image_source && !path_exists(image_source_path))
return true;
}
if (access(source_path, F_OK) != 0)
if (!path_exists(source_path))
return true;
if (path_matches_root_or_child(source_path, "/system_ex/app"))
return true;
char eboot_path[MAX_PATH];
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
if (access(eboot_path, F_OK) == 0)
if (path_exists(eboot_path))
return false;
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
@@ -165,7 +173,7 @@ static bool source_path_needs_cleanup(const char *source_path,
*tried_image_recovery = true;
}
return access(eboot_path, F_OK) != 0;
return !path_exists(eboot_path);
}
static bool resolve_mount_image_source_path(const title_link_paths_t *paths,
@@ -759,7 +767,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
title_id, src_path, (unsigned)(src_st.st_mode & 077777));
return false;
}
if (access(src_eboot, F_OK) != 0) {
if (!path_exists(src_eboot)) {
bool tried_image_recovery = false;
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
@@ -777,7 +785,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
if (state.mounted) {
if (state.has_our_nullfs &&
(state.top_is_our_nullfs || state.has_our_backport) &&
access(dst_eboot, F_OK) == 0) {
path_exists(dst_eboot)) {
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
return true;
}
@@ -820,7 +828,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
return false;
}
if (access(dst_eboot, F_OK) != 0) {
if (!path_exists(dst_eboot)) {
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
dst_eboot);
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
@@ -880,11 +888,11 @@ static bool cleanup_mount_links_entry(const char *title_id,
ctx->removed_source_root));
if (!matches_removed_source)
return true;
should_remove = (has_image_source && access(image_source_path, F_OK) != 0) ||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
source_path_needs_cleanup(source_path,
&ctx->tried_image_recovery);
} else {
should_remove = (has_image_source && access(image_source_path, F_OK) != 0) ||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
source_path_needs_cleanup(source_path,
&ctx->tried_image_recovery);
}
+2 -2
View File
@@ -114,7 +114,7 @@ void prune_game_cache(void) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (access(g_game_cache[k].path, F_OK) == 0)
if (path_exists(g_game_cache[k].path))
continue;
clear_game_cache_slot(k, "source removed");
}
@@ -135,7 +135,7 @@ void prune_game_cache_for_root(const char *root) {
: g_game_cache[k].path;
if (!path_matches_root_or_child(entry_root, root))
continue;
if (access(g_game_cache[k].path, F_OK) == 0)
if (path_exists(g_game_cache[k].path))
continue;
clear_game_cache_slot(k, "source removed");
}
+2 -2
View File
@@ -858,7 +858,7 @@ void cleanup_stale_image_mounts(void) {
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (access(cached_entry.path, F_OK) != 0) {
if (!path_exists(cached_entry.path)) {
log_debug(" [IMG][%s] Source removed, unmounting: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
@@ -906,7 +906,7 @@ void cleanup_stale_image_mounts_for_root(const char *root) {
continue;
}
if (access(cached_entry.path, F_OK) != 0) {
if (!path_exists(cached_entry.path)) {
log_debug(" [IMG][%s] Source removed, unmounting: %s",
attach_backend_name(cached_entry.backend), cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
+121 -30
View File
@@ -10,6 +10,7 @@
#include "sm_appdb.h"
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_paths.h"
static bool write_link_file(const char *path, const char *value) {
FILE *f = fopen(path, "w");
@@ -34,17 +35,73 @@ static bool write_link_file(const char *path, const char *value) {
return true;
}
static bool is_appmeta_file(const char *name) {
if (!name)
return false;
if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0)
return true;
const char *ext = strrchr(name, '.');
if (!ext)
return false;
return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 ||
strcasecmp(ext, ".at9") == 0);
}
static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
const char *user_appmeta_dir) {
DIR *d = opendir(src_sce_sys);
if (!d)
return false;
bool ok = true;
struct dirent *e;
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
struct stat st;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
if (!is_appmeta_file(e->d_name))
continue;
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name);
if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode))
continue;
snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name);
if (copy_file(src_path, dst_path) != 0) {
ok = false;
break;
}
}
if (closedir(d) != 0)
ok = false;
return ok;
}
// --- Install/Remount Action ---
static bool mount_and_install(const char *src_path, const char *title_id,
const char *title_name, bool is_remount,
bool should_register) {
char user_appmeta_dir[MAX_PATH];
char user_app_dir[MAX_PATH];
char user_sce_sys[MAX_PATH];
char src_sce_sys[MAX_PATH];
char src_snd0[MAX_PATH];
char image_source_path[MAX_PATH];
bool has_image_source = false;
bool appmeta_missing = false;
bool metadata_restaged = false;
bool restage_staging = false;
bool restage_appmeta = false;
bool has_src_snd0 = false;
snprintf(user_app_dir, sizeof(user_app_dir), "/user/app/%s", title_id);
snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE,
title_id);
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
if (is_under_image_mount_base(src_path)) {
has_image_source = resolve_image_source_from_mount_cache(
@@ -55,32 +112,66 @@ static bool mount_and_install(const char *src_path, const char *title_id,
}
}
appmeta_missing = !has_appmeta_data(title_id);
if (is_remount && appmeta_missing) {
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
}
restage_staging = (!is_remount || should_register);
restage_appmeta = (!is_remount || appmeta_missing);
// COPY FILES
if (!is_remount) {
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_app_dir, 0777);
mkdir(user_sce_sys, 0777);
if (restage_staging || restage_appmeta) {
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
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);
return false;
}
char icon_src[MAX_PATH], icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/sce_sys/icon0.png", src_path);
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);
return false;
}
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
} else {
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
}
if (restage_staging) {
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_sce_sys, 0777);
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
user_sce_sys);
return false;
}
char icon_src[MAX_PATH];
char icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys);
snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir);
if (copy_file(icon_src, icon_dst) != 0) {
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
icon_dst);
return false;
}
}
if (restage_appmeta) {
mkdir(APPMETA_BASE, 0777);
mkdir(user_appmeta_dir, 0777);
if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) {
log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys,
user_appmeta_dir);
return false;
}
metadata_restaged = true;
}
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
src_path);
return false;
}
// WRITE TRACKER
char lnk_path[MAX_PATH];
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
if (!write_link_file(lnk_path, src_path))
return false;
@@ -104,25 +195,25 @@ static bool mount_and_install(const char *src_path, const char *title_id,
strerror(errno));
}
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] mount.lnk created but nullfs mount failed: title=%s "
"src=%s",
title_id, src_path);
return false;
}
if (!should_register) {
if (metadata_restaged && has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d",
snd0_updates);
}
log_debug(" [REG] Skip (already present in app.db)");
return true;
}
// REGISTER
char src_snd0[MAX_PATH];
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
bool has_src_snd0 = (access(src_snd0, F_OK) == 0);
if (!metadata_restaged) {
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
has_src_snd0 = path_exists(src_snd0);
}
mark_register_attempted(title_id);
int res = sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0);
int res = sceAppInstUtilAppInstallTitleDir(title_id, APP_BASE "/", 0);
sceKernelUsleep(200000);
if (res == 0) {
+2 -1
View File
@@ -6,6 +6,7 @@
#include "sm_log.h"
#include "sm_config_mount.h"
#include "sm_mount_defs.h"
#include "sm_path_utils.h"
#include "sm_stability.h"
const char *attach_backend_name(attach_backend_t backend) {
@@ -19,7 +20,7 @@ const char *attach_backend_name(attach_backend_t backend) {
// --- Device Node Wait and Source Stability ---
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
if ((access(devname, F_OK) == 0) == should_exist)
if (path_exists(devname) == should_exist)
return true;
sceKernelUsleep(LVD_NODE_WAIT_US);
}
+3 -3
View File
@@ -68,7 +68,7 @@ static struct PathStateEntry *create_path_state(const char *path) {
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid)
continue;
if (access(g_path_state[k].path, F_OK) != 0) {
if (!path_exists(g_path_state[k].path)) {
evict_k = k;
break;
}
@@ -156,7 +156,7 @@ void prune_path_state(void) {
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
continue;
if (access(g_path_state[k].path, F_OK) == 0)
if (path_exists(g_path_state[k].path))
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
@@ -177,7 +177,7 @@ void prune_path_state_for_root(const char *root) {
continue;
if (!path_matches_root_or_child(g_path_state[k].path, root))
continue;
if (access(g_path_state[k].path, F_OK) == 0)
if (path_exists(g_path_state[k].path))
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
+5
View File
@@ -9,6 +9,11 @@ const char *get_filename_component(const char *path) {
return base ? base + 1 : path;
}
bool path_exists(const char *path) {
struct stat st;
return (path && path[0] != '\0' && stat(path, &st) == 0);
}
bool is_under_image_mount_base(const char *path) {
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
+30 -2
View File
@@ -20,12 +20,35 @@
typedef struct {
char discovered_param_roots[MAX_PENDING][MAX_PATH];
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
bool checked_appmeta_present[MAX_PENDING];
int checked_appmeta_count;
} scan_workspace_t;
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
// state on the stack each cycle.
static scan_workspace_t g_scan_workspace;
static void reset_scan_workspace(void) {
g_scan_workspace.checked_appmeta_count = 0;
}
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) {
if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0)
return g_scan_workspace.checked_appmeta_present[i];
}
bool present = has_appmeta_data(title_id);
if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) {
int slot = g_scan_workspace.checked_appmeta_count++;
(void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id,
sizeof(g_scan_workspace.checked_appmeta_titles[slot]));
g_scan_workspace.checked_appmeta_present[slot] = present;
}
return present;
}
static bool is_under_discovered_param_root(
const char *path, char discovered_param_roots[][MAX_PATH],
int discovered_count) {
@@ -147,6 +170,8 @@ static existing_directory_result_t handle_existing_directory_candidate(
bool in_app_db = app_db_title_list_contains(app_db_titles, info->title_id);
bool installed = in_app_db && is_installed(info->title_id);
bool appmeta_present =
installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false;
link_matches_source = installed && link_matches_source;
bool source_valid = false;
if (link_matches_source) {
@@ -155,12 +180,13 @@ static existing_directory_result_t handle_existing_directory_candidate(
source_valid = is_data_mounted(info->title_id);
}
if (source_valid) {
if (source_valid && appmeta_present) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
if (installed && has_tracked_path && strcmp(tracked_path, full_path) != 0 &&
if (installed && appmeta_present && has_tracked_path &&
strcmp(tracked_path, full_path) != 0 &&
is_data_mounted(info->title_id)) {
(void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH);
cache_game_entry(tracked_path, info->title_id, info->title_name);
@@ -459,6 +485,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
const struct AppDbTitleList *app_db_titles = NULL;
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
@@ -482,6 +509,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
const struct AppDbTitleList *app_db_titles = NULL;
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);