mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-19 13:19:59 -05:00
Compare commits
60
Commits
1.6test7-fix1
...
dev
| 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 | ||
|
|
2680ed28d7 | ||
|
|
f401660113 | ||
|
|
11a135cf1c | ||
|
|
146942c138 | ||
|
|
a8f1a216bd | ||
|
|
c37088543d | ||
|
|
059f8b40f1 | ||
|
|
6f61e8d29e | ||
|
|
a5471da456 | ||
|
|
0abfecd3e7 | ||
|
|
9f1786905c | ||
|
|
fa714f868f | ||
|
|
17bb561e8d | ||
|
|
0efeec16fa | ||
|
|
a584121d76 | ||
|
|
45da514549 | ||
|
|
e272c9736c | ||
|
|
902a6917df | ||
|
|
6df8a472e3 | ||
|
|
4e9afa7885 | ||
|
|
40967215b4 | ||
|
|
a23822a26f |
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:
|
||||
@@ -27,9 +30,15 @@ jobs:
|
||||
- name: Checkout pacbrew-repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: EchoStretch/pacbrew-repo
|
||||
repository: ps5-payload-dev/pacbrew-repo
|
||||
path: pacbrew-repo
|
||||
|
||||
- name: Checkout SDK stubs
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ps5-payload-dev/sdk
|
||||
path: ps5-sdk
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
sudo apt update
|
||||
@@ -64,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Build payload
|
||||
run: |
|
||||
make clean all
|
||||
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
|
||||
|
||||
- name: Create release archive
|
||||
id: package
|
||||
|
||||
@@ -11,9 +11,12 @@ CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
|
||||
LDFLAGS := -flto=thin -Wl,--gc-sections
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lkernel_sys -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
|
||||
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
|
||||
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
@@ -22,16 +25,23 @@ HEADERS := $(wildcard include/*.h)
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
|
||||
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
|
||||
rm -f src/notify_icon_asset.c
|
||||
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
|
||||
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
|
||||
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
|
||||
$(CC) -shared -Wl,-soname=libkernel_sys.sprx -o $@ $^
|
||||
|
||||
src/notify_icon_asset.c: smp_icon.png
|
||||
xxd -i $< > $@
|
||||
|
||||
src/config_ini_example_asset.c: config.ini.example
|
||||
xxd -i $< > $@
|
||||
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o src/notify_icon_asset.c
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
@@ -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`.
|
||||
@@ -36,27 +49,34 @@ Notes:
|
||||
|
||||
## Runtime config (`/data/shadowmount/config.ini`)
|
||||
|
||||
This file is optional. If it does not exist, built-in defaults are used.
|
||||
This file is optional. If it does not exist, ShadowMountPlus creates it from the bundled `config.ini.example` template on startup and uses built-in defaults until you uncomment overrides.
|
||||
|
||||
Supported keys (all optional):
|
||||
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
|
||||
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
|
||||
- `mount_read_only=1|0` (default: `1`)
|
||||
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`)
|
||||
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
|
||||
- `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: `10`)
|
||||
- `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 the early post-auto-pause crash heuristic and autotune updates; default: `1`)
|
||||
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `20`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `10`)
|
||||
- `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`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
|
||||
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
|
||||
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
|
||||
- `/data/shadowmount/autotune.ini` may also provide per-title pause-delay overrides with highest priority:
|
||||
@@ -92,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: `10`).
|
||||
- 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.
|
||||
|
||||
@@ -104,15 +124,20 @@ Backport overlay behavior:
|
||||
- For each `scanpath`, use:
|
||||
- `<scanpath>/backports/<TITLE_ID>/`
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from the same scan path.
|
||||
- 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.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- If `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:
|
||||
- When `kstuff_game_auto_toggle=1`, ShadowMount watches game `exec/exit` events in the background.
|
||||
- Image-backed launches use `kstuff_pause_delay_image_seconds`; direct/non-image launches use `kstuff_pause_delay_direct_seconds`.
|
||||
- `kstuff_crash_detection=0` disables the crash heuristic and the automatic pause-delay tuning logic, while leaving normal kstuff auto-pause/auto-resume behavior intact.
|
||||
- `kstuff_crash_detection=0` disables crash monitoring and the automatic pause-delay tuning logic, while leaving normal kstuff auto-pause/auto-resume behavior intact.
|
||||
- `kstuff_no_pause` skips auto-pause entirely for matching title IDs.
|
||||
- `kstuff_delay` overrides the pause delay for matching title IDs, regardless of image/direct launch type.
|
||||
- `/data/shadowmount/autotune.ini` overrides both `config.ini` and `autopause.txt` for matching title IDs.
|
||||
@@ -124,7 +149,8 @@ Kstuff game lifecycle behavior:
|
||||
- `direct=<seconds>`
|
||||
- `image=<seconds>`
|
||||
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
|
||||
- When ShadowMountPlus detects an early post-auto-pause crash, it doubles the current effective pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
|
||||
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed and kstuff is not to blame.
|
||||
- When crash monitoring detects an app crash within 2 minutes after kstuff auto-pause, ShadowMountPlus doubles the applied pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
|
||||
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
|
||||
|
||||
|
||||
@@ -137,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:
|
||||
@@ -157,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>/`
|
||||
@@ -266,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;
|
||||
@@ -274,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:
|
||||
@@ -298,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
|
||||
+107
-45
@@ -1,96 +1,158 @@
|
||||
# ShadowMount runtime config example
|
||||
# Place as: /data/shadowmount/config.ini
|
||||
#
|
||||
# All keys are optional.
|
||||
# If a key is missing, built-in defaults are used.
|
||||
# This file is a template.
|
||||
# Nothing below is active until you remove the leading '#'.
|
||||
#
|
||||
# How to use this file:
|
||||
# 1. Uncomment only the parameters you really want to override.
|
||||
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
|
||||
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
|
||||
|
||||
# Debug logging:
|
||||
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
|
||||
# 0/false/no/off -> disable log_debug output
|
||||
debug=1
|
||||
# Default: 1
|
||||
# debug=1
|
||||
|
||||
# Quiet mode:
|
||||
# 1/true/yes/on -> only show the startup version popup and runtime errors
|
||||
# 0/false/no/off -> also show normal informational popups (default)
|
||||
quiet_mode=0
|
||||
# 0/false/no/off -> also show normal informational popups
|
||||
# Default: 0
|
||||
# quiet_mode=0
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
mount_read_only=1
|
||||
# Default: 1
|
||||
# mount_read_only=1
|
||||
|
||||
# Mounting even damaged file systems
|
||||
# Mounting even damaged file systems:
|
||||
# 1/true/yes/on -> include force option
|
||||
# 0/false/no/off -> default mount options (default)
|
||||
force_mount=0
|
||||
# 0/false/no/off -> default mount options
|
||||
# Default: 0
|
||||
# force_mount=0
|
||||
|
||||
# Per-image mount mode override by file name (can be repeated many times):
|
||||
# image_ro=<image_filename>
|
||||
# image_rw=<image_filename>
|
||||
# image_sector=<image_filename>:<sector_size>
|
||||
# image_ro=PPSA12345.ffpfs
|
||||
# image_rw=My Game Dump.exfat
|
||||
# image_sector=My Game Dump.exfat:65536
|
||||
# Use batch registration for newly staged titles:
|
||||
# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll
|
||||
# 0/false/no/off -> register each title directly
|
||||
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
|
||||
# app_install_all=0
|
||||
|
||||
# Scan depth relative to each scan root:
|
||||
# 1 -> scan only first-level subdirectories (default)
|
||||
# 1 -> scan only first-level subdirectories
|
||||
# 2 -> also scan one additional nested level
|
||||
scan_depth=1
|
||||
#
|
||||
# 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
|
||||
scan_interval_seconds=10
|
||||
# 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.
|
||||
stability_wait_seconds=10
|
||||
# Default: 10
|
||||
# stability_wait_seconds=10
|
||||
|
||||
# Backend selection per filesystem:
|
||||
# lvd -> /dev/lvdctl -> /dev/lvdN
|
||||
# md -> /dev/mdctl -> /dev/mdN
|
||||
exfat_backend=lvd
|
||||
ufs_backend=lvd
|
||||
# Defaults: exfat=lvd, ufs=lvd
|
||||
# exfat_backend=lvd
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
backport_fakelib=1
|
||||
# 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
|
||||
kstuff_game_auto_toggle=1
|
||||
# Default: 1
|
||||
# kstuff_game_auto_toggle=1
|
||||
|
||||
# Detect likely early crashes after auto-pause and autotune pause delay:
|
||||
# 1/true/yes/on -> enable crash heuristic and autotune updates (default)
|
||||
# Detect crash-candidate state and autotune pause delay:
|
||||
# 1/true/yes/on -> enable crash monitoring and autotune updates
|
||||
# 0/false/no/off -> disable crash detection/autotune logic
|
||||
kstuff_crash_detection=1
|
||||
# Default: 1
|
||||
# kstuff_crash_detection=1
|
||||
|
||||
# Delay before pausing kstuff for image-backed titles.
|
||||
kstuff_pause_delay_image_seconds=20
|
||||
# Default: 25
|
||||
# kstuff_pause_delay_image_seconds=25
|
||||
|
||||
# Delay before pausing kstuff for direct/non-image launches.
|
||||
kstuff_pause_delay_direct_seconds=10
|
||||
|
||||
# Keep kstuff enabled for specific title IDs (can be repeated).
|
||||
# kstuff_no_pause=PPSA12345
|
||||
|
||||
# Override pause delay for specific title IDs (can be repeated).
|
||||
# Format: TITLEID:SECONDS
|
||||
# kstuff_delay=PPSA12345:30
|
||||
# Default: 15
|
||||
# kstuff_pause_delay_direct_seconds=15
|
||||
|
||||
# Backend-specific sector size overrides:
|
||||
lvd_exfat_sector_size=512
|
||||
lvd_ufs_sector_size=4096
|
||||
lvd_pfs_sector_size=32768
|
||||
md_exfat_sector_size=512
|
||||
md_ufs_sector_size=512
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
|
||||
# lvd_exfat_sector_size=512
|
||||
# lvd_ufs_sector_size=4096
|
||||
# lvd_pfs_sector_size=32768
|
||||
# md_exfat_sector_size=512
|
||||
# md_ufs_sector_size=512
|
||||
|
||||
# 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
|
||||
# scanpath=/data/etaHEN/games
|
||||
|
||||
## Per-image mount mode override by file name (repeatable):
|
||||
## image_ro=<image_filename>
|
||||
## image_rw=<image_filename>
|
||||
## image_sector=<image_filename>:<sector_size>
|
||||
##
|
||||
## Examples:
|
||||
## image_ro=PPSA12345.ffpfs
|
||||
## image_rw=My Game Dump.exfat
|
||||
## image_sector=My Game Dump.exfat:65536
|
||||
|
||||
## Keep kstuff enabled for specific title IDs (repeatable):
|
||||
## kstuff_no_pause=PPSA12345
|
||||
|
||||
## Override pause delay for specific title IDs (repeatable):
|
||||
## Format: TITLEID:SECONDS
|
||||
## kstuff_delay=PPSA12345:30
|
||||
|
||||
## More custom scan root examples:
|
||||
## scanpath=/data/homebrew
|
||||
## scanpath=/data/etaHEN/games
|
||||
+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,21 +24,26 @@ 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,
|
||||
bool *overlay_active_out);
|
||||
// Mount a prepared backport overlay on top of an already mounted title.
|
||||
void mount_backport_overlay(const char *mount_point,
|
||||
// Returns false only when nmount() itself fails.
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
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.
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Start the shared game lifecycle watcher used by backport fakelib and kstuff.
|
||||
// Start the shared game lifecycle watcher used by runtime game features.
|
||||
bool start_game_lifecycle_watcher(void);
|
||||
// Wake the shared watcher so pending lifecycle work is processed immediately.
|
||||
void wake_game_lifecycle_watcher(void);
|
||||
// Stop the shared watcher and let lifecycle modules clean up tracked state.
|
||||
void stop_game_lifecycle_watcher(void);
|
||||
// Start or stop the shared watcher to match the current runtime config.
|
||||
// Return true when a supported game is currently being tracked as running.
|
||||
bool sm_game_lifecycle_has_active_game(void);
|
||||
// Ensure the shared watcher is running for runtime game-state tracking.
|
||||
bool refresh_game_lifecycle_watcher(void);
|
||||
|
||||
#endif
|
||||
+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
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SM_INSTALL_QUEUE_H
|
||||
#define SM_INSTALL_QUEUE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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.
|
||||
void sm_install_poll_pending(void);
|
||||
// Poll async batch installs for success/timeout completion and auto-submit
|
||||
// queued installs when possible.
|
||||
void sm_install_service_pending(void);
|
||||
// Queue a staged title for the next batch install request.
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0);
|
||||
// Submit all queued titles when no batch install is currently pending.
|
||||
bool sm_install_submit_queued(void);
|
||||
// Record retry state for every queued title after a batch submit failure.
|
||||
void sm_install_note_submit_failure(void);
|
||||
|
||||
#endif
|
||||
+4
-2
@@ -23,8 +23,6 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
uint64_t exec_time_us);
|
||||
// Publish an AppFocus change for processing on the lifecycle/kstuff thread.
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id);
|
||||
// Publish an Lnc system status change for crash heuristic tracking.
|
||||
void sm_kstuff_note_lnc_system_status(uint64_t pattern);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us);
|
||||
// Forget a tracked game and restore kstuff when no paused entries remain.
|
||||
@@ -33,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);
|
||||
|
||||
|
||||
+6
-4
@@ -1,16 +1,17 @@
|
||||
#ifndef SM_LIMITS_H
|
||||
#define SM_LIMITS_H
|
||||
|
||||
#define DEFAULT_SCAN_INTERVAL_US 10000000u
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 20u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_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
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_MDBG_H
|
||||
#define SM_MDBG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Initialize mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_init(void);
|
||||
// Shut down mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_shutdown(void);
|
||||
// Start monitoring the tracked game process for crash-candidate state.
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id);
|
||||
// Record that kstuff auto-pause was actually applied to the tracked game.
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds);
|
||||
// Stop monitoring the tracked game process on exit.
|
||||
void sm_mdbg_game_on_exit(pid_t pid);
|
||||
// Clear tracked game monitoring state unconditionally.
|
||||
void sm_mdbg_game_shutdown(void);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
|
||||
// Poll the tracked process for mdbg crash-candidate state.
|
||||
void sm_mdbg_poll(void);
|
||||
|
||||
#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"
|
||||
|
||||
@@ -28,5 +28,15 @@ bool is_image_mount_limited(const char *path);
|
||||
uint8_t bump_image_mount_attempts(const char *path);
|
||||
// Clear image mount retry state for a path.
|
||||
void clear_image_mount_attempts(const char *path);
|
||||
// Return whether backport overlay remounts are suppressed for a path.
|
||||
bool is_backport_mount_blocked(const char *path);
|
||||
// Record the first failed backport overlay mount for a 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"
|
||||
|
||||
@@ -35,10 +35,16 @@ typedef struct {
|
||||
char unknown2[0x3c];
|
||||
} app_info_t;
|
||||
|
||||
static inline uint32_t sm_firmware_major_version(void) {
|
||||
uint32_t fw = kernel_get_fw_version();
|
||||
uint32_t major_bcd = (fw >> 24) & 0xFFu;
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallTitleDir(const char *title_id,
|
||||
const char *install_path, void *reserved);
|
||||
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.
|
||||
|
||||
+3
-4
@@ -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,
|
||||
@@ -19,10 +21,7 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
int max_candidates,
|
||||
int *total_found_out,
|
||||
bool *unstable_found_out);
|
||||
// Mount stable per-root backport overlays for already mounted titles.
|
||||
// Mount stable backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(bool *unstable_found_out);
|
||||
// Mount stable per-root backport overlays for titles owned by one scan root.
|
||||
void mount_backport_overlays_for_scan_root(const char *scan_root,
|
||||
bool *unstable_found_out);
|
||||
|
||||
#endif
|
||||
@@ -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 {
|
||||
@@ -106,10 +108,19 @@ typedef struct runtime_config {
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool backport_fakelib_enabled;
|
||||
bool 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;
|
||||
@@ -129,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 {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
asm(".global \"C49jelxiaVE\"\n"
|
||||
".type \"C49jelxiaVE\" @function\n"
|
||||
"\"C49jelxiaVE\":\n");
|
||||
+140
-6
@@ -17,9 +17,11 @@
|
||||
#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"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#ifndef SHADOWMOUNT_VERSION
|
||||
@@ -31,13 +33,20 @@
|
||||
#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
|
||||
#define AUTHID_BASE 0x4801000000000013L
|
||||
|
||||
//#define AUTHID_BASE 0x4801000000000013L
|
||||
#define AUTHID_BASE 0x4800000000000006ull
|
||||
|
||||
|
||||
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;
|
||||
@@ -49,6 +58,9 @@ static immediate_scan_request_t g_scan_now = {
|
||||
.reason = {0},
|
||||
};
|
||||
|
||||
extern unsigned char config_ini_example[];
|
||||
extern unsigned int config_ini_example_len;
|
||||
|
||||
static void on_signal(int sig) {
|
||||
(void)sig;
|
||||
g_stop_requested = 1;
|
||||
@@ -70,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;
|
||||
@@ -94,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);
|
||||
|
||||
@@ -269,6 +337,60 @@ static void ensure_kstuff_noautomount_file(void) {
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
static bool write_buffer_to_fd(int fd, const unsigned char *buf, size_t size) {
|
||||
size_t offset = 0;
|
||||
while (offset < size) {
|
||||
ssize_t written = write(fd, buf + offset, size - offset);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (written == 0) {
|
||||
errno = EIO;
|
||||
return false;
|
||||
}
|
||||
offset += (size_t)written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensure_runtime_config_file(void) {
|
||||
int fd = open(CONFIG_FILE, O_WRONLY | O_CREAT | O_EXCL, 0666);
|
||||
if (fd < 0) {
|
||||
if (errno == EEXIST)
|
||||
return;
|
||||
printf("[CFG] Failed to create %s: %s\n", CONFIG_FILE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t template_size = (size_t)config_ini_example_len;
|
||||
int saved_errno = 0;
|
||||
if (!write_buffer_to_fd(fd, config_ini_example, template_size))
|
||||
saved_errno = errno;
|
||||
if (close(fd) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
printf("[CFG] Failed to write %s: %s\n", CONFIG_FILE, strerror(errno));
|
||||
(void)unlink(CONFIG_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[CFG] Created default config from template: %s\n", CONFIG_FILE);
|
||||
}
|
||||
|
||||
static void cleanup_kstuff_noautomount_files(void) {
|
||||
if (unlink(KSTUFF_NOAUTOMOUNT_FILE) == 0) {
|
||||
log_debug("[KSTUFF] removed shutdown sentinel: %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
} else if (errno != ENOENT) {
|
||||
log_debug("[KSTUFF] failed to remove %s: %s", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_conflicting_backpork(void) {
|
||||
if (!runtime_config()->backport_fakelib_enabled)
|
||||
return;
|
||||
@@ -305,6 +427,7 @@ int main(void) {
|
||||
install_signal_handlers();
|
||||
|
||||
mkdir(LOG_DIR, 0777);
|
||||
ensure_runtime_config_file();
|
||||
ensure_kstuff_noautomount_file();
|
||||
existing_pid = find_pid_by_name(PAYLOAD_NAME, true);
|
||||
if (existing_pid < 0) {
|
||||
@@ -348,6 +471,7 @@ int main(void) {
|
||||
stop_conflicting_backpork();
|
||||
if (!sm_shellcore_flags_start())
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
sm_mdbg_init();
|
||||
sm_kstuff_init();
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
@@ -375,10 +499,18 @@ int main(void) {
|
||||
goto shutdown;
|
||||
}
|
||||
|
||||
log_debug("[STARTUP] cleanup_staged_mount_links begin");
|
||||
cleanup_staged_mount_links();
|
||||
log_debug("[STARTUP] cleanup_duplicate_title_mounts begin");
|
||||
cleanup_duplicate_title_mounts();
|
||||
if (!sm_scanner_run_startup_sync())
|
||||
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");
|
||||
goto shutdown;
|
||||
}
|
||||
log_debug("[STARTUP] scanner startup sync done");
|
||||
sm_scanner_run_loop();
|
||||
|
||||
shutdown:
|
||||
@@ -386,6 +518,8 @@ shutdown:
|
||||
stop_game_lifecycle_watcher();
|
||||
sm_scanner_shutdown();
|
||||
sm_kstuff_shutdown();
|
||||
sm_mdbg_shutdown();
|
||||
cleanup_kstuff_noautomount_files();
|
||||
shutdown_title_mounts();
|
||||
if (!shutdown_image_mounts()) {
|
||||
log_debug("[SHUTDOWN] some image mounts or devices were not fully released");
|
||||
|
||||
+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;
|
||||
}
|
||||
+195
-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,
|
||||
@@ -96,6 +98,7 @@ static bool lookup_kstuff_delay_override_in_file(const char *path,
|
||||
static bool upsert_kstuff_delay_override_in_file(const char *path,
|
||||
const char *title_id,
|
||||
uint32_t delay_seconds);
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state);
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
@@ -149,6 +152,19 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state) {
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
state->cfg.app_install_all_enabled = true;
|
||||
state->cfg.app_install_all_forced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state->cfg.app_install_all_forced = false;
|
||||
}
|
||||
|
||||
static const runtime_config_state_t *active_runtime_state(void) {
|
||||
return &g_runtime_state_slots[atomic_load_explicit(&g_runtime_state_active_index,
|
||||
memory_order_acquire)];
|
||||
@@ -208,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) {
|
||||
@@ -228,10 +249,18 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.quiet_mode = false;
|
||||
state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
state->cfg.force_mount = false;
|
||||
state->cfg.app_install_all_enabled = false;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
state->cfg.backport_fakelib_enabled = true;
|
||||
state->cfg.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;
|
||||
@@ -249,6 +278,7 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules));
|
||||
clear_kstuff_title_rules(state);
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
apply_firmware_runtime_overrides(state);
|
||||
}
|
||||
|
||||
static config_file_stamp_t read_config_file_stamp(void) {
|
||||
@@ -279,20 +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.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,
|
||||
@@ -340,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)
|
||||
@@ -410,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();
|
||||
@@ -491,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)
|
||||
@@ -514,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);
|
||||
@@ -550,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)
|
||||
@@ -576,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)
|
||||
@@ -1014,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)
|
||||
@@ -1119,6 +1192,16 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "app_install_all") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.app_install_all_enabled = bval;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "image_ro") == 0 ||
|
||||
strcasecmp(key, "image_rw") == 0) {
|
||||
bool rule_read_only = (strcasecmp(key, "image_ro") == 0);
|
||||
@@ -1160,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);
|
||||
@@ -1169,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);
|
||||
@@ -1321,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;
|
||||
@@ -1331,6 +1489,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
log_debug(" [CFG] recursive_scan=1 is deprecated; forcing scan_depth=2");
|
||||
}
|
||||
|
||||
apply_firmware_runtime_overrides(state);
|
||||
|
||||
int image_rule_count = 0;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (state->image_mode_rules[k].valid)
|
||||
@@ -1343,8 +1503,12 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
kstuff_delay_rule_count++;
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d scan_depth=%u "
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d "
|
||||
"app_install_all=%d app_install_all_forced=%d scan_depth=%u "
|
||||
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
|
||||
"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 "
|
||||
@@ -1353,9 +1517,17 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
"kstuff_no_pause=%d kstuff_delay_rules=%d",
|
||||
state->cfg.debug_enabled ? 1 : 0, state->cfg.quiet_mode ? 1 : 0,
|
||||
state->cfg.mount_read_only ? 1 : 0,
|
||||
state->cfg.force_mount ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.force_mount ? 1 : 0,
|
||||
state->cfg.app_install_all_enabled ? 1 : 0,
|
||||
state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.legacy_recursive_scan_forced ? 1 : 0,
|
||||
state->cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.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);
|
||||
}
|
||||
+190
-31
@@ -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) {
|
||||
@@ -509,20 +541,20 @@ bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
return backport_present;
|
||||
}
|
||||
|
||||
void mount_backport_overlay(const char *mount_point,
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id) {
|
||||
struct stat backport_st;
|
||||
if (stat(backport_path, &backport_st) != 0 || !S_ISDIR(backport_st.st_mode))
|
||||
return;
|
||||
return true;
|
||||
|
||||
struct statfs mounted_sfs;
|
||||
if (statfs(mount_point, &mounted_sfs) != 0 ||
|
||||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0)
|
||||
return;
|
||||
return true;
|
||||
|
||||
bool mount_read_only = ((mounted_sfs.f_flags & MNT_RDONLY) != 0);
|
||||
struct iovec overlay_iov[] = {
|
||||
@@ -530,13 +562,15 @@ void mount_backport_overlay(const char *mount_point,
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(backport_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
|
||||
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
|
||||
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
|
||||
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)
|
||||
};
|
||||
|
||||
int overlay_flags = mount_read_only ? MNT_RDONLY : 0;
|
||||
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), overlay_flags) == 0) {
|
||||
log_debug(" [IMG] backport overlay mounted (%s): %s -> %s",
|
||||
mount_read_only ? "ro" : "rw", backport_path, mount_point);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
int overlay_err = errno;
|
||||
@@ -544,6 +578,7 @@ void mount_backport_overlay(const char *mount_point,
|
||||
mount_point, strerror(overlay_err));
|
||||
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
|
||||
backport_path, (uint32_t)overlay_err);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
@@ -559,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) {
|
||||
@@ -575,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. "
|
||||
@@ -607,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;
|
||||
@@ -646,6 +743,8 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
|
||||
if (ret == 0 && hit)
|
||||
log_debug(" [COPY] param.json patched: %s", dst);
|
||||
if (ret != 0)
|
||||
(void)unlink(dst);
|
||||
|
||||
free(buf);
|
||||
return ret;
|
||||
@@ -684,6 +783,8 @@ int copy_file(const char *src, const char *dst) {
|
||||
ret = -1;
|
||||
if (fclose(fs) != 0)
|
||||
ret = -1;
|
||||
if (ret != 0)
|
||||
(void)unlink(dst);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -701,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;
|
||||
@@ -754,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;
|
||||
@@ -786,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,
|
||||
@@ -804,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;
|
||||
@@ -823,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);
|
||||
@@ -844,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,
|
||||
@@ -870,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;
|
||||
@@ -881,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 &&
|
||||
@@ -889,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 {
|
||||
@@ -906,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;
|
||||
}
|
||||
@@ -932,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 : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -969,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) {
|
||||
@@ -987,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;
|
||||
}
|
||||
|
||||
|
||||
+140
-14
@@ -2,14 +2,19 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#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"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
@@ -33,9 +38,45 @@ static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
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);
|
||||
|
||||
static void publish_active_game_pid(pid_t pid) {
|
||||
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
|
||||
if (previous_pid == pid)
|
||||
return;
|
||||
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;
|
||||
}
|
||||
@@ -133,6 +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, 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;
|
||||
@@ -157,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)
|
||||
@@ -264,6 +331,7 @@ static const struct timespec *compute_game_wait_timeout(
|
||||
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
|
||||
if (next_wake_us == 0)
|
||||
return NULL;
|
||||
|
||||
@@ -314,6 +382,7 @@ static void poll_game_modules(int kq) {
|
||||
}
|
||||
|
||||
sm_kstuff_game_poll();
|
||||
sm_mdbg_poll();
|
||||
}
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid) {
|
||||
@@ -352,11 +421,38 @@ 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);
|
||||
if (atomic_load(&g_active_game_pid) == 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) {
|
||||
@@ -399,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);
|
||||
@@ -415,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 {
|
||||
@@ -428,20 +557,21 @@ 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(0, NULL);
|
||||
close(kq);
|
||||
log_debug(" [GAME] lifecycle watcher stopped");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool start_game_lifecycle_watcher(void) {
|
||||
if (!sm_fakelib_game_feature_enabled() && !sm_kstuff_game_feature_enabled())
|
||||
return true;
|
||||
if (g_game_lifecycle_thread_started)
|
||||
return true;
|
||||
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
|
||||
@@ -508,19 +638,15 @@ 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(0, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
bool sm_game_lifecycle_has_active_game(void) {
|
||||
return atomic_load(&g_active_game_pid) > 0;
|
||||
}
|
||||
|
||||
bool refresh_game_lifecycle_watcher(void) {
|
||||
bool should_run =
|
||||
sm_fakelib_game_feature_enabled() || sm_kstuff_game_feature_enabled();
|
||||
|
||||
if (!should_run) {
|
||||
if (g_game_lifecycle_thread_started)
|
||||
stop_game_lifecycle_watcher();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return start_game_lifecycle_watcher();
|
||||
|
||||
|
||||
+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, "", "");
|
||||
|
||||
+353
-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).
|
||||
@@ -889,6 +1156,7 @@ void cleanup_stale_image_mounts(void) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
|
||||
log_debug(" [IMG] stale image cleanup begin");
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (should_stop_requested())
|
||||
@@ -896,6 +1164,10 @@ void cleanup_stale_image_mounts(void) {
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
|
||||
log_debug(" [IMG][%s] stale cleanup check: slot=%d source=%s mount=%s",
|
||||
attach_backend_name(cached_entry.backend), k, cached_entry.path,
|
||||
cached_entry.mount_point);
|
||||
|
||||
if (!path_exists(cached_entry.path)) {
|
||||
log_debug(" [IMG][%s] Source removed, unmounting: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
@@ -905,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))
|
||||
@@ -922,6 +1195,7 @@ void cleanup_stale_image_mounts(void) {
|
||||
notify_image_mount_failed(source_path, mount_err);
|
||||
}
|
||||
}
|
||||
log_debug(" [IMG] stale image cleanup done");
|
||||
}
|
||||
|
||||
void cleanup_stale_image_mounts_for_root(const char *root) {
|
||||
@@ -953,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))
|
||||
@@ -972,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;
|
||||
}
|
||||
|
||||
@@ -986,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) {
|
||||
@@ -1015,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;
|
||||
}
|
||||
+205
-20
@@ -1,6 +1,8 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_log.h"
|
||||
@@ -11,6 +13,53 @@
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_manual.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
typedef int (*app_install_title_dir_fn_t)(const char *title_id,
|
||||
const char *install_path,
|
||||
void *reserved);
|
||||
|
||||
static const char *const k_app_inst_util_sprx_path =
|
||||
"/system/common/lib/libSceAppInstUtil.sprx";
|
||||
static const char *const k_app_install_title_dir_symbol =
|
||||
"sceAppInstUtilAppInstallTitleDir";
|
||||
|
||||
static app_install_title_dir_fn_t g_app_install_title_dir_fn = NULL;
|
||||
static bool g_app_install_title_dir_resolve_attempted = false;
|
||||
|
||||
static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
if (g_app_install_title_dir_fn)
|
||||
return g_app_install_title_dir_fn;
|
||||
if (g_app_install_title_dir_resolve_attempted)
|
||||
return NULL;
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
g_app_install_title_dir_resolve_attempted = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_app_install_title_dir_resolve_attempted = true;
|
||||
void *app_inst_util_handle = dlopen(k_app_inst_util_sprx_path, RTLD_LAZY);
|
||||
if (!app_inst_util_handle) {
|
||||
const char *error = dlerror();
|
||||
log_debug(" [REG] dlopen(%s) failed: %s", k_app_inst_util_sprx_path,
|
||||
error ? error : "unknown");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dlerror();
|
||||
g_app_install_title_dir_fn = (app_install_title_dir_fn_t)dlsym(
|
||||
app_inst_util_handle, k_app_install_title_dir_symbol);
|
||||
if (g_app_install_title_dir_fn)
|
||||
return g_app_install_title_dir_fn;
|
||||
|
||||
const char *error = dlerror();
|
||||
log_debug(" [REG] %s lookup in %s failed: %s",
|
||||
k_app_install_title_dir_symbol, k_app_inst_util_sprx_path,
|
||||
error ? error : "unknown");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
@@ -79,13 +128,66 @@ 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;
|
||||
}
|
||||
|
||||
// --- Install/Remount Action ---
|
||||
static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
bool should_register) {
|
||||
bool should_register,
|
||||
bool use_app_install_all,
|
||||
bool *has_src_snd0_out) {
|
||||
char user_appmeta_dir[MAX_PATH];
|
||||
char user_app_dir[MAX_PATH];
|
||||
char user_sce_sys[MAX_PATH];
|
||||
@@ -112,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);
|
||||
@@ -120,14 +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() || 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);
|
||||
@@ -162,6 +265,12 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
metadata_restaged = true;
|
||||
}
|
||||
|
||||
if (!update_trophy_metadata(title_id, src_sce_sys))
|
||||
return false;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
|
||||
src_path);
|
||||
@@ -170,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);
|
||||
|
||||
@@ -182,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)) {
|
||||
@@ -194,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) {
|
||||
@@ -206,18 +333,31 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (use_app_install_all) {
|
||||
if (has_src_snd0_out)
|
||||
*has_src_snd0_out = has_src_snd0;
|
||||
log_debug(" [REG] Prepared for batch install: %s (%s)", title_name,
|
||||
title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
if (!metadata_restaged) {
|
||||
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
|
||||
has_src_snd0 = path_exists(src_snd0);
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = sceAppInstUtilAppInstallTitleDir(title_id, APP_BASE "/", 0);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
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) {
|
||||
@@ -227,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);
|
||||
@@ -247,24 +388,65 @@ 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];
|
||||
bool has_src_snd0 = false;
|
||||
bool use_app_install_all =
|
||||
runtime_config()->app_install_all_enabled && !c->in_app_db;
|
||||
if (c->installed) {
|
||||
log_debug(" [ACTION] Remounting: %s", c->title_name);
|
||||
} else {
|
||||
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
if (!use_app_install_all) {
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (mount_and_install(c->path, c->title_id, c->title_name, c->installed,
|
||||
!c->in_app_db)) {
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
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() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
} else {
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
}
|
||||
} else {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
@@ -274,4 +456,7 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
|
||||
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
|
||||
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
|
||||
|
||||
typedef enum {
|
||||
INSTALL_TRACK_NONE = 0,
|
||||
INSTALL_TRACK_QUEUED,
|
||||
INSTALL_TRACK_SUBMITTED,
|
||||
} install_track_state_t;
|
||||
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char source_path[MAX_PATH];
|
||||
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;
|
||||
|
||||
static pending_install_entry_t g_pending_installs[MAX_PENDING];
|
||||
static uint64_t g_pending_install_poll_due_us = 0;
|
||||
static uint64_t g_queued_install_submit_due_us = 0;
|
||||
static int g_tracked_install_count = 0;
|
||||
static int g_submitted_install_count = 0;
|
||||
static bool g_queued_install_batch_announced = false;
|
||||
static bool g_queued_install_submit_failure_notified = false;
|
||||
|
||||
static int count_queued_installs(void);
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry);
|
||||
|
||||
static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
continue;
|
||||
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *reserve_pending_install_entry(
|
||||
const char *title_id) {
|
||||
pending_install_entry_t *entry = find_pending_install_entry(title_id);
|
||||
if (entry)
|
||||
return entry;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint64_t next_pending_install_timeout_us(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return 0;
|
||||
|
||||
uint64_t next_deadline = 0;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
|
||||
continue;
|
||||
|
||||
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
|
||||
if (next_deadline == 0 || deadline_us < next_deadline)
|
||||
next_deadline = deadline_us;
|
||||
}
|
||||
|
||||
return next_deadline;
|
||||
}
|
||||
|
||||
static void schedule_pending_install_poll(uint64_t now_us) {
|
||||
if (g_submitted_install_count <= 0) {
|
||||
g_pending_install_poll_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_pending_install_poll_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit_retry(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
|
||||
}
|
||||
|
||||
static void clear_pending_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
|
||||
g_submitted_install_count--;
|
||||
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
|
||||
g_tracked_install_count--;
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
if (g_submitted_install_count <= 0)
|
||||
g_pending_install_poll_due_us = 0;
|
||||
if (g_tracked_install_count <= 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_title_install_pending(const char *title_id) {
|
||||
return find_pending_install_entry(title_id) != NULL;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
|
||||
return;
|
||||
|
||||
size_t used = strlen(message);
|
||||
if (used >= message_size)
|
||||
return;
|
||||
|
||||
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
|
||||
title_name, title_id);
|
||||
if (written < 0 || (size_t)written >= message_size - used)
|
||||
return;
|
||||
|
||||
(*shown_count)++;
|
||||
}
|
||||
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0) {
|
||||
if (!candidate || candidate->title_id[0] == '\0' ||
|
||||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!runtime_config()->app_install_all_enabled)
|
||||
return false;
|
||||
|
||||
bool was_queue_empty = (count_queued_installs() <= 0);
|
||||
pending_install_entry_t *entry =
|
||||
reserve_pending_install_entry(candidate->title_id);
|
||||
if (!entry) {
|
||||
log_debug(" [REG] install queue full, cannot queue %s (%s)",
|
||||
candidate->title_name, candidate->title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
install_track_state_t previous_state = entry->state;
|
||||
if (previous_state == INSTALL_TRACK_SUBMITTED)
|
||||
return true;
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, candidate->title_name,
|
||||
sizeof(entry->title_name));
|
||||
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
|
||||
(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)
|
||||
g_tracked_install_count++;
|
||||
if (was_queue_empty) {
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
schedule_queued_install_submit(monotonic_time_us());
|
||||
return true;
|
||||
}
|
||||
|
||||
static void finalize_pending_install_success(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
|
||||
notify_game_installed_rich(entry->title_id);
|
||||
if (entry->has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(entry->title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
|
||||
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);
|
||||
}
|
||||
|
||||
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
entry->title_name, entry->title_id);
|
||||
(void)note_pending_install_failure(entry);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return;
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
|
||||
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();
|
||||
|
||||
if (!app_db_titles_ready)
|
||||
log_debug(" [DB] app.db title list unavailable while polling installs");
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED)
|
||||
continue;
|
||||
|
||||
if (app_db_titles_ready &&
|
||||
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
|
||||
finalize_pending_install_success(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now_us != 0 && entry->requested_at_us != 0 &&
|
||||
now_us >= entry->requested_at_us &&
|
||||
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
|
||||
finalize_pending_install_timeout(entry);
|
||||
}
|
||||
}
|
||||
|
||||
schedule_pending_install_poll(now_us);
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
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;
|
||||
|
||||
if (g_submitted_install_count <= 0)
|
||||
return g_queued_install_submit_due_us;
|
||||
|
||||
uint64_t next_wake_us = next_pending_install_timeout_us();
|
||||
|
||||
if (g_pending_install_poll_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
|
||||
next_wake_us = g_pending_install_poll_due_us;
|
||||
}
|
||||
|
||||
if (g_queued_install_submit_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
|
||||
next_wake_us = g_queued_install_submit_due_us;
|
||||
}
|
||||
|
||||
if (next_wake_us == 0)
|
||||
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_install_poll_pending(void) {
|
||||
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;
|
||||
|
||||
sm_install_poll_pending();
|
||||
if (count_queued_installs() > 0 && !sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
|
||||
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
|
||||
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
|
||||
entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static int count_queued_installs(void) {
|
||||
int queued_count = g_tracked_install_count - g_submitted_install_count;
|
||||
return queued_count > 0 ? queued_count : 0;
|
||||
}
|
||||
|
||||
static void log_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void notify_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0 || g_queued_install_batch_announced)
|
||||
return;
|
||||
|
||||
char message[3075];
|
||||
int shown_count = 0;
|
||||
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
append_batch_install_line(message, sizeof(message), entry->title_name,
|
||||
entry->title_id, &shown_count);
|
||||
}
|
||||
|
||||
if (shown_count < queued_count) {
|
||||
size_t used = strlen(message);
|
||||
if (used < sizeof(message)) {
|
||||
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
|
||||
queued_count - shown_count);
|
||||
}
|
||||
}
|
||||
|
||||
notify_system_info("%s", message);
|
||||
g_queued_install_batch_announced = true;
|
||||
}
|
||||
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return 0;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
|
||||
log_batch_submit_retry_limit(entry);
|
||||
return failed_attempts;
|
||||
}
|
||||
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
bool sm_install_submit_queued(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return true;
|
||||
|
||||
if (g_submitted_install_count > 0)
|
||||
return true;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
|
||||
now_us < g_queued_install_submit_due_us) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
queued_count, (uint32_t)res);
|
||||
g_queued_install_submit_failure_notified = true;
|
||||
}
|
||||
schedule_queued_install_submit_retry(now_us);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
entry->state = INSTALL_TRACK_SUBMITTED;
|
||||
entry->requested_at_us = now_us;
|
||||
g_submitted_install_count++;
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
}
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
schedule_pending_install_poll(now_us);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_install_note_submit_failure(void) {
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
|
||||
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
|
||||
drop_queued_install_entry(entry);
|
||||
}
|
||||
}
|
||||
+65
-111
@@ -5,7 +5,9 @@
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#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"
|
||||
|
||||
@@ -14,9 +16,6 @@
|
||||
#define KSTUFF_SYSENTVEC_TOGGLE_OFFSET 14
|
||||
#define KSTUFF_SYSENTVEC_ENABLED 0xdeb7u
|
||||
#define KSTUFF_SYSENTVEC_DISABLED 0xffffu
|
||||
#define KSTUFF_CRASH_HEURISTIC_MAX_RUNTIME_US (120ull * 1000000ull)
|
||||
#define KSTUFF_CRASH_HEURISTIC_MAX_POST_AUTOPAUSE_RUNTIME_US (10ull * 1000000ull)
|
||||
#define KSTUFF_CRASH_HEURISTIC_APPFOCUS_TO_LNC_WINDOW_US (5ull * 1000000ull)
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool image_backed;
|
||||
@@ -27,7 +26,6 @@ typedef struct {
|
||||
uint32_t app_id;
|
||||
uint32_t autopause_delay_seconds;
|
||||
uint64_t launch_time_us;
|
||||
uint64_t auto_pause_time_us;
|
||||
uint64_t pause_deadline_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} kstuff_game_entry_t;
|
||||
@@ -38,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;
|
||||
@@ -48,17 +47,6 @@ static kstuff_state_t g_kstuff;
|
||||
static _Atomic uint32_t g_pending_app_focus_id;
|
||||
static _Atomic bool g_pending_app_focus_valid;
|
||||
static _Atomic bool g_pending_config_reload;
|
||||
static _Atomic uint64_t g_last_relevant_app_focus_time_us;
|
||||
static _Atomic bool g_crash_sequence_seen_since_launch;
|
||||
|
||||
static bool kstuff_crash_detection_enabled(void) {
|
||||
return runtime_config()->kstuff_crash_detection_enabled;
|
||||
}
|
||||
|
||||
static bool should_log_kstuff_crash_heuristic(const kstuff_game_entry_t *entry,
|
||||
uint64_t exit_time_us,
|
||||
uint64_t *elapsed_us_out);
|
||||
static bool is_ignored_focus_app_id(uint32_t app_id);
|
||||
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry);
|
||||
|
||||
static bool refresh_kstuff_support_state(void);
|
||||
@@ -333,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;
|
||||
}
|
||||
@@ -415,8 +410,8 @@ static void finish_tracked_game_clear(const char *reason) {
|
||||
}
|
||||
|
||||
static void apply_kstuff_config_reload(void) {
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, 0);
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, false);
|
||||
if (!runtime_config()->kstuff_crash_detection_enabled)
|
||||
sm_mdbg_game_shutdown();
|
||||
|
||||
if (!runtime_config()->kstuff_game_auto_toggle) {
|
||||
sm_kstuff_game_shutdown();
|
||||
@@ -430,6 +425,7 @@ static void apply_kstuff_config_reload(void) {
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
log_debug(" [KSTUFF] cleared tracked game by config reload: %s",
|
||||
g_kstuff.game.title_id);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("config reload");
|
||||
@@ -555,6 +551,7 @@ static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
log_debug(" [KSTUFF] skipping auto-pause for %s pid=%ld: kstuff not "
|
||||
"available",
|
||||
entry->title_id, (long)entry->pid);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
return;
|
||||
}
|
||||
@@ -570,7 +567,8 @@ static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
|
||||
if (was_enabled) {
|
||||
mark_restore_needed();
|
||||
entry->auto_pause_time_us = now_us;
|
||||
sm_mdbg_game_on_kstuff_pause(entry->pid, now_us,
|
||||
get_effective_pause_delay_seconds(entry));
|
||||
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
|
||||
entry->title_id, (long)entry->pid,
|
||||
entry->image_backed ? "image" : "direct");
|
||||
@@ -746,6 +744,7 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
log_debug(" [KSTUFF] handoff tracked game pid=%ld (%s) -> pid=%ld (%s)",
|
||||
(long)g_kstuff.game.pid, g_kstuff.game.title_id, (long)pid,
|
||||
title_id);
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("tracked game handoff");
|
||||
@@ -775,12 +774,10 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
g_kstuff.game.app_id = app_id;
|
||||
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
|
||||
g_kstuff.game.launch_time_us = base_time_us;
|
||||
g_kstuff.game.auto_pause_time_us = 0;
|
||||
g_kstuff.game.pause_deadline_us =
|
||||
base_time_us == 0 ? 0 : base_time_us + (uint64_t)delay_seconds * 1000000ull;
|
||||
(void)strlcpy(g_kstuff.game.title_id, title_id, sizeof(g_kstuff.game.title_id));
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, 0);
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, false);
|
||||
sm_mdbg_game_on_exec(pid, title_id, app_id);
|
||||
|
||||
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
|
||||
"pause_delay=%us", title_id, (long)pid, app_id,
|
||||
@@ -788,63 +785,19 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
}
|
||||
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id) {
|
||||
if (kstuff_crash_detection_enabled() && g_kstuff.game.active &&
|
||||
!is_ignored_focus_app_id(app_id)) {
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, monotonic_time_us());
|
||||
}
|
||||
atomic_store(&g_pending_app_focus_id, app_id);
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
}
|
||||
|
||||
void sm_kstuff_note_lnc_system_status(uint64_t pattern) {
|
||||
if (!kstuff_crash_detection_enabled())
|
||||
return;
|
||||
if (pattern != 0x0000000000000002ULL) // SHELLUI_FG_GAME_BG_CPU_MODE
|
||||
return;
|
||||
if (!g_kstuff.game.active)
|
||||
return;
|
||||
|
||||
uint64_t app_focus_time_us = atomic_load(&g_last_relevant_app_focus_time_us);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (app_focus_time_us == 0 || now_us == 0 || now_us < app_focus_time_us)
|
||||
return;
|
||||
if (now_us - app_focus_time_us >
|
||||
KSTUFF_CRASH_HEURISTIC_APPFOCUS_TO_LNC_WINDOW_US) {
|
||||
return;
|
||||
}
|
||||
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, true);
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exit(pid_t pid) {
|
||||
if (!g_kstuff.game.active || g_kstuff.game.pid != pid)
|
||||
return;
|
||||
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
uint64_t elapsed_us = 0;
|
||||
if (should_log_kstuff_crash_heuristic(&g_kstuff.game, monotonic_time_us(),
|
||||
&elapsed_us)) {
|
||||
log_debug(" [GAME] detected app crash after kstuff auto-pause: "
|
||||
"%s %us after launch",
|
||||
g_kstuff.game.title_id, (unsigned)(elapsed_us / 1000000ull));
|
||||
uint32_t current_delay_seconds =
|
||||
get_effective_pause_delay_seconds(&g_kstuff.game);
|
||||
uint32_t tuned_delay_seconds = 0;
|
||||
if (upsert_kstuff_autotune_pause_delay(g_kstuff.game.title_id,
|
||||
current_delay_seconds,
|
||||
&tuned_delay_seconds)) {
|
||||
log_debug(" [KSTUFF] autotune pause delay updated: %s=%us",
|
||||
g_kstuff.game.title_id, tuned_delay_seconds);
|
||||
notify_system_info("Crash detected: pause delay for %s increased to %us. "
|
||||
"Launch the game again.",
|
||||
g_kstuff.game.title_id, tuned_delay_seconds);
|
||||
}
|
||||
}
|
||||
sm_mdbg_game_on_exit(pid);
|
||||
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
|
||||
g_kstuff.game.title_id, (long)pid);
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, 0);
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, false);
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("tracked game exit");
|
||||
}
|
||||
@@ -866,9 +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) {
|
||||
@@ -880,8 +877,6 @@ void sm_kstuff_init(void) {
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
atomic_store(&g_pending_config_reload, false);
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, 0);
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, false);
|
||||
|
||||
if (!resolve_kstuff_sysentvec_addrs(&g_kstuff.sysentvec_ps5,
|
||||
&g_kstuff.sysentvec_ps4)) {
|
||||
@@ -908,50 +903,9 @@ void sm_kstuff_shutdown(void) {
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
atomic_store(&g_pending_config_reload, false);
|
||||
atomic_store(&g_last_relevant_app_focus_time_us, 0);
|
||||
atomic_store(&g_crash_sequence_seen_since_launch, false);
|
||||
memset(&g_kstuff, 0, sizeof(g_kstuff));
|
||||
}
|
||||
|
||||
static bool should_log_kstuff_crash_heuristic(const kstuff_game_entry_t *entry,
|
||||
uint64_t exit_time_us,
|
||||
uint64_t *elapsed_us_out) {
|
||||
if (elapsed_us_out)
|
||||
*elapsed_us_out = 0;
|
||||
if (!kstuff_crash_detection_enabled())
|
||||
return false;
|
||||
if (!entry || !entry->active || entry->launch_time_us == 0 || exit_time_us == 0)
|
||||
return false;
|
||||
if (exit_time_us <= entry->launch_time_us)
|
||||
return false;
|
||||
|
||||
uint64_t elapsed_us = exit_time_us - entry->launch_time_us;
|
||||
if (elapsed_us >= KSTUFF_CRASH_HEURISTIC_MAX_RUNTIME_US)
|
||||
return false;
|
||||
if (entry->auto_pause_time_us == 0 || entry->auto_pause_time_us < entry->launch_time_us ||
|
||||
entry->auto_pause_time_us > exit_time_us) {
|
||||
return false;
|
||||
}
|
||||
if (exit_time_us - entry->auto_pause_time_us >
|
||||
KSTUFF_CRASH_HEURISTIC_MAX_POST_AUTOPAUSE_RUNTIME_US) {
|
||||
return false;
|
||||
}
|
||||
if (!atomic_load(&g_crash_sequence_seen_since_launch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elapsed_us_out)
|
||||
*elapsed_us_out = elapsed_us;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_ignored_focus_app_id(uint32_t app_id) {
|
||||
if (app_id == 0)
|
||||
return true;
|
||||
return g_kstuff.game.active && g_kstuff.game.app_id != 0 &&
|
||||
app_id == g_kstuff.game.app_id;
|
||||
}
|
||||
|
||||
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry) {
|
||||
if (!entry || entry->launch_time_us == 0 || entry->pause_deadline_us == 0 ||
|
||||
entry->pause_deadline_us <= entry->launch_time_us) {
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
int sceKernelDebugGetPrivateLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
int sceKernelDebugGetSdkLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
uint64_t sceKernelDebugGetLogBufferSize(void) __asm__("C49jelxiaVE");
|
||||
int mdbg_call(void *cmd, void *req, void *res);
|
||||
|
||||
#define MDBG_CMD_TYPE_SERVICE 1ull
|
||||
#define MDBG_CMD_PROCESS_STATE 30ull
|
||||
#define MDBG_SUBCMD_FLAGS 2ull
|
||||
|
||||
#define MDBG_FLAG_EXCEPTION_STOP 0x00080000ull
|
||||
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
|
||||
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
|
||||
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
|
||||
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_USE_PRIVATE_LOG_TEXT 0
|
||||
#endif
|
||||
#ifndef MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define MDBG_SKIP_PRIVILEGE_ELEVATION 0
|
||||
#endif
|
||||
|
||||
#if MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetPrivateLogText
|
||||
#else
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetSdkLogText
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint64_t type;
|
||||
uint64_t cmd;
|
||||
} mdbg_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t pid;
|
||||
int64_t subcmd;
|
||||
uint64_t arg;
|
||||
uint64_t reserved[5];
|
||||
} mdbg_req_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
uint64_t reserved[2];
|
||||
} mdbg_res_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool log_monitoring_active;
|
||||
bool pause_seen;
|
||||
pid_t pid;
|
||||
uint32_t pause_delay_seconds;
|
||||
uint64_t monitor_deadline_us;
|
||||
uint64_t pause_time_us;
|
||||
uint64_t next_poll_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char rtld_error_prefix[MDBG_RTLD_ERROR_PREFIX_SIZE];
|
||||
} mdbg_game_state_t;
|
||||
|
||||
typedef struct {
|
||||
bool privilege_probe_done;
|
||||
bool privilege_ready;
|
||||
size_t log_buffer_size;
|
||||
size_t log_snapshot_length;
|
||||
size_t log_line_length;
|
||||
char *log_snapshot;
|
||||
char *log_storage;
|
||||
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
|
||||
mdbg_game_state_t game;
|
||||
} sm_mdbg_state_t;
|
||||
|
||||
static sm_mdbg_state_t g_mdbg;
|
||||
|
||||
static bool sm_mdbg_enabled(void);
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
static int elevate_to_coredump(void);
|
||||
#endif
|
||||
static bool ensure_mdbg_privileges(void);
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out);
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
|
||||
static bool is_mdbg_process_gone_error(int ret);
|
||||
static void reset_log_line_buffer(void);
|
||||
static void reset_log_snapshot(void);
|
||||
static void free_log_buffers(void);
|
||||
static bool ensure_log_buffers(void);
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out);
|
||||
static size_t find_log_overlap(const char *current, size_t current_len);
|
||||
static void update_log_snapshot(const char *text, size_t text_len);
|
||||
static void clear_tracked_game(void);
|
||||
static bool matches_tracked_rtld_error(const char *text);
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error);
|
||||
static void handle_pre_pause_failure(const char *reason);
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
|
||||
static void process_log_line(const char *line, uint64_t now_us);
|
||||
static void append_log_char(char ch, uint64_t now_us);
|
||||
static void poll_log_monitor(uint64_t now_us);
|
||||
static void start_log_monitoring(void);
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
|
||||
|
||||
static bool sm_mdbg_enabled(void) {
|
||||
return runtime_config()->kstuff_crash_detection_enabled &&
|
||||
runtime_config()->kstuff_game_auto_toggle;
|
||||
}
|
||||
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define SCE_AUTHID_COREDUMP 0x4800000000000006ull
|
||||
static int elevate_to_coredump(void) {
|
||||
static const uint8_t k_priv_caps[16] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
||||
pid_t pid = getpid();
|
||||
|
||||
if (kernel_set_ucred_authid(pid, SCE_AUTHID_COREDUMP) < 0)
|
||||
return -1;
|
||||
if (kernel_set_ucred_caps(pid, k_priv_caps) < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool ensure_mdbg_privileges(void) {
|
||||
if (g_mdbg.privilege_probe_done)
|
||||
return g_mdbg.privilege_ready;
|
||||
|
||||
g_mdbg.privilege_probe_done = true;
|
||||
#if MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
g_mdbg.privilege_ready = true;
|
||||
log_debug(" [MDBG] coredump privilege probe skipped");
|
||||
#else
|
||||
g_mdbg.privilege_ready = elevate_to_coredump() == 0;
|
||||
if (g_mdbg.privilege_ready) {
|
||||
log_debug(" [MDBG] coredump privileges enabled");
|
||||
} else {
|
||||
log_debug(" [MDBG] failed to enable coredump privileges; mdbg polling "
|
||||
"disabled, log monitoring disabled");
|
||||
}
|
||||
#endif
|
||||
|
||||
return g_mdbg.privilege_ready;
|
||||
}
|
||||
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out) {
|
||||
mdbg_cmd_t cmd = {MDBG_CMD_TYPE_SERVICE, MDBG_CMD_PROCESS_STATE};
|
||||
mdbg_req_t req = {
|
||||
.pid = pid,
|
||||
.subcmd = (int64_t)subcmd,
|
||||
.arg = arg,
|
||||
};
|
||||
mdbg_res_t res = {0};
|
||||
int ret = mdbg_call(&cmd, &req, &res);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
if (status_out)
|
||||
*status_out = res.status;
|
||||
if (value_out)
|
||||
*value_out = res.value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
if (status != 0)
|
||||
return (int)status;
|
||||
if (flags_out)
|
||||
*flags_out = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool is_mdbg_process_gone_error(int ret) {
|
||||
return ret == -ESRCH || ret == -ENOENT || ret == ESRCH || ret == ENOENT;
|
||||
}
|
||||
|
||||
static void reset_log_line_buffer(void) {
|
||||
g_mdbg.log_line_length = 0;
|
||||
g_mdbg.log_line[0] = '\0';
|
||||
}
|
||||
|
||||
static void reset_log_snapshot(void) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void free_log_buffers(void) {
|
||||
free(g_mdbg.log_snapshot);
|
||||
free(g_mdbg.log_storage);
|
||||
g_mdbg.log_snapshot = NULL;
|
||||
g_mdbg.log_storage = NULL;
|
||||
g_mdbg.log_buffer_size = 0;
|
||||
reset_log_snapshot();
|
||||
}
|
||||
|
||||
static bool ensure_log_buffers(void) {
|
||||
if (g_mdbg.log_storage && g_mdbg.log_snapshot && g_mdbg.log_buffer_size != 0)
|
||||
return true;
|
||||
|
||||
free_log_buffers();
|
||||
|
||||
const char *title_id =
|
||||
g_mdbg.game.title_id[0] != '\0' ? g_mdbg.game.title_id : "?";
|
||||
uint64_t raw_size = sceKernelDebugGetLogBufferSize();
|
||||
if (raw_size == 0) {
|
||||
log_debug(" [MDBG] invalid log buffer size for %s: 0x%016" PRIx64,
|
||||
title_id,
|
||||
raw_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t buffer_size = (size_t)raw_size;
|
||||
char *storage = malloc(buffer_size);
|
||||
char *snapshot = malloc(buffer_size);
|
||||
if (!storage || !snapshot) {
|
||||
free(storage);
|
||||
free(snapshot);
|
||||
log_debug(" [MDBG] failed to allocate log buffers for %s: size=0x%zx",
|
||||
title_id,
|
||||
buffer_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mdbg.log_storage = storage;
|
||||
g_mdbg.log_snapshot = snapshot;
|
||||
g_mdbg.log_buffer_size = buffer_size;
|
||||
reset_log_snapshot();
|
||||
log_debug(" [MDBG] log monitor ready: %s buffer_size=0x%zx", title_id,
|
||||
buffer_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out) {
|
||||
char *raw_text = NULL;
|
||||
uint64_t raw_len = 0;
|
||||
|
||||
*text_out = NULL;
|
||||
*text_len_out = 0;
|
||||
|
||||
int ret =
|
||||
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
|
||||
&raw_len);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
if (!raw_text || raw_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (raw_len > g_mdbg.log_buffer_size)
|
||||
return -EOVERFLOW;
|
||||
|
||||
*text_out = raw_text;
|
||||
*text_len_out = (size_t)raw_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t find_log_overlap(const char *current, size_t current_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
|
||||
current_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
|
||||
? g_mdbg.log_snapshot_length
|
||||
: current_len;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void update_log_snapshot(const char *text, size_t text_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_buffer_size == 0)
|
||||
return;
|
||||
|
||||
if (!text || text_len == 0) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text_len > g_mdbg.log_buffer_size)
|
||||
text_len = g_mdbg.log_buffer_size;
|
||||
|
||||
memcpy(g_mdbg.log_snapshot, text, text_len);
|
||||
g_mdbg.log_snapshot_length = text_len;
|
||||
}
|
||||
|
||||
static void clear_tracked_game(void) {
|
||||
memset(&g_mdbg.game, 0, sizeof(g_mdbg.game));
|
||||
free_log_buffers();
|
||||
}
|
||||
|
||||
static bool matches_tracked_rtld_error(const char *text) {
|
||||
return text &&
|
||||
g_mdbg.game.rtld_error_prefix[0] != '\0' &&
|
||||
strstr(text, g_mdbg.game.rtld_error_prefix) != NULL;
|
||||
}
|
||||
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error) {
|
||||
if (!summary_out || summary_out_size == 0)
|
||||
return;
|
||||
|
||||
summary_out[0] = '\0';
|
||||
if (!reason || reason[0] == '\0')
|
||||
return;
|
||||
|
||||
if (is_rtld_error) {
|
||||
const char *open_paren = strrchr(reason, '(');
|
||||
const char *close_paren =
|
||||
open_paren ? strchr(open_paren + 1, ')') : NULL;
|
||||
if (open_paren && close_paren && close_paren > open_paren + 1) {
|
||||
int written = snprintf(summary_out, summary_out_size,
|
||||
"can't load module %.*s after KStuff pause",
|
||||
(int)(close_paren - open_paren - 1),
|
||||
open_paren + 1);
|
||||
if (written > 0 && (size_t)written < summary_out_size)
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, "can't load module after KStuff pause",
|
||||
summary_out_size);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, reason, summary_out_size);
|
||||
}
|
||||
|
||||
static void handle_pre_pause_failure(const char *reason) {
|
||||
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
|
||||
"blame.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
|
||||
now_us < g_mdbg.game.pause_time_us) {
|
||||
handle_pre_pause_failure(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t post_pause_us = now_us - g_mdbg.game.pause_time_us;
|
||||
if (post_pause_us > MDBG_AUTOTUNE_WINDOW_US) {
|
||||
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
|
||||
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
|
||||
reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
char reason_summary[128];
|
||||
bool is_rtld_error = matches_tracked_rtld_error(reason);
|
||||
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
|
||||
is_rtld_error);
|
||||
|
||||
uint32_t tuned_delay_seconds = 0;
|
||||
if (upsert_kstuff_autotune_pause_delay(g_mdbg.game.title_id,
|
||||
g_mdbg.game.pause_delay_seconds,
|
||||
&tuned_delay_seconds)) {
|
||||
log_debug(" [MDBG] autotune pause delay updated: %s=%us",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
if (reason_summary[0] != '\0')
|
||||
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
|
||||
if (is_rtld_error) {
|
||||
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
g_mdbg.game.title_id,
|
||||
reason_summary[0] != '\0' ? reason_summary
|
||||
: "Can't load module after "
|
||||
"KStuff pause",
|
||||
tuned_delay_seconds);
|
||||
} else {
|
||||
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
|
||||
"increased to %us. Launch the game again.",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
}
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
|
||||
"autotune delay.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void process_log_line(const char *line, uint64_t now_us) {
|
||||
if (!matches_tracked_rtld_error(line))
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
|
||||
handle_post_pause_failure(line, now_us);
|
||||
}
|
||||
|
||||
static void append_log_char(char ch, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (g_mdbg.log_line_length == 0)
|
||||
return;
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
|
||||
}
|
||||
|
||||
static void poll_log_monitor(uint64_t now_us) {
|
||||
if (!g_mdbg.game.active || !g_mdbg.game.log_monitoring_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
size_t skip = 0;
|
||||
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
|
||||
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
|
||||
skip = g_mdbg.log_snapshot_length;
|
||||
} else {
|
||||
skip = find_log_overlap(text, text_len);
|
||||
if (skip == 0 && g_mdbg.log_snapshot_length != 0) {
|
||||
log_debug(" [MDBG] log stream reset or truncated for %s",
|
||||
g_mdbg.game.title_id);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = skip; i < text_len; ++i) {
|
||||
append_log_char(text[i], now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
}
|
||||
|
||||
static void start_log_monitoring(void) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.game.log_monitoring_active = true;
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] initial log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
|
||||
|
||||
handle_post_pause_failure("crash-candidate", now_us);
|
||||
}
|
||||
|
||||
void sm_mdbg_init(void) {
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_shutdown(void) {
|
||||
free_log_buffers();
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
|
||||
if (pid <= 0 || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
if (!sm_mdbg_enabled()) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.game.active && g_mdbg.game.pid != pid) {
|
||||
log_debug(" [MDBG] replacing tracked game pid=%ld (%s) with pid=%ld (%s)",
|
||||
(long)g_mdbg.game.pid, g_mdbg.game.title_id, (long)pid, title_id);
|
||||
}
|
||||
|
||||
clear_tracked_game();
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
g_mdbg.game.active = true;
|
||||
g_mdbg.game.pid = pid;
|
||||
g_mdbg.game.next_poll_us = now_us;
|
||||
(void)strlcpy(g_mdbg.game.title_id, title_id, sizeof(g_mdbg.game.title_id));
|
||||
int rtld_prefix_len =
|
||||
snprintf(g_mdbg.game.rtld_error_prefix,
|
||||
sizeof(g_mdbg.game.rtld_error_prefix),
|
||||
"[rtld] <%ld> ERROR", (long)pid);
|
||||
if (rtld_prefix_len <= 0 ||
|
||||
(size_t)rtld_prefix_len >= sizeof(g_mdbg.game.rtld_error_prefix)) {
|
||||
g_mdbg.game.rtld_error_prefix[0] = '\0';
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
|
||||
g_mdbg.game.title_id, (long)pid, app_id);
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
g_mdbg.game.pause_seen = true;
|
||||
g_mdbg.game.pause_time_us = pause_time_us;
|
||||
g_mdbg.game.monitor_deadline_us =
|
||||
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
|
||||
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
|
||||
g_mdbg.game.next_poll_us = pause_time_us;
|
||||
start_log_monitoring();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exit(pid_t pid) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_shutdown(void) {
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us) {
|
||||
(void)now_us;
|
||||
if (!sm_mdbg_enabled())
|
||||
return 0;
|
||||
if (!g_mdbg.game.active)
|
||||
return 0;
|
||||
|
||||
uint64_t next_wake_us = g_mdbg.game.next_poll_us;
|
||||
if (g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
(next_wake_us == 0 || g_mdbg.game.monitor_deadline_us < next_wake_us)) {
|
||||
next_wake_us = g_mdbg.game.monitor_deadline_us;
|
||||
}
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_mdbg_poll(void) {
|
||||
if (!sm_mdbg_enabled())
|
||||
return;
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us < g_mdbg.game.next_poll_us) {
|
||||
return;
|
||||
}
|
||||
if (g_mdbg.game.pause_seen && g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
now_us >= g_mdbg.game.monitor_deadline_us) {
|
||||
log_debug(" [MDBG] crash monitoring window expired for %s pid=%ld",
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.next_poll_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
|
||||
uint64_t flags = 0;
|
||||
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
|
||||
if (is_mdbg_process_gone_error(ret)) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
if (ret != 0)
|
||||
return;
|
||||
|
||||
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
|
||||
return;
|
||||
|
||||
handle_crash_candidate(flags, now_us);
|
||||
}
|
||||
+56
-1
@@ -12,6 +12,8 @@ struct PathStateEntry {
|
||||
uint8_t image_mount_attempts;
|
||||
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;
|
||||
@@ -68,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;
|
||||
}
|
||||
@@ -79,6 +82,8 @@ static struct PathStateEntry *create_path_state(const char *path) {
|
||||
continue;
|
||||
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;
|
||||
@@ -158,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;
|
||||
}
|
||||
@@ -179,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;
|
||||
}
|
||||
@@ -249,3 +258,49 @@ void clear_image_mount_attempts(const char *path) {
|
||||
entry->image_mount_attempts = 0;
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
|
||||
bool is_backport_mount_blocked(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry->backport_mount_blocked : false;
|
||||
}
|
||||
|
||||
void note_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->backport_mount_blocked)
|
||||
return;
|
||||
entry->backport_mount_blocked = true;
|
||||
}
|
||||
|
||||
void clear_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
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;
|
||||
}
|
||||
+430
-63
@@ -17,12 +17,16 @@
|
||||
#include "sm_title_state.h"
|
||||
#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
|
||||
@@ -31,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) {
|
||||
@@ -82,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;
|
||||
}
|
||||
@@ -113,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)++;
|
||||
@@ -132,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;
|
||||
@@ -149,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];
|
||||
@@ -160,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;
|
||||
@@ -168,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;
|
||||
@@ -193,6 +285,9 @@ static existing_directory_result_t handle_existing_directory_candidate(
|
||||
return EXISTING_DIRECTORY_PREFER_CACHED;
|
||||
}
|
||||
|
||||
if (!in_app_db && is_title_install_pending(info->title_id))
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
|
||||
if (!in_app_db && was_register_attempted(info->title_id)) {
|
||||
return EXISTING_DIRECTORY_HANDLED;
|
||||
}
|
||||
@@ -205,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) {
|
||||
@@ -243,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;
|
||||
@@ -275,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,
|
||||
@@ -301,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;
|
||||
}
|
||||
|
||||
@@ -308,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)
|
||||
@@ -335,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;
|
||||
}
|
||||
@@ -351,23 +467,190 @@ 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 bool build_backport_mount_context(const char *title_id,
|
||||
const char *owning_scan_path,
|
||||
char backport_path[MAX_PATH],
|
||||
char system_ex_path[MAX_PATH]) {
|
||||
if (!owning_scan_path || owning_scan_path[0] == '\0')
|
||||
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;
|
||||
char backport_root[MAX_PATH];
|
||||
if (!build_backports_root_path(owning_scan_path, backport_root))
|
||||
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;
|
||||
}
|
||||
|
||||
static bool build_backport_path_for_scan_root(const char *title_id,
|
||||
const char *scan_path,
|
||||
char backport_path[MAX_PATH]) {
|
||||
if (!title_id || title_id[0] == '\0' || !scan_path || scan_path[0] == '\0')
|
||||
return false;
|
||||
|
||||
snprintf(backport_path, MAX_PATH, "%s/%s", backport_root, title_id);
|
||||
snprintf(system_ex_path, MAX_PATH, "/system_ex/app/%s", title_id);
|
||||
return true;
|
||||
char backport_root[MAX_PATH];
|
||||
if (!build_backports_root_path(scan_path, backport_root))
|
||||
return false;
|
||||
|
||||
int written = snprintf(backport_path, MAX_PATH, "%s/%s", backport_root,
|
||||
title_id);
|
||||
return written > 0 && (size_t)written < MAX_PATH;
|
||||
}
|
||||
|
||||
static bool backport_path_is_dir(const char *backport_path) {
|
||||
struct stat st;
|
||||
return backport_path && stat(backport_path, &st) == 0 &&
|
||||
S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
static bool resolve_backport_path(const char *title_id,
|
||||
const char *owning_scan_path,
|
||||
char backport_path[MAX_PATH]) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
backport_path[0] = '\0';
|
||||
if (build_backport_path_for_scan_root(title_id, owning_scan_path,
|
||||
backport_path) &&
|
||||
backport_path_is_dir(backport_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
|
||||
continue;
|
||||
|
||||
char candidate_path[MAX_PATH];
|
||||
if (!build_backport_path_for_scan_root(title_id, scan_path, candidate_path))
|
||||
continue;
|
||||
if (!backport_path_is_dir(candidate_path))
|
||||
continue;
|
||||
|
||||
(void)strlcpy(backport_path, candidate_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
backport_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -381,16 +664,16 @@ 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;
|
||||
char backport_path[MAX_PATH];
|
||||
char system_ex_path[MAX_PATH];
|
||||
if (!build_backport_mount_context(title_id, owning_scan_root, backport_path,
|
||||
system_ex_path)) {
|
||||
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
|
||||
return true;
|
||||
}
|
||||
if (is_backport_mount_blocked(backport_path))
|
||||
return true;
|
||||
|
||||
bool overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
@@ -410,26 +693,22 @@ static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
return true;
|
||||
}
|
||||
|
||||
mount_backport_overlay(system_ex_path, backport_path, title_id);
|
||||
char system_ex_path[MAX_PATH];
|
||||
snprintf(system_ex_path, sizeof(system_ex_path), "/system_ex/app/%s",
|
||||
title_id);
|
||||
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
|
||||
note_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
}
|
||||
clear_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mount_backport_overlays_internal(const char *scan_root_filter,
|
||||
bool *unstable_found_out) {
|
||||
void mount_backport_overlays(bool *unstable_found_out) {
|
||||
backport_overlay_ctx_t ctx = {
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
for_each_cached_game_entry(scan_root_filter, mount_backport_overlay_for_cached_game,
|
||||
&ctx);
|
||||
}
|
||||
|
||||
void mount_backport_overlays(bool *unstable_found_out) {
|
||||
mount_backport_overlays_internal(NULL, unstable_found_out);
|
||||
}
|
||||
|
||||
void mount_backport_overlays_for_scan_root(const char *scan_root,
|
||||
bool *unstable_found_out) {
|
||||
mount_backport_overlays_internal(scan_root, unstable_found_out);
|
||||
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
|
||||
}
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
@@ -451,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 = {
|
||||
@@ -480,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,
|
||||
@@ -487,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;
|
||||
}
|
||||
|
||||
@@ -511,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);
|
||||
|
||||
+541
-118
File diff suppressed because it is too large.
Load diff
+72
-33
@@ -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;
|
||||
@@ -661,13 +691,22 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
sm_kstuff_note_app_focus((uint32_t)result_pattern);
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0)
|
||||
sm_kstuff_note_lnc_system_status(result_pattern);
|
||||
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