Compare commits

..
24 Commits
Author SHA1 Message Date
Roman Tarasov 24fba2be21 Reduce scanner directory watchers by default 2026-06-03 21:13:07 +03:00
Roman Tarasov f20b97db65 funding 2026-06-03 17:53:44 +03:00
Roman Tarasov 9d6ab61369 Added description of compressed PFS format 2026-06-03 17:49:43 +03:00
Roman Tarasov 25182da557 Fix PFS mount 2026-06-03 16:09:11 +03:00
Drakmor 72cf4460a9 Merge pull request #55 from drakmor/alert-autofix-1
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2026-06-03 02:26:06 +03:00
DrakmorandCopilot Autofix powered by AI cc0f4e0684 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-03 01:49:51 +03:00
Roman Tarasov 8cdcd112f5 Nested images 2026-06-03 01:34:23 +03:00
Roman Tarasov c41c3eeba8 Attempting to restore occupied mount points for nullfs 2026-06-03 01:19:38 +03:00
Drakmor 3d6a300742 Update README.md 2026-06-02 11:51:43 +03:00
Roman Tarasov 51d01011ff Fixed mounting of exfat 2026-06-02 10:14:47 +03:00
Roman Tarasov 9e803cff7f Added support for compressed PFS 2026-06-01 21:28:57 +03:00
Roman Tarasov dee175e2bd Added copying of trophy data 2026-06-01 01:12:30 +03:00
Roman Tarasov 860e9de3b1 Fix duplicate uninstall requests during scan cleanup 2026-06-01 00:05:13 +03:00
Roman Tarasov e0b41b3ca6 Fix cleanup for non-uninstallable app.db entries 2026-05-30 17:00:11 +03:00
Roman Tarasov 5c4470cdcd Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-05-30 11:13:24 +03:00
Roman Tarasov 5b6be76272 Fix same-session manual reinstalls by clearing stale install/register state and refreshing app.db before manual scans 2026-05-30 11:12:44 +03:00
Roman Tarasov c66bff6d2a Prevent kstuff auto-resume during shutdown 2026-05-30 10:59:17 +03:00
Drakmor 2df61d7e2d Update README.md 2026-05-25 22:51:30 +03:00
Drakmor e960fc8079 Update README.md 2026-05-25 22:51:03 +03:00
Drakmor d1c6978e78 Update README.md 2026-05-25 22:44:36 +03:00
Roman Tarasov 4eadb06f57 Add manual install list support 2026-05-25 18:00:14 +03:00
Drakmor 78f536b41a maximum mounted images 64->256 2026-05-23 18:47:58 +03:00
Drakmor 99e000eed9 Update README.md
kstuff link
2026-05-23 16:17:25 +03:00
Drakmor 8622d8757f Update README.md 2026-05-14 11:55:28 +03:00
32 changed files with 1778 additions and 227 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
custom:
- https://github.com/drakmor/shadowMountPlus#support-development
+3
View File
@@ -13,6 +13,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
build:
outputs:
+149 -7
View File
@@ -2,18 +2,26 @@
**Repository:** https://github.com/drakmor/shadowMountPlus
**Discord:** https://discord.gg/x2Ppvzwjhm
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff-lite v1.02+**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
**💜 Support Development**
## 💜 Support Development
If you want to support this project, you can donate
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
If you want to support this project, you can donate USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
## Current image support
@@ -24,6 +32,7 @@
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
| `.ffpfs` | `pfs` | `LVD` | Experimental |
| `.ffpfsc` | `pfs` container | `LVD` | Experimental container for nested images |
Notes:
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
@@ -52,6 +61,8 @@ Supported keys (all optional):
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
@@ -101,7 +112,7 @@ Per-image sector override behavior:
Scan path behavior:
- If at least one `scanpath=...` is present, only those custom paths are used.
- `/mnt/shadowmnt` is always added automatically, even with custom paths.
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always added automatically, even with custom paths.
- With `scan_depth=1` (default), only first-level subfolders are checked.
- With `scan_depth=2`, one additional nested level is checked.
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
@@ -152,12 +163,102 @@ Image mountpoints are created under:
`/mnt/shadowmnt/<image_name>_<hash>`
PFSC container mountpoints are created under:
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
- Game files must be placed at the image root.
- Do not add an extra top-level folder inside the image.
- Valid example: `/sce_sys/param.json` exists directly from image root.
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
PFSC container layout requirement (`.ffpfsc`):
- Do not place game files directly in the container root.
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
## Compressed PFS containers (`.ffpfsc`)
Compressed PFS mode is intended only for nested images. During packing, data is
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
as an outer container for another image rather than as a direct game-file
layout.
Recommended layouts:
- exFAT image inside compressed PFS.
- Uncompressed PFS image inside compressed PFS.
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
speed. Keep this in mind when choosing which games to pack. Games that read large
amounts of data or stream textures continuously may stutter.
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
PFS images.
### Packing an uncompressed PFS image into compressed PFS
First create an uncompressed nested PFS image:
```bash
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
'./PPSA07923/PPSA07923-app' \
'./pfs_image.dat'
```
Then pack the nested image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./pfs_image.dat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary nested image can be removed:
```bash
rm './pfs_image.dat'
```
### Packing an exFAT image into compressed PFS
First create a normal exFAT image using one of the methods from
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
extension, for example `PPSA12345.exfat`.
Linux:
```bash
chmod +x mkexfat.sh
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
```
Windows:
```cmd
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
```
The exFAT image must contain the game files at the image root, without an extra
top-level folder.
Then pack the exFAT image into a compressed PFS container:
```bash
mkpfs pack file --verify --version PS5 --inode-bits 32 \
'./PPSA12345.exfat' \
'./PPSA12345.ffpfsc'
```
After successful packing, the temporary exFAT image can be removed:
```bash
rm './PPSA12345.exfat'
```
## Scan paths
Default scan locations:
@@ -172,10 +273,49 @@ Default scan locations:
- `/mnt/usb0` .. `/mnt/usb7`
- `/mnt/ext0`
- `/mnt/ext1`
- `/mnt/shadowmnt/pfsc` (mounted PFSC container scan)
- `/mnt/shadowmnt` (mounted image content scan)
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
## Manual install list
For games that should not live under the normal scan paths, ShadowMountPlus also
checks:
`/data/shadowmount/manual.lst`
Add one source per line:
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
- Empty lines and lines starting with `#` are ignored.
Example:
```text
/mnt/usb0/MyGames/PPSA12345
/mnt/usb0/images/PPSA54321.ffpkg
# /mnt/usb0/disabled/PPSA00000
```
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
rescans shortly after the write, mounts images when needed, and installs the
game through the same pipeline as normal scan path discoveries.
Manual install state is tracked in:
`/data/shadowmount/manual.status`
This status file is managed by ShadowMountPlus. If a manually installed game is
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
as deleted in `manual.status` and removes the matching source line from
`manual.lst` so it is not installed again automatically. If you later add the
same source path back to `manual.lst`, it will be processed again and the status
will be updated after installation.
You can use **Dump Installer** to install or prepare game dumps for this manual
workflow, then add the resulting game folder path or image file path to
`manual.lst`.
Recommended folder structure:
- Default mode (`scan_depth=1`):
- `/data/homebrew/<TITLE_ID>/`
@@ -281,7 +421,7 @@ If a game is not mounted:
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
- Verify scan roots:
- if `scanpath=...` is set, only these paths are scanned;
- `/mnt/shadowmnt` is always scanned.
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always scanned.
- Verify scan depth:
- `scan_depth=1` scans only first-level subfolders;
- `scan_depth=2` scans one additional nested level;
@@ -289,9 +429,10 @@ If a game is not mounted:
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
If a game is mounted but does not start:
@@ -313,5 +454,6 @@ If a game is mounted but does not start:
* Gezine
* earthonion
* LightningMods
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
* john-tornblom for SDK
* PS5 R&D Community
+13 -1
View File
@@ -45,6 +45,18 @@
# Default: 1
# scan_depth=1
# Directory watcher granularity:
# 1 -> keep vnode watchers on scan root subdirectories
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
# Default: 0
# watch_subdirs=0
# Image file watcher granularity:
# 1 -> keep vnode watchers on discovered image files
# 0 -> detect image file changes through targeted/full scans
# Default: 1
# watch_image_files=1
# Legacy compatibility:
# recursive_scan=1 forces scan_depth=2
# Default: disabled
@@ -120,7 +132,7 @@
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
# "/mnt/shadowmnt" is always added automatically.
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
# Default: use built-in scan path list
# scanpath=/data/homebrew
+2
View File
@@ -22,5 +22,7 @@ void free_app_db_title_list(struct AppDbTitleList *list);
void invalidate_app_db_title_cache(void);
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
// Copy the cached PPSA titles that app.db marks as not uninstallable.
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
#endif
+2
View File
@@ -18,6 +18,8 @@ const runtime_config_t *runtime_config(void);
int get_scan_path_count(void);
// Return a scan root by index, or NULL if out of range.
const char *get_scan_path(int index);
// Return scan depth for a root, including managed container-root expansion.
uint32_t get_scan_depth_for_root(const char *scan_path);
// Resolve a per-image read-only override from the file name.
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
// Resolve a per-image sector-size override from autotune.ini or config.ini.
+6 -1
View File
@@ -22,9 +22,14 @@ bool shutdown_image_mounts(void);
// Remove empty directories left under the image mount root.
void cleanup_mount_dirs(void);
// Mount an image file if it is stable and not currently rate-limited.
void maybe_mount_image_file(const char *full_path, const char *name,
bool maybe_mount_image_file(const char *full_path, const char *name,
bool *unstable_out);
// Build the deterministic runtime mount point for an image source path.
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]);
// Return true when the filename has a supported image extension.
bool is_supported_image_file_name(const char *name);
// Return true when the source path is a supported image, including nested names.
bool is_supported_image_file_path(const char *full_path, const char *name);
#endif
+2
View File
@@ -8,6 +8,8 @@ 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);
// Drop queued/submitted async install state for a title after confirmed removal.
void sm_install_forget_pending_title(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.
+1 -1
View File
@@ -8,7 +8,7 @@
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
#define MAX_PENDING 512
#define MAX_IMAGE_MOUNTS 64
#define MAX_IMAGE_MOUNTS 256
#define MAX_IMAGE_MODE_RULES 128
#define MAX_KSTUFF_TITLE_RULES 128
#define MAX_FAKELIB_EXCLUDE_RULES 128
+20
View File
@@ -0,0 +1,20 @@
#ifndef SM_MANUAL_H
#define SM_MANUAL_H
#include <stdbool.h>
struct AppDbTitleList;
// Visit each manual source path from manual.lst.
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx);
// Mark previously installed manual titles as deleted when they disappear from
// app.db, and remove their source lines from manual.lst.
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready);
// Record that a manual source is installed and associated with a title.
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name);
#endif
+4 -1
View File
@@ -45,6 +45,8 @@
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
#define PFS_NESTED_OUTER_IMG_TYPE 0x02u
#define PFS_NESTED_INNER_IMG_TYPE 0x82u
#define LVD_ATTACH_LAYER_COUNT 1
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
#define LVD_ENTRY_TYPE_FILE 1
@@ -69,10 +71,11 @@
#define DEVPFS_MKEYMODE_GD "GD"
#define DEVPFS_MKEYMODE_AC "AC"
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_SD
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_AC
#define PFS_MOUNT_SIGVERIFY 0
#define PFS_MOUNT_PLAYGO 0
#define PFS_MOUNT_DISC 0
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
// 4x64-bit PFS key encoded as 64 hex chars.
#define PFS_ZERO_EKPFS_KEY_HEX \
"0000000000000000000000000000000000000000000000000000000000000000"
+4
View File
@@ -34,5 +34,9 @@ bool is_backport_mount_blocked(const char *path);
void note_backport_mount_failure(const char *path);
// Clear failed backport overlay mount state for a path.
void clear_backport_mount_failure(const char *path);
// Return true once for a missing manual source until the path appears again.
bool note_manual_missing_source_once(const char *path);
// Allow a manual source to be reported missing again after it becomes visible.
void clear_manual_missing_source(const char *path);
#endif
+2
View File
@@ -12,6 +12,8 @@ const char *get_filename_component(const char *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);
// Return true when a path is the PFSC container mount root or lives under it.
bool is_pfsc_image_mount_base_or_child(const char *path);
// Return true when path is equal to root or is under it.
bool path_matches_root_or_child(const char *path, const char *root);
// Return true when a path lives on USB-backed storage.
+3
View File
@@ -2,6 +2,7 @@
#define SM_PATHS_H
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
#define PFSC_IMAGE_MOUNT_BASE "/mnt/shadowmnt/pfsc"
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
@@ -10,6 +11,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 MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
#define APPMETA_BASE "/user/appmeta"
#define APP_BASE "/user/app"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
+1
View File
@@ -44,6 +44,7 @@ static inline uint32_t sm_firmware_major_version(void) {
// --- SDK Imports ---
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void);
int sceAppInstUtilAppUnInstall(const char *title_id);
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
int sceKernelUsleep(unsigned int microseconds);
int sceUserServiceInitialize(void *);
+1 -1
View File
@@ -3,7 +3,7 @@
#include <stdbool.h>
// Initialize scanner service resources such as wake pipe and control-dir watch.
// Initialize scanner service resources such as wake pipe and file watches.
bool sm_scanner_init(void);
// Wake a blocked scanner wait. Safe to call from signal context.
void sm_scanner_wake(void);
+2
View File
@@ -10,6 +10,8 @@ bool was_register_attempted(const char *title_id);
uint8_t get_register_attempts(const char *title_id);
// Record a registration attempt for a title.
void mark_register_attempted(const char *title_id);
// Clear registration attempt state after success or confirmed user removal.
void clear_register_attempts(const char *title_id);
// Notify once about duplicate title IDs from different paths.
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b);
+5
View File
@@ -89,8 +89,10 @@ typedef struct scan_candidate {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char manual_source_path[MAX_PATH];
bool installed;
bool in_app_db;
bool manual;
} scan_candidate_t;
typedef enum {
@@ -114,6 +116,8 @@ typedef struct runtime_config {
bool kstuff_game_auto_toggle;
bool kstuff_crash_detection_enabled;
bool legacy_recursive_scan_forced;
bool watch_subdirs;
bool watch_image_files;
char global_fakelib_path[MAX_PATH];
uint32_t global_fakelib_exclude_title_count;
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
@@ -136,6 +140,7 @@ typedef enum {
IMAGE_FS_UFS,
IMAGE_FS_EXFAT,
IMAGE_FS_PFS,
IMAGE_FS_PFSC_CONTAINER,
} image_fs_type_t;
typedef struct sm_error {
+132 -52
View File
@@ -14,8 +14,11 @@ static sqlite3 *g_app_db;
static sqlite3_stmt *g_app_db_stmt_update_snd0;
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
static struct AppDbTitleList g_app_db_title_cache;
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
static bool g_app_db_title_cache_ready = false;
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
static time_t g_app_db_title_cache_mtime = 0;
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
@@ -46,8 +49,11 @@ void free_app_db_title_list(struct AppDbTitleList *list) {
void shutdown_app_db(void) {
pthread_mutex_lock(&g_app_db_mutex);
free_app_db_title_list(&g_app_db_title_cache);
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
close_app_db();
pthread_mutex_unlock(&g_app_db_mutex);
}
@@ -348,6 +354,49 @@ bool app_db_title_list_contains(const struct AppDbTitleList *list,
compare_title_id_str) != NULL;
}
static bool step_app_db_title_query(struct AppDbTitleList *list,
sqlite3_stmt *stmt,
const char *label) {
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 (!append_app_db_title(list, title_id)) {
log_debug(" [DB] %s allocation failed", label);
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();
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] %s query failed: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
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;
}
static bool load_app_db_title_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
@@ -363,54 +412,64 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
if (prep_rc != SQLITE_OK)
return false;
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 (!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;
}
bool loaded = step_app_db_title_query(list, stmt, "title list");
if (loaded)
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
return loaded;
}
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
sqlite3_finalize(stmt);
close_app_db();
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
return false;
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId LIKE 'PPSA%' "
"AND uninstallable = 0 "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc =
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
"blocked PPSA uninstallable query");
if (prep_rc != SQLITE_OK)
return false;
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
if (loaded)
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
list->count);
return loaded;
}
void invalidate_app_db_title_cache(void) {
pthread_mutex_lock(&g_app_db_mutex);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
pthread_mutex_unlock(&g_app_db_mutex);
}
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
const struct AppDbTitleList *src) {
if (!dst || !src)
return false;
free_app_db_title_list(dst);
if (src->count <= 0)
return true;
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
if (!ids)
return false;
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
dst->ids = ids;
dst->count = src->count;
dst->capacity = src->count;
return true;
}
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
@@ -438,21 +497,42 @@ bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
}
}
bool copied = true;
if (g_app_db_title_cache.count > 0) {
char(*ids)[MAX_TITLE_ID] =
malloc((size_t)g_app_db_title_cache.count * sizeof(*ids));
if (ids) {
memcpy(ids, g_app_db_title_cache.ids,
(size_t)g_app_db_title_cache.count * sizeof(*ids));
list_out->ids = ids;
list_out->count = g_app_db_title_cache.count;
list_out->capacity = g_app_db_title_cache.count;
} else {
copied = false;
}
}
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return copied;
}
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
free_app_db_title_list(list_out);
pthread_mutex_lock(&g_app_db_mutex);
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
(app_db_stat_rc == 0 &&
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
struct AppDbTitleList fresh = {0};
if (app_db_stat_rc == 0 &&
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_blocked_uninstall_ppsa_cache = fresh;
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
} else {
free_app_db_title_list(&fresh);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
pthread_mutex_unlock(&g_app_db_mutex);
return false;
}
}
}
bool loaded =
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return loaded;
}
+39 -3
View File
@@ -224,11 +224,16 @@ static bool add_runtime_scan_path(runtime_config_state_t *state,
return true;
}
static void add_runtime_managed_scan_paths(runtime_config_state_t *state) {
(void)add_runtime_scan_path(state, PFSC_IMAGE_MOUNT_BASE);
(void)add_runtime_scan_path(state, IMAGE_MOUNT_BASE);
}
static void init_runtime_scan_paths_defaults(runtime_config_state_t *state) {
clear_runtime_scan_paths(state);
for (int i = 0; k_default_scan_paths[i] != NULL; i++)
(void)add_runtime_scan_path(state, k_default_scan_paths[i]);
(void)add_runtime_scan_path(state, IMAGE_MOUNT_BASE);
add_runtime_managed_scan_paths(state);
}
static void clear_kstuff_title_rules(runtime_config_state_t *state) {
@@ -252,6 +257,8 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
state->cfg.kstuff_game_auto_toggle = true;
state->cfg.kstuff_crash_detection_enabled = true;
state->cfg.legacy_recursive_scan_forced = false;
state->cfg.watch_subdirs = false;
state->cfg.watch_image_files = true;
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
sizeof(state->cfg.global_fakelib_path));
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
@@ -353,6 +360,15 @@ const char *get_scan_path(int index) {
return state->scan_path_storage[index];
}
uint32_t get_scan_depth_for_root(const char *scan_path) {
uint32_t scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
if (is_pfsc_image_mount_base_or_child(scan_path))
scan_depth++;
return scan_depth;
}
bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
ensure_runtime_config_ready();
if (!filename || !mount_read_only_out)
@@ -1227,6 +1243,24 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
continue;
}
if (strcasecmp(key, "watch_subdirs") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
state->cfg.watch_subdirs = bval;
continue;
}
if (strcasecmp(key, "watch_image_files") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
continue;
}
state->cfg.watch_image_files = bval;
continue;
}
if (strcasecmp(key, "backport_fakelib") == 0) {
if (!parse_bool_ini(value, &bval)) {
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
@@ -1446,9 +1480,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
if (has_custom_scanpaths && state->scan_path_count == 0) {
log_debug(" [CFG] no valid scanpath entries, using defaults");
init_runtime_scan_paths_defaults(state);
} else {
(void)add_runtime_scan_path(state, IMAGE_MOUNT_BASE);
}
add_runtime_managed_scan_paths(state);
if (legacy_recursive_scan_requested) {
state->cfg.scan_depth = 2u;
@@ -1475,6 +1508,7 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
"global_fakelib=%d global_fakelib_priority=%s "
"global_fakelib_path=%s global_fakelib_exclude=%u "
"watch_subdirs=%d watch_image_files=%d "
"kstuff_game_auto_toggle=%d kstuff_crash_detection=%d "
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
"exfat_backend=%s ufs_backend=%s "
@@ -1492,6 +1526,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
state->cfg.global_fakelib_mount_first ? "game" : "global",
state->cfg.global_fakelib_path,
state->cfg.global_fakelib_exclude_title_count,
state->cfg.watch_subdirs ? 1 : 0,
state->cfg.watch_image_files ? 1 : 0,
state->cfg.kstuff_game_auto_toggle ? 1 : 0,
state->cfg.kstuff_crash_detection_enabled ? 1 : 0,
state->cfg.kstuff_pause_delay_image_seconds,
+56 -2
View File
@@ -594,6 +594,58 @@ static bool unmount_controlled_mount_stack(const char *path) {
return !get_top_mount(path, &mount_st);
}
static void unmount_mount_point_for_recovery(const char *path) {
if (!unmount_controlled_mount_stack(path)) {
log_debug(" [LINK] failed to reset mount stack for recovery: %s", path);
}
if (unmount(path, 0) == 0) {
log_debug(" [LINK] extra unmount for recovery: %s", path);
return;
}
if (errno == ENOENT || errno == EINVAL)
return;
if (unmount(path, MNT_FORCE) == 0) {
log_debug(" [LINK] extra forced unmount for recovery: %s", path);
return;
}
if (errno == ENOENT || errno == EINVAL)
return;
log_debug(" [LINK] extra unmount for recovery did not complete for %s: %s",
path, strerror(errno));
}
static bool recover_mount_directory_after_stat_failure(
const char *path, struct stat *st_out) {
log_debug(" [LINK] resetting mount stack after stat failure: %s", path);
unmount_mount_point_for_recovery(path);
if (stat(path, st_out) == 0)
return true;
int stat_errno = errno;
if (stat_errno != ENOENT) {
log_debug(" [LINK] mount directory stat failed after reset for %s: %s",
path, strerror(stat_errno));
return false;
}
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
log_debug(" [LINK] failed to recreate mount directory %s: %s", path,
strerror(errno));
return false;
}
if (stat(path, st_out) == 0)
return true;
log_debug(" [LINK] mount directory stat failed after recreate for %s: %s",
path, strerror(errno));
return false;
}
static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
const title_link_paths_t *paths,
void *ctx) {
@@ -845,9 +897,11 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
}
struct stat dst_st;
if (stat(dst, &dst_st) != 0) {
int stat_errno = errno;
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
dst, strerror(errno));
return false;
dst, strerror(stat_errno));
if (!recover_mount_directory_after_stat_failure(dst, &dst_st))
return false;
}
if (!S_ISDIR(dst_st.st_mode)) {
log_debug(" [LINK] mount target is not a directory: %s mode=0%o", dst,
+247 -30
View File
@@ -14,12 +14,45 @@
#include "sm_path_utils.h"
#include "sm_paths.h"
static bool image_fs_type_is_pfs(image_fs_type_t fs_type) {
return fs_type == IMAGE_FS_PFS || fs_type == IMAGE_FS_PFSC_CONTAINER;
}
static bool pfs_path_is_nested_inner(const char *path, image_fs_type_t fs_type) {
return fs_type == IMAGE_FS_PFS && is_pfsc_image_mount_base_or_child(path);
}
static bool pfs_path_uses_nested_profile(const char *path,
image_fs_type_t fs_type) {
return fs_type == IMAGE_FS_PFSC_CONTAINER ||
pfs_path_is_nested_inner(path, fs_type);
}
static uint8_t get_nested_pfs_img_type(const char *path,
image_fs_type_t fs_type) {
if (pfs_path_is_nested_inner(path, fs_type))
return PFS_NESTED_INNER_IMG_TYPE;
return PFS_NESTED_OUTER_IMG_TYPE;
}
static uint16_t pfs_lvd_game_image_type_from_selector(uint8_t selector) {
static const uint16_t table[8] = {1, 2, 3, 4, 8, 9, 10, 11};
uint32_t idx = (((uint32_t)selector >> 7) & 1u) +
2u * ((uint32_t)selector & 0x1Fu);
idx ^= 1u;
if (idx >= 8u)
return LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA;
return table[idx];
}
static uint32_t get_lvd_sector_size_fallback(image_fs_type_t fs_type) {
const runtime_config_t *cfg = runtime_config();
switch (fs_type) {
case IMAGE_FS_UFS:
return cfg->lvd_sector_ufs;
case IMAGE_FS_PFS:
case IMAGE_FS_PFSC_CONTAINER:
return cfg->lvd_sector_pfs;
case IMAGE_FS_EXFAT:
default:
@@ -39,8 +72,15 @@ static uint32_t get_image_sector_size_override_or_default(
}
static uint32_t get_lvd_sector_size(const char *path, image_fs_type_t fs_type) {
bool nested_pfs_profile = pfs_path_uses_nested_profile(path, fs_type);
uint32_t default_sector =
nested_pfs_profile ? LVD_SECTOR_SIZE_PFS
: get_lvd_sector_size_fallback(fs_type);
uint32_t fallback = get_image_sector_size_override_or_default(
path, get_lvd_sector_size_fallback(fs_type));
path, default_sector);
if (nested_pfs_profile)
return fallback;
struct statfs sfs;
if (statfs(path, &sfs) != 0)
return fallback;
@@ -91,7 +131,7 @@ static unsigned int get_md_attach_options(bool mount_read_only) {
static uint16_t get_lvd_attach_raw_flags(image_fs_type_t fs_type,
bool mount_read_only) {
if (fs_type == IMAGE_FS_UFS) {
if (fs_type == IMAGE_FS_UFS) {
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_DD_RO
: LVD_ATTACH_RAW_FLAGS_DD_RW;
}
@@ -123,10 +163,14 @@ static uint16_t normalize_lvd_raw_flags(uint16_t raw_flags) {
return (uint16_t)(8u * ((uint32_t)raw_flags & 1u) + 4u);
}
static uint16_t get_lvd_image_type(image_fs_type_t fs_type) {
static uint16_t get_lvd_image_type(const char *path, image_fs_type_t fs_type) {
if (fs_type == IMAGE_FS_UFS)
return LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA;
if (fs_type == IMAGE_FS_PFS)
if (pfs_path_uses_nested_profile(path, fs_type)) {
return pfs_lvd_game_image_type_from_selector(
get_nested_pfs_img_type(path, fs_type));
}
if (image_fs_type_is_pfs(fs_type))
return LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA;
return LVD_ATTACH_IMAGE_TYPE_SINGLE;
}
@@ -145,6 +189,9 @@ static uint16_t get_lvd_source_type(const char *path) {
// --- Image Path and Naming Helpers ---
static image_fs_type_t detect_image_fs_type(const char *name) {
if (!name || name[0] == '\0')
return IMAGE_FS_UNKNOWN;
const char *dot = strrchr(name, '.');
if (!dot)
return IMAGE_FS_UNKNOWN;
@@ -154,6 +201,30 @@ static image_fs_type_t detect_image_fs_type(const char *name) {
return IMAGE_FS_EXFAT;
if (strcasecmp(dot, ".ffpfs") == 0)
return IMAGE_FS_PFS;
if (strcasecmp(dot, ".ffpfsc") == 0)
return IMAGE_FS_PFSC_CONTAINER;
return IMAGE_FS_UNKNOWN;
}
static bool is_nested_pfs_image_file_name(const char *name) {
return name && strcasecmp(name, "pfs_image.dat") == 0;
}
static image_fs_type_t detect_image_fs_type_for_path(const char *path,
const char *name) {
const char *filename = name;
if ((!filename || filename[0] == '\0') && path)
filename = get_filename_component(path);
image_fs_type_t fs_type = detect_image_fs_type(filename);
if (fs_type != IMAGE_FS_UNKNOWN)
return fs_type;
if (is_nested_pfs_image_file_name(filename) &&
is_pfsc_image_mount_base_or_child(path)) {
return IMAGE_FS_PFS;
}
return IMAGE_FS_UNKNOWN;
}
@@ -161,6 +232,10 @@ bool is_supported_image_file_name(const char *name) {
return detect_image_fs_type(name) != IMAGE_FS_UNKNOWN;
}
bool is_supported_image_file_path(const char *full_path, const char *name) {
return detect_image_fs_type_for_path(full_path, name) != IMAGE_FS_UNKNOWN;
}
static const char *image_fs_name(image_fs_type_t fs_type) {
switch (fs_type) {
case IMAGE_FS_UFS:
@@ -169,6 +244,8 @@ static const char *image_fs_name(image_fs_type_t fs_type) {
return "exfatfs";
case IMAGE_FS_PFS:
return "pfs";
case IMAGE_FS_PFSC_CONTAINER:
return "pfsc";
default:
return "unknown";
}
@@ -220,8 +297,9 @@ static void strip_extension(const char *filename, char *out, size_t out_size) {
out[len] = '\0';
}
static void build_image_mount_point(const char *file_path,
char mount_point[MAX_PATH]) {
static void build_image_mount_point_for_fs(const char *file_path,
image_fs_type_t fs_type,
char mount_point[MAX_PATH]) {
const char *filename = get_filename_component(file_path);
char base_name[MAX_PATH];
char mount_name[MAX_PATH];
@@ -236,7 +314,21 @@ static void build_image_mount_point(const char *file_path,
snprintf(mount_name + base_len, sizeof(mount_name) - base_len, "_%08x",
sm_fnv1a32(file_path));
snprintf(mount_point, MAX_PATH, "%s/%s", IMAGE_MOUNT_BASE, mount_name);
const char *mount_base =
fs_type == IMAGE_FS_PFSC_CONTAINER ? PFSC_IMAGE_MOUNT_BASE
: IMAGE_MOUNT_BASE;
snprintf(mount_point, MAX_PATH, "%s/%s", mount_base, mount_name);
}
static void build_image_mount_point(const char *file_path,
char mount_point[MAX_PATH]) {
build_image_mount_point_for_fs(
file_path, detect_image_fs_type_for_path(file_path, NULL), mount_point);
}
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]) {
build_image_mount_point(file_path, mount_point);
}
typedef bool (*image_attach_fn)(const char *file_path, image_fs_type_t fs_type,
@@ -310,6 +402,35 @@ static bool attach_lvd_backend(const char *file_path, image_fs_type_t fs_type,
bool mount_read_only, off_t file_size,
int *unit_id_out, char *devname_out,
size_t devname_size) {
/* if (pfs_path_is_nested_inner(file_path, fs_type)) {
struct stat req;
memset(&req, 0, sizeof(req));
req.st_dev = 1;
req.st_mode = 0;
req.st_gid = 1;
req.st_atim.tv_sec = 0;
int cache_fd = open(file_path, O_RDONLY);
if (cache_fd < 0) {
log_debug(" [IMG][%s] GDDR5 cache open failed for %s: %s",
attach_backend_name(ATTACH_BACKEND_LVD), file_path,
strerror(errno));
return false;
}
int cache_ret = ioctl(cache_fd, PFS_GDDR5_CACHE_IOCTL, &req);
int cache_errno = cache_ret != 0 ? errno : 0;
close(cache_fd);
if (cache_ret != 0) {
errno = cache_errno;
log_debug(" [IMG][%s] GDDR5 cache ioctl failed for %s: %s",
attach_backend_name(ATTACH_BACKEND_LVD), file_path,
strerror(errno));
return false;
}
log_debug(" [IMG][%s] GDDR5 cache enabled for nested PFS: %s",
attach_backend_name(ATTACH_BACKEND_LVD), file_path);
}
*/
int lvd_fd = open(LVD_CTRL_PATH, O_RDWR);
if (lvd_fd < 0) {
log_debug(" [IMG][%s] open %s failed: %s",
@@ -334,7 +455,7 @@ static bool attach_lvd_backend(const char *file_path, image_fs_type_t fs_type,
lvd_ioctl_attach_v0_t req;
memset(&req, 0, sizeof(req));
req.io_version = LVD_ATTACH_IO_VERSION_V0;
req.image_type = get_lvd_image_type(fs_type);
req.image_type = get_lvd_image_type(file_path, fs_type);
req.layer_count = LVD_ATTACH_LAYER_COUNT;
req.device_size = (uint64_t)file_size;
req.layers_ptr = layers;
@@ -568,6 +689,8 @@ static bool stat_image_file(const char *file_path, struct stat *st_out) {
static void ensure_mount_dirs(const char *mount_point) {
mkdir(IMAGE_MOUNT_BASE, 0777);
if (is_pfsc_image_mount_base_or_child(mount_point))
mkdir(PFSC_IMAGE_MOUNT_BASE, 0777);
mkdir(mount_point, 0777);
}
@@ -612,7 +735,20 @@ static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
const char *sigverify = PFS_MOUNT_SIGVERIFY ? "1" : "0";
const char *playgo = PFS_MOUNT_PLAYGO ? "1" : "0";
const char *disc = PFS_MOUNT_DISC ? "1" : "0";
const char *finalized = "0";
const char *pubkey_ver = "0";
const char *key_ver = "0";
const char *ekpfs_key = PFS_ZERO_EKPFS_KEY_HEX;
uint8_t nested_img_type = 0;
bool nested_pfs_profile = pfs_path_uses_nested_profile(file_path, fs_type);
if (nested_pfs_profile) {
nested_img_type = get_nested_pfs_img_type(file_path, fs_type);
// Temporarily disabled: keep nested PFS signature option wired but do not
// force sigverify=1.
// sigverify = "1";
disc = (nested_img_type & 0x40u) ? "1" : "0";
finalized = (nested_img_type & 0x20u) ? "1" : "0";
}
struct iovec iov_ufs[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("ufs"),
@@ -656,19 +792,56 @@ static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
struct iovec iov_nested_pfs[] = {
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("pfs"),
IOVEC_ENTRY("sigverify"), IOVEC_ENTRY(sigverify),
IOVEC_ENTRY("mkeymode"), IOVEC_ENTRY(PFS_MOUNT_MKEYMODE),
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(PFS_MOUNT_BUDGET_ID),
IOVEC_ENTRY("playgo"), IOVEC_ENTRY(playgo),
IOVEC_ENTRY("disc"), IOVEC_ENTRY(disc),
// IOVEC_ENTRY("pubkey_ver"), IOVEC_ENTRY(pubkey_ver),
// IOVEC_ENTRY("key_ver"), IOVEC_ENTRY(key_ver),
// IOVEC_ENTRY("finalized"), IOVEC_ENTRY(finalized),
IOVEC_ENTRY("ekpfs"), IOVEC_ENTRY(ekpfs_key),
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
if (fs_type == IMAGE_FS_UFS) {
iov = iov_ufs;
iovlen = (unsigned int)IOVEC_SIZE(iov_ufs) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_EXFAT) {
iov = iov_exfat;
iovlen = (unsigned int)IOVEC_SIZE(iov_exfat) - (force_mount ? 0u : 2u);
} else if (fs_type == IMAGE_FS_PFS) {
log_debug(" [IMG][%s] PFS ro=%d budgetid=%s mkeymode=%s "
"sigverify=%s playgo=%s disc=%s ekpfs=zero",
attach_backend_name(attach_backend), mount_read_only ? 1 : 0,
PFS_MOUNT_BUDGET_ID, PFS_MOUNT_MKEYMODE, sigverify, playgo, disc);
iov = iov_pfs;
iovlen = (unsigned int)IOVEC_SIZE(iov_pfs) - (force_mount ? 0u : 2u);
} else if (image_fs_type_is_pfs(fs_type)) {
if (nested_pfs_profile) {
log_debug(" [IMG][%s] PFS ro=%d budgetid=%s mkeymode=%s "
"sigverify=%s playgo=%s disc=%s finalized=%s "
"pubkey_ver=%s key_ver=%s",
attach_backend_name(attach_backend), mount_read_only ? 1 : 0,
PFS_MOUNT_BUDGET_ID, PFS_MOUNT_MKEYMODE, sigverify, playgo,
disc, finalized, pubkey_ver, key_ver);
log_debug(" [IMG][%s] nested PFS profile: selector=0x%02x "
"lvd_img=%u gddr5=%d",
attach_backend_name(attach_backend), nested_img_type,
get_lvd_image_type(file_path, fs_type),
pfs_path_is_nested_inner(file_path, fs_type) ? 1 : 0);
iov = iov_nested_pfs;
iovlen =
(unsigned int)IOVEC_SIZE(iov_nested_pfs) - (force_mount ? 0u : 2u);
} else {
log_debug(" [IMG][%s] PFS ro=%d budgetid=%s mkeymode=%s "
"sigverify=%s playgo=%s disc=%s ekpfs=zero",
attach_backend_name(attach_backend), mount_read_only ? 1 : 0,
PFS_MOUNT_BUDGET_ID, PFS_MOUNT_MKEYMODE, sigverify, playgo,
disc);
iov = iov_pfs;
iovlen = (unsigned int)IOVEC_SIZE(iov_pfs) - (force_mount ? 0u : 2u);
}
} else {
log_debug(" [IMG][%s] unsupported fstype=%s",
attach_backend_name(attach_backend), image_fs_name(fs_type));
@@ -766,12 +939,14 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
if (filename[0] != '\0')
mount_mode_overridden =
get_image_mode_override(filename, &mount_read_only);
if (pfs_path_uses_nested_profile(file_path, fs_type))
mount_read_only = true;
if (runtime_sleep_mode_active())
return false;
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
build_image_mount_point_for_fs(file_path, fs_type, mount_point);
bool handled = false;
bool success = false;
@@ -864,9 +1039,36 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
return true;
}
static bool unmount_child_image_mounts_for_container(const char *mount_point) {
bool all_unmounted = true;
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
image_cache_entry_t cached_entry;
if (!get_image_cache_entry(k, &cached_entry))
continue;
if (!path_matches_root_or_child(cached_entry.path, mount_point))
continue;
log_debug(" [IMG] container child unmount: %s source=%s",
mount_point, cached_entry.path);
if (unmount_image(cached_entry.path, cached_entry.unit_id,
cached_entry.backend)) {
invalidate_image_cache_entry(k);
continue;
}
all_unmounted = false;
log_debug(" [IMG] container child unmount pending: %s source=%s",
mount_point, cached_entry.path);
}
return all_unmounted;
}
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend) {
char mount_point[MAX_PATH];
build_image_mount_point(file_path, mount_point);
image_fs_type_t fs_type = detect_image_fs_type_for_path(file_path, NULL);
build_image_mount_point_for_fs(file_path, fs_type, mount_point);
int resolved_unit = unit_id;
attach_backend_t resolved_backend = backend;
@@ -882,6 +1084,11 @@ bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend)
attach_backend_name(resolved_backend), file_path, mount_point,
resolved_unit);
if (fs_type == IMAGE_FS_PFSC_CONTAINER &&
!unmount_child_image_mounts_for_container(mount_point)) {
return false;
}
// Remove mount.lnk and unmount /system_ex/app/<titleid> that point to this
// source before unmounting the virtual disk itself.
cleanup_mount_links_for_source_unmount(mount_point);
@@ -970,7 +1177,8 @@ void cleanup_stale_image_mounts(void) {
continue;
}
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
image_fs_type_t fs_type =
detect_image_fs_type_for_path(cached_entry.path, NULL);
char mount_point[MAX_PATH];
char source_path[MAX_PATH];
if (!prepare_image_mount_retry(&cached_entry, mount_point, source_path))
@@ -1019,7 +1227,8 @@ void cleanup_stale_image_mounts_for_root(const char *root) {
continue;
}
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
image_fs_type_t fs_type =
detect_image_fs_type_for_path(cached_entry.path, NULL);
char mount_point[MAX_PATH];
char source_path[MAX_PATH];
if (!prepare_image_mount_retry(&cached_entry, mount_point, source_path))
@@ -1066,11 +1275,11 @@ bool unmount_usb_image_mounts_for_suspend(void) {
return all_unmounted;
}
void cleanup_mount_dirs(void) {
DIR *d = opendir(IMAGE_MOUNT_BASE);
static void cleanup_mount_dirs_under(const char *base, const char *skip_name) {
DIR *d = opendir(base);
if (!d) {
if (errno != ENOENT)
log_debug(" [IMG] open %s failed: %s", IMAGE_MOUNT_BASE, strerror(errno));
log_debug(" [IMG] open %s failed: %s", base, strerror(errno));
return;
}
@@ -1080,10 +1289,11 @@ void cleanup_mount_dirs(void) {
break;
if (entry->d_name[0] == '.')
continue;
if (skip_name && strcmp(entry->d_name, skip_name) == 0)
continue;
char full_path[MAX_PATH];
snprintf(full_path, sizeof(full_path), "%s/%s", IMAGE_MOUNT_BASE,
entry->d_name);
snprintf(full_path, sizeof(full_path), "%s/%s", base, entry->d_name);
bool is_dir = false;
if (entry->d_type == DT_DIR) {
@@ -1109,28 +1319,35 @@ void cleanup_mount_dirs(void) {
closedir(d);
}
void maybe_mount_image_file(const char *full_path, const char *display_name,
void cleanup_mount_dirs(void) {
cleanup_mount_dirs_under(PFSC_IMAGE_MOUNT_BASE, NULL);
cleanup_mount_dirs_under(IMAGE_MOUNT_BASE, "pfsc");
}
bool maybe_mount_image_file(const char *full_path, const char *display_name,
bool *unstable_out) {
image_fs_type_t fs_type = detect_image_fs_type(display_name);
image_fs_type_t fs_type =
detect_image_fs_type_for_path(full_path, display_name);
if (fs_type == IMAGE_FS_UNKNOWN)
return;
return false;
if (!is_source_stable_for_mount(full_path, display_name, "IMG")) {
if (unstable_out)
*unstable_out = true;
return;
return false;
}
if (is_image_mount_limited(full_path))
return;
return false;
if (mount_image(full_path, fs_type)) {
clear_image_mount_attempts(full_path);
return;
return true;
}
int mount_err = errno;
if (bump_image_mount_attempts(full_path) == 1 && !sm_error_notified()) {
notify_image_mount_failed(full_path, mount_err);
}
return false;
}
bool shutdown_image_mounts(void) {
+65 -11
View File
@@ -13,6 +13,7 @@
#include "sm_title_state.h"
#include "sm_image_cache.h"
#include "sm_paths.h"
#include "sm_manual.h"
#include <dlfcn.h>
@@ -127,6 +128,57 @@ static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
if (closedir(d) != 0)
ok = false;
return ok;
}
static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
const char *dst_base,
const char *relative_path) {
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
if (access(src_path, F_OK) != 0)
return true;
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
if (access(dst_path, F_OK) == 0)
return true;
if (copy_file(src_path, dst_path) == 0)
return true;
log_debug(" [COPY] Failed to copy trophy metadata: %s -> %s", src_path,
dst_path);
return false;
}
static bool update_trophy_metadata(const char *title_id,
const char *src_sce_sys) {
char dst_base[MAX_PATH];
char dst_dir[MAX_PATH];
bool ok = true;
snprintf(dst_base, sizeof(dst_base), "/system_data/priv/appmeta/%s",
title_id);
mkdir("/system_data/priv/appmeta", 0755);
mkdir(dst_base, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/trophy2", dst_base);
mkdir(dst_dir, 0755);
snprintf(dst_dir, sizeof(dst_dir), "%s/uds", dst_base);
mkdir(dst_dir, 0755);
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"trophy2/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
"uds/npbind.dat"))
ok = false;
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base, "param.json"))
ok = false;
return ok;
}
@@ -162,6 +214,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
}
}
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
appmeta_missing = !has_appmeta_data(title_id);
if (is_remount && appmeta_missing) {
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
@@ -174,13 +227,10 @@ static bool mount_and_install(const char *src_path, const char *title_id,
return false;
// COPY FILES
if (restage_staging || restage_appmeta) {
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
} else {
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
if (!restage_staging && !restage_appmeta)
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
}
if (restage_staging) {
mkdir(APP_BASE, 0777);
@@ -215,6 +265,9 @@ static bool mount_and_install(const char *src_path, const char *title_id,
metadata_restaged = true;
}
if (!update_trophy_metadata(title_id, src_sce_sys))
return false;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
@@ -280,11 +333,6 @@ static bool mount_and_install(const char *src_path, const char *title_id,
return true;
}
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;
@@ -309,6 +357,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
if (res == 0) {
invalidate_app_db_title_cache();
clear_register_attempts(title_id);
log_debug(" [REG] Installed NEW!");
notify_game_installed_rich(title_id);
if (has_src_snd0) {
@@ -318,6 +367,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
}
} else if ((uint32_t)res == 0x80990002u) {
invalidate_app_db_title_cache();
clear_register_attempts(title_id);
log_debug(" [REG] Restored.");
if (has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
@@ -368,6 +418,10 @@ void process_scan_candidates(const scan_candidate_t *candidates,
return;
if (mounted) {
if (c->manual && !use_app_install_all) {
sm_manual_note_installed(c->manual_source_path, c->title_id,
c->title_name);
}
if (use_app_install_all) {
if (!sm_install_queue_candidate(c, has_src_snd0)) {
log_debug(" [REG] Failed to queue staged install: %s (%s)",
+16
View File
@@ -7,6 +7,7 @@
#include "sm_game_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_manual.h"
#include "sm_runtime.h"
#include "sm_time.h"
#include "sm_title_state.h"
@@ -25,8 +26,10 @@ typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char source_path[MAX_PATH];
char manual_source_path[MAX_PATH];
uint64_t requested_at_us;
bool has_src_snd0;
bool manual;
install_track_state_t state;
} pending_install_entry_t;
@@ -143,6 +146,12 @@ bool is_title_install_pending(const char *title_id) {
return find_pending_install_entry(title_id) != NULL;
}
void sm_install_forget_pending_title(const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
clear_pending_install_entry(entry);
}
static void append_batch_install_line(char *message, size_t message_size,
const char *title_name,
const char *title_id, int *shown_count) {
@@ -189,6 +198,9 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
(void)strlcpy(entry->title_name, candidate->title_name,
sizeof(entry->title_name));
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
sizeof(entry->manual_source_path));
entry->manual = candidate->manual;
entry->has_src_snd0 = has_src_snd0;
entry->state = INSTALL_TRACK_QUEUED;
if (previous_state == INSTALL_TRACK_NONE)
@@ -213,6 +225,10 @@ static void finalize_pending_install_success(pending_install_entry_t *entry) {
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
if (entry->manual)
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
entry->title_name);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
clear_pending_install_entry(entry);
}
+7 -1
View File
@@ -7,6 +7,7 @@
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_path_utils.h"
#include "sm_runtime.h"
#include "sm_time.h"
#include "sm_types.h"
@@ -818,7 +819,12 @@ void sm_kstuff_game_poll(void) {
void sm_kstuff_game_shutdown(void) {
bool restore_needed = tracked_game_requires_restore();
if (g_kstuff.sleep_pause_restore) {
if (restore_needed && !g_kstuff.sleep_pause_restore &&
runtime_sleep_mode_active() && should_stop_requested()) {
restore_needed = false;
g_kstuff.restore_on_empty = false;
g_kstuff.restore_retry_us = 0;
} else if (g_kstuff.sleep_pause_restore) {
restore_needed = true;
g_kstuff.sleep_pause_restore = false;
}
+420
View File
@@ -0,0 +1,420 @@
#include "sm_platform.h"
#include "sm_manual.h"
#include "sm_appdb.h"
#include "sm_install_queue.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
#include "sm_title_state.h"
#define MANUAL_STATUS_CAPACITY 1024
typedef enum {
MANUAL_STATUS_UNKNOWN = 0,
MANUAL_STATUS_INSTALLED,
MANUAL_STATUS_DELETED,
} manual_status_t;
typedef struct {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
manual_status_t status;
bool remove_from_list;
} manual_status_entry_t;
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
static void trim_manual_line(char *line) {
if (!line)
return;
char *start = line;
while (*start != '\0' && isspace((unsigned char)*start))
start++;
if (start != line)
memmove(line, start, strlen(start) + 1u);
size_t len = strlen(line);
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
line[len - 1u] = '\0';
len--;
}
while (len > 1 && line[len - 1u] == '/') {
line[len - 1u] = '\0';
len--;
}
}
static void drain_manual_file_line(FILE *f) {
int ch;
while ((ch = fgetc(f)) != EOF && ch != '\n') {
}
}
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
int *saved_errno) {
int ch;
while ((ch = fgetc(in)) != EOF) {
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
*saved_errno = errno;
if (ch == '\n')
break;
}
}
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx) {
if (!visit)
return false;
FILE *f = fopen(MANUAL_LIST_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
return false;
}
char line[MAX_PATH + 64];
bool completed = true;
while (fgets(line, sizeof(line), f) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
trim_manual_line(line);
if (line[0] == '\0' || line[0] == '#') {
if (truncated)
drain_manual_file_line(f);
continue;
}
if (truncated || strlen(line) >= MAX_PATH) {
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
if (truncated)
drain_manual_file_line(f);
continue;
}
if (!visit(line, ctx)) {
completed = false;
break;
}
}
if (ferror(f)) {
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
completed = false;
}
fclose(f);
return completed;
}
static const char *manual_status_name(manual_status_t status) {
switch (status) {
case MANUAL_STATUS_INSTALLED:
return "installed";
case MANUAL_STATUS_DELETED:
return "deleted";
default:
return "unknown";
}
}
static manual_status_t parse_manual_status_name(const char *name) {
if (!name)
return MANUAL_STATUS_UNKNOWN;
if (strcasecmp(name, "installed") == 0)
return MANUAL_STATUS_INSTALLED;
if (strcasecmp(name, "deleted") == 0)
return MANUAL_STATUS_DELETED;
return MANUAL_STATUS_UNKNOWN;
}
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
char *status = strtok(line, "\t");
char *title_id = strtok(NULL, "\t");
char *path = strtok(NULL, "\t");
char *title_name = strtok(NULL, "\n");
if (!status || !title_id || !path)
return false;
trim_manual_line(status);
trim_manual_line(title_id);
trim_manual_line(path);
if (title_name)
trim_manual_line(title_name);
manual_status_t parsed_status = parse_manual_status_name(status);
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
path[0] == '\0') {
return false;
}
memset(entry, 0, sizeof(*entry));
entry->status = parsed_status;
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->path, path, sizeof(entry->path));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
return true;
}
static int load_manual_status(void) {
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] status open failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
return 0;
}
int count = 0;
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
while (fgets(line, sizeof(line), f) != NULL) {
if (count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status limit reached (%d)",
MANUAL_STATUS_CAPACITY);
break;
}
manual_status_entry_t entry;
if (!parse_manual_status_line(line, &entry))
continue;
g_manual_status_entries[count++] = entry;
}
if (ferror(f))
log_debug(" [MANUAL] status read failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
fclose(f);
return count;
}
static bool write_manual_status(int entry_count) {
mkdir(LOG_DIR, 0777);
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
FILE *f = fopen(tmp_path, "w");
if (!f) {
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
strerror(errno));
return false;
}
int saved_errno = 0;
for (int i = 0; i < entry_count; i++) {
const manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
entry->title_id[0] == '\0') {
continue;
}
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
entry->title_id, entry->path, entry->title_name) < 0) {
saved_errno = errno;
break;
}
}
if (fflush(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] status write failed for %s: %s", tmp_path,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
log_debug(" [MANUAL] status rename failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
static int find_manual_status_entry(int entry_count, const char *path,
const char *title_id) {
for (int i = 0; i < entry_count; i++) {
if (path && path[0] != '\0' &&
strcmp(g_manual_status_entries[i].path, path) == 0) {
return i;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
return i;
}
}
return -1;
}
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
FILE *in = fopen(MANUAL_LIST_FILE, "r");
if (!in) {
if (errno == ENOENT)
return true;
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
return false;
}
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
FILE *out = fopen(tmp_path, "w");
if (!out) {
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
strerror(errno));
fclose(in);
return false;
}
bool changed = false;
int saved_errno = 0;
char line[MAX_PATH + 64];
while (fgets(line, sizeof(line), in) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
char normalized[MAX_PATH + 64];
(void)strlcpy(normalized, line, sizeof(normalized));
trim_manual_line(normalized);
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
bool remove_line = false;
for (int i = 0; i < entry_count; i++) {
if (g_manual_status_entries[i].remove_from_list &&
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
remove_line = true;
break;
}
}
if (remove_line) {
changed = true;
if (truncated)
drain_manual_file_line(in);
continue;
}
}
if (fputs(line, out) == EOF) {
saved_errno = errno;
break;
}
if (truncated)
copy_manual_file_line_remainder(in, out, &saved_errno);
}
if (ferror(in) && saved_errno == 0)
saved_errno = errno;
if (fflush(out) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(out) != 0 && saved_errno == 0)
saved_errno = errno;
fclose(in);
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (!changed) {
(void)unlink(tmp_path);
return true;
}
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
bool sm_manual_reconcile_deleted_titles(
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
if (!app_db_titles_ready || !app_db_titles)
return true;
int entry_count = load_manual_status();
if (entry_count <= 0)
return true;
bool changed = false;
for (int i = 0; i < entry_count; i++) {
manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status != MANUAL_STATUS_INSTALLED ||
entry->title_id[0] == '\0') {
continue;
}
if (app_db_title_list_contains(app_db_titles, entry->title_id))
continue;
entry->status = MANUAL_STATUS_DELETED;
entry->remove_from_list = true;
sm_install_forget_pending_title(entry->title_id);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
changed = true;
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
"%s (%s)",
entry->title_name, entry->title_id);
}
if (!changed)
return true;
if (!rewrite_manual_list_without_deleted_entries(entry_count))
return false;
return write_manual_status(entry_count);
}
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name) {
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
title_id[0] == '\0') {
return;
}
int entry_count = load_manual_status();
int index =
find_manual_status_entry(entry_count, manual_source_path, title_id);
if (index < 0) {
if (entry_count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status full, cannot record %s (%s)",
manual_source_path, title_id);
return;
}
index = entry_count++;
memset(&g_manual_status_entries[index], 0,
sizeof(g_manual_status_entries[index]));
}
manual_status_entry_t *entry = &g_manual_status_entries[index];
manual_status_t previous_status = entry->status;
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
strcmp(entry->path, manual_source_path) == 0 &&
strcmp(entry->title_id, title_id) == 0 &&
(!title_name || title_name[0] == '\0' ||
strcmp(entry->title_name, title_name) == 0);
if (unchanged)
return;
entry->status = MANUAL_STATUS_INSTALLED;
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
if (!write_manual_status(entry_count))
return;
if (previous_status != MANUAL_STATUS_INSTALLED) {
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
entry->title_name, entry->title_id, entry->path);
}
}
+27 -1
View File
@@ -13,6 +13,7 @@ struct PathStateEntry {
bool missing_param_limit_logged;
bool image_mount_limit_logged;
bool backport_mount_blocked;
bool manual_missing_source_logged;
bool game_info_cached;
bool game_info_valid;
time_t game_info_mtime;
@@ -69,7 +70,8 @@ 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 (!path_exists(g_path_state[k].path)) {
if (!path_exists(g_path_state[k].path) &&
!g_path_state[k].manual_missing_source_logged) {
evict_k = k;
break;
}
@@ -81,6 +83,7 @@ static struct PathStateEntry *create_path_state(const char *path) {
if (g_path_state[k].missing_param_attempts == 0 &&
g_path_state[k].image_mount_attempts == 0 &&
!g_path_state[k].backport_mount_blocked &&
!g_path_state[k].manual_missing_source_logged &&
!g_path_state[k].game_info_cached) {
evict_k = k;
break;
@@ -160,6 +163,8 @@ void prune_path_state(void) {
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
@@ -181,6 +186,8 @@ void prune_path_state_for_root(const char *root) {
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
@@ -278,3 +285,22 @@ void clear_backport_mount_failure(const char *path) {
return;
entry->backport_mount_blocked = false;
}
bool note_manual_missing_source_once(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry || entry->manual_missing_source_logged)
return false;
entry->manual_missing_source_logged = true;
return true;
}
void clear_manual_missing_source(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->manual_missing_source_logged = false;
}
+4
View File
@@ -20,6 +20,10 @@ bool is_under_image_mount_base(const char *path) {
path[image_prefix_len] == '/');
}
bool is_pfsc_image_mount_base_or_child(const char *path) {
return path_matches_root_or_child(path, PFSC_IMAGE_MOUNT_BASE);
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
+348 -31
View File
@@ -18,12 +18,15 @@
#include "sm_image_cache.h"
#include "sm_image.h"
#include "sm_install_queue.h"
#include "sm_manual.h"
typedef struct {
char discovered_param_roots[MAX_PENDING][MAX_PATH];
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
bool checked_appmeta_present[MAX_PENDING];
int checked_appmeta_count;
int blocked_ppsa_uninstall_count;
} scan_workspace_t;
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
@@ -32,6 +35,63 @@ static scan_workspace_t g_scan_workspace;
static void reset_scan_workspace(void) {
g_scan_workspace.checked_appmeta_count = 0;
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
}
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return false;
for (int i = 0; i < g_scan_workspace.blocked_ppsa_uninstall_count; i++) {
if (strcmp(g_scan_workspace.blocked_ppsa_uninstall_titles[i], title_id) == 0)
return true;
}
return false;
}
static void remember_blocked_ppsa_uninstall(const char *title_id) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return;
if (g_scan_workspace.blocked_ppsa_uninstall_count >= MAX_PENDING)
return;
int slot = g_scan_workspace.blocked_ppsa_uninstall_count++;
(void)strlcpy(g_scan_workspace.blocked_ppsa_uninstall_titles[slot], title_id,
sizeof(g_scan_workspace.blocked_ppsa_uninstall_titles[slot]));
}
static bool is_blocked_ppsa_title(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready, const char *title_id) {
return blocked_ppsa_titles_ready &&
app_db_title_list_contains(blocked_ppsa_titles, title_id);
}
static bool request_blocked_ppsa_uninstall(const char *title_id,
const char *source_path) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return false;
remember_blocked_ppsa_uninstall(title_id);
int res = sceAppInstUtilAppUnInstall(title_id);
if (res == 0) {
sm_install_forget_pending_title(title_id);
invalidate_app_db_title_cache();
if (source_path && source_path[0] != '\0') {
log_debug(" [REG] requested blocked PPSA uninstall: %s source=%s",
title_id, source_path);
} else {
log_debug(" [REG] requested blocked PPSA uninstall: %s", title_id);
}
return true;
}
log_debug(" [REG] blocked PPSA uninstall failed: %s code=0x%08X", title_id,
(uint32_t)res);
return false;
}
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
@@ -83,12 +143,24 @@ typedef enum {
EXISTING_DIRECTORY_PREFER_CACHED,
} existing_directory_result_t;
typedef struct {
const struct AppDbTitleList *titles;
const struct AppDbTitleList *blocked_ppsa_titles;
bool titles_ready;
bool blocked_ppsa_titles_ready;
} scan_app_db_context_t;
static directory_candidate_probe_t probe_directory_candidate(
const char *full_path, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, directory_candidate_info_t *info_out) {
int *discovered_param_root_count, bool allow_known_param_root,
directory_candidate_info_t *info_out) {
struct stat param_st;
if (is_under_discovered_param_root(full_path, discovered_param_roots,
if (is_pfsc_image_mount_base_or_child(full_path))
return DIRECTORY_CANDIDATE_DESCEND;
if (!allow_known_param_root &&
is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
@@ -114,7 +186,9 @@ static directory_candidate_probe_t probe_directory_candidate(
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (*discovered_param_root_count < MAX_PENDING) {
if (*discovered_param_root_count < MAX_PENDING &&
!is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
MAX_PATH);
(*discovered_param_root_count)++;
@@ -133,6 +207,16 @@ static int find_scan_candidate_index_by_title_id(scan_candidate_t *candidates,
return -1;
}
static void mark_scan_candidate_manual(scan_candidate_t *candidate,
const char *manual_source_path) {
if (!candidate || !manual_source_path || manual_source_path[0] == '\0')
return;
candidate->manual = true;
(void)strlcpy(candidate->manual_source_path, manual_source_path,
sizeof(candidate->manual_source_path));
}
static void remove_scan_candidate_at(scan_candidate_t *candidates,
int *candidate_count, int index) {
int trailing_count = *candidate_count - index - 1;
@@ -150,8 +234,8 @@ static void notify_duplicate_scan_candidate(const char *title_id,
}
static existing_directory_result_t handle_existing_directory_candidate(
const char *full_path, const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready, const directory_candidate_info_t *info,
const char *full_path, const scan_app_db_context_t *app_db,
const directory_candidate_info_t *info,
bool *installed_out, bool *in_app_db_out,
char preferred_existing_path_out[MAX_PATH]) {
char tracked_path[MAX_PATH];
@@ -161,7 +245,14 @@ static existing_directory_result_t handle_existing_directory_candidate(
bool link_matches_source =
has_tracked_path && strcmp(tracked_path, full_path) == 0;
if (!app_db_titles_ready) {
if (is_blocked_ppsa_title(app_db->blocked_ppsa_titles,
app_db->blocked_ppsa_titles_ready,
info->title_id)) {
request_blocked_ppsa_uninstall(info->title_id, full_path);
return EXISTING_DIRECTORY_HANDLED;
}
if (!app_db->titles_ready) {
if (link_matches_source && is_data_mounted(info->title_id)) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
@@ -169,7 +260,7 @@ static existing_directory_result_t handle_existing_directory_candidate(
return EXISTING_DIRECTORY_HANDLED;
}
bool in_app_db = app_db_title_list_contains(app_db_titles, info->title_id);
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;
@@ -209,7 +300,8 @@ static existing_directory_result_t handle_existing_directory_candidate(
static bool enqueue_directory_candidate(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const directory_candidate_info_t *info, bool installed,
bool in_app_db, bool *unstable_found_out) {
bool in_app_db, const char *manual_source_path,
bool *unstable_found_out) {
char metadata_path[MAX_PATH];
uint8_t failed_attempts = get_failed_mount_attempts(info->title_id);
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
@@ -247,21 +339,26 @@ static bool enqueue_directory_candidate(
sizeof(candidates[*candidate_count].title_id));
(void)strlcpy(candidates[*candidate_count].title_name, info->title_name,
sizeof(candidates[*candidate_count].title_name));
candidates[*candidate_count].manual_source_path[0] = '\0';
candidates[*candidate_count].installed = installed;
candidates[*candidate_count].in_app_db = in_app_db;
candidates[*candidate_count].manual = false;
mark_scan_candidate_manual(&candidates[*candidate_count], manual_source_path);
(*candidate_count)++;
return true;
}
static bool try_collect_candidate_for_directory(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
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, bool *unstable_found_out) {
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
const char *manual_source_path, bool *unstable_found_out) {
directory_candidate_info_t info;
directory_candidate_probe_t probe_result =
probe_directory_candidate(full_path, discovered_param_roots,
discovered_param_root_count, &info);
discovered_param_root_count,
allow_known_param_root, &info);
if (probe_result == DIRECTORY_CANDIDATE_SKIP_DESCEND)
return true;
@@ -279,11 +376,13 @@ static bool try_collect_candidate_for_directory(
bool in_app_db = false;
char preferred_existing_path[MAX_PATH];
existing_directory_result_t existing_result =
handle_existing_directory_candidate(full_path, app_db_titles,
app_db_titles_ready, &info,
handle_existing_directory_candidate(full_path, app_db, &info,
&installed, &in_app_db,
preferred_existing_path);
if (existing_result == EXISTING_DIRECTORY_PREFER_CURRENT) {
if (manual_source_path)
sm_manual_note_installed(manual_source_path, info.title_id,
info.title_name);
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, duplicate_candidate_path,
@@ -305,6 +404,9 @@ static bool try_collect_candidate_for_directory(
if (duplicate_candidate_index >= 0 && !duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
if (duplicate_candidate_index >= 0)
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
@@ -312,25 +414,35 @@ static bool try_collect_candidate_for_directory(
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
return enqueue_directory_candidate(full_path, candidates, max_candidates,
candidate_count, &info, installed,
in_app_db, unstable_found_out);
in_app_db, manual_source_path,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const struct AppDbTitleList *app_db_titles;
bool app_db_titles_ready;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
const char *manual_source_path;
bool *unstable_found_out;
} collect_candidates_walk_ctx_t;
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out);
static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
if (depth_from_root == 0u)
@@ -339,8 +451,8 @@ static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (try_collect_candidate_for_directory(
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db_titles, ctx->app_db_titles_ready,
ctx->discovered_param_roots, ctx->discovered_param_root_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, false, ctx->manual_source_path,
ctx->unstable_found_out)) {
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
}
@@ -355,7 +467,136 @@ static bool collect_candidate_image_visit(const char *image_path,
(void)depth_from_root;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out);
if (!maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out))
return true;
if (ctx->manual_source_path && is_pfsc_image_mount_base_or_child(image_path)) {
char mount_point[MAX_PATH];
get_image_mount_point_for_source(image_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, ctx->manual_source_path, ctx->candidates,
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
ctx->discovered_param_roots, ctx->discovered_param_root_count,
ctx->unstable_found_out);
}
return true;
}
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
bool pfsc_container_root = is_pfsc_image_mount_base_or_child(scan_path);
bool try_root_candidate = !pfsc_container_root;
if (try_root_candidate && is_under_image_mount_base(scan_path)) {
struct stat param_st;
try_root_candidate = directory_has_param_json(scan_path, &param_st);
}
if (try_root_candidate) {
if (try_collect_candidate_for_directory(
scan_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count, true,
manual_source_path, unstable_found_out)) {
return;
}
}
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = manual_source_path,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = pfsc_container_root ? collect_candidate_image_visit
: NULL,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_path(
const char *manual_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
struct stat st;
if (stat(manual_path, &st) != 0) {
if (note_manual_missing_source_once(manual_path))
log_debug(" [MANUAL] source unavailable: %s", manual_path);
return;
}
clear_manual_missing_source(manual_path);
const char *name = get_filename_component(manual_path);
if (S_ISREG(st.st_mode) &&
is_supported_image_file_path(manual_path, name)) {
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
return;
char mount_point[MAX_PATH];
get_image_mount_point_for_source(manual_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
return;
}
struct stat param_st;
if (!S_ISDIR(st.st_mode) || !directory_has_param_json(manual_path, &param_st)) {
log_debug(" [MANUAL] expected game directory or image file: %s",
manual_path);
return;
}
collect_scan_candidates_from_manual_root(
manual_path, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
bool *unstable_found_out;
int path_count;
} manual_scan_ctx_t;
static bool collect_manual_path_visit(const char *manual_path, void *ctx_ptr) {
manual_scan_ctx_t *ctx = (manual_scan_ctx_t *)ctx_ptr;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (*ctx->candidate_count >= ctx->max_candidates) {
log_debug(" [MANUAL] candidate queue full (%d), remaining sources deferred",
ctx->max_candidates);
return false;
}
ctx->path_count++;
collect_scan_candidates_from_manual_path(
manual_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, ctx->unstable_found_out);
return true;
}
@@ -501,24 +742,22 @@ void unmount_usb_sources_for_suspend(void) {
static void collect_scan_candidates_from_root(
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH],
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
unsigned int scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db_titles = app_db_titles,
.app_db_titles_ready = app_db_titles_ready,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = NULL,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
@@ -528,6 +767,54 @@ static void collect_scan_candidates_from_root(
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_list(
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
(void)sm_manual_reconcile_deleted_titles(app_db->titles,
app_db->titles_ready);
manual_scan_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.unstable_found_out = unstable_found_out,
.path_count = 0,
};
if (!sm_manual_for_each_path(collect_manual_path_visit, &ctx) ||
ctx.path_count <= 0) {
return;
}
log_debug(" [MANUAL] scanned %d manual source(s)", ctx.path_count);
}
static void uninstall_blocked_ppsa_titles(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready) {
if (!blocked_ppsa_titles_ready || !blocked_ppsa_titles)
return;
int requested = 0;
for (int i = 0; i < blocked_ppsa_titles->count &&
!should_stop_requested() && !runtime_sleep_mode_active();
i++) {
const char *title_id = blocked_ppsa_titles->ids[i];
if (request_blocked_ppsa_uninstall(title_id, NULL))
requested++;
}
if (requested > 0)
log_debug(" [REG] requested %d blocked PPSA uninstall(s)", requested);
}
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
@@ -536,21 +823,32 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
collect_scan_candidates_from_root(scan_root, candidates, max_candidates,
&candidate_count, &app_db_titles,
app_db_titles_ready,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
@@ -561,26 +859,45 @@ int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
for (int i = 0; i < get_scan_path_count(); i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
break;
collect_scan_candidates_from_root(get_scan_path(i), candidates,
max_candidates,
&candidate_count,
&app_db_titles, app_db_titles_ready,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
}
collect_scan_candidates_from_manual_list(
candidates, max_candidates, &candidate_count, &app_db,
g_scan_workspace.discovered_param_roots, &discovered_param_root_count,
unstable_found_out);
uninstall_blocked_ppsa_titles(&blocked_ppsa_titles,
blocked_ppsa_titles_ready);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
+5 -2
View File
@@ -71,11 +71,14 @@ bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
if (!d)
return true;
bool scan_root_is_pfsc_mount_base =
is_pfsc_image_mount_base_or_child(scan_root);
bool skip_backports_root =
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
bool allow_image_file_visits =
callbacks->on_image_file &&
!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE);
(!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE) ||
scan_root_is_pfsc_mount_base);
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
@@ -98,7 +101,7 @@ bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
classify_scan_tree_entry(full_path, entry->d_type, &is_dir, &is_regular);
if (allow_image_file_visits && is_regular &&
is_supported_image_file_name(entry->d_name)) {
is_supported_image_file_path(full_path, entry->d_name)) {
if (!callbacks->on_image_file(full_path, entry->d_name, depth_from_root + 1u,
ctx)) {
closedir(d);
+184 -82
View File
@@ -6,6 +6,7 @@
#include <sys/select.h>
#include "sm_config_mount.h"
#include "sm_appdb.h"
#include "sm_fakelib.h"
#include "sm_filesystem.h"
#include "sm_game_lifecycle.h"
@@ -30,6 +31,8 @@
#define SCANNER_WATCH_INDEX_NONE ((size_t)-1)
#define SCANNER_CONFIG_RELOAD_DEBOUNCE_US 250000ull
#define SCANNER_CONFIG_PROBE_INTERVAL_US 10000000ull
#define SCANNER_MANUAL_RELOAD_DEBOUNCE_US 250000ull
#define SCANNER_MANUAL_PROBE_INTERVAL_US 10000000ull
typedef enum {
SCANNER_WATCH_SCAN_ROOT = 0,
@@ -63,8 +66,8 @@ typedef struct {
} scanner_root_state_t;
static int g_scanner_wake_pipe[2] = {-1, -1};
static int g_scanner_control_dir_fd = -1;
static int g_scanner_config_fd = -1;
static int g_scanner_manual_fd = -1;
static volatile sig_atomic_t g_scanner_wake_write_fd = -1;
static scan_candidate_t g_scanner_scan_candidates[MAX_PENDING];
static scanner_watch_entry_t *g_scanner_watch_entries = NULL;
@@ -77,6 +80,8 @@ static scanner_root_state_t g_scanner_root_states[MAX_SCAN_PATHS];
static bool g_scanner_config_reload_pending = false;
static uint64_t g_scanner_config_reload_ready_after_us = 0;
static uint64_t g_scanner_config_probe_due_us = 0;
static uint64_t g_scanner_manual_scan_due_us = 0;
static uint64_t g_scanner_manual_probe_due_us = 0;
static uint64_t scanner_stability_wait_us(void) {
return (uint64_t)runtime_config()->stability_wait_seconds * 1000000ull;
@@ -96,6 +101,14 @@ static void schedule_config_probe(uint64_t now_us) {
g_scanner_config_probe_due_us = now_us + SCANNER_CONFIG_PROBE_INTERVAL_US;
}
static void schedule_manual_scan(uint64_t now_us) {
g_scanner_manual_scan_due_us = now_us + SCANNER_MANUAL_RELOAD_DEBOUNCE_US;
}
static void schedule_manual_probe(uint64_t now_us) {
g_scanner_manual_probe_due_us = now_us + SCANNER_MANUAL_PROBE_INTERVAL_US;
}
static bool set_fd_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
@@ -133,13 +146,6 @@ static void close_scanner_wake_pipe(void) {
}
}
static void close_scanner_control_dir(void) {
if (g_scanner_control_dir_fd >= 0) {
close(g_scanner_control_dir_fd);
g_scanner_control_dir_fd = -1;
}
}
static void close_scanner_config_file(void) {
if (g_scanner_config_fd >= 0) {
close(g_scanner_config_fd);
@@ -147,6 +153,13 @@ static void close_scanner_config_file(void) {
}
}
static void close_scanner_manual_file(void) {
if (g_scanner_manual_fd >= 0) {
close(g_scanner_manual_fd);
g_scanner_manual_fd = -1;
}
}
static void clear_scanner_watch_entries(void) {
for (size_t i = 0; i < g_scanner_watch_count; i++) {
if (g_scanner_watch_entries[i].fd >= 0)
@@ -203,6 +216,11 @@ static void clear_scanner_config_reload_state(void) {
g_scanner_config_probe_due_us = 0;
}
static void clear_scanner_manual_scan_state(void) {
g_scanner_manual_scan_due_us = 0;
g_scanner_manual_probe_due_us = 0;
}
static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
const runtime_config_t *new_cfg) {
return old_cfg->backport_fakelib_enabled !=
@@ -222,7 +240,12 @@ static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
static uint32_t scanner_config_topology_hash(void) {
uint32_t hash = 2166136261u;
int scan_path_count = get_scan_path_count();
uint32_t values[] = {runtime_config()->scan_depth, (uint32_t)scan_path_count};
uint32_t values[] = {
runtime_config()->scan_depth,
runtime_config()->watch_subdirs ? 1u : 0u,
runtime_config()->watch_image_files ? 1u : 0u,
(uint32_t)scan_path_count,
};
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
for (unsigned shift = 0; shift < 32u; shift += 8u) {
@@ -566,6 +589,7 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
typedef struct {
int kq;
int scan_root_index;
bool watch_subdirs;
} register_watch_tree_ctx_t;
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
@@ -574,9 +598,11 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit(
scanner_watch_kind_t kind =
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
: SCANNER_WATCH_SCAN_SUBDIR;
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path, kind,
(uint8_t)depth_from_root)) {
return SM_SCAN_TREE_DIR_ABORT;
if (depth_from_root == 0u || ctx->watch_subdirs) {
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path,
kind, (uint8_t)depth_from_root)) {
return SM_SCAN_TREE_DIR_ABORT;
}
}
return SM_SCAN_TREE_DIR_DESCEND;
@@ -619,21 +645,28 @@ static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
return true;
}
unsigned int scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file = register_watch_image_visit,
};
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
&walk_ctx)) {
return false;
const runtime_config_t *cfg = runtime_config();
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
if (!register_scanner_watch_entry(kq, scan_root_index, scan_root,
SCANNER_WATCH_SCAN_ROOT, 0u)) {
return false;
}
} else {
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
.watch_subdirs = cfg->watch_subdirs,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file =
cfg->watch_image_files ? register_watch_image_visit : NULL,
};
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
&walk_ctx)) {
return false;
}
}
char backport_root[MAX_PATH];
@@ -674,12 +707,15 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
return true;
}
unsigned int scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
return false;
const runtime_config_t *cfg = runtime_config();
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
clear_scan_root_watch_tree_state(scan_root_index);
return true;
}
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
if (rebuild_depth > scan_depth) {
clear_scan_root_watch_tree_state(scan_root_index);
return true;
@@ -688,10 +724,11 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
register_watch_tree_ctx_t walk_ctx = {
.kq = kq,
.scan_root_index = scan_root_index,
.watch_subdirs = cfg->watch_subdirs,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = register_watch_directory_visit,
.on_image_file = register_watch_image_visit,
.on_image_file = cfg->watch_image_files ? register_watch_image_visit : NULL,
};
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {
@@ -764,7 +801,8 @@ static bool scanner_event_requires_watch_tree_refresh(
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
case SCANNER_WATCH_SCAN_SUBDIR:
return entry->depth < runtime_config()->scan_depth &&
return entry->depth <
get_scan_depth_for_root(get_scan_path(entry->scan_root_index)) &&
(fflags & tree_change_flags) != 0;
case SCANNER_WATCH_SCAN_IMAGE_FILE:
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
@@ -823,38 +861,6 @@ static void schedule_scan_root_watch_tree_rebuild(
sizeof(state->watch_tree_rebuild_path));
}
static bool register_control_dir_watch(int kq) {
if (g_scanner_control_dir_fd < 0)
return true;
struct kevent kev;
EV_SET(&kev, (uintptr_t)g_scanner_control_dir_fd, EVFILT_VNODE,
EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
NOTE_REVOKE,
0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [SCAN] control dir watcher registration failed: %s",
strerror(errno));
return false;
}
return true;
}
static bool reopen_control_dir_watch(int kq) {
close_scanner_control_dir();
g_scanner_control_dir_fd = open(LOG_DIR, O_RDONLY | O_DIRECTORY);
if (g_scanner_control_dir_fd < 0) {
log_debug(" [SCAN] control dir watch unavailable for %s: %s", LOG_DIR,
strerror(errno));
return true;
}
return register_control_dir_watch(kq);
}
static void register_config_file_watch(int kq, uint64_t now_us) {
if (g_scanner_config_fd < 0)
return;
@@ -892,6 +898,49 @@ static void reopen_config_file_watch(int kq, uint64_t now_us) {
register_config_file_watch(kq, now_us);
}
static void register_manual_file_watch(int kq, uint64_t now_us) {
if (g_scanner_manual_fd < 0)
return;
struct kevent kev;
EV_SET(&kev, (uintptr_t)g_scanner_manual_fd, EVFILT_VNODE,
EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
NOTE_REVOKE,
0, NULL);
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
log_debug(" [MANUAL] manual.lst watcher registration failed: %s",
strerror(errno));
close_scanner_manual_file();
schedule_manual_probe(now_us);
return;
}
g_scanner_manual_probe_due_us = 0;
}
static int open_manual_list_file(void) {
mkdir(LOG_DIR, 0777);
int fd = open(MANUAL_LIST_FILE, O_RDONLY | O_CREAT, 0666);
if (fd < 0) {
log_debug(" [MANUAL] manual.lst open/create failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
}
return fd;
}
static void reopen_manual_file_watch(int kq, uint64_t now_us) {
close_scanner_manual_file();
g_scanner_manual_fd = open_manual_list_file();
if (g_scanner_manual_fd < 0) {
schedule_manual_probe(now_us);
return;
}
register_manual_file_watch(kq, now_us);
}
static bool apply_runtime_config_reload_effects(int kq,
const runtime_config_t *old_cfg,
const runtime_config_t *new_cfg,
@@ -1067,6 +1116,16 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
next_deadline = g_scanner_config_probe_due_us;
}
if (g_scanner_manual_scan_due_us != 0 &&
(next_deadline == 0 || g_scanner_manual_scan_due_us < next_deadline)) {
next_deadline = g_scanner_manual_scan_due_us;
}
if (g_scanner_manual_fd < 0 && g_scanner_manual_probe_due_us != 0 &&
(next_deadline == 0 || g_scanner_manual_probe_due_us < next_deadline)) {
next_deadline = g_scanner_manual_probe_due_us;
}
for (int i = 0; i < get_scan_path_count(); i++) {
const scanner_root_state_t *state = &g_scanner_root_states[i];
if (!state->dirty)
@@ -1156,14 +1215,6 @@ static bool process_scanner_events(int kq, const struct timespec *timeout,
if (event->filter != EVFILT_VNODE)
continue;
if (event->ident == (uintptr_t)g_scanner_control_dir_fd) {
if ((event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0 &&
!reopen_control_dir_watch(kq)) {
return false;
}
continue;
}
if (event->ident == (uintptr_t)g_scanner_config_fd) {
if ((event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0)
reopen_config_file_watch(kq, now_us);
@@ -1171,6 +1222,16 @@ static bool process_scanner_events(int kq, const struct timespec *timeout,
continue;
}
if (event->ident == (uintptr_t)g_scanner_manual_fd) {
if ((event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0) {
close_scanner_manual_file();
g_scanner_manual_probe_due_us = now_us == 0 ? 1 : now_us;
continue;
}
schedule_manual_scan(now_us);
continue;
}
scanner_watch_entry_t *watch_entry =
find_scanner_watch_entry_by_fd(event->ident);
if (!watch_entry)
@@ -1241,11 +1302,12 @@ static void request_scanner_shutdown(const char *reason) {
bool sm_scanner_init(void) {
close_scanner_wake_pipe();
close_scanner_control_dir();
close_scanner_config_file();
close_scanner_manual_file();
clear_scanner_watch_entries();
reset_scanner_root_states();
clear_scanner_config_reload_state();
clear_scanner_manual_scan_state();
if (pipe(g_scanner_wake_pipe) != 0) {
log_debug(" [SCAN] wake pipe creation failed: %s", strerror(errno));
@@ -1261,12 +1323,6 @@ bool sm_scanner_init(void) {
}
g_scanner_wake_write_fd = (sig_atomic_t)g_scanner_wake_pipe[1];
g_scanner_control_dir_fd = open(LOG_DIR, O_RDONLY | O_DIRECTORY);
if (g_scanner_control_dir_fd < 0) {
log_debug(" [SCAN] control dir watch unavailable for %s: %s", LOG_DIR,
strerror(errno));
}
g_scanner_config_fd = open(CONFIG_FILE, O_RDONLY);
if (g_scanner_config_fd < 0 && errno != ENOENT) {
log_debug(" [CFG] config watch unavailable for %s: %s", CONFIG_FILE,
@@ -1275,6 +1331,10 @@ bool sm_scanner_init(void) {
if (g_scanner_config_fd < 0)
schedule_config_probe(monotonic_time_us());
g_scanner_manual_fd = open_manual_list_file();
if (g_scanner_manual_fd < 0)
schedule_manual_probe(monotonic_time_us());
return true;
}
@@ -1331,10 +1391,12 @@ void sm_scanner_run_loop(void) {
}
register_config_file_watch(kq, monotonic_time_us());
if (!register_control_dir_watch(kq) || !rebuild_all_scan_root_watch_trees(kq)) {
register_manual_file_watch(kq, monotonic_time_us());
if (!rebuild_all_scan_root_watch_trees(kq)) {
close(kq);
clear_scanner_watch_entries();
close_scanner_config_file();
close_scanner_manual_file();
request_scanner_shutdown("scanner watcher initialization failed");
return;
}
@@ -1419,6 +1481,15 @@ void sm_scanner_run_loop(void) {
continue;
}
if (g_scanner_manual_fd < 0 && g_scanner_manual_probe_due_us != 0 &&
now_us >= g_scanner_manual_probe_due_us) {
g_scanner_manual_probe_due_us = 0;
reopen_manual_file_watch(kq, now_us);
if (g_scanner_manual_fd >= 0)
schedule_manual_scan(now_us);
continue;
}
if (config_reload_due(now_us)) {
g_scanner_config_reload_pending = false;
g_scanner_config_reload_ready_after_us = 0;
@@ -1455,6 +1526,36 @@ void sm_scanner_run_loop(void) {
continue;
}
if (g_scanner_manual_scan_due_us != 0 &&
now_us >= g_scanner_manual_scan_due_us) {
g_scanner_manual_scan_due_us = 0;
invalidate_app_db_title_cache();
bool unstable_found = false;
if (!run_full_scan_cycle(false, "manual.lst changed", &unstable_found)) {
if (runtime_sleep_mode_active())
continue;
break;
}
clear_all_dirty_scan_roots();
if (!rebuild_all_scan_root_watch_trees(kq) ||
!drain_scanner_events_nowait(kq)) {
close(kq);
clear_scanner_watch_entries();
request_scanner_shutdown("scanner watcher refresh after manual scan failed");
return;
}
now_us = monotonic_time_us();
next_full_resync_us = now_us + scanner_full_resync_interval_us();
if (unstable_found) {
uint64_t retry_due = now_us + scanner_stability_wait_us();
if (retry_due < next_full_resync_us)
next_full_resync_us = retry_due;
}
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();
@@ -1585,8 +1686,9 @@ void sm_scanner_run_loop(void) {
void sm_scanner_shutdown(void) {
clear_scanner_watch_entries();
close_scanner_config_file();
close_scanner_control_dir();
close_scanner_manual_file();
close_scanner_wake_pipe();
reset_scanner_root_states();
clear_scanner_config_reload_state();
clear_scanner_manual_scan_state();
}
+6
View File
@@ -123,6 +123,12 @@ void mark_register_attempted(const char *title_id) {
entry->register_attempts++;
}
void clear_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->register_attempts = 0;
}
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);