diff --git a/include/sm_fakelib.h b/include/sm_fakelib.h index ba845ca..45ebbbc 100644 --- a/include/sm_fakelib.h +++ b/include/sm_fakelib.h @@ -20,11 +20,13 @@ bool sm_fakelib_game_on_sandbox_ready(const char *title_id); void sm_fakelib_game_on_usb_mount_change(const char *source_path, bool mounted); // Reconcile USB, extended storage and disc aliases after filesystem events. void sm_fakelib_game_refresh_storage(void); -// Detach only owned sandbox /mnt/usbN aliases for sleep; reconcile USB aliases -// on resume. Other layers and borrowed mounts retain their lifetime/ownership. +// Detach owned sandbox USB, extended-storage and disc aliases for sleep and +// reconcile them on resume. The call is rejected when the session belongs to +// another title or PID. Other layers and borrowed mounts keep their ownership. // Failed aliases or an incomplete retained session return false so USB source // cleanup can be skipped. Stale sleep phases and shutdown requests are rejected. -bool sm_fakelib_game_set_usb_suspended(bool suspended); +bool sm_fakelib_game_set_storage_suspended(bool suspended, pid_t pid, + const char *title_id); // Roll back unbound pre-spawn mounts when ShellCore rejects the launch. void sm_fakelib_game_on_launch_failed(const char *title_id); // Bind pre-spawn mounts to the process, or mount as a NOTE_EXEC fallback. diff --git a/src/sm_fakelib.c b/src/sm_fakelib.c index 629f7df..b2cce2d 100644 --- a/src/sm_fakelib.c +++ b/src/sm_fakelib.c @@ -63,6 +63,8 @@ typedef struct { bool mounts_ready; uint64_t cleanup_next_us; uint64_t cleanup_deadline_us; + uint64_t storage_refresh_next_us; + uint8_t storage_refresh_attempts; } fakelib_session_t; typedef struct { @@ -464,7 +466,7 @@ static bool sync_sandbox_storage_root(const char *title_id, } static bool mount_sandbox_storage_roots(const char *title_id, - const char *app0_path, bool usb_only) { + const char *app0_path) { struct statfs *mounts = NULL; int mount_count = sm_mount_table_snapshot(&mounts); if (mount_count < 0) { @@ -474,10 +476,9 @@ static bool mount_sandbox_storage_roots(const char *title_id, } int sync_errno = 0; - unsigned root_count = usb_only ? SANDBOX_USB_ROOT_COUNT - : SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT + 1u; // Keep sandbox /mnt local and expose only mounted storage volumes. - for (unsigned slot = 0; slot < root_count; ++slot) { + for (unsigned slot = 0; + slot <= SANDBOX_USB_ROOT_COUNT + SANDBOX_EXT_ROOT_COUNT; ++slot) { char source_path[MAX_PATH]; if (slot < SANDBOX_USB_ROOT_COUNT) (void)snprintf(source_path, sizeof(source_path), "/mnt/usb%u", slot); @@ -515,6 +516,17 @@ static bool mount_sandbox_storage_roots(const char *title_id, return true; } +static bool reconcile_storage_roots_locked(void) { + if (!g_fakelib_mount.mounts_ready) + return true; + if (!mount_sandbox_storage_roots(g_fakelib_mount.title_id, + g_fakelib_mount.sandbox_app0_path)) + return false; + g_fakelib_mount.storage_refresh_next_us = 0; + g_fakelib_mount.storage_refresh_attempts = 0; + return true; +} + static bool resolve_sandbox_paths(const char *title_id, char app0_path[MAX_PATH], char mount_path[MAX_PATH]) { @@ -1799,6 +1811,8 @@ static bool cleanup_fakelib_mount(void) { } g_fakelib_mount.mounts_ready = false; + g_fakelib_mount.storage_refresh_next_us = 0; + g_fakelib_mount.storage_refresh_attempts = 0; int cleanup_errno = 0; bool removed[sizeof(g_fakelib_mount.layers) / sizeof(g_fakelib_mount.layers[0])] = {false}; @@ -1886,9 +1900,39 @@ static void schedule_fakelib_cleanup(void) { wake_game_lifecycle_watcher(); } +static void schedule_storage_refresh(void) { + if (runtime_sleep_mode_active() || should_stop_requested()) + return; + uint64_t now_us = monotonic_time_us(); + if (now_us == 0) + return; + if (g_fakelib_mount.storage_refresh_attempts >= 6u) { + g_fakelib_mount.storage_refresh_next_us = 0; + log_debug(" [FAKELIB] storage refresh gave up for %s", + g_fakelib_mount.title_id); + return; + } + ++g_fakelib_mount.storage_refresh_attempts; + g_fakelib_mount.storage_refresh_next_us = + now_us + GAME_LIFECYCLE_POLL_INTERVAL_US; + wake_game_lifecycle_watcher(); +} + +static void retry_storage_refresh_locked(uint64_t now_us) { + uint64_t due_us = g_fakelib_mount.storage_refresh_next_us; + if (due_us == 0 || (now_us != 0 && now_us < due_us)) + return; + g_fakelib_mount.storage_refresh_next_us = 0; + if (!reconcile_storage_roots_locked()) + schedule_storage_refresh(); +} + uint64_t sm_fakelib_game_next_cleanup_us(void) { pthread_mutex_lock(&g_fakelib_mutex); uint64_t next_us = g_fakelib_mount.cleanup_next_us; + uint64_t refresh_us = g_fakelib_mount.storage_refresh_next_us; + if (refresh_us != 0 && (next_us == 0 || refresh_us < next_us)) + next_us = refresh_us; pthread_mutex_unlock(&g_fakelib_mutex); return next_us; } @@ -1901,6 +1945,7 @@ void sm_fakelib_game_poll_cleanup(void) { return; } uint64_t now_us = monotonic_time_us(); + retry_storage_refresh_locked(now_us); if (g_fakelib_mount.cleanup_next_us != 0 && (now_us == 0 || now_us >= g_fakelib_mount.cleanup_next_us)) { if (!cleanup_fakelib_mount()) @@ -2167,7 +2212,7 @@ static bool mount_fakelib_for_game_locked(pid_t pid, const char *title_id, label, true, 0)) || !ensure_sandbox_parent(sandbox_app0_path, "/mnt") || !mount_sandbox_directory(title_id, sandbox_app0_path, "/data", 0) || - !mount_sandbox_storage_roots(title_id, sandbox_app0_path, false) || + !mount_sandbox_storage_roots(title_id, sandbox_app0_path) || !mount_sandbox_fonts(title_id, sandbox_app0_path)) { int mount_errno = errno; if (!cleanup_fakelib_mount()) @@ -2219,7 +2264,19 @@ static bool is_sandbox_usb_root(const char *path) { path[9] == '\0'; } -bool sm_fakelib_game_set_usb_suspended(bool suspended) { +static bool is_sandbox_removable_root(const char *path) { + if (is_usb_storage_path(path)) + return true; + if (!path || strncmp(path, "/mnt/disc", 9) != 0) + return false; + const char *rest = path + 9; + while (*rest >= '0' && *rest <= '9') + ++rest; + return *rest == '\0' || *rest == '/'; +} + +bool sm_fakelib_game_set_storage_suspended(bool suspended, pid_t pid, + const char *title_id) { int sync_errno = 0; pthread_mutex_lock(&g_fakelib_mutex); // Sleep may have ended while the lifecycle watcher waited for game exit. @@ -2229,6 +2286,21 @@ bool sm_fakelib_game_set_usb_suspended(bool suspended) { errno = EBUSY; return false; } + if (fakelib_session_active()) { + // A terminated game has pid 0 but its title still identifies the session; + // a resume with no surviving process has no title to compare. + bool title_mismatch = + title_id && strcmp(g_fakelib_mount.title_id, title_id) != 0; + bool pid_mismatch = + pid > 0 && g_fakelib_mount.pid > 0 && g_fakelib_mount.pid != pid; + bool unknown_owner = + !title_id && pid <= 0 && g_fakelib_mount.pid > 0; + if (title_mismatch || pid_mismatch || unknown_owner) { + pthread_mutex_unlock(&g_fakelib_mutex); + errno = EBUSY; + return false; + } + } if (suspended) { // USB aliases are siblings. Unmount and compact in one pass, preserving @@ -2236,12 +2308,12 @@ bool sm_fakelib_game_set_usb_suspended(bool suspended) { size_t retained = 0; for (size_t i = 0; i < g_fakelib_mount.layer_count; ++i) { fakelib_layer_t *layer = &g_fakelib_mount.layers[i]; - bool usb_alias = layer->owned_mount && layer->label && + bool removable_alias = layer->owned_mount && layer->label && strcmp(layer->label, "directory") == 0 && - is_sandbox_usb_root(layer->source_path); - if (usb_alias && unmount_sandbox_layer(layer)) + is_sandbox_removable_root(layer->source_path); + if (removable_alias && unmount_sandbox_layer(layer)) continue; - if (usb_alias) { + if (removable_alias) { // Do not mask a real failure with an earlier transient busy error. if (sync_errno == 0 || sync_errno == EBUSY) sync_errno = errno != 0 ? errno : EIO; @@ -2254,14 +2326,13 @@ bool sm_fakelib_game_set_usb_suspended(bool suspended) { (g_fakelib_mount.layer_count - retained) * sizeof(g_fakelib_mount.layers[0])); g_fakelib_mount.layer_count = retained; - } else if (g_fakelib_mount.mounts_ready && - !mount_sandbox_storage_roots(g_fakelib_mount.title_id, - g_fakelib_mount.sandbox_app0_path, - true)) { + } else if (!reconcile_storage_roots_locked()) { sync_errno = errno != 0 ? errno : EIO; + schedule_storage_refresh(); } - // Exit or rollback can leave a busy core layer after removing every USB - // alias. Its retained dependencies must still block USB source cleanup. + // Cleanup or rollback can leave an active session incomplete after removing + // removable aliases. Report this state for both phases; on suspend, the + // result must still block USB source cleanup. if (sync_errno == 0 && fakelib_session_active() && !g_fakelib_mount.mounts_ready) sync_errno = EBUSY; @@ -2298,11 +2369,10 @@ void sm_fakelib_game_on_usb_mount_change(const char *source_path, bool mounted) void sm_fakelib_game_refresh_storage(void) { pthread_mutex_lock(&g_fakelib_mutex); if (g_fakelib_mount.mounts_ready && !runtime_sleep_mode_active() && - !should_stop_requested() && - !mount_sandbox_storage_roots(g_fakelib_mount.title_id, - g_fakelib_mount.sandbox_app0_path, false)) { + !should_stop_requested() && !reconcile_storage_roots_locked()) { log_debug(" [FAKELIB] sandbox storage refresh failed for %s: %s", g_fakelib_mount.title_id, strerror(errno)); + schedule_storage_refresh(); } pthread_mutex_unlock(&g_fakelib_mutex); } diff --git a/src/sm_game_lifecycle.c b/src/sm_game_lifecycle.c index ebea7fe..365bb4e 100644 --- a/src/sm_game_lifecycle.c +++ b/src/sm_game_lifecycle.c @@ -1139,7 +1139,7 @@ static void *game_lifecycle_watcher_main(void *arg) { if (g_pending_game_exit.active) finalize_pending_game_exit(); suspended_game_pid = atomic_load(&g_active_game_pid); - char suspended_game_title_id[MAX_TITLE_ID]; + char suspended_game_title_id[MAX_TITLE_ID] = {0}; bool active_game = snapshot_active_game(suspended_game_pid, suspended_game_title_id); char source_path[MAX_PATH]; @@ -1178,8 +1178,10 @@ static void *game_lifecycle_watcher_main(void *arg) { kq, usb_game); } // The wait can process another game exit. Check the retained session - // afterwards and release only USB aliases before USB source cleanup. - if (!sm_fakelib_game_set_usb_suspended(true)) + // afterwards and release removable aliases before USB source cleanup. + if (!sm_fakelib_game_set_storage_suspended( + true, suspended_game_pid, + active_game ? suspended_game_title_id : NULL)) sleep_cleanup_complete = false; if (sleep_cleanup_complete) { runtime_mount_state_lock(); @@ -1219,7 +1221,13 @@ static void *game_lifecycle_watcher_main(void *arg) { is_process_alive(suspended_game_pid)) { handle_game_exec(kq, suspended_game_pid); } - (void)sm_fakelib_game_set_usb_suspended(false); + pid_t resume_pid = atomic_load(&g_active_game_pid); + char resume_title[MAX_TITLE_ID]; + const char *resume_title_ptr = NULL; + if (resume_pid > 0 && snapshot_active_game(resume_pid, resume_title)) + resume_title_ptr = resume_title; + (void)sm_fakelib_game_set_storage_suspended( + false, resume_pid, resume_title_ptr); suspended_game_pid = 0; }