mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 05:40:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24fba2be21 | ||
|
|
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 | ||
|
|
30b04358f4 | ||
|
|
a6ad4922a9 | ||
|
|
ebd649e0ea | ||
|
|
4072ced15d | ||
|
|
fee1029b0a | ||
|
|
cbd0fce4f9 | ||
|
|
9fa6ad94de | ||
|
|
d4c4cda739 | ||
|
|
954b3a7d17 | ||
|
|
3652f506c0 | ||
|
|
6c188a03bb |
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,13 +2,25 @@
|
||||
|
||||
**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 v1.6.6+ (Recommended 1.6.6)** or **Kstuff-lite v1.0+**.
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
|
||||
|
||||
|
||||
## 💜 Support Development
|
||||
|
||||
If you want to support this project, you can donate
|
||||
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
|
||||
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
|
||||
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
|
||||
|
||||
|
||||
|
||||
## Current image support
|
||||
@@ -20,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`.
|
||||
@@ -48,12 +61,18 @@ Supported keys (all optional):
|
||||
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
|
||||
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
|
||||
- `watch_subdirs=1|0` (`1` keeps vnode watchers on scan root subdirectories; default: `0`)
|
||||
- `watch_image_files=1|0` (`1` keeps vnode watchers on discovered image files; default: `1`)
|
||||
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `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`)
|
||||
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
|
||||
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
|
||||
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
|
||||
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
|
||||
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
|
||||
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
|
||||
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
|
||||
@@ -93,11 +112,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.
|
||||
|
||||
@@ -107,8 +126,12 @@ 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+ also mounts it into that game's sandbox `common/lib`.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher.
|
||||
- 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 `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.
|
||||
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib.
|
||||
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
|
||||
|
||||
Kstuff game lifecycle behavior:
|
||||
@@ -140,12 +163,102 @@ Image mountpoints are created under:
|
||||
|
||||
`/mnt/shadowmnt/<image_name>_<hash>`
|
||||
|
||||
PFSC container mountpoints are created under:
|
||||
|
||||
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
|
||||
|
||||
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Game files must be placed at the image root.
|
||||
- Do not add an extra top-level folder inside the image.
|
||||
- Valid example: `/sce_sys/param.json` exists directly from image root.
|
||||
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
|
||||
|
||||
PFSC container layout requirement (`.ffpfsc`):
|
||||
- Do not place game files directly in the container root.
|
||||
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
|
||||
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
|
||||
|
||||
## Compressed PFS containers (`.ffpfsc`)
|
||||
|
||||
Compressed PFS mode is intended only for nested images. During packing, data is
|
||||
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
|
||||
as an outer container for another image rather than as a direct game-file
|
||||
layout.
|
||||
|
||||
Recommended layouts:
|
||||
- exFAT image inside compressed PFS.
|
||||
- Uncompressed PFS image inside compressed PFS.
|
||||
|
||||
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
|
||||
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
|
||||
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
|
||||
speed. Keep this in mind when choosing which games to pack. Games that read large
|
||||
amounts of data or stream textures continuously may stutter.
|
||||
|
||||
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
|
||||
PFS images.
|
||||
|
||||
### Packing an uncompressed PFS image into compressed PFS
|
||||
|
||||
First create an uncompressed nested PFS image:
|
||||
|
||||
```bash
|
||||
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
|
||||
'./PPSA07923/PPSA07923-app' \
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./pfs_image.dat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary nested image can be removed:
|
||||
|
||||
```bash
|
||||
rm './pfs_image.dat'
|
||||
```
|
||||
|
||||
### Packing an exFAT image into compressed PFS
|
||||
|
||||
First create a normal exFAT image using one of the methods from
|
||||
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
|
||||
extension, for example `PPSA12345.exfat`.
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
chmod +x mkexfat.sh
|
||||
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```cmd
|
||||
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
|
||||
```
|
||||
|
||||
The exFAT image must contain the game files at the image root, without an extra
|
||||
top-level folder.
|
||||
|
||||
Then pack the exFAT image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./PPSA12345.exfat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary exFAT image can be removed:
|
||||
|
||||
```bash
|
||||
rm './PPSA12345.exfat'
|
||||
```
|
||||
|
||||
## Scan paths
|
||||
|
||||
Default scan locations:
|
||||
@@ -160,10 +273,49 @@ Default scan locations:
|
||||
- `/mnt/usb0` .. `/mnt/usb7`
|
||||
- `/mnt/ext0`
|
||||
- `/mnt/ext1`
|
||||
- `/mnt/shadowmnt/pfsc` (mounted PFSC container scan)
|
||||
- `/mnt/shadowmnt` (mounted image content scan)
|
||||
|
||||
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
|
||||
|
||||
## Manual install list
|
||||
|
||||
For games that should not live under the normal scan paths, ShadowMountPlus also
|
||||
checks:
|
||||
|
||||
`/data/shadowmount/manual.lst`
|
||||
|
||||
Add one source per line:
|
||||
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
|
||||
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
|
||||
- Empty lines and lines starting with `#` are ignored.
|
||||
|
||||
Example:
|
||||
```text
|
||||
/mnt/usb0/MyGames/PPSA12345
|
||||
/mnt/usb0/images/PPSA54321.ffpkg
|
||||
# /mnt/usb0/disabled/PPSA00000
|
||||
```
|
||||
|
||||
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
|
||||
rescans shortly after the write, mounts images when needed, and installs the
|
||||
game through the same pipeline as normal scan path discoveries.
|
||||
|
||||
Manual install state is tracked in:
|
||||
|
||||
`/data/shadowmount/manual.status`
|
||||
|
||||
This status file is managed by ShadowMountPlus. If a manually installed game is
|
||||
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
|
||||
as deleted in `manual.status` and removes the matching source line from
|
||||
`manual.lst` so it is not installed again automatically. If you later add the
|
||||
same source path back to `manual.lst`, it will be processed again and the status
|
||||
will be updated after installation.
|
||||
|
||||
You can use **Dump Installer** to install or prepare game dumps for this manual
|
||||
workflow, then add the resulting game folder path or image file path to
|
||||
`manual.lst`.
|
||||
|
||||
Recommended folder structure:
|
||||
- Default mode (`scan_depth=1`):
|
||||
- `/data/homebrew/<TITLE_ID>/`
|
||||
@@ -269,7 +421,7 @@ If a game is not mounted:
|
||||
- Check `/data/shadowmount/debug.log` and system notifications from ShadowMount+.
|
||||
- Verify scan roots:
|
||||
- if `scanpath=...` is set, only these paths are scanned;
|
||||
- `/mnt/shadowmnt` is always scanned.
|
||||
- `/mnt/shadowmnt/pfsc` and `/mnt/shadowmnt` are always scanned.
|
||||
- Verify scan depth:
|
||||
- `scan_depth=1` scans only first-level subfolders;
|
||||
- `scan_depth=2` scans one additional nested level;
|
||||
@@ -277,9 +429,10 @@ If a game is not mounted:
|
||||
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
|
||||
- Verify game structure:
|
||||
- folder game: `<GAME_DIR>/sce_sys/param.json`;
|
||||
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
|
||||
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
|
||||
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
|
||||
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
|
||||
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
|
||||
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
|
||||
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
|
||||
|
||||
If a game is mounted but does not start:
|
||||
@@ -301,5 +454,6 @@ If a game is mounted but does not start:
|
||||
* Gezine
|
||||
* earthonion
|
||||
* LightningMods
|
||||
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
|
||||
* john-tornblom for SDK
|
||||
* PS5 R&D Community
|
||||
+33
-3
@@ -45,14 +45,26 @@
|
||||
# Default: 1
|
||||
# scan_depth=1
|
||||
|
||||
# Directory watcher granularity:
|
||||
# 1 -> keep vnode watchers on scan root subdirectories
|
||||
# 0 -> watch only scan roots/parents and rely on targeted/full scans for children
|
||||
# Default: 0
|
||||
# watch_subdirs=0
|
||||
|
||||
# Image file watcher granularity:
|
||||
# 1 -> keep vnode watchers on discovered image files
|
||||
# 0 -> detect image file changes through targeted/full scans
|
||||
# Default: 1
|
||||
# watch_image_files=1
|
||||
|
||||
# Legacy compatibility:
|
||||
# recursive_scan=1 forces scan_depth=2
|
||||
# Default: disabled
|
||||
# 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.
|
||||
@@ -72,6 +84,24 @@
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
# Global fakelib overlay:
|
||||
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
|
||||
# 0/false/no/off -> do not use the global fakelib folder
|
||||
# Default: 1
|
||||
# global_fakelib=1
|
||||
#
|
||||
# Default global fakelib folder:
|
||||
# global_fakelib_path=/data/shadowmount/fakelib
|
||||
#
|
||||
# Priority when both global and per-game fakelib exist:
|
||||
# game -> mount global first, then game-specific fakelib (game wins)
|
||||
# global -> mount game-specific fakelib first, then global fakelib (global wins)
|
||||
# Default: game
|
||||
# global_fakelib_priority=game
|
||||
#
|
||||
## Exclude specific title IDs from the global fakelib overlay (repeatable):
|
||||
## global_fakelib_exclude=PPSA12345
|
||||
|
||||
# Auto-manage kstuff around game launches:
|
||||
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
|
||||
# 0/false/no/off -> leave kstuff state untouched
|
||||
@@ -102,7 +132,7 @@
|
||||
|
||||
# Optional custom scan roots (can be repeated):
|
||||
# If at least one scanpath=... is present, only these paths are used.
|
||||
# "/mnt/shadowmnt" is always added automatically.
|
||||
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
|
||||
# Default: use built-in scan path list
|
||||
# scanpath=/data/homebrew
|
||||
|
||||
|
||||
+10
-2
@@ -7,14 +7,22 @@ struct AppDbTitleList;
|
||||
|
||||
// Close the cached app.db handle and release related resources.
|
||||
void shutdown_app_db(void);
|
||||
// Normalize existing snd0info rows during process startup.
|
||||
bool app_db_run_startup_maintenance(void);
|
||||
// Update snd0 metadata for a registered title.
|
||||
int update_snd0info(const char *title_id);
|
||||
// Normalize an existing snd0Info path for one title.
|
||||
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.
|
||||
@@ -39,5 +41,7 @@ bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
bool upsert_image_sector_size_autotune(const char *filename,
|
||||
uint32_t sector_size,
|
||||
uint32_t *sector_size_out);
|
||||
// Return true when the global fakelib overlay is disabled for this title.
|
||||
bool is_global_fakelib_excluded_for_title(const char *title_id);
|
||||
|
||||
#endif
|
||||
@@ -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,
|
||||
@@ -35,11 +37,13 @@ bool mount_backport_overlay(const char *mount_point,
|
||||
const char *title_id);
|
||||
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
|
||||
void shutdown_title_mounts(void);
|
||||
// Return true when path is equal to root or is under it.
|
||||
bool path_matches_root_or_child(const char *path, const char *root);
|
||||
// Remove stale mount links and optionally restore image-backed mounts.
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind);
|
||||
// Unmount and remove title links that point under a source root.
|
||||
void cleanup_mount_links_for_source_unmount(const char *source_root);
|
||||
// Unmount and remove title links backed by USB sources or USB-backed images.
|
||||
void cleanup_usb_mount_links_for_suspend(void);
|
||||
// Recursively copy a directory tree.
|
||||
int copy_dir(const char *src, const char *dst);
|
||||
// Copy a single file.
|
||||
|
||||
+8
-1
@@ -15,14 +15,21 @@ bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend)
|
||||
void cleanup_stale_image_mounts(void);
|
||||
// Reconcile cached image mounts that belong to a specific scan root.
|
||||
void cleanup_stale_image_mounts_for_root(const char *root);
|
||||
// Unmount cached image mounts backed by USB storage during suspend.
|
||||
bool unmount_usb_image_mounts_for_suspend(void);
|
||||
// Unmount every cached image mount during shutdown.
|
||||
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.
|
||||
|
||||
@@ -31,6 +31,10 @@ void sm_kstuff_game_on_exit(pid_t pid);
|
||||
void sm_kstuff_game_poll(void);
|
||||
// Clear tracked game state and restore kstuff if the watcher paused it.
|
||||
void sm_kstuff_game_shutdown(void);
|
||||
// Pause kstuff for runtime sleep without restoring game-driven pauses.
|
||||
void sm_kstuff_sleep_enter(void);
|
||||
// Restore kstuff after runtime sleep when sleep entry disabled it.
|
||||
void sm_kstuff_sleep_leave(void);
|
||||
// Re-apply runtime config changes to the currently tracked game, if any.
|
||||
void sm_kstuff_on_config_reload(void);
|
||||
|
||||
|
||||
+4
-2
@@ -1,16 +1,17 @@
|
||||
#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
|
||||
#define PATH_STATE_CAPACITY MAX_PENDING
|
||||
#define TITLE_STATE_CAPACITY MAX_PENDING
|
||||
#define STATE_HASH_SIZE 1024u
|
||||
@@ -44,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,12 @@ 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.
|
||||
bool is_usb_storage_path(const char *path);
|
||||
// Build "<scan_path>/backports" for a managed scan root.
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
#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"
|
||||
#define LOG_DIR "/data/shadowmount"
|
||||
#define LOG_FILE "/data/shadowmount/debug.log"
|
||||
#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 *);
|
||||
|
||||
@@ -13,6 +13,14 @@ pid_t find_pid_by_name(const char *name, bool exclude_self);
|
||||
bool should_stop_requested(void);
|
||||
// Request graceful shutdown with a descriptive source string.
|
||||
void request_shutdown_stop(const char *reason);
|
||||
// Return true when the payload is paused for system suspend/resume.
|
||||
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.
|
||||
|
||||
@@ -9,6 +9,8 @@ typedef struct scan_candidate scan_candidate_t;
|
||||
void cleanup_lost_sources_before_scan(void);
|
||||
// Unmount and clean up mounts whose backing sources disappeared under one root.
|
||||
void cleanup_lost_sources_for_scan_root(const char *scan_root);
|
||||
// Immediately unmount runtime mounts backed by USB storage for suspend.
|
||||
void unmount_usb_sources_for_suspend(void);
|
||||
// Scan configured roots and collect install candidates.
|
||||
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
int *total_found_out,
|
||||
|
||||
@@ -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 {
|
||||
@@ -109,9 +111,16 @@ typedef struct runtime_config {
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool backport_fakelib_enabled;
|
||||
bool global_fakelib_enabled;
|
||||
bool global_fakelib_mount_first;
|
||||
bool kstuff_game_auto_toggle;
|
||||
bool kstuff_crash_detection_enabled;
|
||||
bool legacy_recursive_scan_forced;
|
||||
bool watch_subdirs;
|
||||
bool watch_image_files;
|
||||
char global_fakelib_path[MAX_PATH];
|
||||
uint32_t global_fakelib_exclude_title_count;
|
||||
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
|
||||
uint32_t scan_depth;
|
||||
uint32_t scan_interval_us;
|
||||
uint32_t stability_wait_seconds;
|
||||
@@ -131,6 +140,7 @@ typedef enum {
|
||||
IMAGE_FS_UFS,
|
||||
IMAGE_FS_EXFAT,
|
||||
IMAGE_FS_PFS,
|
||||
IMAGE_FS_PFSC_CONTAINER,
|
||||
} image_fs_type_t;
|
||||
|
||||
typedef struct sm_error {
|
||||
|
||||
+67
-4
@@ -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
|
||||
|
||||
@@ -41,7 +43,10 @@
|
||||
|
||||
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;
|
||||
@@ -77,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;
|
||||
@@ -101,19 +118,63 @@ void request_shutdown_stop(const char *reason) {
|
||||
}
|
||||
g_stop_requested = 1;
|
||||
sm_scanner_wake();
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
|
||||
bool runtime_sleep_mode_active(void) {
|
||||
return atomic_load_explicit(&g_runtime_sleep_mode_active,
|
||||
memory_order_acquire);
|
||||
}
|
||||
|
||||
static void clear_scan_now_request(void) {
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
g_scan_now.reason[0] = '\0';
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
}
|
||||
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason) {
|
||||
bool previous = atomic_exchange_explicit(&g_runtime_sleep_mode_active, active,
|
||||
memory_order_acq_rel);
|
||||
if (previous == active)
|
||||
return false;
|
||||
|
||||
if (active)
|
||||
clear_scan_now_request();
|
||||
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown sleep source";
|
||||
log_debug("[SLEEP] %s by %s", active ? "entered" : "left",
|
||||
resolved_reason);
|
||||
sm_scanner_wake();
|
||||
wake_game_lifecycle_watcher();
|
||||
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";
|
||||
bool resume_scan =
|
||||
strcmp(resolved_reason, "SceSystemStateMgrInfo=WORKING") == 0;
|
||||
if (runtime_sleep_mode_active() && !resume_scan)
|
||||
return;
|
||||
|
||||
char log_reason[sizeof(g_scan_now.reason)];
|
||||
bool should_log = false;
|
||||
bool should_log = !resume_scan;
|
||||
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
if (g_scan_now.reason[0] == '\0') {
|
||||
(void)strlcpy(g_scan_now.reason, resolved_reason, sizeof(g_scan_now.reason));
|
||||
(void)strlcpy(log_reason, g_scan_now.reason, sizeof(log_reason));
|
||||
should_log = true;
|
||||
} else {
|
||||
should_log = false;
|
||||
}
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
|
||||
@@ -442,6 +503,8 @@ int main(void) {
|
||||
cleanup_staged_mount_links();
|
||||
log_debug("[STARTUP] cleanup_duplicate_title_mounts begin");
|
||||
cleanup_duplicate_title_mounts();
|
||||
if (!app_db_run_startup_maintenance())
|
||||
log_debug(" [DB] startup snd0info maintenance unavailable");
|
||||
log_debug("[STARTUP] scanner startup sync begin");
|
||||
if (!sm_scanner_run_startup_sync()) {
|
||||
log_debug("[STARTUP] scanner startup sync aborted");
|
||||
|
||||
+328
-49
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
@@ -11,22 +12,34 @@
|
||||
|
||||
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
|
||||
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_normalize_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
|
||||
g_app_db_stmt_normalize_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -34,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) {
|
||||
@@ -89,7 +107,122 @@ static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool app_db_exec_with_retry(const char *sql, int max_attempts,
|
||||
const char *label, int *changes_out) {
|
||||
if (changes_out)
|
||||
*changes_out = 0;
|
||||
|
||||
for (int attempt = 0; attempt < max_attempts; attempt++) {
|
||||
if (!ensure_app_db_open())
|
||||
return false;
|
||||
|
||||
char *err_msg = NULL;
|
||||
int rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err_msg);
|
||||
if (rc == SQLITE_OK) {
|
||||
if (changes_out)
|
||||
*changes_out = sqlite3_changes(g_app_db);
|
||||
sqlite3_free(err_msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, max_attempts, false)) {
|
||||
sqlite3_free(err_msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (err_msg) {
|
||||
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc, err_msg);
|
||||
} else {
|
||||
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
sqlite3_free(err_msg);
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
static int app_db_query_int(const char *sql, const char *label) {
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_STARTUP_MAINTENANCE_RETRIES,
|
||||
label);
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
|
||||
int result = -1;
|
||||
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
result = sqlite3_column_int(stmt, 0);
|
||||
break;
|
||||
}
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] query failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
break;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool app_db_run_startup_maintenance(void) {
|
||||
bool ok = false;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!ensure_app_db_open())
|
||||
goto out;
|
||||
(void)sqlite3_busy_timeout(g_app_db,
|
||||
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
|
||||
|
||||
const char *backfill_count_sql =
|
||||
"SELECT COUNT(*) FROM tbl_contentinfo "
|
||||
"WHERE instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0;";
|
||||
int pending_backfill =
|
||||
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
|
||||
if (pending_backfill < 0) {
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int changes = 0;
|
||||
if (pending_backfill > 0) {
|
||||
const char *backfill_sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0;";
|
||||
|
||||
if (!app_db_exec_with_retry(backfill_sql,
|
||||
APP_DB_STARTUP_MAINTENANCE_RETRIES,
|
||||
"snd0info startup normalize", &changes))
|
||||
goto out;
|
||||
}
|
||||
|
||||
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
|
||||
changes, pending_backfill);
|
||||
|
||||
close_app_db();
|
||||
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 "
|
||||
@@ -99,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++) {
|
||||
@@ -107,34 +240,86 @@ int update_snd0info(const char *title_id) {
|
||||
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
if (g_app_db) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
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))
|
||||
continue;
|
||||
|
||||
if (g_app_db) {
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
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 "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_normalize_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
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();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
@@ -163,46 +348,32 @@ 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 load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK) {
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
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 (title_id && title_id[0] != '\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;
|
||||
}
|
||||
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();
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
@@ -212,8 +383,8 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
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);
|
||||
@@ -226,15 +397,85 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId LIKE 'PPSA%' "
|
||||
"AND uninstallable = 0 "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc =
|
||||
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"blocked PPSA uninstallable query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
|
||||
list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
|
||||
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
const struct AppDbTitleList *src) {
|
||||
if (!dst || !src)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(dst);
|
||||
if (src->count <= 0)
|
||||
return true;
|
||||
|
||||
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
|
||||
if (!ids)
|
||||
return false;
|
||||
|
||||
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
|
||||
dst->ids = ids;
|
||||
dst->count = src->count;
|
||||
dst->capacity = src->count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
*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);
|
||||
@@ -249,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;
|
||||
}
|
||||
+161
-23
@@ -71,6 +71,8 @@ static bool parse_u32_ini(const char *value, uint32_t *out);
|
||||
static bool is_valid_sector_size(uint32_t size);
|
||||
static bool set_kstuff_pause_delay_override_rule(runtime_config_state_t *state,
|
||||
const char *value);
|
||||
static bool add_global_fakelib_exclude_rule(runtime_config_state_t *state,
|
||||
const char *value);
|
||||
static bool normalize_image_filename_value(const char *value,
|
||||
char out[MAX_PATH]);
|
||||
static bool set_image_sector_rule(runtime_config_state_t *state,
|
||||
@@ -222,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) {
|
||||
@@ -245,9 +252,15 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.app_install_all_enabled = false;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
state->cfg.backport_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
state->cfg.kstuff_game_auto_toggle = true;
|
||||
state->cfg.kstuff_crash_detection_enabled = true;
|
||||
state->cfg.legacy_recursive_scan_forced = false;
|
||||
state->cfg.watch_subdirs = false;
|
||||
state->cfg.watch_image_files = true;
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
|
||||
state->cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
|
||||
state->cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
|
||||
@@ -296,22 +309,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.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,
|
||||
@@ -359,6 +360,15 @@ const char *get_scan_path(int index) {
|
||||
return state->scan_path_storage[index];
|
||||
}
|
||||
|
||||
uint32_t get_scan_depth_for_root(const char *scan_path) {
|
||||
uint32_t scan_depth = runtime_config()->scan_depth;
|
||||
if (scan_depth < MIN_SCAN_DEPTH)
|
||||
scan_depth = MIN_SCAN_DEPTH;
|
||||
if (is_pfsc_image_mount_base_or_child(scan_path))
|
||||
scan_depth++;
|
||||
return scan_depth;
|
||||
}
|
||||
|
||||
bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
|
||||
ensure_runtime_config_ready();
|
||||
if (!filename || !mount_read_only_out)
|
||||
@@ -429,6 +439,22 @@ bool is_kstuff_pause_disabled_for_title(const char *title_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_global_fakelib_excluded_for_title(const char *title_id) {
|
||||
ensure_runtime_config_ready();
|
||||
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
for (uint32_t i = 0; i < cfg->global_fakelib_exclude_title_count; ++i) {
|
||||
if (strcmp(cfg->global_fakelib_exclude_title_ids[i], normalized) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out) {
|
||||
ensure_runtime_config_ready();
|
||||
@@ -510,7 +536,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)
|
||||
@@ -533,7 +560,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);
|
||||
@@ -569,7 +597,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)
|
||||
@@ -595,7 +624,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)
|
||||
@@ -1033,6 +1063,30 @@ static bool add_kstuff_no_pause_title_rule(runtime_config_state_t *state,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool add_global_fakelib_exclude_rule(runtime_config_state_t *state,
|
||||
const char *value) {
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(value, normalized))
|
||||
return false;
|
||||
|
||||
for (uint32_t i = 0; i < state->cfg.global_fakelib_exclude_title_count;
|
||||
++i) {
|
||||
if (strcmp(state->cfg.global_fakelib_exclude_title_ids[i], normalized) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state->cfg.global_fakelib_exclude_title_count >=
|
||||
MAX_FAKELIB_EXCLUDE_RULES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t index = state->cfg.global_fakelib_exclude_title_count++;
|
||||
(void)strlcpy(state->cfg.global_fakelib_exclude_title_ids[index],
|
||||
normalized,
|
||||
sizeof(state->cfg.global_fakelib_exclude_title_ids[index]));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool set_kstuff_pause_delay_override_rule(runtime_config_state_t *state,
|
||||
const char *value) {
|
||||
if (!state || !value)
|
||||
@@ -1189,6 +1243,24 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_subdirs") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_subdirs = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "watch_image_files") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.watch_image_files = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "backport_fakelib") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
@@ -1198,6 +1270,64 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib") == 0 ||
|
||||
strcasecmp(key, "global_fakelib_enabled") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.global_fakelib_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_path") == 0) {
|
||||
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) {
|
||||
// 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);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "global_fakelib_exclude") == 0 ||
|
||||
strcasecmp(key, "global_fakelib_exclude_title") == 0) {
|
||||
if (!add_global_fakelib_exclude_rule(state, value)) {
|
||||
log_debug(" [CFG] invalid global fakelib exclude rule at line %d: "
|
||||
"%s=%s",
|
||||
line_no, key, value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_game_auto_toggle") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
@@ -1350,9 +1480,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
if (has_custom_scanpaths && state->scan_path_count == 0) {
|
||||
log_debug(" [CFG] no valid scanpath entries, using defaults");
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
} else {
|
||||
(void)add_runtime_scan_path(state, IMAGE_MOUNT_BASE);
|
||||
}
|
||||
add_runtime_managed_scan_paths(state);
|
||||
|
||||
if (legacy_recursive_scan_requested) {
|
||||
state->cfg.scan_depth = 2u;
|
||||
@@ -1377,6 +1506,9 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d "
|
||||
"app_install_all=%d app_install_all_forced=%d scan_depth=%u "
|
||||
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
|
||||
"global_fakelib=%d global_fakelib_priority=%s "
|
||||
"global_fakelib_path=%s global_fakelib_exclude=%u "
|
||||
"watch_subdirs=%d watch_image_files=%d "
|
||||
"kstuff_game_auto_toggle=%d kstuff_crash_detection=%d "
|
||||
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
@@ -1390,6 +1522,12 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.legacy_recursive_scan_forced ? 1 : 0,
|
||||
state->cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_mount_first ? "game" : "global",
|
||||
state->cfg.global_fakelib_path,
|
||||
state->cfg.global_fakelib_exclude_title_count,
|
||||
state->cfg.watch_subdirs ? 1 : 0,
|
||||
state->cfg.watch_image_files ? 1 : 0,
|
||||
state->cfg.kstuff_game_auto_toggle ? 1 : 0,
|
||||
state->cfg.kstuff_crash_detection_enabled ? 1 : 0,
|
||||
state->cfg.kstuff_pause_delay_image_seconds,
|
||||
|
||||
+161
-35
@@ -5,13 +5,23 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
typedef struct {
|
||||
char source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
const char *label;
|
||||
} fakelib_layer_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
char mount_path[MAX_PATH];
|
||||
} fakelib_mount_t;
|
||||
fakelib_layer_t layers[2];
|
||||
size_t layer_count;
|
||||
} fakelib_session_t;
|
||||
|
||||
static fakelib_mount_t g_fakelib_mount;
|
||||
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;
|
||||
@@ -19,7 +29,8 @@ bool sm_fakelib_game_feature_enabled(void) {
|
||||
|
||||
static bool mount_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path) {
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
|
||||
@@ -29,31 +40,50 @@ static bool mount_fakelib_overlay(const char *title_id,
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
|
||||
|
||||
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
|
||||
notify_system_info("Game backported: %s", title_id);
|
||||
log_debug(" [FAKELIB] libraries mounted for %s: %s -> %s", title_id,
|
||||
source_path, mount_path);
|
||||
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
|
||||
title_id, source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] mount failed for %s (%s -> %s): %s", title_id,
|
||||
source_path, mount_path, strerror(errno));
|
||||
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
|
||||
title_id, source_path, mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_fakelib_overlay(const char *mount_path) {
|
||||
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
|
||||
const char *mount_path = layer->mount_path;
|
||||
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
|
||||
layer->source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] unmount failed for %s: %s", mount_path,
|
||||
strerror(errno));
|
||||
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
|
||||
mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
char source_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
static bool track_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
|
||||
return false;
|
||||
|
||||
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
|
||||
layer->label = label;
|
||||
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
|
||||
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_context(const char *title_id,
|
||||
char game_source_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
game_source_path[0] = '\0';
|
||||
mount_path[0] = '\0';
|
||||
|
||||
char sandbox_id[MAX_TITLE_ID];
|
||||
DIR *d = opendir("/mnt/sandbox");
|
||||
if (!d)
|
||||
@@ -74,8 +104,14 @@ static bool resolve_sandbox_fakelib_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;
|
||||
|
||||
@@ -88,10 +124,12 @@ static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
snprintf(source_path, MAX_PATH, "/mnt/sandbox/%s/app0/fakelib", sandbox_id);
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
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);
|
||||
@@ -119,12 +157,46 @@ static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (!g_fakelib_mount.active)
|
||||
return true;
|
||||
static bool resolve_global_fakelib_source(const char *title_id,
|
||||
char source_path[MAX_PATH]) {
|
||||
source_path[0] = '\0';
|
||||
|
||||
if (!unmount_fakelib_overlay(g_fakelib_mount.mount_path)) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->global_fakelib_enabled)
|
||||
return false;
|
||||
if (is_global_fakelib_excluded_for_title(title_id))
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cfg->global_fakelib_path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
|
||||
title_id, cfg->global_fakelib_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode)) {
|
||||
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
|
||||
title_id, cfg->global_fakelib_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (g_fakelib_mount.layer_count == 0) {
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
return true;
|
||||
}
|
||||
|
||||
while (g_fakelib_mount.layer_count > 0) {
|
||||
fakelib_layer_t *layer =
|
||||
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
|
||||
if (!unmount_fakelib_overlay(layer))
|
||||
return false;
|
||||
memset(layer, 0, sizeof(*layer));
|
||||
g_fakelib_mount.layer_count--;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
@@ -132,48 +204,102 @@ 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())
|
||||
return;
|
||||
|
||||
if (g_fakelib_mount.active && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.active && g_fakelib_mount.pid != pid) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
|
||||
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
|
||||
(long)g_fakelib_mount.pid, (long)pid, 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;
|
||||
}
|
||||
}
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
char game_source_path[MAX_PATH];
|
||||
char global_source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
if (!resolve_sandbox_fakelib_context(title_id, source_path, mount_path))
|
||||
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path))
|
||||
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) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
g_fakelib_mount.active = true;
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
g_fakelib_mount.pid = pid;
|
||||
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
|
||||
sizeof(g_fakelib_mount.mount_path));
|
||||
|
||||
bool global_first = runtime_config()->global_fakelib_mount_first;
|
||||
bool same_source =
|
||||
has_global && has_game &&
|
||||
strcmp(global_source_path, game_source_path) == 0;
|
||||
if (global_first && has_global && !same_source) {
|
||||
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) {
|
||||
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
|
||||
"game")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!global_first && has_global && !same_source) {
|
||||
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.active || 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);
|
||||
}
|
||||
+176
-24
@@ -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;
|
||||
@@ -830,12 +935,21 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
|
||||
};
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (nmount(iov, IOVEC_SIZE(iov), 0) != 0) {
|
||||
log_debug(" [LINK] Failed to auto-mount nullfs title=%s src=%s dst=%s: %s",
|
||||
title_id, src_path, dst, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL)
|
||||
(void)unmount(dst, MNT_FORCE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path_exists(dst_eboot)) {
|
||||
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
|
||||
dst_eboot);
|
||||
@@ -851,19 +965,12 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool path_matches_root_or_child(const char *path, const char *root) {
|
||||
if (!path || !root || root[0] == '\0')
|
||||
return false;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
return (path[root_len] == '\0' || path[root_len] == '/');
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *removed_source_root;
|
||||
bool unmount_system_ex_bind;
|
||||
bool tried_image_recovery;
|
||||
bool force_remove_matching_source;
|
||||
bool match_usb_sources;
|
||||
} cleanup_mount_links_ctx_t;
|
||||
|
||||
static bool cleanup_mount_links_entry(const char *title_id,
|
||||
@@ -877,6 +984,8 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
source_path[0] = '\0';
|
||||
image_source_path[0] = '\0';
|
||||
bool has_image_source = false;
|
||||
bool should_remove = false;
|
||||
bool matches_removed_source = false;
|
||||
@@ -888,7 +997,14 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
|
||||
if (!should_remove) {
|
||||
if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
|
||||
if (ctx->match_usb_sources) {
|
||||
matches_removed_source =
|
||||
is_usb_storage_path(source_path) ||
|
||||
(has_image_source && is_usb_storage_path(image_source_path));
|
||||
if (!matches_removed_source)
|
||||
return true;
|
||||
should_remove = true;
|
||||
} else if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
|
||||
matches_removed_source =
|
||||
path_matches_root_or_child(source_path, ctx->removed_source_root) ||
|
||||
(has_image_source &&
|
||||
@@ -896,7 +1012,8 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
ctx->removed_source_root));
|
||||
if (!matches_removed_source)
|
||||
return true;
|
||||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
|
||||
should_remove = ctx->force_remove_matching_source ||
|
||||
(has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
} else {
|
||||
@@ -913,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;
|
||||
}
|
||||
@@ -939,6 +1057,11 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] failed to unmount mount stack for %s",
|
||||
state.system_ex_path);
|
||||
} else if (ctx->match_usb_sources) {
|
||||
log_debug(" [LINK] unmounted USB-backed mount stack: %s source=%s%s%s",
|
||||
state.system_ex_path, source_path,
|
||||
has_image_source ? " image=" : "",
|
||||
has_image_source ? image_source_path : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -976,11 +1099,40 @@ void cleanup_mount_links(const char *removed_source_root,
|
||||
.removed_source_root = removed_source_root,
|
||||
.unmount_system_ex_bind = unmount_system_ex_bind,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = false,
|
||||
};
|
||||
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
|
||||
&ctx);
|
||||
}
|
||||
|
||||
void cleanup_mount_links_for_source_unmount(const char *source_root) {
|
||||
if (!source_root || source_root[0] == '\0')
|
||||
return;
|
||||
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = source_root,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = true,
|
||||
.match_usb_sources = false,
|
||||
};
|
||||
for_each_title_app_dir(" during source unmount cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
}
|
||||
|
||||
void cleanup_usb_mount_links_for_suspend(void) {
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = NULL,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = true,
|
||||
};
|
||||
for_each_title_app_dir(" during USB suspend cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
}
|
||||
|
||||
static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
@@ -994,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;
|
||||
}
|
||||
|
||||
|
||||
+121
-6
@@ -4,7 +4,9 @@
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
@@ -37,6 +39,8 @@ static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_game_lifecycle_start_ready = false;
|
||||
static bool g_game_lifecycle_start_success = false;
|
||||
static _Atomic pid_t g_active_game_pid = 0;
|
||||
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_active_game_title_id[MAX_TITLE_ID];
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid);
|
||||
|
||||
@@ -47,6 +51,32 @@ static void publish_active_game_pid(pid_t pid) {
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
static void publish_active_game(pid_t pid, const char *title_id) {
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && title_id && title_id[0] != '\0')
|
||||
(void)strlcpy(g_active_game_title_id, title_id,
|
||||
sizeof(g_active_game_title_id));
|
||||
else
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
publish_active_game_pid(pid);
|
||||
}
|
||||
|
||||
static bool consume_active_game_title(pid_t pid,
|
||||
char title_id_out[MAX_TITLE_ID]) {
|
||||
bool matched = false;
|
||||
pthread_mutex_lock(&g_active_game_mutex);
|
||||
if (pid > 0 && atomic_load(&g_active_game_pid) == pid &&
|
||||
g_active_game_title_id[0] != '\0') {
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
matched = true;
|
||||
}
|
||||
if (matched)
|
||||
g_active_game_title_id[0] = '\0';
|
||||
pthread_mutex_unlock(&g_active_game_mutex);
|
||||
return matched;
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
}
|
||||
@@ -144,7 +174,7 @@ static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
|
||||
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
publish_active_game_pid(pid);
|
||||
publish_active_game(pid, title_id);
|
||||
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
|
||||
sm_fakelib_game_on_exec(pid, title_id);
|
||||
return true;
|
||||
@@ -169,6 +199,31 @@ static void close_game_lifecycle_wake_pipe(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void drain_game_lifecycle_wake_pipe(void) {
|
||||
char drain_buf[32];
|
||||
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
|
||||
while (true) {
|
||||
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
|
||||
&timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
@@ -366,6 +421,8 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
char title_id[MAX_TITLE_ID] = {0};
|
||||
bool had_active_title = consume_active_game_title(pid, title_id);
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
@@ -373,6 +430,29 @@ static void handle_game_exit(pid_t pid) {
|
||||
publish_active_game_pid(0);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
if (had_active_title) {
|
||||
int snd0_updates = normalize_snd0info_for_title(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
|
||||
snd0_updates, title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
|
||||
if (pid <= 0 || !is_process_alive(pid))
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (!resolve_game_title_id(pid, title_id, &app_id))
|
||||
return;
|
||||
if (!is_supported_title_id(title_id))
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
(void)dispatch_game_launch(kq, pid, now_us, title_id, app_id);
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
@@ -415,7 +495,42 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
set_game_lifecycle_start_result(true);
|
||||
log_debug(" [GAME] lifecycle watcher started");
|
||||
|
||||
bool sleep_cleanup_done = false;
|
||||
pid_t suspended_game_pid = 0;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
publish_active_game(0, NULL);
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
|
||||
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
|
||||
NULL);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
|
||||
struct kevent event;
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
|
||||
@@ -431,9 +546,7 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
char drain_buf[32];
|
||||
(void)read(g_game_lifecycle_wake_pipe[0], drain_buf,
|
||||
sizeof(drain_buf));
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
|
||||
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
|
||||
} else {
|
||||
@@ -444,13 +557,15 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
poll_game_modules(kq);
|
||||
}
|
||||
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_game_shutdown();
|
||||
publish_active_game_pid(0);
|
||||
publish_active_game(0, NULL);
|
||||
close(kq);
|
||||
log_debug(" [GAME] lifecycle watcher stopped");
|
||||
return NULL;
|
||||
@@ -523,7 +638,7 @@ void stop_game_lifecycle_watcher(void) {
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
publish_active_game_pid(0);
|
||||
publish_active_game(0, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
|
||||
+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, "", "");
|
||||
|
||||
+347
-42
@@ -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,16 +735,30 @@ 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"), IOVEC_ENTRY("from"),
|
||||
IOVEC_ENTRY(devname), IOVEC_ENTRY("fspath"),
|
||||
IOVEC_ENTRY(mount_point), IOVEC_ENTRY("budgetid"),
|
||||
IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("ufs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
|
||||
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
|
||||
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
|
||||
// IOVEC_ENTRY("compressedfile"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
|
||||
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
|
||||
|
||||
@@ -631,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));
|
||||
@@ -741,20 +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);
|
||||
@@ -774,15 +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;
|
||||
}
|
||||
|
||||
@@ -799,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;
|
||||
|
||||
@@ -822,9 +1084,14 @@ 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(mount_point, true);
|
||||
cleanup_mount_links_for_source_unmount(mount_point);
|
||||
clear_cached_game(mount_point);
|
||||
|
||||
// Unmount stacked layers (unionfs over image fs).
|
||||
@@ -910,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))
|
||||
@@ -959,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))
|
||||
@@ -978,11 +1247,39 @@ void cleanup_stale_image_mounts_for_root(const char *root) {
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup_mount_dirs(void) {
|
||||
DIR *d = opendir(IMAGE_MOUNT_BASE);
|
||||
bool unmount_usb_image_mounts_for_suspend(void) {
|
||||
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 (!is_usb_storage_path(cached_entry.path))
|
||||
continue;
|
||||
|
||||
log_debug(" [IMG][%s] USB suspend unmount: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
log_debug(" [IMG][%s] USB suspend unmounted: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
invalidate_image_cache_entry(k);
|
||||
continue;
|
||||
}
|
||||
|
||||
all_unmounted = false;
|
||||
log_debug(" [IMG][%s] USB suspend unmount pending: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
}
|
||||
|
||||
return all_unmounted;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -992,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) {
|
||||
@@ -1021,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;
|
||||
}
|
||||
+100
-21
@@ -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);
|
||||
@@ -170,17 +223,14 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
restage_staging = (!is_remount || should_register);
|
||||
restage_appmeta = (!is_remount || appmeta_missing);
|
||||
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
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,7 +265,10 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
metadata_restaged = true;
|
||||
}
|
||||
|
||||
if (should_stop_requested())
|
||||
if (!update_trophy_metadata(title_id, src_sce_sys))
|
||||
return false;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
@@ -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);
|
||||
@@ -320,10 +388,13 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
// --- Execution (per discovered candidate) ---
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
sm_install_poll_pending();
|
||||
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
const scan_candidate_t *c = &candidates[i];
|
||||
@@ -340,14 +411,22 @@ 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)",
|
||||
c->title_name, c->title_id);
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
@@ -365,7 +444,7 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
}
|
||||
} else {
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
|
||||
+32
-2
@@ -7,6 +7,8 @@
|
||||
#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"
|
||||
|
||||
@@ -24,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;
|
||||
|
||||
@@ -142,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) {
|
||||
@@ -188,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)
|
||||
@@ -212,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);
|
||||
}
|
||||
@@ -234,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();
|
||||
|
||||
@@ -247,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;
|
||||
}
|
||||
@@ -262,9 +279,13 @@ 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) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return 0;
|
||||
|
||||
if (!install_queue_active())
|
||||
return 0;
|
||||
|
||||
@@ -290,10 +311,16 @@ uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
}
|
||||
|
||||
void sm_install_poll_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
poll_pending_installs();
|
||||
}
|
||||
|
||||
void sm_install_service_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
if (!install_queue_active())
|
||||
return;
|
||||
|
||||
@@ -379,6 +406,9 @@ static void drop_queued_install_entry(pending_install_entry_t *entry) {
|
||||
}
|
||||
|
||||
bool sm_install_submit_queued(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return true;
|
||||
|
||||
+54
-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"
|
||||
|
||||
@@ -35,6 +36,7 @@ typedef struct {
|
||||
bool probe_available;
|
||||
bool supported;
|
||||
bool restore_on_empty;
|
||||
bool sleep_pause_restore;
|
||||
bool current_app_focus_valid;
|
||||
uint32_t current_app_focus_id;
|
||||
uint64_t restore_retry_us;
|
||||
@@ -319,10 +321,17 @@ static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out)
|
||||
case 0x12200000:
|
||||
case 0x12400000:
|
||||
case 0x12600000:
|
||||
case 0x12700000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcc978;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdccaf0;
|
||||
return true;
|
||||
|
||||
case 0x13000000:
|
||||
case 0x13200000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8a8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda20;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -810,10 +819,53 @@ void sm_kstuff_game_poll(void) {
|
||||
|
||||
void sm_kstuff_game_shutdown(void) {
|
||||
bool restore_needed = tracked_game_requires_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;
|
||||
}
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("watcher shutdown");
|
||||
if (restore_needed) {
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("watcher shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_enter(void) {
|
||||
bool was_enabled = false;
|
||||
bool fully_disabled = false;
|
||||
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
|
||||
if (!sm_kstuff_is_supported())
|
||||
return;
|
||||
|
||||
was_enabled = sm_kstuff_is_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");
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_leave(void) {
|
||||
if (!g_kstuff.sleep_pause_restore)
|
||||
return;
|
||||
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("system sleep");
|
||||
}
|
||||
|
||||
void sm_kstuff_on_config_reload(void) {
|
||||
|
||||
+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;
|
||||
}
|
||||
+33
-2
@@ -20,10 +20,41 @@ 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;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
bool is_usb_storage_path(const char *path) {
|
||||
static const char *usb_roots[] = {
|
||||
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
|
||||
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
|
||||
"/mnt/ext0",
|
||||
};
|
||||
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
|
||||
if (path_matches_root_or_child(path, usb_roots[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
+365
-36
@@ -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;
|
||||
}
|
||||
|
||||
@@ -423,7 +664,7 @@ static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
void *ctx_ptr) {
|
||||
(void)title_name;
|
||||
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
|
||||
@@ -489,26 +730,34 @@ void cleanup_lost_sources_for_scan_root(const char *scan_root) {
|
||||
prune_path_state_for_root(scan_root);
|
||||
}
|
||||
|
||||
void unmount_usb_sources_for_suspend(void) {
|
||||
log_debug("[SLEEP] USB-backed mount cleanup begin");
|
||||
cleanup_usb_mount_links_for_suspend();
|
||||
if (!unmount_usb_image_mounts_for_suspend()) {
|
||||
log_debug("[SLEEP] some USB-backed image mounts remain busy");
|
||||
}
|
||||
cleanup_mount_dirs();
|
||||
log_debug("[SLEEP] USB-backed mount cleanup done");
|
||||
}
|
||||
|
||||
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())
|
||||
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 = {
|
||||
@@ -518,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,
|
||||
@@ -525,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;
|
||||
}
|
||||
|
||||
@@ -549,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())
|
||||
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;
|
||||
}
|
||||
+7
-4
@@ -47,7 +47,7 @@ bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
|
||||
unsigned int depth_from_root,
|
||||
unsigned int remaining_depth,
|
||||
const sm_scan_tree_callbacks_t *callbacks, void *ctx) {
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (depth_from_root > 0u &&
|
||||
@@ -71,15 +71,18 @@ 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) {
|
||||
if (should_stop_requested()) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active()) {
|
||||
closedir(d);
|
||||
return true;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+467
-96
@@ -3,11 +3,14 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/event.h>
|
||||
#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"
|
||||
@@ -28,9 +31,12 @@
|
||||
#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,
|
||||
SCANNER_WATCH_SCAN_ROOT_PARENT,
|
||||
SCANNER_WATCH_SCAN_BACKPORT_ROOT,
|
||||
SCANNER_WATCH_SCAN_SUBDIR,
|
||||
SCANNER_WATCH_SCAN_IMAGE_FILE,
|
||||
@@ -50,15 +56,18 @@ typedef struct {
|
||||
bool dirty;
|
||||
bool cleanup_pending;
|
||||
bool watch_tree_stale;
|
||||
bool root_present;
|
||||
uint8_t watch_tree_rebuild_depth;
|
||||
scanner_watch_kind_t watch_tree_rebuild_kind;
|
||||
uint64_t root_device;
|
||||
uint64_t root_inode;
|
||||
uint64_t ready_after_us;
|
||||
char watch_tree_rebuild_path[MAX_PATH];
|
||||
} 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;
|
||||
@@ -71,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;
|
||||
@@ -90,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)
|
||||
@@ -127,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);
|
||||
@@ -141,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)
|
||||
@@ -197,6 +216,50 @@ 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 !=
|
||||
new_cfg->backport_fakelib_enabled ||
|
||||
old_cfg->global_fakelib_enabled != new_cfg->global_fakelib_enabled ||
|
||||
old_cfg->global_fakelib_mount_first !=
|
||||
new_cfg->global_fakelib_mount_first ||
|
||||
strcmp(old_cfg->global_fakelib_path,
|
||||
new_cfg->global_fakelib_path) != 0 ||
|
||||
old_cfg->global_fakelib_exclude_title_count !=
|
||||
new_cfg->global_fakelib_exclude_title_count ||
|
||||
memcmp(old_cfg->global_fakelib_exclude_title_ids,
|
||||
new_cfg->global_fakelib_exclude_title_ids,
|
||||
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,
|
||||
runtime_config()->watch_subdirs ? 1u : 0u,
|
||||
runtime_config()->watch_image_files ? 1u : 0u,
|
||||
(uint32_t)scan_path_count,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (unsigned shift = 0; shift < 32u; shift += 8u) {
|
||||
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));
|
||||
@@ -440,6 +503,59 @@ static bool build_parent_directory_path(const char *path,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool stat_directory_identity(const char *path, uint64_t *device_out,
|
||||
uint64_t *inode_out) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
|
||||
if (device_out)
|
||||
*device_out = (uint64_t)st.st_dev;
|
||||
if (inode_out)
|
||||
*inode_out = (uint64_t)st.st_ino;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool update_scan_root_presence_state(int scan_root_index,
|
||||
const char *scan_root,
|
||||
bool *present_out) {
|
||||
uint64_t root_device = 0;
|
||||
uint64_t root_inode = 0;
|
||||
bool present =
|
||||
stat_directory_identity(scan_root, &root_device, &root_inode);
|
||||
scanner_root_state_t *state = &g_scanner_root_states[scan_root_index];
|
||||
bool changed = state->root_present != present;
|
||||
if (present && state->root_present &&
|
||||
(state->root_device != root_device || state->root_inode != root_inode)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
state->root_present = present;
|
||||
state->root_device = present ? root_device : 0;
|
||||
state->root_inode = present ? root_inode : 0;
|
||||
*present_out = present;
|
||||
return changed;
|
||||
}
|
||||
|
||||
static bool resolve_existing_parent_directory_path(
|
||||
const char *path, char parent_path[MAX_PATH]) {
|
||||
char current_path[MAX_PATH];
|
||||
(void)strlcpy(current_path, path, sizeof(current_path));
|
||||
|
||||
while (true) {
|
||||
char candidate_parent[MAX_PATH];
|
||||
if (!build_parent_directory_path(current_path, candidate_parent))
|
||||
return false;
|
||||
if (strcmp(candidate_parent, current_path) == 0)
|
||||
return false;
|
||||
if (stat_directory_identity(candidate_parent, NULL, NULL)) {
|
||||
(void)strlcpy(parent_path, candidate_parent, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
(void)strlcpy(current_path, candidate_parent, sizeof(current_path));
|
||||
}
|
||||
}
|
||||
|
||||
static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry,
|
||||
char rebuild_path[MAX_PATH],
|
||||
uint8_t *rebuild_depth_out,
|
||||
@@ -452,6 +568,12 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
*rebuild_depth_out = entry->depth;
|
||||
*kind_out = entry->kind;
|
||||
return true;
|
||||
case SCANNER_WATCH_SCAN_ROOT_PARENT:
|
||||
(void)strlcpy(rebuild_path, get_scan_path(entry->scan_root_index),
|
||||
MAX_PATH);
|
||||
*rebuild_depth_out = 0;
|
||||
*kind_out = SCANNER_WATCH_SCAN_ROOT;
|
||||
return true;
|
||||
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
||||
if (!build_parent_directory_path(entry->path, rebuild_path))
|
||||
return false;
|
||||
@@ -467,6 +589,7 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
typedef struct {
|
||||
int kq;
|
||||
int scan_root_index;
|
||||
bool watch_subdirs;
|
||||
} register_watch_tree_ctx_t;
|
||||
|
||||
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
@@ -475,9 +598,11 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
scanner_watch_kind_t kind =
|
||||
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
|
||||
: SCANNER_WATCH_SCAN_SUBDIR;
|
||||
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path, kind,
|
||||
(uint8_t)depth_from_root)) {
|
||||
return SM_SCAN_TREE_DIR_ABORT;
|
||||
if (depth_from_root == 0u || ctx->watch_subdirs) {
|
||||
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path,
|
||||
kind, (uint8_t)depth_from_root)) {
|
||||
return SM_SCAN_TREE_DIR_ABORT;
|
||||
}
|
||||
}
|
||||
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
@@ -495,26 +620,53 @@ static bool register_watch_image_visit(const char *image_path,
|
||||
(uint8_t)depth_from_root);
|
||||
}
|
||||
|
||||
static bool register_scan_root_parent_watch(int kq, int scan_root_index,
|
||||
const char *scan_root) {
|
||||
char parent_path[MAX_PATH];
|
||||
if (!resolve_existing_parent_directory_path(scan_root, parent_path))
|
||||
return true;
|
||||
|
||||
return register_scanner_watch_entry(kq, scan_root_index, parent_path,
|
||||
SCANNER_WATCH_SCAN_ROOT_PARENT, 0u);
|
||||
}
|
||||
|
||||
static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
||||
const char *scan_root = get_scan_path(scan_root_index);
|
||||
if (!remove_scan_root_watch_entries(scan_root_index))
|
||||
return false;
|
||||
|
||||
unsigned int scan_depth = runtime_config()->scan_depth;
|
||||
if (scan_depth < MIN_SCAN_DEPTH)
|
||||
scan_depth = MIN_SCAN_DEPTH;
|
||||
bool root_present = false;
|
||||
(void)update_scan_root_presence_state(scan_root_index, scan_root,
|
||||
&root_present);
|
||||
if (!root_present) {
|
||||
if (!register_scan_root_parent_watch(kq, scan_root_index, scan_root))
|
||||
return false;
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
}
|
||||
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
||||
&walk_ctx)) {
|
||||
return false;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
|
||||
if (!register_scanner_watch_entry(kq, scan_root_index, scan_root,
|
||||
SCANNER_WATCH_SCAN_ROOT, 0u)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.watch_subdirs = cfg->watch_subdirs,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file =
|
||||
cfg->watch_image_files ? register_watch_image_visit : NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
||||
&walk_ctx)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
char backport_root[MAX_PATH];
|
||||
@@ -525,6 +677,9 @@ static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!register_scan_root_parent_watch(kq, scan_root_index, scan_root))
|
||||
return false;
|
||||
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
}
|
||||
@@ -552,12 +707,15 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = runtime_config()->scan_depth;
|
||||
if (scan_depth < MIN_SCAN_DEPTH)
|
||||
scan_depth = MIN_SCAN_DEPTH;
|
||||
|
||||
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
||||
return false;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->watch_subdirs && !cfg->watch_image_files) {
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
if (rebuild_depth > scan_depth) {
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
@@ -566,10 +724,11 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.watch_subdirs = cfg->watch_subdirs,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
.on_image_file = cfg->watch_image_files ? register_watch_image_visit : NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
|
||||
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {
|
||||
@@ -642,7 +801,8 @@ static bool scanner_event_requires_watch_tree_refresh(
|
||||
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
|
||||
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
case SCANNER_WATCH_SCAN_SUBDIR:
|
||||
return entry->depth < runtime_config()->scan_depth &&
|
||||
return entry->depth <
|
||||
get_scan_depth_for_root(get_scan_path(entry->scan_root_index)) &&
|
||||
(fflags & tree_change_flags) != 0;
|
||||
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
||||
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
@@ -701,38 +861,6 @@ static void schedule_scan_root_watch_tree_rebuild(
|
||||
sizeof(state->watch_tree_rebuild_path));
|
||||
}
|
||||
|
||||
static bool register_control_dir_watch(int kq) {
|
||||
if (g_scanner_control_dir_fd < 0)
|
||||
return true;
|
||||
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)g_scanner_control_dir_fd, EVFILT_VNODE,
|
||||
EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
|
||||
NOTE_REVOKE,
|
||||
0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [SCAN] control dir watcher registration failed: %s",
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool reopen_control_dir_watch(int kq) {
|
||||
close_scanner_control_dir();
|
||||
|
||||
g_scanner_control_dir_fd = open(LOG_DIR, O_RDONLY | O_DIRECTORY);
|
||||
if (g_scanner_control_dir_fd < 0) {
|
||||
log_debug(" [SCAN] control dir watch unavailable for %s: %s", LOG_DIR,
|
||||
strerror(errno));
|
||||
return true;
|
||||
}
|
||||
|
||||
return register_control_dir_watch(kq);
|
||||
}
|
||||
|
||||
static void register_config_file_watch(int kq, uint64_t now_us) {
|
||||
if (g_scanner_config_fd < 0)
|
||||
return;
|
||||
@@ -770,20 +898,78 @@ 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) {
|
||||
if (old_cfg->backport_fakelib_enabled && !new_cfg->backport_fakelib_enabled)
|
||||
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) {
|
||||
return should_stop_requested();
|
||||
return should_stop_requested() || runtime_sleep_mode_active();
|
||||
}
|
||||
|
||||
static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
@@ -834,7 +1020,7 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
total_found_games);
|
||||
}
|
||||
|
||||
return !should_stop_requested();
|
||||
return !should_abort_scan_cycle();
|
||||
}
|
||||
|
||||
static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
@@ -868,7 +1054,7 @@ static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = unstable_found;
|
||||
|
||||
return !should_stop_requested();
|
||||
return !should_abort_scan_cycle();
|
||||
}
|
||||
|
||||
static int find_pending_cleanup_scan_root(void) {
|
||||
@@ -930,6 +1116,16 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_config_probe_due_us;
|
||||
}
|
||||
|
||||
if (g_scanner_manual_scan_due_us != 0 &&
|
||||
(next_deadline == 0 || g_scanner_manual_scan_due_us < next_deadline)) {
|
||||
next_deadline = g_scanner_manual_scan_due_us;
|
||||
}
|
||||
|
||||
if (g_scanner_manual_fd < 0 && g_scanner_manual_probe_due_us != 0 &&
|
||||
(next_deadline == 0 || g_scanner_manual_probe_due_us < next_deadline)) {
|
||||
next_deadline = g_scanner_manual_probe_due_us;
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
||||
if (!state->dirty)
|
||||
@@ -957,6 +1153,35 @@ static const struct timespec *build_wait_timeout(struct timespec *timeout,
|
||||
return timeout;
|
||||
}
|
||||
|
||||
static bool handle_scan_root_parent_event(
|
||||
int kq, const scanner_watch_entry_t *entry, uint64_t now_us) {
|
||||
const char *scan_root = get_scan_path(entry->scan_root_index);
|
||||
bool root_present = false;
|
||||
bool root_changed = update_scan_root_presence_state(
|
||||
entry->scan_root_index, scan_root, &root_present);
|
||||
if (root_present && root_changed) {
|
||||
schedule_scan_root_dirty(entry->scan_root_index, now_us, false);
|
||||
schedule_scan_root_watch_tree_rebuild(entry);
|
||||
return true;
|
||||
}
|
||||
if (root_present)
|
||||
return true;
|
||||
if (root_changed) {
|
||||
schedule_scan_root_cleanup(entry->scan_root_index);
|
||||
schedule_scan_root_dirty(entry->scan_root_index, now_us, true);
|
||||
schedule_scan_root_watch_tree_rebuild(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
char parent_path[MAX_PATH];
|
||||
if (!resolve_existing_parent_directory_path(scan_root, parent_path) ||
|
||||
strcmp(parent_path, entry->path) != 0) {
|
||||
return rebuild_scan_root_watch_tree(kq, entry->scan_root_index);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool process_scanner_events(int kq, const struct timespec *timeout,
|
||||
bool *timed_out_out) {
|
||||
*timed_out_out = false;
|
||||
@@ -990,19 +1215,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;
|
||||
}
|
||||
|
||||
@@ -1011,6 +1237,12 @@ static bool process_scanner_events(int kq, const struct timespec *timeout,
|
||||
if (!watch_entry)
|
||||
continue;
|
||||
|
||||
if (watch_entry->kind == SCANNER_WATCH_SCAN_ROOT_PARENT) {
|
||||
if (!handle_scan_root_parent_event(kq, watch_entry, now_us))
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool immediate =
|
||||
(event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
if (scanner_event_requires_consistency_cleanup(watch_entry, event->fflags))
|
||||
@@ -1039,6 +1271,24 @@ static bool drain_scanner_events_nowait(int kq) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool discard_scanner_events_nowait(int kq) {
|
||||
struct kevent events[SCANNER_EVENT_BATCH];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
|
||||
while (true) {
|
||||
int nev = kevent(kq, NULL, 0, events, SCANNER_EVENT_BATCH, &timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SCAN] stale event drain failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static char g_scanner_shutdown_reason[128];
|
||||
|
||||
static void request_scanner_shutdown(const char *reason) {
|
||||
@@ -1052,11 +1302,12 @@ static void request_scanner_shutdown(const char *reason) {
|
||||
|
||||
bool sm_scanner_init(void) {
|
||||
close_scanner_wake_pipe();
|
||||
close_scanner_control_dir();
|
||||
close_scanner_config_file();
|
||||
close_scanner_manual_file();
|
||||
clear_scanner_watch_entries();
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
|
||||
if (pipe(g_scanner_wake_pipe) != 0) {
|
||||
log_debug(" [SCAN] wake pipe creation failed: %s", strerror(errno));
|
||||
@@ -1072,12 +1323,6 @@ bool sm_scanner_init(void) {
|
||||
}
|
||||
g_scanner_wake_write_fd = (sig_atomic_t)g_scanner_wake_pipe[1];
|
||||
|
||||
g_scanner_control_dir_fd = open(LOG_DIR, O_RDONLY | O_DIRECTORY);
|
||||
if (g_scanner_control_dir_fd < 0) {
|
||||
log_debug(" [SCAN] control dir watch unavailable for %s: %s", LOG_DIR,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
g_scanner_config_fd = open(CONFIG_FILE, O_RDONLY);
|
||||
if (g_scanner_config_fd < 0 && errno != ENOENT) {
|
||||
log_debug(" [CFG] config watch unavailable for %s: %s", CONFIG_FILE,
|
||||
@@ -1086,6 +1331,10 @@ bool sm_scanner_init(void) {
|
||||
if (g_scanner_config_fd < 0)
|
||||
schedule_config_probe(monotonic_time_us());
|
||||
|
||||
g_scanner_manual_fd = open_manual_list_file();
|
||||
if (g_scanner_manual_fd < 0)
|
||||
schedule_manual_probe(monotonic_time_us());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1099,10 +1348,19 @@ void sm_scanner_wake(void) {
|
||||
}
|
||||
|
||||
bool sm_scanner_run_startup_sync(void) {
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
while (!should_stop_requested()) {
|
||||
while (runtime_sleep_mode_active() && !should_stop_requested())
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
return run_full_scan_cycle(true, NULL, NULL);
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
if (run_full_scan_cycle(true, NULL, NULL))
|
||||
return true;
|
||||
if (!runtime_sleep_mode_active())
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void sm_scanner_run_loop(void) {
|
||||
@@ -1133,16 +1391,19 @@ 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;
|
||||
}
|
||||
|
||||
uint64_t next_full_resync_us =
|
||||
monotonic_time_us() + scanner_full_resync_interval_us();
|
||||
bool was_sleeping = false;
|
||||
|
||||
while (true) {
|
||||
if (should_stop_requested()) {
|
||||
@@ -1150,11 +1411,42 @@ void sm_scanner_run_loop(void) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
was_sleeping = true;
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_scanner_wake_pipe[0], &readfds);
|
||||
int rc = select(g_scanner_wake_pipe[0] + 1, &readfds, NULL, NULL, NULL);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SCAN] sleep wait failed: %s", strerror(errno));
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("scanner sleep wait failed");
|
||||
return;
|
||||
}
|
||||
drain_scanner_wake_pipe();
|
||||
continue;
|
||||
}
|
||||
if (was_sleeping) {
|
||||
was_sleeping = false;
|
||||
if (!discard_scanner_events_nowait(kq)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("scanner stale event drain failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char scan_reason[128];
|
||||
if (consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, scan_reason, &unstable_found))
|
||||
if (!run_full_scan_cycle(false, scan_reason, &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)) {
|
||||
@@ -1189,21 +1481,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;
|
||||
}
|
||||
@@ -1216,8 +1564,11 @@ void sm_scanner_run_loop(void) {
|
||||
|
||||
if (next_full_resync_us != 0 && now_us >= next_full_resync_us) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, NULL, &unstable_found))
|
||||
if (!run_full_scan_cycle(false, NULL, &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)) {
|
||||
@@ -1246,6 +1597,8 @@ void sm_scanner_run_loop(void) {
|
||||
|
||||
int dirty_root_index = find_due_dirty_scan_root(now_us);
|
||||
if (dirty_root_index >= 0) {
|
||||
bool cleanup_pending =
|
||||
g_scanner_root_states[dirty_root_index].cleanup_pending;
|
||||
bool rebuild_watch_tree =
|
||||
g_scanner_root_states[dirty_root_index].watch_tree_stale;
|
||||
uint8_t rebuild_watch_tree_depth =
|
||||
@@ -1263,8 +1616,25 @@ void sm_scanner_run_loop(void) {
|
||||
clear_scan_root_watch_tree_state(dirty_root_index);
|
||||
|
||||
bool unstable_found = false;
|
||||
if (!run_targeted_scan_cycle(dirty_root_index, &unstable_found))
|
||||
if (!run_targeted_scan_cycle(dirty_root_index, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
scanner_root_state_t *state =
|
||||
&g_scanner_root_states[dirty_root_index];
|
||||
if (cleanup_pending)
|
||||
schedule_scan_root_cleanup(dirty_root_index);
|
||||
schedule_scan_root_dirty(dirty_root_index, monotonic_time_us(), true);
|
||||
if (rebuild_watch_tree) {
|
||||
state->watch_tree_stale = true;
|
||||
state->watch_tree_rebuild_depth = rebuild_watch_tree_depth;
|
||||
state->watch_tree_rebuild_kind = rebuild_watch_tree_kind;
|
||||
(void)strlcpy(state->watch_tree_rebuild_path,
|
||||
rebuild_watch_tree_path,
|
||||
sizeof(state->watch_tree_rebuild_path));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (rebuild_watch_tree &&
|
||||
!rebuild_scan_root_watch_subtree(kq, dirty_root_index,
|
||||
@@ -1316,8 +1686,9 @@ void sm_scanner_run_loop(void) {
|
||||
void sm_scanner_shutdown(void) {
|
||||
clear_scanner_watch_entries();
|
||||
close_scanner_config_file();
|
||||
close_scanner_control_dir();
|
||||
close_scanner_manual_file();
|
||||
close_scanner_wake_pipe();
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
}
|
||||
+72
-31
@@ -7,10 +7,12 @@
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
|
||||
#define SHELLCORE_FLAG_WAITMODE_OR 2u
|
||||
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
|
||||
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
|
||||
|
||||
typedef intptr_t sm_kernel_event_flag_t;
|
||||
|
||||
@@ -44,8 +46,8 @@ static shellcore_flag_monitor_t g_shellcore_flags[] = {
|
||||
{.name = "SceShellCoreUtilAppFocus", .handle = -1, .required = true},
|
||||
{.name = "SceSystemStateMgrInfo", .handle = -1, .required = true},
|
||||
{.name = "SceLncUtilSystemStatus", .handle = -1, .required = false},
|
||||
/* {.name = "SceSystemStateMgrStatus", .handle = -1},
|
||||
{.name = "SceSysCoreSuspend", .handle = -1},
|
||||
{.name = "SceSystemStateMgrStatus", .handle = -1},
|
||||
/* {.name = "SceSysCoreSuspend", .handle = -1},
|
||||
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
|
||||
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
|
||||
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
|
||||
@@ -64,6 +66,30 @@ static bool g_shellcore_flag_start_ready = false;
|
||||
static bool g_shellcore_flag_start_success = false;
|
||||
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
|
||||
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
|
||||
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
|
||||
{0x0000000000000004ULL, "BD_DRIVE_READY"},
|
||||
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
|
||||
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
|
||||
{0x0000000000000020ULL, "TICK_0x20"},
|
||||
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
|
||||
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
|
||||
{0x0000000000000200ULL, "REBOOT_REQUEST"},
|
||||
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
|
||||
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
|
||||
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
|
||||
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
|
||||
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
|
||||
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
|
||||
{SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS,
|
||||
"SHELLUI_SHUTDOWN_IN_PROGRESS"},
|
||||
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
|
||||
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
|
||||
};
|
||||
|
||||
#if 0
|
||||
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
|
||||
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
|
||||
@@ -99,28 +125,6 @@ static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
|
||||
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
|
||||
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
|
||||
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
|
||||
{0x0000000000000004ULL, "BD_DRIVE_READY"},
|
||||
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
|
||||
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
|
||||
{0x0000000000000020ULL, "TICK_0x20"},
|
||||
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
|
||||
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
|
||||
{0x0000000000000200ULL, "REBOOT_REQUEST"},
|
||||
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
|
||||
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
|
||||
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
|
||||
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
|
||||
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
|
||||
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
|
||||
{0x0000000000200000ULL, "SHELLUI_SHUTDOWN_IN_PROGRESS"},
|
||||
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
|
||||
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
|
||||
{0x0000000000000002ULL, "BIT_0x2"},
|
||||
@@ -175,6 +179,13 @@ static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
return g_lnc_util_system_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_system_state_mgr_status_bits) /
|
||||
sizeof(g_system_state_mgr_status_bits[0]);
|
||||
}
|
||||
return g_system_state_mgr_status_bits;
|
||||
}
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
|
||||
if (count_out)
|
||||
@@ -198,13 +209,6 @@ static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
return g_vshctl_internal_user_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_system_state_mgr_status_bits) /
|
||||
sizeof(g_system_state_mgr_status_bits[0]);
|
||||
}
|
||||
return g_system_state_mgr_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
|
||||
if (count_out)
|
||||
@@ -504,6 +508,14 @@ static void set_shellcore_flag_start_result(bool success) {
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
}
|
||||
|
||||
static void enter_sleep_mode_and_cleanup(const char *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) {
|
||||
if (!flag)
|
||||
return;
|
||||
@@ -610,7 +622,10 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
int rc;
|
||||
bool changed = false;
|
||||
bool entered_shutdown_on_going = false;
|
||||
bool entered_main_on_standby = false;
|
||||
bool entered_suspend_on_going = false;
|
||||
bool entered_resume_working = false;
|
||||
bool entered_shellui_shutdown_in_progress = false;
|
||||
bool is_system_state_mgr_info = false;
|
||||
unsigned current_state = 0;
|
||||
unsigned previous_state = 0;
|
||||
@@ -646,10 +661,25 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
|
||||
entered_main_on_standby =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
|
||||
entered_suspend_on_going =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
|
||||
entered_resume_working =
|
||||
flag->has_last_pattern &&
|
||||
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
|
||||
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
entered_shellui_shutdown_in_progress =
|
||||
(result_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
|
||||
(!flag->has_last_pattern ||
|
||||
(flag->last_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
|
||||
}
|
||||
|
||||
flag->last_pattern = result_pattern;
|
||||
@@ -664,8 +694,19 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
if (entered_shutdown_on_going) {
|
||||
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
|
||||
}
|
||||
if (entered_main_on_standby || entered_suspend_on_going) {
|
||||
const char *sleep_reason =
|
||||
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
|
||||
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
|
||||
enter_sleep_mode_and_cleanup(sleep_reason);
|
||||
}
|
||||
if (entered_shellui_shutdown_in_progress) {
|
||||
enter_sleep_mode_and_cleanup(
|
||||
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
|
||||
}
|
||||
if (entered_resume_working) {
|
||||
request_scan_now("SceSystemStateMgrInfo=WORKING");
|
||||
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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