From 6df8a472e30c4dfe1e07c2f976fe1c3ddf6e9a60 Mon Sep 17 00:00:00 2001 From: Roman Tarasov Date: Sat, 4 Apr 2026 21:53:27 +0300 Subject: [PATCH] Add optional AppInstallAll mode with FW 12+ override --- README.md | 1 + config.ini.example | 6 + include/sm_install_queue.h | 26 +++ include/sm_platform.h | 1 + include/sm_types.h | 2 + src/sm_config_mount.c | 45 +++- src/sm_install.c | 56 ++++- src/sm_install_queue.c | 436 +++++++++++++++++++++++++++++++++++++ src/sm_scan.c | 4 + src/sm_scanner.c | 46 ++-- 10 files changed, 597 insertions(+), 26 deletions(-) create mode 100644 include/sm_install_queue.h create mode 100644 src/sm_install_queue.c diff --git a/README.md b/README.md index 670bd80..5c528c7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Supported keys (all optional): - `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`) - `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 `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`) - `image_ro=` (repeatable; force read-only mode for this image filename) - `image_rw=` (repeatable; force read-write mode for this image filename) - `image_sector=:` (repeatable; force sector size for this image filename) diff --git a/config.ini.example b/config.ini.example index 89e754f..612b87a 100644 --- a/config.ini.example +++ b/config.ini.example @@ -24,6 +24,12 @@ mount_read_only=1 # 0/false/no/off -> default mount options (default) force_mount=0 +# Use batch registration for newly staged titles +# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll +# 0/false/no/off -> register each title directly (default on FW below 12.00) +# Forced on automatically on firmware 12.00 and newer +app_install_all=0 + # Per-image mount mode override by file name (can be repeated many times): # image_ro= # image_rw= diff --git a/include/sm_install_queue.h b/include/sm_install_queue.h new file mode 100644 index 0000000..db3db86 --- /dev/null +++ b/include/sm_install_queue.h @@ -0,0 +1,26 @@ +#ifndef SM_INSTALL_QUEUE_H +#define SM_INSTALL_QUEUE_H + +#include +#include + +typedef struct scan_candidate scan_candidate_t; + +// Return whether a title is already waiting for async batch installation. +bool is_title_install_pending(const char *title_id); +// Return the next pending-install wake deadline in monotonic microseconds. +uint64_t sm_install_next_wake_us(uint64_t now_us); +// Poll async batch installs for success/timeout completion only. +void sm_install_poll_pending(void); +// Poll async batch installs for success/timeout completion and auto-submit +// queued installs when possible. +void sm_install_service_pending(void); +// Queue a staged title for the next batch install request. +bool sm_install_queue_candidate(const scan_candidate_t *candidate, + bool has_src_snd0); +// Submit all queued titles when no batch install is currently pending. +bool sm_install_submit_queued(void); +// Record retry state for every queued title after a batch submit failure. +void sm_install_note_submit_failure(void); + +#endif diff --git a/include/sm_platform.h b/include/sm_platform.h index 35ac4b0..e8a90b6 100644 --- a/include/sm_platform.h +++ b/include/sm_platform.h @@ -37,6 +37,7 @@ typedef struct { // --- SDK Imports --- int sceAppInstUtilInitialize(void); +int sceAppInstUtilAppInstallAll(void); int sceAppInstUtilAppInstallTitleDir(const char *title_id, const char *install_path, void *reserved); int sceKernelGetAppInfo(pid_t pid, app_info_t *info); diff --git a/include/sm_types.h b/include/sm_types.h index 36ce442..50c6ae6 100644 --- a/include/sm_types.h +++ b/include/sm_types.h @@ -106,6 +106,8 @@ typedef struct runtime_config { bool quiet_mode; bool mount_read_only; bool force_mount; + bool app_install_all_enabled; + bool app_install_all_forced; bool backport_fakelib_enabled; bool kstuff_game_auto_toggle; bool kstuff_crash_detection_enabled; diff --git a/src/sm_config_mount.c b/src/sm_config_mount.c index a5d33ef..6330cdd 100644 --- a/src/sm_config_mount.c +++ b/src/sm_config_mount.c @@ -96,6 +96,8 @@ static bool lookup_kstuff_delay_override_in_file(const char *path, static bool upsert_kstuff_delay_override_in_file(const char *path, const char *title_id, uint32_t delay_seconds); +static uint32_t runtime_firmware_major_version(void); +static void apply_firmware_runtime_overrides(runtime_config_state_t *state); static char *trim_ascii(char *s) { while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') @@ -149,6 +151,25 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) { return true; } +static uint32_t runtime_firmware_major_version(void) { + uint32_t fw = kernel_get_fw_version(); + uint32_t major_bcd = (fw >> 24) & 0xFFu; + return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu); +} + +static void apply_firmware_runtime_overrides(runtime_config_state_t *state) { + if (!state) + return; + + if (runtime_firmware_major_version() >= 12u) { + state->cfg.app_install_all_enabled = true; + state->cfg.app_install_all_forced = true; + return; + } + + state->cfg.app_install_all_forced = false; +} + static const runtime_config_state_t *active_runtime_state(void) { return &g_runtime_state_slots[atomic_load_explicit(&g_runtime_state_active_index, memory_order_acquire)]; @@ -228,6 +249,8 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) { state->cfg.quiet_mode = false; state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0); state->cfg.force_mount = false; + state->cfg.app_install_all_enabled = false; + state->cfg.app_install_all_forced = false; state->cfg.backport_fakelib_enabled = true; state->cfg.kstuff_game_auto_toggle = true; state->cfg.kstuff_crash_detection_enabled = false; @@ -249,6 +272,7 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) { memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules)); clear_kstuff_title_rules(state); init_runtime_scan_paths_defaults(state); + apply_firmware_runtime_overrides(state); } static config_file_stamp_t read_config_file_stamp(void) { @@ -283,6 +307,8 @@ static void apply_reloadable_runtime_fields(runtime_config_state_t *dst, dst->cfg.quiet_mode = src->cfg.quiet_mode; dst->cfg.mount_read_only = src->cfg.mount_read_only; dst->cfg.force_mount = src->cfg.force_mount; + dst->cfg.app_install_all_enabled = src->cfg.app_install_all_enabled; + dst->cfg.app_install_all_forced = src->cfg.app_install_all_forced; dst->cfg.backport_fakelib_enabled = src->cfg.backport_fakelib_enabled; dst->cfg.kstuff_game_auto_toggle = src->cfg.kstuff_game_auto_toggle; dst->cfg.kstuff_crash_detection_enabled = @@ -1119,6 +1145,16 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st continue; } + if (strcasecmp(key, "app_install_all") == 0) { + if (!parse_bool_ini(value, &bval)) { + log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value); + continue; + } + state->cfg.app_install_all_enabled = bval; + state->cfg.app_install_all_forced = false; + continue; + } + if (strcasecmp(key, "image_ro") == 0 || strcasecmp(key, "image_rw") == 0) { bool rule_read_only = (strcasecmp(key, "image_ro") == 0); @@ -1331,6 +1367,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st log_debug(" [CFG] recursive_scan=1 is deprecated; forcing scan_depth=2"); } + apply_firmware_runtime_overrides(state); + int image_rule_count = 0; for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) { if (state->image_mode_rules[k].valid) @@ -1343,7 +1381,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st kstuff_delay_rule_count++; } - log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d scan_depth=%u " + log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d " + "app_install_all=%d app_install_all_forced=%d scan_depth=%u " "legacy_recursive_scan_forced=%d backport_fakelib=%d " "kstuff_game_auto_toggle=%d kstuff_crash_detection=%d " "kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u " @@ -1353,7 +1392,9 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st "kstuff_no_pause=%d kstuff_delay_rules=%d", state->cfg.debug_enabled ? 1 : 0, state->cfg.quiet_mode ? 1 : 0, state->cfg.mount_read_only ? 1 : 0, - state->cfg.force_mount ? 1 : 0, state->cfg.scan_depth, + state->cfg.force_mount ? 1 : 0, + state->cfg.app_install_all_enabled ? 1 : 0, + state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth, state->cfg.legacy_recursive_scan_forced ? 1 : 0, state->cfg.backport_fakelib_enabled ? 1 : 0, state->cfg.kstuff_game_auto_toggle ? 1 : 0, diff --git a/src/sm_install.c b/src/sm_install.c index 42ab579..8e6f503 100644 --- a/src/sm_install.c +++ b/src/sm_install.c @@ -1,6 +1,8 @@ #include "sm_platform.h" +#include "sm_config_mount.h" #include "sm_runtime.h" #include "sm_install.h" +#include "sm_install_queue.h" #include "sm_types.h" #include "sm_game_cache.h" #include "sm_log.h" @@ -85,7 +87,9 @@ static bool copy_sce_sys_to_appmeta(const char *src_sce_sys, // --- 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) { + bool should_register, + bool use_app_install_all, + bool *has_src_snd0_out) { char user_appmeta_dir[MAX_PATH]; char user_app_dir[MAX_PATH]; char user_sce_sys[MAX_PATH]; @@ -212,12 +216,20 @@ static bool mount_and_install(const char *src_path, const char *title_id, return true; } - // REGISTER if (!metadata_restaged) { snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path); has_src_snd0 = path_exists(src_snd0); } + if (use_app_install_all) { + if (has_src_snd0_out) + *has_src_snd0_out = has_src_snd0; + log_debug(" [REG] Prepared for batch install: %s (%s)", title_name, + title_id); + return true; + } + + // REGISTER mark_register_attempted(title_id); int res = sceAppInstUtilAppInstallTitleDir(title_id, APP_BASE "/", 0); sceKernelUsleep(200000); @@ -253,23 +265,50 @@ static bool mount_and_install(const char *src_path, const char *title_id, // --- Execution (per discovered candidate) --- void process_scan_candidates(const scan_candidate_t *candidates, int candidate_count) { + sm_install_poll_pending(); + for (int i = 0; i < candidate_count; i++) { if (should_stop_requested()) return; const scan_candidate_t *c = &candidates[i]; + bool has_src_snd0 = false; + bool use_app_install_all = + runtime_config()->app_install_all_enabled && !c->in_app_db; if (c->installed) { log_debug(" [ACTION] Remounting: %s", c->title_name); } else { log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id); - notify_system_info("Installing: %s (%s)...", c->title_name, - c->title_id); + if (!use_app_install_all) { + notify_system_info("Installing: %s (%s)...", c->title_name, + c->title_id); + } } if (mount_and_install(c->path, c->title_id, c->title_name, c->installed, - !c->in_app_db)) { - clear_failed_mount_attempts(c->title_id); - cache_game_entry(c->path, c->title_id, c->title_name); + !c->in_app_db, use_app_install_all, + &has_src_snd0)) { + if (use_app_install_all) { + if (!sm_install_queue_candidate(c, has_src_snd0)) { + log_debug(" [REG] Failed to queue staged install: %s (%s)", + c->title_name, c->title_id); + if (should_stop_requested()) + return; + + uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id); + if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) { + log_debug(" [RETRY] limit reached (%u/%u): %s (%s)", + (unsigned)failed_attempts, + (unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name, + c->title_id); + } + continue; + } + clear_failed_mount_attempts(c->title_id); + } else { + clear_failed_mount_attempts(c->title_id); + cache_game_entry(c->path, c->title_id, c->title_name); + } } else { if (should_stop_requested()) return; @@ -283,4 +322,7 @@ void process_scan_candidates(const scan_candidate_t *candidates, } } } + + if (!sm_install_submit_queued()) + sm_install_note_submit_failure(); } diff --git a/src/sm_install_queue.c b/src/sm_install_queue.c new file mode 100644 index 0000000..a9c2c1d --- /dev/null +++ b/src/sm_install_queue.c @@ -0,0 +1,436 @@ +#include "sm_platform.h" + +#include "sm_install_queue.h" +#include "sm_types.h" +#include "sm_appdb.h" +#include "sm_config_mount.h" +#include "sm_game_cache.h" +#include "sm_limits.h" +#include "sm_log.h" +#include "sm_time.h" +#include "sm_title_state.h" + +#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull) +#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull) +#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull) + +typedef enum { + INSTALL_TRACK_NONE = 0, + INSTALL_TRACK_QUEUED, + INSTALL_TRACK_SUBMITTED, +} install_track_state_t; + +typedef struct { + char title_id[MAX_TITLE_ID]; + char title_name[MAX_TITLE_NAME]; + char source_path[MAX_PATH]; + uint64_t requested_at_us; + bool has_src_snd0; + install_track_state_t state; +} pending_install_entry_t; + +static pending_install_entry_t g_pending_installs[MAX_PENDING]; +static uint64_t g_pending_install_poll_due_us = 0; +static uint64_t g_queued_install_submit_due_us = 0; +static int g_tracked_install_count = 0; +static int g_submitted_install_count = 0; +static bool g_queued_install_batch_announced = false; +static bool g_queued_install_submit_failure_notified = false; + +static int count_queued_installs(void); +static uint8_t note_pending_install_failure(const pending_install_entry_t *entry); +static void drop_queued_install_entry(pending_install_entry_t *entry); + +static bool install_queue_active(void) { + return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0; +} + +static pending_install_entry_t *find_pending_install_entry( + const char *title_id) { + if (!title_id || title_id[0] == '\0') + return NULL; + + for (int i = 0; i < MAX_PENDING; i++) { + if (g_pending_installs[i].state == INSTALL_TRACK_NONE) + continue; + if (strcmp(g_pending_installs[i].title_id, title_id) == 0) + return &g_pending_installs[i]; + } + + return NULL; +} + +static pending_install_entry_t *reserve_pending_install_entry( + const char *title_id) { + pending_install_entry_t *entry = find_pending_install_entry(title_id); + if (entry) + return entry; + + for (int i = 0; i < MAX_PENDING; i++) { + if (g_pending_installs[i].state == INSTALL_TRACK_NONE) + return &g_pending_installs[i]; + } + + return NULL; +} + +static uint64_t next_pending_install_timeout_us(void) { + if (g_submitted_install_count <= 0) + return 0; + + uint64_t next_deadline = 0; + for (int i = 0; i < MAX_PENDING; i++) { + const pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0) + continue; + + uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US; + if (next_deadline == 0 || deadline_us < next_deadline) + next_deadline = deadline_us; + } + + return next_deadline; +} + +static void schedule_pending_install_poll(uint64_t now_us) { + if (g_submitted_install_count <= 0) { + g_pending_install_poll_due_us = 0; + return; + } + + g_pending_install_poll_due_us = + now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US; +} + +static void schedule_queued_install_submit(uint64_t now_us) { + if (count_queued_installs() <= 0 || g_submitted_install_count > 0) { + g_queued_install_submit_due_us = 0; + return; + } + + g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us; +} + +static void schedule_queued_install_submit_retry(uint64_t now_us) { + if (count_queued_installs() <= 0 || g_submitted_install_count > 0) { + g_queued_install_submit_due_us = 0; + return; + } + + g_queued_install_submit_due_us = + now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US; +} + +static void clear_pending_install_entry(pending_install_entry_t *entry) { + if (!entry) + return; + if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0) + g_submitted_install_count--; + if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0) + g_tracked_install_count--; + memset(entry, 0, sizeof(*entry)); + if (g_submitted_install_count <= 0) + g_pending_install_poll_due_us = 0; + if (g_tracked_install_count <= 0) { + g_queued_install_submit_due_us = 0; + g_queued_install_batch_announced = false; + g_queued_install_submit_failure_notified = false; + } +} + +bool is_title_install_pending(const char *title_id) { + return find_pending_install_entry(title_id) != NULL; +} + +static void append_batch_install_line(char *message, size_t message_size, + const char *title_name, + const char *title_id, int *shown_count) { + if (!message || message_size == 0 || !title_name || !title_id || !shown_count) + return; + + size_t used = strlen(message); + if (used >= message_size) + return; + + int written = snprintf(message + used, message_size - used, "\n%s (%s)", + title_name, title_id); + if (written < 0 || (size_t)written >= message_size - used) + return; + + (*shown_count)++; +} + +bool sm_install_queue_candidate(const scan_candidate_t *candidate, + bool has_src_snd0) { + if (!candidate || candidate->title_id[0] == '\0' || + candidate->title_name[0] == '\0' || candidate->path[0] == '\0') { + return false; + } + + if (!runtime_config()->app_install_all_enabled) + return false; + + bool was_queue_empty = (count_queued_installs() <= 0); + pending_install_entry_t *entry = + reserve_pending_install_entry(candidate->title_id); + if (!entry) { + log_debug(" [REG] install queue full, cannot queue %s (%s)", + candidate->title_name, candidate->title_id); + return false; + } + + install_track_state_t previous_state = entry->state; + if (previous_state == INSTALL_TRACK_SUBMITTED) + return true; + + memset(entry, 0, sizeof(*entry)); + (void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id)); + (void)strlcpy(entry->title_name, candidate->title_name, + sizeof(entry->title_name)); + (void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path)); + entry->has_src_snd0 = has_src_snd0; + entry->state = INSTALL_TRACK_QUEUED; + if (previous_state == INSTALL_TRACK_NONE) + g_tracked_install_count++; + if (was_queue_empty) { + g_queued_install_batch_announced = false; + g_queued_install_submit_failure_notified = false; + } + schedule_queued_install_submit(monotonic_time_us()); + return true; +} + +static void finalize_pending_install_success(pending_install_entry_t *entry) { + if (!entry) + return; + + log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id); + notify_game_installed_rich(entry->title_id); + if (entry->has_src_snd0) { + int snd0_updates = update_snd0info(entry->title_id); + if (snd0_updates >= 0) + log_debug(" [DB] snd0info updated rows=%d", snd0_updates); + } + cache_game_entry(entry->source_path, entry->title_id, entry->title_name); + clear_failed_mount_attempts(entry->title_id); + clear_pending_install_entry(entry); +} + +static void finalize_pending_install_timeout(pending_install_entry_t *entry) { + if (!entry) + return; + + log_debug(" [REG] Install timeout after 5 minutes: %s (%s)", + entry->title_name, entry->title_id); + notify_system("Install failed: %s (%s)\nTimed out after 5 minutes", + entry->title_name, entry->title_id); + (void)note_pending_install_failure(entry); + clear_pending_install_entry(entry); +} + +static void poll_pending_installs(void) { + if (g_submitted_install_count <= 0) + return; + + invalidate_app_db_title_cache(); + + const struct AppDbTitleList *app_db_titles = NULL; + bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles); + uint64_t now_us = monotonic_time_us(); + + if (!app_db_titles_ready) + log_debug(" [DB] app.db title list unavailable while polling installs"); + + for (int i = 0; i < MAX_PENDING; i++) { + pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_SUBMITTED) + continue; + + if (app_db_titles_ready && + app_db_title_list_contains(app_db_titles, entry->title_id)) { + finalize_pending_install_success(entry); + continue; + } + + if (now_us != 0 && entry->requested_at_us != 0 && + now_us >= entry->requested_at_us && + now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) { + finalize_pending_install_timeout(entry); + } + } + + schedule_pending_install_poll(now_us); + if (g_submitted_install_count == 0) + schedule_queued_install_submit(now_us); +} + +uint64_t sm_install_next_wake_us(uint64_t now_us) { + if (!install_queue_active()) + return 0; + + if (g_submitted_install_count <= 0) + return g_queued_install_submit_due_us; + + uint64_t next_wake_us = next_pending_install_timeout_us(); + + if (g_pending_install_poll_due_us != 0 && + (next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) { + next_wake_us = g_pending_install_poll_due_us; + } + + if (g_queued_install_submit_due_us != 0 && + (next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) { + next_wake_us = g_queued_install_submit_due_us; + } + + if (next_wake_us == 0) + next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US; + + return next_wake_us; +} + +void sm_install_poll_pending(void) { + poll_pending_installs(); +} + +void sm_install_service_pending(void) { + if (!install_queue_active()) + return; + + sm_install_poll_pending(); + if (count_queued_installs() > 0 && !sm_install_submit_queued()) + sm_install_note_submit_failure(); +} + +static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) { + uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id); + if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) { + log_debug(" [RETRY] limit reached (%u/%u): %s (%s)", + (unsigned)failed_attempts, + (unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name, + entry->title_id); + } +} + +static int count_queued_installs(void) { + int queued_count = g_tracked_install_count - g_submitted_install_count; + return queued_count > 0 ? queued_count : 0; +} + +static void log_queued_install_batch(void) { + int queued_count = count_queued_installs(); + if (queued_count <= 0) + return; + + log_debug(" [REG] Batch install request contains %d title(s):", queued_count); + for (int i = 0; i < MAX_PENDING; i++) { + const pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_QUEUED) + continue; + log_debug(" - %s (%s)", entry->title_name, entry->title_id); + } +} + +static void notify_queued_install_batch(void) { + int queued_count = count_queued_installs(); + if (queued_count <= 0 || g_queued_install_batch_announced) + return; + + char message[3075]; + int shown_count = 0; + snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count); + + for (int i = 0; i < MAX_PENDING; i++) { + const pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_QUEUED) + continue; + append_batch_install_line(message, sizeof(message), entry->title_name, + entry->title_id, &shown_count); + } + + if (shown_count < queued_count) { + size_t used = strlen(message); + if (used < sizeof(message)) { + (void)snprintf(message + used, sizeof(message) - used, "\n... and %d more", + queued_count - shown_count); + } + } + + notify_system_info("%s", message); + g_queued_install_batch_announced = true; +} + +static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) { + if (!entry) + return 0; + + uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id); + log_batch_submit_retry_limit(entry); + return failed_attempts; +} + +static void drop_queued_install_entry(pending_install_entry_t *entry) { + if (!entry || entry->state != INSTALL_TRACK_QUEUED) + return; + + log_debug(" [REG] Dropping queued install after retry limit: %s (%s)", + entry->title_name, entry->title_id); + clear_pending_install_entry(entry); +} + +bool sm_install_submit_queued(void) { + int queued_count = count_queued_installs(); + if (queued_count <= 0) + return true; + + if (g_submitted_install_count > 0) + return true; + + uint64_t now_us = monotonic_time_us(); + if (g_queued_install_submit_due_us != 0 && now_us != 0 && + now_us < g_queued_install_submit_due_us) { + return true; + } + + log_queued_install_batch(); + notify_queued_install_batch(); + + int res = sceAppInstUtilAppInstallAll(); + if (res != 0) { + log_debug(" [REG] Batch install request failed: 0x%x", res); + if (!g_queued_install_submit_failure_notified) { + notify_system("Batch install failed for %d app(s).\ncode=0x%08X", + queued_count, (uint32_t)res); + g_queued_install_submit_failure_notified = true; + } + schedule_queued_install_submit_retry(now_us); + return false; + } + + g_queued_install_submit_due_us = 0; + g_queued_install_submit_failure_notified = false; + for (int i = 0; i < MAX_PENDING; i++) { + pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_QUEUED) + continue; + entry->state = INSTALL_TRACK_SUBMITTED; + entry->requested_at_us = now_us; + g_submitted_install_count++; + clear_failed_mount_attempts(entry->title_id); + } + + invalidate_app_db_title_cache(); + schedule_pending_install_poll(now_us); + return true; +} + +void sm_install_note_submit_failure(void) { + for (int i = 0; i < MAX_PENDING; i++) { + pending_install_entry_t *entry = &g_pending_installs[i]; + if (entry->state != INSTALL_TRACK_QUEUED) + continue; + + if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS) + drop_queued_install_entry(entry); + } +} diff --git a/src/sm_scan.c b/src/sm_scan.c index aca71be..6090624 100644 --- a/src/sm_scan.c +++ b/src/sm_scan.c @@ -17,6 +17,7 @@ #include "sm_title_state.h" #include "sm_image_cache.h" #include "sm_image.h" +#include "sm_install_queue.h" typedef struct { char discovered_param_roots[MAX_PENDING][MAX_PATH]; @@ -193,6 +194,9 @@ static existing_directory_result_t handle_existing_directory_candidate( return EXISTING_DIRECTORY_PREFER_CACHED; } + if (!in_app_db && is_title_install_pending(info->title_id)) + return EXISTING_DIRECTORY_HANDLED; + if (!in_app_db && was_register_attempted(info->title_id)) { return EXISTING_DIRECTORY_HANDLED; } diff --git a/src/sm_scanner.c b/src/sm_scanner.c index 7a508cc..b57d332 100644 --- a/src/sm_scanner.c +++ b/src/sm_scanner.c @@ -10,6 +10,7 @@ #include "sm_game_lifecycle.h" #include "sm_image.h" #include "sm_install.h" +#include "sm_install_queue.h" #include "sm_kstuff.h" #include "sm_limits.h" #include "sm_log.h" @@ -807,22 +808,20 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason, if (should_abort_scan_cycle()) return false; - if (candidate_count > 0) { - if (startup_sync) { - int new_games = 0; - for (int i = 0; i < candidate_count; i++) { - if (!candidates[i].installed) - new_games++; - } - if (new_games > 0) - notify_system_info("Found %d new games. Executing...", new_games); + if (candidate_count > 0 && startup_sync) { + int new_games = 0; + for (int i = 0; i < candidate_count; i++) { + if (!candidates[i].installed) + new_games++; } - - process_scan_candidates(candidates, candidate_count); - if (should_abort_scan_cycle()) - return false; + if (new_games > 0) + notify_system_info("Found %d new games. Executing...", new_games); } + process_scan_candidates(candidates, candidate_count); + if (should_abort_scan_cycle()) + return false; + mount_backport_overlays(&unstable_found); if (should_abort_scan_cycle()) return false; @@ -858,8 +857,7 @@ static bool run_targeted_scan_cycle(int scan_root_index, if (should_abort_scan_cycle()) return false; - if (candidate_count > 0) - process_scan_candidates(candidates, candidate_count); + process_scan_candidates(candidates, candidate_count); if (should_abort_scan_cycle()) return false; @@ -911,8 +909,15 @@ static int find_due_dirty_scan_root(uint64_t now_us) { return selected_root; } -static uint64_t compute_next_scan_deadline_us(uint64_t full_resync_due_us) { +static uint64_t compute_next_scan_deadline_us(uint64_t now_us, + uint64_t full_resync_due_us) { uint64_t next_deadline = full_resync_due_us; + uint64_t install_wake_us = sm_install_next_wake_us(now_us); + + if (install_wake_us != 0 && + (next_deadline == 0 || install_wake_us < next_deadline)) { + next_deadline = install_wake_us; + } if (g_scanner_config_reload_pending && (next_deadline == 0 || @@ -1192,6 +1197,12 @@ void sm_scanner_run_loop(void) { continue; } + uint64_t install_wake_us = sm_install_next_wake_us(now_us); + if (install_wake_us != 0 && now_us >= install_wake_us) { + sm_install_service_pending(); + continue; + } + if (now_us >= next_full_resync_us) { bool unstable_found = false; if (!run_full_scan_cycle(false, NULL, &unstable_found)) @@ -1266,7 +1277,8 @@ void sm_scanner_run_loop(void) { continue; } - uint64_t deadline_us = compute_next_scan_deadline_us(next_full_resync_us); + uint64_t deadline_us = + compute_next_scan_deadline_us(now_us, next_full_resync_us); struct timespec timeout; build_wait_timeout(&timeout, now_us, deadline_us);