mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 13:50:00 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3530d8d88e | ||
|
|
4dafdde002 | ||
|
|
8566c0294c | ||
|
|
56dd338abd | ||
|
|
090723879c | ||
|
|
16873e01f2 | ||
|
|
f20b97db65 | ||
|
|
9d6ab61369 | ||
|
|
25182da557 | ||
|
|
72cf4460a9 | ||
|
|
cc0f4e0684 | ||
|
|
8cdcd112f5 | ||
|
|
c41c3eeba8 | ||
|
|
3d6a300742 | ||
|
|
51d01011ff | ||
|
|
9e803cff7f | ||
|
|
dee175e2bd | ||
|
|
860e9de3b1 | ||
|
|
e0b41b3ca6 | ||
|
|
5c4470cdcd | ||
|
|
5b6be76272 | ||
|
|
c66bff6d2a | ||
|
|
2df61d7e2d | ||
|
|
e960fc8079 | ||
|
|
d1c6978e78 | ||
|
|
4eadb06f57 | ||
|
|
78f536b41a | ||
|
|
99e000eed9 | ||
|
|
8622d8757f | ||
|
|
bcffa18997 | ||
|
|
e00dda0075 | ||
|
|
315265f5db |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
custom:
|
||||
- https://github.com/drakmor/shadowMountPlus#support-development
|
||||
@@ -13,6 +13,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
outputs:
|
||||
|
||||
@@ -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`.
|
||||
@@ -53,7 +62,7 @@ Supported keys (all optional):
|
||||
- `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`)
|
||||
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `60`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
- `exfat_backend=lvd|md` (default: `lvd`)
|
||||
- `ufs_backend=lvd|md` (default: `lvd`)
|
||||
@@ -101,11 +110,11 @@ 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`.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `60`).
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `15`).
|
||||
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
|
||||
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
|
||||
|
||||
@@ -115,7 +124,7 @@ Backport overlay behavior:
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib2` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`; otherwise it falls back to `app0/fakelib`.
|
||||
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
|
||||
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
|
||||
- `global_fakelib_priority=global` reverses that priority.
|
||||
@@ -152,12 +161,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 (**pfs_image.dat**) 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 +271,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 +419,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 +427,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 +452,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
|
||||
+4
-4
@@ -51,8 +51,8 @@
|
||||
# recursive_scan=0
|
||||
|
||||
# Full scan loop interval (seconds), range: 1..3600
|
||||
# Default: 60
|
||||
# scan_interval_seconds=60
|
||||
# Default: 15
|
||||
# scan_interval_seconds=15
|
||||
|
||||
# Source stabilization wait window (seconds), range: 0..3600
|
||||
# If source mtime/ctime is newer than this window, it will be deferred to next scan.
|
||||
@@ -67,7 +67,7 @@
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
@@ -120,7 +120,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
|
||||
|
||||
|
||||
+6
-2
@@ -16,9 +16,13 @@ int normalize_snd0info_for_title(const char *title_id);
|
||||
// Check whether a title ID exists in a cached app.db title list.
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id);
|
||||
// Release a title-list snapshot returned by get_app_db_title_list_cached().
|
||||
void free_app_db_title_list(struct AppDbTitleList *list);
|
||||
// Drop the cached app.db title list so it is reloaded on next access.
|
||||
void invalidate_app_db_title_cache(void);
|
||||
// Return the cached app.db title list, loading it if needed.
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out);
|
||||
// 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
|
||||
@@ -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.
|
||||
|
||||
@@ -24,6 +24,8 @@ void cleanup_duplicate_title_mounts(void);
|
||||
int remount_system_ex(void);
|
||||
// Mount a title source into /system_ex/app/<title_id> via nullfs.
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path);
|
||||
// Roll back a just-mounted title nullfs layer when publication is aborted.
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
|
||||
// Reconcile the title mount stack against the expected source/backport state.
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
|
||||
+6
-1
@@ -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
|
||||
@@ -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.
|
||||
|
||||
+3
-2
@@ -1,14 +1,14 @@
|
||||
#ifndef SM_LIMITS_H
|
||||
#define SM_LIMITS_H
|
||||
|
||||
#define DEFAULT_SCAN_INTERVAL_US 60000000u
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
|
||||
#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
|
||||
@@ -45,5 +45,6 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 *);
|
||||
|
||||
@@ -18,6 +18,9 @@ bool runtime_sleep_mode_active(void);
|
||||
// Enter or leave sleep mode with a descriptive source string.
|
||||
// Returns true when the runtime sleep state changed.
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason);
|
||||
// Serialize mount/link/cache publication against suspend cleanup.
|
||||
void runtime_mount_state_lock(void);
|
||||
void runtime_mount_state_unlock(void);
|
||||
// Request an immediate scan cycle with a descriptive source string.
|
||||
void request_scan_now(const char *reason);
|
||||
// Consume a pending immediate scan request and copy its reason into caller storage.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -136,6 +138,7 @@ typedef enum {
|
||||
IMAGE_FS_UFS,
|
||||
IMAGE_FS_EXFAT,
|
||||
IMAGE_FS_PFS,
|
||||
IMAGE_FS_PFSC_CONTAINER,
|
||||
} image_fs_type_t;
|
||||
|
||||
typedef struct sm_error {
|
||||
|
||||
+26
-2
@@ -17,6 +17,7 @@
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_limits.h"
|
||||
@@ -32,6 +33,7 @@
|
||||
#define BACKPORK_PROCESS_NAME_ALT "ps5-backpork.elf"
|
||||
#define RESTART_WAIT_POLL_US 200000u
|
||||
#define RESTART_WAIT_MAX_US 60000000u
|
||||
#define STOP_FILE_POLL_INTERVAL_US 3000000ull
|
||||
#define KINFO_PID_OFFSET 72
|
||||
#define KINFO_TDNAME_OFFSET 447
|
||||
|
||||
@@ -43,6 +45,8 @@ static volatile sig_atomic_t g_stop_requested = 0;
|
||||
static atomic_bool g_shutdown_on_going_stop_requested = false;
|
||||
static atomic_bool g_runtime_sleep_mode_active = false;
|
||||
static _Atomic(uintptr_t) g_shutdown_stop_reason_bits = 0;
|
||||
static atomic_uint_fast64_t g_next_stop_file_poll_us = 0;
|
||||
static pthread_mutex_t g_runtime_mount_state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t reason_mutex;
|
||||
@@ -78,8 +82,20 @@ void install_signal_handlers(void) {
|
||||
bool should_stop_requested(void) {
|
||||
if (g_stop_requested)
|
||||
return true;
|
||||
if (path_exists(KILL_FILE)) {
|
||||
remove(KILL_FILE);
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0) {
|
||||
uint64_t next_poll_us =
|
||||
atomic_load_explicit(&g_next_stop_file_poll_us, memory_order_acquire);
|
||||
if (next_poll_us != 0 && now_us < next_poll_us)
|
||||
return false;
|
||||
atomic_store_explicit(&g_next_stop_file_poll_us,
|
||||
now_us + STOP_FILE_POLL_INTERVAL_US,
|
||||
memory_order_release);
|
||||
}
|
||||
|
||||
if (remove(KILL_FILE) == 0) {
|
||||
g_stop_requested = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -134,6 +150,14 @@ bool request_runtime_sleep_mode(bool active, const char *reason) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void runtime_mount_state_lock(void) {
|
||||
pthread_mutex_lock(&g_runtime_mount_state_mutex);
|
||||
}
|
||||
|
||||
void runtime_mount_state_unlock(void) {
|
||||
pthread_mutex_unlock(&g_runtime_mount_state_mutex);
|
||||
}
|
||||
|
||||
void request_scan_now(const char *reason) {
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown scan source";
|
||||
|
||||
+183
-57
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
@@ -13,8 +14,12 @@ 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
|
||||
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
|
||||
@@ -34,7 +39,7 @@ static void close_app_db(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free(list->ids);
|
||||
list->ids = NULL;
|
||||
list->count = 0;
|
||||
@@ -42,10 +47,15 @@ static 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);
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
@@ -164,8 +174,11 @@ static int app_db_query_int(const char *sql, const char *label) {
|
||||
}
|
||||
|
||||
bool app_db_run_startup_maintenance(void) {
|
||||
bool ok = false;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!ensure_app_db_open())
|
||||
return false;
|
||||
goto out;
|
||||
(void)sqlite3_busy_timeout(g_app_db,
|
||||
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
|
||||
|
||||
@@ -177,7 +190,7 @@ bool app_db_run_startup_maintenance(void) {
|
||||
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
|
||||
if (pending_backfill < 0) {
|
||||
close_app_db();
|
||||
return false;
|
||||
goto out;
|
||||
}
|
||||
|
||||
int changes = 0;
|
||||
@@ -192,17 +205,24 @@ bool app_db_run_startup_maintenance(void) {
|
||||
if (!app_db_exec_with_retry(backfill_sql,
|
||||
APP_DB_STARTUP_MAINTENANCE_RETRIES,
|
||||
"snd0info startup normalize", &changes))
|
||||
return false;
|
||||
goto out;
|
||||
}
|
||||
|
||||
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
|
||||
changes, pending_backfill);
|
||||
|
||||
close_app_db();
|
||||
return true;
|
||||
ok = true;
|
||||
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!g_app_db_stmt_update_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
@@ -212,7 +232,7 @@ int update_snd0info(const char *title_id) {
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
@@ -223,14 +243,15 @@ int update_snd0info(const char *title_id) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
return changes;
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
@@ -239,14 +260,19 @@ int update_snd0info(const char *title_id) {
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
int normalize_snd0info_for_title(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!g_app_db_stmt_normalize_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
@@ -259,7 +285,7 @@ int normalize_snd0info_for_title(const char *title_id) {
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
@@ -270,14 +296,15 @@ int normalize_snd0info_for_title(const char *title_id) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
return changes;
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
@@ -286,11 +313,13 @@ int normalize_snd0info_for_title(const char *title_id) {
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
@@ -319,10 +348,55 @@ static int compare_title_id_str(const void *a, const void *b) {
|
||||
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (list->count <= 0)
|
||||
return false;
|
||||
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
|
||||
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);
|
||||
|
||||
@@ -338,56 +412,70 @@ 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);
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
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;
|
||||
*list_out = NULL;
|
||||
|
||||
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);
|
||||
@@ -402,11 +490,49 @@ bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready)
|
||||
if (!g_app_db_title_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*list_out = &g_app_db_title_cache;
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
+49
-81
@@ -66,10 +66,6 @@ static char *trim_ascii(char *s);
|
||||
static bool parse_ini_line(char *line, char **key_out, char **value_out);
|
||||
static bool normalize_title_id_value(const char *value,
|
||||
char out[MAX_TITLE_ID]);
|
||||
static bool normalize_absolute_path_value(const char *value,
|
||||
char out[MAX_PATH]);
|
||||
static bool parse_global_fakelib_priority_ini(const char *value,
|
||||
bool *mount_first_out);
|
||||
static config_load_status_t load_runtime_config_state(runtime_config_state_t *state);
|
||||
static bool parse_u32_ini(const char *value, uint32_t *out);
|
||||
static bool is_valid_sector_size(uint32_t size);
|
||||
@@ -228,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) {
|
||||
@@ -306,33 +307,10 @@ static bool config_file_stamp_equals(const config_file_stamp_t *a,
|
||||
|
||||
static void apply_reloadable_runtime_fields(runtime_config_state_t *dst,
|
||||
const runtime_config_state_t *src) {
|
||||
dst->cfg.debug_enabled = src->cfg.debug_enabled;
|
||||
dst->cfg.quiet_mode = src->cfg.quiet_mode;
|
||||
dst->cfg.mount_read_only = src->cfg.mount_read_only;
|
||||
dst->cfg.force_mount = src->cfg.force_mount;
|
||||
dst->cfg.app_install_all_enabled = src->cfg.app_install_all_enabled;
|
||||
dst->cfg.app_install_all_forced = src->cfg.app_install_all_forced;
|
||||
dst->cfg.backport_fakelib_enabled = src->cfg.backport_fakelib_enabled;
|
||||
dst->cfg.global_fakelib_enabled = src->cfg.global_fakelib_enabled;
|
||||
dst->cfg.global_fakelib_mount_first =
|
||||
src->cfg.global_fakelib_mount_first;
|
||||
(void)strlcpy(dst->cfg.global_fakelib_path,
|
||||
src->cfg.global_fakelib_path,
|
||||
sizeof(dst->cfg.global_fakelib_path));
|
||||
dst->cfg.global_fakelib_exclude_title_count =
|
||||
src->cfg.global_fakelib_exclude_title_count;
|
||||
memcpy(dst->cfg.global_fakelib_exclude_title_ids,
|
||||
src->cfg.global_fakelib_exclude_title_ids,
|
||||
sizeof(dst->cfg.global_fakelib_exclude_title_ids));
|
||||
dst->cfg.kstuff_game_auto_toggle = src->cfg.kstuff_game_auto_toggle;
|
||||
dst->cfg.kstuff_crash_detection_enabled =
|
||||
src->cfg.kstuff_crash_detection_enabled;
|
||||
dst->cfg.scan_interval_us = src->cfg.scan_interval_us;
|
||||
dst->cfg.stability_wait_seconds = src->cfg.stability_wait_seconds;
|
||||
dst->cfg.kstuff_pause_delay_image_seconds =
|
||||
src->cfg.kstuff_pause_delay_image_seconds;
|
||||
dst->cfg.kstuff_pause_delay_direct_seconds =
|
||||
src->cfg.kstuff_pause_delay_direct_seconds;
|
||||
dst->cfg = src->cfg;
|
||||
dst->scan_path_count = src->scan_path_count;
|
||||
memcpy(dst->scan_path_storage, src->scan_path_storage,
|
||||
sizeof(dst->scan_path_storage));
|
||||
memcpy(dst->image_mode_rules, src->image_mode_rules,
|
||||
sizeof(dst->image_mode_rules));
|
||||
memcpy(dst->kstuff_no_pause_title_ids, src->kstuff_no_pause_title_ids,
|
||||
@@ -380,6 +358,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)
|
||||
@@ -547,7 +534,8 @@ static bool normalize_title_id_value(const char *value,
|
||||
return false;
|
||||
|
||||
char local[MAX_TITLE_ID];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_TITLE_ID)
|
||||
@@ -570,7 +558,8 @@ static bool normalize_image_filename_value(const char *value,
|
||||
return false;
|
||||
|
||||
char local[MAX_PATH];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
char *trimmed = trim_ascii(local);
|
||||
const char *filename = get_filename_component(trimmed);
|
||||
size_t len = strlen(filename);
|
||||
@@ -581,27 +570,6 @@ static bool normalize_image_filename_value(const char *value,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool normalize_absolute_path_value(const char *value,
|
||||
char out[MAX_PATH]) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
|
||||
char local[MAX_PATH];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_PATH || trimmed[0] != '/')
|
||||
return false;
|
||||
|
||||
while (len > 1 && trimmed[len - 1] == '/') {
|
||||
trimmed[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
(void)strlcpy(out, trimmed, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_bool_ini(const char *value, bool *out) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
@@ -620,24 +588,6 @@ static bool parse_bool_ini(const char *value, bool *out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parse_global_fakelib_priority_ini(const char *value,
|
||||
bool *mount_first_out) {
|
||||
if (!value || !mount_first_out)
|
||||
return false;
|
||||
|
||||
if (strcasecmp(value, "game") == 0) {
|
||||
*mount_first_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcasecmp(value, "global") == 0) {
|
||||
*mount_first_out = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parse_kstuff_delay_rule_value(const char *value,
|
||||
char title_id_out[MAX_TITLE_ID],
|
||||
uint32_t *delay_seconds_out) {
|
||||
@@ -645,7 +595,8 @@ static bool parse_kstuff_delay_rule_value(const char *value,
|
||||
return false;
|
||||
|
||||
char local[128];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
|
||||
char *sep = strchr(local, ':');
|
||||
if (!sep)
|
||||
@@ -671,7 +622,8 @@ static bool parse_image_sector_rule_value(const char *value,
|
||||
return false;
|
||||
|
||||
char local[MAX_PATH];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local))
|
||||
return false;
|
||||
|
||||
char *sep = strrchr(local, ':');
|
||||
if (!sep)
|
||||
@@ -1309,18 +1261,35 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_path") == 0) {
|
||||
if (!normalize_absolute_path_value(value,
|
||||
state->cfg.global_fakelib_path)) {
|
||||
char local[MAX_PATH];
|
||||
if (strlcpy(local, value, sizeof(local)) >= sizeof(local)) {
|
||||
log_debug(" [CFG] invalid global fakelib path at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_PATH || trimmed[0] != '/') {
|
||||
log_debug(" [CFG] invalid global fakelib path at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
while (len > 1 && trimmed[len - 1] == '/') {
|
||||
trimmed[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, trimmed,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_priority") == 0) {
|
||||
if (!parse_global_fakelib_priority_ini(
|
||||
value, &state->cfg.global_fakelib_mount_first)) {
|
||||
// Lower-priority overlay must be mounted first.
|
||||
if (strcasecmp(value, "game") == 0) {
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
} else if (strcasecmp(value, "global") == 0) {
|
||||
state->cfg.global_fakelib_mount_first = false;
|
||||
} else {
|
||||
log_debug(" [CFG] invalid global fakelib priority at line %d: %s=%s "
|
||||
"(use game or global)",
|
||||
line_no, key, value);
|
||||
@@ -1491,9 +1460,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;
|
||||
|
||||
+41
-11
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
typedef struct {
|
||||
char source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
@@ -20,6 +21,7 @@ typedef struct {
|
||||
} fakelib_session_t;
|
||||
|
||||
static fakelib_session_t g_fakelib_mount;
|
||||
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
bool sm_fakelib_game_feature_enabled(void) {
|
||||
return runtime_config()->backport_fakelib_enabled;
|
||||
@@ -102,8 +104,14 @@ static bool resolve_sandbox_context(const char *title_id,
|
||||
const char *suffix = entry->d_name + title_len + 1u;
|
||||
if (!isdigit((unsigned char)suffix[0]))
|
||||
continue;
|
||||
if (strlen(entry->d_name) >= sizeof(sandbox_id))
|
||||
continue;
|
||||
|
||||
int idx = atoi(suffix);
|
||||
errno = 0;
|
||||
long idx_long = strtol(suffix, NULL, 10);
|
||||
if (errno == ERANGE || idx_long > INT_MAX)
|
||||
continue;
|
||||
int idx = (int)idx_long;
|
||||
if (idx < best_index)
|
||||
continue;
|
||||
|
||||
@@ -117,11 +125,17 @@ static bool resolve_sandbox_context(const char *title_id,
|
||||
return false;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib2",
|
||||
sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
} else {
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
}
|
||||
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
@@ -159,9 +173,6 @@ static bool resolve_global_fakelib_source(const char *title_id,
|
||||
if (is_global_fakelib_excluded_for_title(title_id))
|
||||
return false;
|
||||
|
||||
if (!read_mount_link(title_id, source_path, MAX_PATH))
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cfg->global_fakelib_path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
@@ -199,12 +210,16 @@ static bool cleanup_fakelib_mount(void) {
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!sm_fakelib_game_feature_enabled())
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,6 +229,7 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!cleanup_fakelib_mount()) {
|
||||
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
|
||||
(long)g_fakelib_mount.pid, title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -221,14 +237,18 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
char game_source_path[MAX_PATH];
|
||||
char global_source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
if (!resolve_sandbox_context(title_id, game_source_path, mount_path))
|
||||
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_global =
|
||||
resolve_global_fakelib_source(title_id, global_source_path);
|
||||
bool has_game = (game_source_path[0] != '\0');
|
||||
if (!has_global && !has_game)
|
||||
if (!has_global && !has_game) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
g_fakelib_mount.pid = pid;
|
||||
@@ -243,6 +263,7 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -251,6 +272,7 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
|
||||
"game")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -259,23 +281,31 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_game)
|
||||
notify_system_info("Game backported: %s", title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exit(pid_t pid) {
|
||||
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid)
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
|
||||
g_fakelib_mount.mount_path);
|
||||
cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_shutdown(void) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
+119
-13
@@ -46,9 +46,20 @@ static bool read_mount_link_file(const char *lnk_path, char *out,
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
size_t len = strcspn(out, "\r\n");
|
||||
bool truncated = false;
|
||||
if (out[len] == '\0' && len + 1u >= out_size) {
|
||||
int next = fgetc(f);
|
||||
truncated = (next != EOF && next != '\n' && next != '\r');
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (truncated) {
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
out[len] = '\0';
|
||||
return out[0] != '\0';
|
||||
}
|
||||
@@ -164,7 +175,10 @@ static bool source_path_needs_cleanup(const char *source_path,
|
||||
return true;
|
||||
|
||||
char eboot_path[MAX_PATH];
|
||||
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
|
||||
int written =
|
||||
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(eboot_path))
|
||||
return true;
|
||||
if (path_exists(eboot_path))
|
||||
return false;
|
||||
|
||||
@@ -469,6 +483,24 @@ static bool unmount_top_controlled_layer(const char *path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool title_stack_top_is_managed(const title_mount_state_t *state) {
|
||||
return state->top_is_our_nullfs || state->top_kind == TITLE_STACK_TOP_BACKPORT;
|
||||
}
|
||||
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state))
|
||||
return false;
|
||||
if (!state.top_is_our_nullfs)
|
||||
return false;
|
||||
|
||||
if (!unmount_top_controlled_layer(state.system_ex_path))
|
||||
return false;
|
||||
log_debug(" [LINK] rolled back unpublished nullfs mount: %s -> %s",
|
||||
src_path, state.system_ex_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
@@ -562,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) {
|
||||
@@ -578,6 +662,12 @@ static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
|
||||
!state.has_our_nullfs) {
|
||||
return true;
|
||||
}
|
||||
if (!title_stack_top_is_managed(&state)) {
|
||||
log_debug(" [LINK] duplicate managed mount layers kept for %s: top layer "
|
||||
"is not managed by ShadowMount",
|
||||
state.system_ex_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(
|
||||
" [LINK] duplicate managed mount layers for %s: nullfs=%d backport=%d. "
|
||||
@@ -610,6 +700,10 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
if ((unsigned long)file_size > MAX_PARAM_JSON_SIZE) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
rewind(fs);
|
||||
|
||||
size_t len = (size_t)file_size;
|
||||
@@ -708,8 +802,13 @@ int copy_dir(const char *src, const char *dst) {
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
|
||||
continue;
|
||||
snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
|
||||
snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
|
||||
int src_written = snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
|
||||
int dst_written = snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
|
||||
if (src_written < 0 || (size_t)src_written >= sizeof(ss) ||
|
||||
dst_written < 0 || (size_t)dst_written >= sizeof(dd)) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (lstat(ss, &lst) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
@@ -761,7 +860,12 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
char src_eboot[MAX_PATH];
|
||||
char dst_eboot[MAX_PATH];
|
||||
snprintf(dst, sizeof(dst), "/system_ex/app/%s", title_id);
|
||||
snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
|
||||
int written = snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(src_eboot)) {
|
||||
log_debug(" [LINK] source eboot path too long for %s: %s", title_id,
|
||||
src_path);
|
||||
return false;
|
||||
}
|
||||
snprintf(dst_eboot, sizeof(dst_eboot), "%s/eboot.bin", dst);
|
||||
|
||||
struct stat src_st;
|
||||
@@ -793,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,
|
||||
@@ -811,8 +917,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
return false;
|
||||
}
|
||||
if (state.mounted) {
|
||||
if (state.has_our_nullfs &&
|
||||
(state.top_is_our_nullfs || state.has_our_backport) &&
|
||||
if (state.has_our_nullfs && title_stack_top_is_managed(&state) &&
|
||||
path_exists(dst_eboot)) {
|
||||
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
|
||||
return true;
|
||||
@@ -925,13 +1030,14 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
bool mount_link_staged = false;
|
||||
if (ctx->unmount_system_ex_bind) {
|
||||
title_mount_state_t state;
|
||||
memset(&state, 0, sizeof(state));
|
||||
bool inspected = inspect_title_stack(title_id, source_path,
|
||||
ctx->removed_source_root, &state);
|
||||
bool top_is_ours = state.top_is_our_nullfs || state.has_our_backport;
|
||||
bool top_is_ours = inspected && title_stack_top_is_managed(&state);
|
||||
bool stack_matches_link = false;
|
||||
if (source_path[0] != '\0') {
|
||||
if (inspected && source_path[0] != '\0') {
|
||||
stack_matches_link = state.has_our_nullfs;
|
||||
} else if (matches_removed_source && ctx->removed_source_root &&
|
||||
} else if (inspected && matches_removed_source && ctx->removed_source_root &&
|
||||
ctx->removed_source_root[0] != '\0') {
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
@@ -1040,7 +1146,7 @@ static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state) ||
|
||||
!state.has_our_nullfs ||
|
||||
(!state.top_is_our_nullfs && !state.has_our_backport)) {
|
||||
!title_stack_top_is_managed(&state)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ static bool consume_active_game_title(pid_t pid,
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
return strncmp(title_id, "PPSA", 4) == 0 ||
|
||||
strncmp(title_id, "CUSA", 4) == 0 ||
|
||||
strncmp(title_id, "FAKE", 4) == 0;
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
|
||||
+7
-2
@@ -41,13 +41,18 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
if (load_cached_game_info(base_path, param_st, out_id, out_name, &cached_valid))
|
||||
return cached_valid;
|
||||
|
||||
if (param_st->st_size <= 0 || param_st->st_size > 1024 * 1024) {
|
||||
if (param_st->st_size <= 0 || param_st->st_size > MAX_PARAM_JSON_SIZE) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "%s/sce_sys/param.json", base_path);
|
||||
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
|
||||
base_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(path)) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
|
||||
+303
-37
@@ -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;
|
||||
@@ -528,6 +649,30 @@ static bool reuse_existing_image_mount(const char *file_path,
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool handle_cached_or_existing_image_mount_locked(
|
||||
const char *file_path, const char *mount_point, bool *success_out) {
|
||||
if (success_out)
|
||||
*success_out = false;
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (is_cached_image_path(file_path)) {
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cache_failed = false;
|
||||
if (reuse_existing_image_mount(file_path, mount_point, &cache_failed)) {
|
||||
if (success_out)
|
||||
*success_out = !runtime_sleep_mode_active();
|
||||
return true;
|
||||
}
|
||||
|
||||
return cache_failed;
|
||||
}
|
||||
|
||||
static bool stat_image_file(const char *file_path, struct stat *st_out) {
|
||||
if (stat(file_path, st_out) != 0) {
|
||||
log_debug(" [IMG] stat failed for %s: %s", file_path, strerror(errno));
|
||||
@@ -544,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);
|
||||
}
|
||||
|
||||
@@ -588,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"),
|
||||
@@ -632,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));
|
||||
@@ -742,22 +939,42 @@ 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 (is_cached_image_path(file_path))
|
||||
return 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;
|
||||
runtime_mount_state_lock();
|
||||
handled =
|
||||
handle_cached_or_existing_image_mount_locked(file_path, mount_point,
|
||||
&success);
|
||||
runtime_mount_state_unlock();
|
||||
if (handled)
|
||||
return success;
|
||||
|
||||
struct stat st;
|
||||
bool cache_failed = false;
|
||||
if (reuse_existing_image_mount(file_path, mount_point, &cache_failed))
|
||||
return true;
|
||||
if (cache_failed)
|
||||
return false;
|
||||
if (!stat_image_file(file_path, &st))
|
||||
return false;
|
||||
if (runtime_sleep_mode_active())
|
||||
return false;
|
||||
runtime_mount_state_lock();
|
||||
if (runtime_sleep_mode_active()) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
handled =
|
||||
handle_cached_or_existing_image_mount_locked(file_path, mount_point,
|
||||
&success);
|
||||
if (handled) {
|
||||
runtime_mount_state_unlock();
|
||||
return success;
|
||||
}
|
||||
|
||||
log_debug(" [IMG] Mounting image (%s): %s -> %s", image_fs_name(fs_type),
|
||||
file_path, mount_point);
|
||||
@@ -777,23 +994,28 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
memset(devname, 0, sizeof(devname));
|
||||
if (!attach_image_device(file_path, fs_type, mount_read_only, st.st_size,
|
||||
attach_backend, &unit_id, devname, sizeof(devname))) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)detach_attached_unit(attach_backend, unit_id);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
if (!perform_image_nmount(file_path, fs_type, attach_backend, unit_id, devname,
|
||||
mount_point, mount_read_only, force_mount)) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validate_mounted_image(file_path, fs_type, attach_backend, unit_id,
|
||||
devname, mount_point)) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -810,14 +1032,43 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
file_path, mount_point);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = ENOSPC;
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
runtime_mount_state_unlock();
|
||||
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;
|
||||
|
||||
@@ -833,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);
|
||||
@@ -921,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))
|
||||
@@ -970,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))
|
||||
@@ -1017,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;
|
||||
}
|
||||
|
||||
@@ -1031,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) {
|
||||
@@ -1060,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) {
|
||||
|
||||
+33
-6
@@ -1,4 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
|
||||
@@ -11,6 +13,7 @@ struct ImageCache {
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
@@ -51,23 +54,32 @@ static int upsert_image_source_mapping(const char *path, const char *mount_point
|
||||
}
|
||||
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
return upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS || !g_image_cache[index].valid) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,38 +90,53 @@ bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS)
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return;
|
||||
}
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0)
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
*backend_out = entry->backend;
|
||||
*unit_out = entry->unit_id;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
|
||||
path_out[0] = '\0';
|
||||
(void)strlcpy(path_out, entry->path, path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
+92
-16
@@ -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;
|
||||
|
||||
@@ -226,11 +279,21 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
|
||||
// WRITE TRACKER
|
||||
char lnk_path[MAX_PATH];
|
||||
runtime_mount_state_lock();
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path))
|
||||
if (!write_link_file(lnk_path, src_path)) {
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
|
||||
|
||||
@@ -238,8 +301,12 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
|
||||
user_app_dir);
|
||||
if (has_image_source) {
|
||||
if (!write_link_file(img_lnk_path, image_source_path))
|
||||
if (!write_link_file(img_lnk_path, image_source_path)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
@@ -250,6 +317,10 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
strerror(errno));
|
||||
}
|
||||
bool sleep_started = runtime_sleep_mode_active();
|
||||
runtime_mount_state_unlock();
|
||||
if (sleep_started || runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (!should_register) {
|
||||
if (metadata_restaged && has_src_snd0) {
|
||||
@@ -262,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;
|
||||
@@ -291,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) {
|
||||
@@ -300,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);
|
||||
@@ -343,9 +411,17 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
}
|
||||
}
|
||||
|
||||
if (mount_and_install(c->path, c->title_id, c->title_name, c->installed,
|
||||
!c->in_app_db, use_app_install_all,
|
||||
&has_src_snd0)) {
|
||||
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
|
||||
c->installed, !c->in_app_db,
|
||||
use_app_install_all, &has_src_snd0);
|
||||
if (runtime_sleep_mode_active())
|
||||
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)",
|
||||
|
||||
+19
-2
@@ -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);
|
||||
}
|
||||
@@ -235,7 +251,7 @@ static void poll_pending_installs(void) {
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
|
||||
const struct AppDbTitleList *app_db_titles = NULL;
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
|
||||
@@ -248,7 +264,7 @@ static void poll_pending_installs(void) {
|
||||
continue;
|
||||
|
||||
if (app_db_titles_ready &&
|
||||
app_db_title_list_contains(app_db_titles, entry->title_id)) {
|
||||
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
|
||||
finalize_pending_install_success(entry);
|
||||
continue;
|
||||
}
|
||||
@@ -263,6 +279,7 @@ static void poll_pending_installs(void) {
|
||||
schedule_pending_install_poll(now_us);
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
|
||||
+8
-2
@@ -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;
|
||||
}
|
||||
@@ -842,13 +848,13 @@ void sm_kstuff_sleep_enter(void) {
|
||||
return;
|
||||
|
||||
was_enabled = sm_kstuff_is_enabled();
|
||||
g_kstuff.sleep_pause_restore = was_enabled;
|
||||
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to pause for system sleep");
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.sleep_pause_restore = was_enabled;
|
||||
if (was_enabled)
|
||||
log_debug(" [KSTUFF] paused for system sleep");
|
||||
}
|
||||
|
||||
+9
-5
@@ -8,6 +8,7 @@
|
||||
|
||||
static sm_error_t g_last_error;
|
||||
static bool g_notifications_initialized = false;
|
||||
static pthread_mutex_t g_notifications_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static FILE *g_log_file = NULL;
|
||||
|
||||
@@ -64,10 +65,10 @@ static bool ensure_notification_icon_present(void) {
|
||||
}
|
||||
|
||||
void sm_notifications_init(void) {
|
||||
if (g_notifications_initialized)
|
||||
return;
|
||||
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
if (!g_notifications_initialized)
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
}
|
||||
|
||||
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
|
||||
@@ -142,7 +143,10 @@ static bool send_rich_notification(const char *message) {
|
||||
return false;
|
||||
|
||||
sm_notifications_init();
|
||||
if (!g_notifications_initialized)
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
bool notifications_ready = g_notifications_initialized;
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
if (!notifications_ready)
|
||||
return false;
|
||||
|
||||
escaped_message[0] = '\0';
|
||||
|
||||
+420
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -281,11 +281,21 @@ static size_t find_log_overlap(const char *current, size_t current_len) {
|
||||
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
|
||||
? g_mdbg.log_snapshot_length
|
||||
: current_len;
|
||||
for (size_t overlap = max_overlap; overlap > 0; --overlap) {
|
||||
if (!memcmp(g_mdbg.log_snapshot + g_mdbg.log_snapshot_length - overlap,
|
||||
current, overlap)) {
|
||||
|
||||
const char *search = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length -
|
||||
max_overlap;
|
||||
const char *end = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length;
|
||||
while (search < end) {
|
||||
const char *candidate =
|
||||
memchr(search, current[0], (size_t)(end - search));
|
||||
if (!candidate)
|
||||
break;
|
||||
|
||||
size_t overlap = (size_t)(end - candidate);
|
||||
if (!memcmp(candidate, current, overlap))
|
||||
return overlap;
|
||||
}
|
||||
|
||||
search = candidate + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
+27
-1
@@ -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;
|
||||
}
|
||||
+7
-2
@@ -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;
|
||||
@@ -50,6 +54,7 @@ bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
|
||||
if (is_under_image_mount_base(scan_path))
|
||||
return false;
|
||||
|
||||
snprintf(out, MAX_PATH, "%s/%s", scan_path, DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return true;
|
||||
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
|
||||
DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return written >= 0 && (size_t)written < MAX_PATH;
|
||||
}
|
||||
+352
-33
@@ -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, ¶m_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, ¶m_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,
|
||||
@@ -535,22 +822,34 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
bool *unstable_found_out) {
|
||||
reset_scan_workspace();
|
||||
int candidate_count = 0;
|
||||
const struct AppDbTitleList *app_db_titles = NULL;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -559,26 +858,46 @@ int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
bool *unstable_found_out) {
|
||||
reset_scan_workspace();
|
||||
int candidate_count = 0;
|
||||
const struct AppDbTitleList *app_db_titles = NULL;
|
||||
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
@@ -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);
|
||||
|
||||
+198
-70
@@ -6,9 +6,11 @@
|
||||
#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"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_install_queue.h"
|
||||
@@ -29,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,
|
||||
@@ -62,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;
|
||||
@@ -76,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;
|
||||
@@ -95,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)
|
||||
@@ -132,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);
|
||||
@@ -146,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)
|
||||
@@ -202,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 !=
|
||||
@@ -218,6 +237,24 @@ static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
|
||||
sizeof(old_cfg->global_fakelib_exclude_title_ids)) != 0;
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (unsigned shift = 0; shift < 32u; shift += 8u) {
|
||||
hash ^= (values[i] >> shift) & 0xffu;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < scan_path_count; i++) {
|
||||
hash ^= sm_fnv1a32(get_scan_path(i));
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static size_t scanner_watch_fd_hash(uintptr_t ident) {
|
||||
return (size_t)((ident * 11400714819323198485ull) >>
|
||||
(sizeof(uintptr_t) >= sizeof(uint64_t) ? 0 : 1));
|
||||
@@ -600,9 +637,7 @@ 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;
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
@@ -655,9 +690,7 @@ 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;
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
|
||||
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
||||
return false;
|
||||
@@ -745,7 +778,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;
|
||||
@@ -804,38 +838,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;
|
||||
@@ -873,17 +875,74 @@ static void reopen_config_file_watch(int kq, uint64_t now_us) {
|
||||
register_config_file_watch(kq, now_us);
|
||||
}
|
||||
|
||||
static void apply_runtime_config_reload_effects(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg) {
|
||||
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,
|
||||
bool scan_topology_changed) {
|
||||
if (old_cfg->backport_fakelib_enabled &&
|
||||
fakelib_runtime_config_changed(old_cfg, new_cfg))
|
||||
sm_fakelib_game_shutdown();
|
||||
|
||||
sm_kstuff_on_config_reload();
|
||||
|
||||
if (scan_topology_changed) {
|
||||
clear_scanner_watch_entries();
|
||||
reset_scanner_root_states();
|
||||
if (!rebuild_all_scan_root_watch_trees(kq)) {
|
||||
log_debug(" [CFG] scanner watch rebuild failed after config reload");
|
||||
return false;
|
||||
} else {
|
||||
log_debug(" [CFG] scanner watches rebuilt after scan config reload");
|
||||
}
|
||||
}
|
||||
|
||||
if (!refresh_game_lifecycle_watcher()) {
|
||||
log_debug(" [CFG] lifecycle watcher refresh failed after config reload");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool should_abort_scan_cycle(void) {
|
||||
@@ -1034,6 +1093,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)
|
||||
@@ -1123,19 +1192,20 @@ 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);
|
||||
if (g_scanner_config_fd >= 0)
|
||||
schedule_config_reload(now_us);
|
||||
schedule_config_reload(now_us);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1209,11 +1279,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));
|
||||
@@ -1229,12 +1300,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,
|
||||
@@ -1243,6 +1308,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;
|
||||
}
|
||||
|
||||
@@ -1299,10 +1368,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;
|
||||
}
|
||||
@@ -1387,21 +1458,77 @@ 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;
|
||||
|
||||
uint32_t old_scan_topology_hash = scanner_config_topology_hash();
|
||||
runtime_config_t old_cfg = *runtime_config();
|
||||
bool reloaded = false;
|
||||
if (!reload_runtime_config_if_changed(&reloaded)) {
|
||||
log_debug(" [CFG] runtime config reload failed");
|
||||
} else if (reloaded) {
|
||||
const runtime_config_t *new_cfg = runtime_config();
|
||||
apply_runtime_config_reload_effects(&old_cfg, new_cfg);
|
||||
bool scan_topology_changed =
|
||||
old_scan_topology_hash != scanner_config_topology_hash();
|
||||
if (!apply_runtime_config_reload_effects(kq, &old_cfg, new_cfg,
|
||||
scan_topology_changed)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("scanner watcher rebuild after config reload failed");
|
||||
return;
|
||||
}
|
||||
if (scan_topology_changed && !discard_scanner_events_nowait(kq)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("scanner stale event drain after config reload failed");
|
||||
return;
|
||||
}
|
||||
notify_system("ShadowMount+: config reloaded.");
|
||||
log_debug(" [CFG] runtime config reloaded");
|
||||
now_us = monotonic_time_us();
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
next_full_resync_us =
|
||||
scan_topology_changed ? now_us
|
||||
: now_us + scanner_full_resync_interval_us();
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1536,8 +1663,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();
|
||||
}
|
||||
@@ -509,8 +509,11 @@ static void set_shellcore_flag_start_result(bool success) {
|
||||
}
|
||||
|
||||
static void enter_sleep_mode_and_cleanup(const char *reason) {
|
||||
if (request_runtime_sleep_mode(true, reason))
|
||||
if (request_runtime_sleep_mode(true, reason)) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user