Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions include/sm_fakelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
108 changes: 89 additions & 19 deletions src/sm_fakelib.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}
Expand All @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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.
Expand All @@ -2229,19 +2286,34 @@ 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
// the order and ownership of all other layers and any failed aliases.
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
16 changes: 12 additions & 4 deletions src/sm_game_lifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}

Expand Down