mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-19 21:29:58 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e48103149 | ||
|
|
124e757199 | ||
|
|
0fcc970a20 | ||
|
|
7684e8431d | ||
|
|
3166434db1 | ||
|
|
c785daaddf | ||
|
|
2db6b49439 | ||
|
|
aff2fc0e6c | ||
|
|
ae36dd5871 | ||
|
|
cc3e07d201 | ||
|
|
2c03ede25c | ||
|
|
a212a883b3 | ||
|
|
99a2956c28 | ||
|
|
93071924b5 | ||
|
|
47e47477d1 | ||
|
|
31c28e1896 | ||
|
|
c1f34ffa22 |
No files matched your search
@@ -3,3 +3,6 @@
|
||||
daemon.lock
|
||||
debug.log
|
||||
notify.txt
|
||||
src/notify_icon_asset.c
|
||||
refs/*
|
||||
docs/*
|
||||
@@ -1,22 +1,37 @@
|
||||
PS5_PAYLOAD_SDK ?= /opt/ps5-payload-sdk
|
||||
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
|
||||
VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo unknown)
|
||||
|
||||
# Standard Flags (No extra libraries)
|
||||
CFLAGS := -O2 -Wall -D_BSD_SOURCE -std=gnu11 -Isrc
|
||||
CFLAGS := -O2 -Wall -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -Isrc
|
||||
CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
|
||||
|
||||
# Linker
|
||||
LDFLAGS :=
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
LIBS := -lkernel_sys -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
|
||||
# Targets
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: src/main.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)
|
||||
shadowmountplus.elf: $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
|
||||
rm -f src/notify_icon_asset.c
|
||||
|
||||
src/notify_icon_asset.c: smp_icon.png
|
||||
xxd -i $< > $@
|
||||
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o src/notify_icon_asset.c
|
||||
@@ -1,6 +1,6 @@
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Version:** `1.6beta1`
|
||||
**Version:** `1.6beta5`
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
**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.7**.
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.6+ (Recommended 1.6.6)** or **Kstuff-lite v1.0+**.
|
||||
|
||||
|
||||
## Current image support
|
||||
@@ -19,19 +19,22 @@
|
||||
|
||||
| Extension | Mounted FS | Attach backend | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Recommended |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Compatibility / external-drive-only titles |
|
||||
| `.ffpfs` | `pfs` | `LVD` | Experimental |
|
||||
|
||||
Notes:
|
||||
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
|
||||
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
|
||||
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
|
||||
- **UFS (`.ffpkg`) is the recommended image format for normal use.**
|
||||
- **Use exFAT (`.exfat`) only for titles that need external-drive-style compatibility.**
|
||||
- **When building exFAT images manually, keep the cluster size at `64 KB`; smaller clusters can reduce performance.**
|
||||
|
||||
## Recommended FS choice
|
||||
|
||||
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
|
||||
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
|
||||
- Prefer **UFS (`.ffpkg`)** in most cases: this is the recommended default image format for ShadowMountPlus.
|
||||
- Use **exFAT (`.exfat`)** only for games that do not work correctly unless they are handled like external-drive content.
|
||||
- If you create an **exFAT (`.exfat`)** image manually, use a **`64 KB` cluster size**. Smaller clusters can cause a noticeable performance loss.
|
||||
|
||||
## Runtime config (`/data/shadowmount/config.ini`)
|
||||
|
||||
@@ -39,16 +42,22 @@ This file is optional. If it does not exist, built-in defaults are used.
|
||||
|
||||
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`)
|
||||
- `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)
|
||||
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
|
||||
- `backports_path=<absolute_path>` (default: `/data/backports`; if `<backports_path>/<TITLE_ID>` exists, it is overlaid on mounted image via `unionfs`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
|
||||
- `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`)
|
||||
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; 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_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)
|
||||
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
|
||||
- `lvd_exfat_sector_size=<value>` (default: `512`)
|
||||
- `lvd_ufs_sector_size=<value>` (default: `4096`)
|
||||
@@ -71,15 +80,29 @@ image_ro=legacy_dump.ffpkg
|
||||
|
||||
Scan path behavior:
|
||||
- If at least one `scanpath=...` is present, only those custom paths are used.
|
||||
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always added automatically, even with custom paths.
|
||||
- `/mnt/shadowmnt` is always added automatically, even with custom paths.
|
||||
- With `recursive_scan=0` (default), only first-level subfolders are checked.
|
||||
- With `recursive_scan=1`, subfolders are scanned recursively.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
|
||||
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
|
||||
|
||||
Backport overlay behavior:
|
||||
- If `${backports_path}/${TITLE_ID}` exists, ShadowMount+ mounts it over the game root with `unionfs`.
|
||||
- Files from backport folder override files from the mounted image.
|
||||
- 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.
|
||||
- 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_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.
|
||||
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
|
||||
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
|
||||
|
||||
|
||||
Validation:
|
||||
- See `config.ini.example` for a ready-to-use template.
|
||||
@@ -88,9 +111,7 @@ Validation:
|
||||
|
||||
Image mountpoints are created under:
|
||||
|
||||
`/data/imgmnt/<fs>/<image_name>`
|
||||
|
||||
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
|
||||
`/mnt/shadowmnt/<image_name>_<hash>`
|
||||
|
||||
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Game files must be placed at the image root.
|
||||
@@ -112,7 +133,7 @@ Default scan locations:
|
||||
- `/mnt/usb0` .. `/mnt/usb7`
|
||||
- `/mnt/ext0`
|
||||
- `/mnt/ext1`
|
||||
- `/data/imgmnt/ufsmnt`, `/data/imgmnt/exfatmnt`, `/data/imgmnt/pfsmnt` (mounted image content scan)
|
||||
- `/mnt/shadowmnt` (mounted image content scan)
|
||||
|
||||
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
|
||||
|
||||
@@ -120,14 +141,19 @@ Recommended folder structure:
|
||||
- Default mode (`recursive_scan=0`):
|
||||
- `/data/homebrew/<TITLE_ID>/`
|
||||
- `/data/etaHEN/games/<TITLE_ID>/`
|
||||
- `/data/homebrew/backports/<TITLE_ID>/`
|
||||
- `/data/etaHEN/games/backports/<TITLE_ID>/`
|
||||
|
||||
- Recursive mode (`recursive_scan=1`):
|
||||
- `/data/homebrew/PS5/<AnyFolder>/<TITLE_ID>/`
|
||||
- `/mnt/ext0/etaHEN/games/<Collection>/<TITLE_ID>/`
|
||||
- `/mnt/ext0/etaHEN/games/backports/<TITLE_ID>/`
|
||||
|
||||
|
||||
## Creating an exFAT image
|
||||
|
||||
Recommended only for titles that need external-drive-style compatibility. For general use, prefer `.ffpkg`.
|
||||
|
||||
Linux (Ubuntu/Debian):
|
||||
- Required components installation:
|
||||
- `sudo apt-get update && sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync`
|
||||
@@ -139,6 +165,7 @@ Linux (Ubuntu/Debian):
|
||||
- Notes:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- Auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- For manual exFAT builds, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- Automatically selects exFAT cluster profile:
|
||||
- Large-file profile: `64K`
|
||||
- Small/mixed-file profile: `32K`
|
||||
@@ -149,6 +176,7 @@ Windows:
|
||||
- Install OSFMount: https://www.osforensics.com/tools/mount-disk-images.html.
|
||||
- Keep `make_image.bat` and `New-OsfExfatImage.ps1` in the same folder.
|
||||
- Run `cmd.exe` as Administrator.
|
||||
- If you build an exFAT image manually instead of using the script, keep the cluster size at `64 KB` or you may lose performance.
|
||||
- Usage:
|
||||
- `make_image.bat "C:\images\game.exfat" "C:\payload\APPXXXX"`
|
||||
- Behavior:
|
||||
@@ -170,14 +198,20 @@ FreeBSD:
|
||||
- Source folder must be the game root and contain `eboot.bin`.
|
||||
- The script auto-calculates image size using rounded file allocation + metadata + safety margin.
|
||||
- Recommended `newfs` parameters for UFS2:
|
||||
- Large-file profile: `newfs -O 2 -b 65536 -f 4096 -m 0 -i 262144`
|
||||
- Small/mixed-file profile: `newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144`
|
||||
- `mkufs2.sh` selects between these two profiles automatically (based on average file size).
|
||||
- `newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096`
|
||||
- `mkufs2.sh` keeps this fixed block/fragment/sector profile and auto-tunes `-i` based on source file/directory count.
|
||||
- Rough manual `-i` estimate for manual builds:
|
||||
- `target_inodes ~= file_count + dir_count + 2048`
|
||||
- `bytes_per_inode ~= image_size_bytes / target_inodes`
|
||||
- Round `bytes_per_inode` down to a multiple of `4096`, then keep it in the practical range `65536..262144`.
|
||||
- Practical rule of thumb: use `262144` for normal game dumps, `131072` for tens of thousands of files, and `65536` only for very file-dense images.
|
||||
- Example: for an `8 GiB` image with `60000` files and `4000` directories, `-i ~= 8*1024^3 / (60000 + 4000 + 2048) ~= 130312`, so use `-i 131072`.
|
||||
|
||||
Windows:
|
||||
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
|
||||
- Example:
|
||||
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
|
||||
- `UFS2Tool.exe newfs -O 2 -b 65536 -f 65536 -m 0 -S 4096 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
|
||||
- For manual builds, use `-i 262144` as the baseline and lower it for images with many small files.
|
||||
|
||||
|
||||
## Installation and usage
|
||||
@@ -186,23 +220,16 @@ Windows:
|
||||
### Method 1: Manual Payload Injection (Port 9021)
|
||||
Use a payload sender (such as NetCat GUI or a web-based loader) to send the files to **Port 9021**.
|
||||
|
||||
1. Send `notify.elf` (Optional).
|
||||
* *Only send this if you want graphical pop-ups. Skip if you prefer standard notifications.*
|
||||
2. Send `shadowmountplus.elf`.
|
||||
3. Wait for the notification: *"ShadowMount+"*.
|
||||
1. Send `shadowmountplus.elf`.
|
||||
2. Wait for the notification: *"ShadowMount+"*.
|
||||
|
||||
### Method 2: PLK Autoloader (Recommended)
|
||||
Add ShadowMountPlus to your `autoload.txt` for **plk-autoloader** to ensure it starts automatically on every boot.
|
||||
|
||||
**Sample Configuration:**
|
||||
```ini
|
||||
!1000
|
||||
notify.elf ; Optional - Remove this line if you do not want Rich Toasts
|
||||
!1000
|
||||
shadowmountplus.elf
|
||||
!3000
|
||||
ps5-backpork.elf
|
||||
!1000
|
||||
kstuff.elf
|
||||
```
|
||||
|
||||
@@ -215,7 +242,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;
|
||||
- `/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}` are always scanned.
|
||||
- `/mnt/shadowmnt` is always scanned.
|
||||
- Verify scan depth:
|
||||
- `recursive_scan=0` scans only first-level subfolders;
|
||||
- `recursive_scan=1` scans recursively.
|
||||
@@ -223,7 +250,7 @@ If a game is not mounted:
|
||||
- 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).
|
||||
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/data/imgmnt/<fs>/<image_name>/` and include `sce_sys/param.json`.
|
||||
- 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 duplicate titleId notification, keep only one source per `<TITLE_ID>`.
|
||||
|
||||
@@ -240,10 +267,11 @@ If a game is mounted but does not start:
|
||||
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
|
||||
|
||||
* **Special Thanks:**
|
||||
* VoidWhisper for ShadowMount [](https://ko-fi.com/voidwhisper)
|
||||
* EchoStretch
|
||||
* VoidWhisper for ShadowMount
|
||||
* BestPig for BackPort
|
||||
* EchoStretch for kstuff-toggle and etc
|
||||
* Gezine
|
||||
* earthonion
|
||||
* LightningMods
|
||||
* john-tornblom
|
||||
* john-tornblom for SDK
|
||||
* PS5 R&D Community
|
||||
+34
-10
@@ -9,6 +9,11 @@
|
||||
# 0/false/no/off -> disable log_debug output
|
||||
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
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
@@ -30,10 +35,6 @@ force_mount=0
|
||||
# 0/false/no/off -> scan only first-level subdirectories (default)
|
||||
recursive_scan=0
|
||||
|
||||
# Optional backport overlay root:
|
||||
# If "<backports_path>/<TITLE_ID>" exists, it is mounted over image root via unionfs.
|
||||
backports_path=/data/backports
|
||||
|
||||
# Full scan loop interval (seconds), range: 1..3600
|
||||
scan_interval_seconds=10
|
||||
|
||||
@@ -47,12 +48,35 @@ stability_wait_seconds=10
|
||||
exfat_backend=lvd
|
||||
ufs_backend=lvd
|
||||
|
||||
# Backend-specific overrides (optional):
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Delay before pausing kstuff for image-backed titles.
|
||||
kstuff_pause_delay_image_seconds=20
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Optional custom scan roots (can be repeated):
|
||||
# If at least one scanpath=... is present, only these paths are used.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef SM_APPDB_H
|
||||
#define SM_APPDB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Close the cached app.db handle and release related resources.
|
||||
void shutdown_app_db(void);
|
||||
// Update snd0 metadata for a registered title.
|
||||
int update_snd0info(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);
|
||||
// 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);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef SM_CONFIG_MOUNT_H
|
||||
#define SM_CONFIG_MOUNT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct runtime_config runtime_config_t;
|
||||
|
||||
// Ensure runtime configuration is loaded before use.
|
||||
void ensure_runtime_config_ready(void);
|
||||
// Load runtime configuration from disk and apply defaults.
|
||||
bool load_runtime_config(void);
|
||||
// Return the current runtime configuration.
|
||||
const runtime_config_t *runtime_config(void);
|
||||
// Return the number of configured scan roots.
|
||||
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);
|
||||
// Resolve a per-image read-only override from the file name.
|
||||
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
|
||||
// Return true when kstuff auto-pause is disabled for the given title ID.
|
||||
bool is_kstuff_pause_disabled_for_title(const char *title_id);
|
||||
// Resolve a per-title kstuff pause-delay override in seconds.
|
||||
bool get_kstuff_pause_delay_override_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef SM_FAKELIB_H
|
||||
#define SM_FAKELIB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Return true when per-game fakelib backport automation is enabled.
|
||||
bool sm_fakelib_game_feature_enabled(void);
|
||||
// Track a supported game launch and mount its fakelib overlay immediately.
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id);
|
||||
// Remove a tracked fakelib overlay for a stopped game.
|
||||
void sm_fakelib_game_on_exit(pid_t pid);
|
||||
// Unmount all tracked overlays during watcher shutdown.
|
||||
void sm_fakelib_game_shutdown(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef SM_FILESYSTEM_H
|
||||
#define SM_FILESYSTEM_H
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Check whether a title is present in the installed app set.
|
||||
bool is_installed(const char *title_id);
|
||||
// Check whether a title currently has mounted data.
|
||||
bool is_data_mounted(const char *title_id);
|
||||
// Read the mount.lnk source path for an installed title.
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size);
|
||||
// Validate a /user/app/<TITLE_ID> entry and optionally build its path.
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size);
|
||||
// Recover staged mount links and warm image source mappings on startup.
|
||||
void cleanup_staged_mount_links(void);
|
||||
// Remove duplicated managed mount layers left from previous runs.
|
||||
void cleanup_duplicate_title_mounts(void);
|
||||
// Remount /system_ex with the expected flags.
|
||||
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);
|
||||
// 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,
|
||||
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);
|
||||
// Recursively copy a directory tree.
|
||||
int copy_dir(const char *src, const char *dst);
|
||||
// Copy a single file.
|
||||
int copy_file(const char *src, const char *dst);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef SM_GAME_CACHE_H
|
||||
#define SM_GAME_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Cache resolved metadata for a mounted or discovered game.
|
||||
void cache_game_entry(const char *path, const char *title_id,
|
||||
const char *title_name);
|
||||
// Drop invalid or stale entries from the game cache.
|
||||
void prune_game_cache(void);
|
||||
// Look up a cached game entry by path or title ID.
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out);
|
||||
// Remove a game cache entry by path.
|
||||
void clear_cached_game(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef SM_GAME_LIFECYCLE_H
|
||||
#define SM_GAME_LIFECYCLE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Start the shared game lifecycle watcher used by backport fakelib and kstuff.
|
||||
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);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef SM_GAMEINFO_H
|
||||
#define SM_GAMEINFO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Read title ID and title name from a mounted game directory.
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name);
|
||||
// Check whether a directory contains sce_sys/param.json and stat it.
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_HASH_H
|
||||
#define SM_HASH_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Return a stable 32-bit FNV-1a hash for a NUL-terminated string.
|
||||
static inline uint32_t sm_fnv1a32(const char *s) {
|
||||
uint32_t h = 2166136261u;
|
||||
while (s && *s) {
|
||||
h ^= (uint8_t)(*s++);
|
||||
h *= 16777619u;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef SM_IMAGE_H
|
||||
#define SM_IMAGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Log filesystem statistics for a mounted path.
|
||||
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
// Attach and mount an image file to its runtime mount point.
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Unmount an image mount point and detach its backing device.
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
|
||||
// Reconcile cached image mounts with current sources and remount if needed.
|
||||
void cleanup_stale_image_mounts(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);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef SM_IMAGE_CACHE_H
|
||||
#define SM_IMAGE_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
} image_cache_entry_t;
|
||||
|
||||
// Cache a successful image mount and its attached device.
|
||||
// Returns false when no free cache slot is available.
|
||||
bool cache_image_mount(const char *path, const char *mount_point,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache an image source mapping without attach metadata.
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point);
|
||||
// Return a cached image mount entry by index.
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out);
|
||||
// Resolve the original image file path for a cached runtime mount point.
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_INSTALL_H
|
||||
#define SM_INSTALL_H
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Install or remount the collected scan candidates.
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef SM_KSTUFF_H
|
||||
#define SM_KSTUFF_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Resolve firmware-specific kstuff sysentvec addresses and initialize state.
|
||||
void sm_kstuff_init(void);
|
||||
// Shut down runtime kstuff control state.
|
||||
void sm_kstuff_shutdown(void);
|
||||
// Return true when firmware-specific kstuff control is available.
|
||||
bool sm_kstuff_is_supported(void);
|
||||
// Return true when both tracked kstuff sysentvecs are enabled.
|
||||
bool sm_kstuff_is_enabled(void);
|
||||
// Set both tracked kstuff sysentvecs and return the resulting enabled state.
|
||||
bool sm_kstuff_set_enabled(bool enabled, bool notify_user);
|
||||
|
||||
// Return true when game-driven kstuff automation should run.
|
||||
bool sm_kstuff_game_feature_enabled(void);
|
||||
// Track a supported game launch for delayed auto-pause handling.
|
||||
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);
|
||||
// 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.
|
||||
void sm_kstuff_game_on_exit(pid_t pid);
|
||||
// Apply any delayed kstuff pause whose deadline has elapsed.
|
||||
void sm_kstuff_game_poll(void);
|
||||
// Clear tracked game state and restore kstuff if the watcher paused it.
|
||||
void sm_kstuff_game_shutdown(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef SM_LIMITS_H
|
||||
#define SM_LIMITS_H
|
||||
|
||||
#define DEFAULT_SCAN_INTERVAL_US 10000000u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 20u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 10u
|
||||
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_IMAGE_MOUNTS 64
|
||||
#define MAX_IMAGE_MODE_RULES 128
|
||||
#define MAX_KSTUFF_TITLE_RULES 128
|
||||
#define PATH_STATE_CAPACITY MAX_PENDING
|
||||
#define TITLE_STATE_CAPACITY MAX_PENDING
|
||||
#define STATE_HASH_SIZE 1024u
|
||||
#define MAX_SCAN_PATHS 256
|
||||
|
||||
#define MAX_FAILED_MOUNT_ATTEMPTS 2
|
||||
#define MAX_REGISTER_ATTEMPTS 2
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
|
||||
#define LVD_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD_RELEASE_WAIT_POLL_US 500000u
|
||||
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
|
||||
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
#define MAX_STABILITY_WAIT_SECONDS 3600u
|
||||
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
|
||||
|
||||
#define APP_DB_QUERY_BUSY_RETRIES 3
|
||||
#define APP_DB_UPDATE_BUSY_RETRIES 25
|
||||
#define APP_DB_PREPARE_BUSY_RETRIES 25
|
||||
#define APP_DB_BUSY_RETRY_SLEEP_US 200000u
|
||||
#define APP_DB_BUSY_TIMEOUT_MS 5000
|
||||
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef SM_LOG_H
|
||||
#define SM_LOG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct sm_error sm_error_t;
|
||||
|
||||
// Write a formatted line to the debug log.
|
||||
void log_debug(const char *fmt, ...);
|
||||
// Prepare notification assets such as the packaged icon file.
|
||||
void sm_notifications_init(void);
|
||||
// Send a rich toast notification with packaged icon and version header.
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...);
|
||||
// Send the "game installed" rich toast for the given title ID.
|
||||
void notify_game_installed_rich(const char *title_id);
|
||||
// Send an informational system notification (suppressed in quiet mode).
|
||||
void notify_system_info(const char *fmt, ...);
|
||||
// Send a system notification with formatted text.
|
||||
void notify_system(const char *fmt, ...);
|
||||
// Clear the last recorded subsystem error.
|
||||
void sm_error_clear(void);
|
||||
// Store the last subsystem error with formatted details.
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...);
|
||||
// Return the last recorded subsystem error.
|
||||
const sm_error_t *sm_last_error(void);
|
||||
// Return whether the current error was already notified.
|
||||
bool sm_error_notified(void);
|
||||
// Mark the current error as already shown to the user.
|
||||
void sm_error_mark_notified(void);
|
||||
// Notify the user about an image mount failure once per error state.
|
||||
void notify_image_mount_failed(const char *path, int mount_err);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef SM_MOUNT_DEFS_H
|
||||
#define SM_MOUNT_DEFS_H
|
||||
|
||||
#define IMAGE_MOUNT_READ_ONLY 1
|
||||
|
||||
// 1 = use legacy /dev/mdctl backend for .exfat images, 0 = use LVD for all.
|
||||
#define EXFAT_ATTACH_USE_MDCTL 0
|
||||
// 1 = allow mounting .ffpkg images via /dev/mdctl, 0 = keep UFS on LVD.
|
||||
#define UFS_ATTACH_USE_MDCTL 0
|
||||
|
||||
// --- LVD Definitions ---
|
||||
// Kernel exposes base attach (V0), extended attach (V1/Attach2) and detach.
|
||||
// This code only builds V0 layer descriptors, so it uses the base attach path.
|
||||
// Wrapper-side raw attach presets:
|
||||
// - 0x8/0x9 -> single-image/save-data family (normalized 0x14/0x1C)
|
||||
// - 0xC/0xD -> download-data/LWFS family (normalized 0x16/0x1E)
|
||||
// raw bit3 (0x8) sets the DONT_MAP_BM-style mode, raw bit2 (0x4) switches to
|
||||
// the DD/LWFS family, and raw bit0 (0x1) selects the alternate preset inside a
|
||||
// family. Firmware pairs bit0 with read-only mount variants, but this is not a
|
||||
// standalone LVD readonly bit by itself.
|
||||
// image_type values accepted by validator: 0..0xC (13 values total).
|
||||
// layer source_type observed: 1=file, 2=device/special source (/dev/sbram0, char/block).
|
||||
// layer descriptor flag bit0 is "no bitmap file specified".
|
||||
#define LVD_CTRL_PATH "/dev/lvdctl"
|
||||
#define MD_CTRL_PATH "/dev/mdctl"
|
||||
#define SCE_LVD_IOC_ATTACH_V0 0xC0286D00
|
||||
#define SCE_LVD_IOC_ATTACH_V1 0xC0286D09
|
||||
#define SCE_LVD_IOC_DETACH 0xC0286D01
|
||||
#define LVD_ATTACH_IO_VERSION_V0 0u
|
||||
#define LVD_ATTACH_IO_VERSION_V1 1u
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RO 0x9
|
||||
#define LVD_ATTACH_RAW_FLAGS_SINGLE_RW 0x8
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RO 0xD
|
||||
#define LVD_ATTACH_RAW_FLAGS_DD_RW 0xC
|
||||
#
|
||||
#define LVD_SECTOR_SIZE_EXFAT 512u
|
||||
#define LVD_SECTOR_SIZE_UFS 4096u
|
||||
#define LVD_SECTOR_SIZE_PFS 4096u
|
||||
#define LVD_SECONDARY_UNIT_SINGLE_IMAGE 0x10000u
|
||||
#define MD_SECTOR_SIZE_EXFAT 512u
|
||||
#define MD_SECTOR_SIZE_UFS 512u
|
||||
// Raw option bits are normalized by sceFsLvdAttachCommon before validation:
|
||||
// raw:0x1->norm:0x08, raw:0x2->norm:0x80, raw:0x4->norm:0x02, raw:0x8->norm:0x10.
|
||||
// The normalized masks are then checked against validator constraints (0x82/0x92).
|
||||
#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 LVD_ATTACH_LAYER_COUNT 1
|
||||
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
|
||||
#define LVD_ENTRY_TYPE_FILE 1
|
||||
#define LVD_ENTRY_TYPE_SPECIAL 2
|
||||
#define LVD_ENTRY_FLAG_NO_BITMAP 0x1
|
||||
#define LVD_NODE_WAIT_US 100000
|
||||
#define LVD_NODE_WAIT_RETRIES 100
|
||||
#define UFS_NMOUNT_FLAG_RW 0x10000000u
|
||||
#define UFS_NMOUNT_FLAG_RO 0x10000001u
|
||||
|
||||
// --- devpfs/pfs option defaults ---
|
||||
// PFS nmount key/value variants observed in refs:
|
||||
// - fstype: "pfs", "transaction_pfs", "ppr_pfs"
|
||||
// - mkeymode: "SD"
|
||||
// - budgetid: "game"/"system" in init paths
|
||||
// - sigverify/playgo/disc: "0" or "1"
|
||||
// - optional keys in specific flows: ekpfs/eekpfs, eekc, pubkey_ver, key_ver,
|
||||
// finalized, ppkg_opt, sblock_offset, maxpkgszingib
|
||||
#define DEVPFS_BUDGET_GAME "game"
|
||||
#define DEVPFS_BUDGET_SYSTEM "system"
|
||||
#define DEVPFS_MKEYMODE_SD "SD"
|
||||
#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_SIGVERIFY 0
|
||||
#define PFS_MOUNT_PLAYGO 0
|
||||
#define PFS_MOUNT_DISC 0
|
||||
// 4x64-bit PFS key encoded as 64 hex chars.
|
||||
#define PFS_ZERO_EKPFS_KEY_HEX \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef SM_MOUNT_DEVICE_H
|
||||
#define SM_MOUNT_DEVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Return a printable name for an attach backend enum value.
|
||||
const char *attach_backend_name(attach_backend_t backend);
|
||||
// Check whether a source path has aged enough to be mounted safely.
|
||||
bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
const char *tag);
|
||||
// Resolve backend and unit ID for a mounted image path.
|
||||
bool resolve_device_from_mount(const char *mount_point,
|
||||
attach_backend_t *backend_out, int *unit_out);
|
||||
// Return true if a path is an active mount point.
|
||||
bool is_active_image_mount_point(const char *path);
|
||||
// Wait until /dev/lvd2 is no longer mounted during startup.
|
||||
bool wait_for_lvd_release(void);
|
||||
// Detach a previously attached MD or LVD unit.
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef SM_PATH_STATE_H
|
||||
#define SM_PATH_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Load cached game info if the param file state still matches.
|
||||
bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
char *out_id, char *out_name, bool *valid_out);
|
||||
// Store parsed game info together with the current param file state.
|
||||
void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
bool valid, const char *title_id,
|
||||
const char *title_name);
|
||||
// Drop expired entries from path-based retry and metadata state.
|
||||
void prune_path_state(void);
|
||||
// Return whether scans for a path are temporarily suppressed after failures.
|
||||
bool is_missing_param_scan_limited(const char *path);
|
||||
// Record a missing param.json failure for scan backoff.
|
||||
void record_missing_param_failure(const char *path);
|
||||
// Clear missing-param backoff state for a path.
|
||||
void clear_missing_param_entry(const char *path);
|
||||
// Return whether image mount retries are temporarily suppressed for a path.
|
||||
bool is_image_mount_limited(const char *path);
|
||||
// Increment and return the image mount retry counter for a 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);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_PATH_UTILS_H
|
||||
#define SM_PATH_UTILS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
// Return the last path component without modifying the input string.
|
||||
const char *get_filename_component(const char *path);
|
||||
// Return true when a path lives under the image mount root.
|
||||
bool is_under_image_mount_base(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]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef SM_PATHS_H
|
||||
#define SM_PATHS_H
|
||||
|
||||
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
|
||||
|
||||
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
|
||||
#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 KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
#define KILL_FILE "/data/shadowmount/STOP"
|
||||
#define TOAST_FILE "/data/shadowmount/notify.txt"
|
||||
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
|
||||
#define NOTIFY_ICON_FILE "/user/data/shadowmount/smp_icon.png"
|
||||
#define APP_DB_PATH "/system_data/priv/mms/app.db"
|
||||
|
||||
/* Compile-time default scan roots (used when config has no scanpath entries). */
|
||||
#define SM_DEFAULT_SCAN_PATHS_INITIALIZER \
|
||||
{ \
|
||||
/* Internal */ \
|
||||
"/data/homebrew", "/data/etaHEN/games", \
|
||||
/* Extended Storage */ \
|
||||
"/mnt/ext0/homebrew", "/mnt/ext0/etaHEN/games", \
|
||||
/* M.2 Drive */ \
|
||||
"/mnt/ext1/homebrew", "/mnt/ext1/etaHEN/games", \
|
||||
/* USB Subfolders */ \
|
||||
"/mnt/usb0/homebrew", "/mnt/usb1/homebrew", "/mnt/usb2/homebrew", \
|
||||
"/mnt/usb3/homebrew", "/mnt/usb4/homebrew", "/mnt/usb5/homebrew", \
|
||||
"/mnt/usb6/homebrew", "/mnt/usb7/homebrew", \
|
||||
"/mnt/usb0/etaHEN/games", "/mnt/usb1/etaHEN/games", \
|
||||
"/mnt/usb2/etaHEN/games", "/mnt/usb3/etaHEN/games", \
|
||||
"/mnt/usb4/etaHEN/games", "/mnt/usb5/etaHEN/games", \
|
||||
"/mnt/usb6/etaHEN/games", "/mnt/usb7/etaHEN/games", \
|
||||
/* USB Root Paths */ \
|
||||
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3", "/mnt/usb4", \
|
||||
"/mnt/usb5", "/mnt/usb6", "/mnt/usb7", "/mnt/ext0", "/mnt/ext1", NULL \
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef SM_PLATFORM_H
|
||||
#define SM_PLATFORM_H
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mdioctl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/uio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t app_id;
|
||||
uint64_t unknown1;
|
||||
char title_id[14];
|
||||
char unknown2[0x3c];
|
||||
} app_info_t;
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallTitleDir(const char *title_id,
|
||||
const char *install_path, void *reserved);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
int sceUserServiceInitialize(void *);
|
||||
void sceUserServiceTerminate(void);
|
||||
|
||||
// Standard Notification
|
||||
typedef struct notify_request {
|
||||
char unused[45];
|
||||
char message[3075];
|
||||
} notify_request_t;
|
||||
int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int);
|
||||
|
||||
#define IOVEC_ENTRY(x) \
|
||||
{(void *)(x), \
|
||||
((const char *)(x) == NULL ? 0u : (size_t)(strlen((const char *)(x)) + 1u))}
|
||||
#define IOVEC_SIZE(x) (sizeof(x) / sizeof(struct iovec))
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef SM_RUNTIME_H
|
||||
#define SM_RUNTIME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Install process signal handlers used for graceful shutdown.
|
||||
void install_signal_handlers(void);
|
||||
// Return process pid with the given name, or 0 if not found.
|
||||
pid_t find_pid_by_name(const char *name, bool exclude_self);
|
||||
// Return true when shutdown was requested by signal or kill file.
|
||||
bool should_stop_requested(void);
|
||||
// Request graceful shutdown with a descriptive source string.
|
||||
void request_shutdown_stop(const char *reason);
|
||||
// Request an immediate scan cycle with a descriptive source string.
|
||||
void request_scan_now(const char *reason);
|
||||
// Consume a pending immediate scan request, returning true if one was pending.
|
||||
bool consume_scan_now_request(const char **reason_out);
|
||||
// Sleep in chunks and stop early if shutdown was requested.
|
||||
bool sleep_with_stop_check(unsigned int total_us);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef SM_SCAN_H
|
||||
#define SM_SCAN_H
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Unmount and clean up mounts whose backing sources disappeared.
|
||||
void cleanup_lost_sources_before_scan(void);
|
||||
// Scan configured roots and collect install candidates.
|
||||
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
int *total_found_out);
|
||||
// Mount stable per-root backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef SM_SHELLCORE_FLAGS_H
|
||||
#define SM_SHELLCORE_FLAGS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
bool sm_shellcore_flags_start(void);
|
||||
void sm_shellcore_flags_stop(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef SM_STABILITY_H
|
||||
#define SM_STABILITY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Check whether a path is old enough to be treated as stable.
|
||||
bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
int *stat_errno_out);
|
||||
// Wait briefly for a path to become stable before giving up.
|
||||
bool wait_for_stability_fast(const char *path, const char *name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef SM_TIME_H
|
||||
#define SM_TIME_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Return a monotonic timestamp in microseconds, or 0 on failure.
|
||||
uint64_t monotonic_time_us(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef SM_TITLE_STATE_H
|
||||
#define SM_TITLE_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Return whether registration was already attempted for a title.
|
||||
bool was_register_attempted(const char *title_id);
|
||||
// Return the number of registration attempts for a title.
|
||||
uint8_t get_register_attempts(const char *title_id);
|
||||
// Record a registration attempt for a title.
|
||||
void mark_register_attempted(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);
|
||||
// Clear the one-shot duplicate notification state for a title.
|
||||
void clear_duplicate_title_notification(const char *title_id);
|
||||
// Return the number of failed install/remount attempts for a title.
|
||||
uint8_t get_failed_mount_attempts(const char *title_id);
|
||||
// Clear failed install/remount attempts for a title.
|
||||
void clear_failed_mount_attempts(const char *title_id);
|
||||
// Increment and return failed install/remount attempts for a title.
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,141 @@
|
||||
#ifndef SM_TYPES_H
|
||||
#define SM_TYPES_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
typedef struct {
|
||||
// +0x00: 1 -> mount read-only, 0 -> allow write.
|
||||
uint32_t ro;
|
||||
// +0x04: reserved in observed Shell/FSMP callers.
|
||||
uint32_t reserved0;
|
||||
// +0x08: logical budget/domain string, usually "game" or "system".
|
||||
const char *budget_id;
|
||||
// +0x10: reserved in observed Shell/FSMP callers.
|
||||
uint32_t reserved1;
|
||||
// +0x14: bitmask consumed by devpfs mount logic.
|
||||
uint32_t flags;
|
||||
// +0x18: optional "maxpkgszingib" value (GiB), 0 means not set.
|
||||
uint64_t max_pkg_gib;
|
||||
} devpfs_mount_opt_t;
|
||||
|
||||
typedef struct {
|
||||
// Human-readable profile id for logs.
|
||||
const char *name;
|
||||
// Raw option payload mapped to FSMP mount behavior.
|
||||
devpfs_mount_opt_t opt;
|
||||
} devpfs_mount_profile_t;
|
||||
|
||||
typedef struct {
|
||||
// Source object class (observed: 1=file, 2=device-like/special source).
|
||||
uint16_t source_type;
|
||||
// Layer descriptor flags (observed bit0 = no bitmap file path).
|
||||
uint16_t flags;
|
||||
// Must be zero.
|
||||
uint32_t reserved0;
|
||||
// Backing file or device path.
|
||||
const char *path;
|
||||
// Data start offset in backing object (bytes).
|
||||
uint64_t offset;
|
||||
// Data size exposed via this layer (bytes).
|
||||
uint64_t size;
|
||||
// Optional bitmap file path.
|
||||
const char *bitmap_path;
|
||||
// Bitmap offset in bitmap file (bytes).
|
||||
uint64_t bitmap_offset;
|
||||
// Bitmap size (bytes), 0 when bitmap is unused.
|
||||
uint64_t bitmap_size;
|
||||
} lvd_ioctl_layer_v0_t;
|
||||
|
||||
typedef struct {
|
||||
// Protocol version for /dev/lvdctl ioctl payload (0 = V0/base attach).
|
||||
uint32_t io_version;
|
||||
// Input: usually -1 for auto-assign. Output: created lvd unit id.
|
||||
int32_t device_id;
|
||||
// User-visible sector size exported by /dev/lvdN.
|
||||
uint32_t sector_size;
|
||||
// Secondary unit/granularity validated against sector_size.
|
||||
uint32_t secondary_unit;
|
||||
// Normalized attach flags produced from wrapper raw options.
|
||||
uint16_t flags;
|
||||
// LVD image type id (validator accepts 0..0xC).
|
||||
uint16_t image_type;
|
||||
// Number of valid entries in layers_ptr.
|
||||
uint32_t layer_count;
|
||||
// Total exported virtual size (bytes).
|
||||
uint64_t device_size;
|
||||
// Pointer to V0 layer array in user payload.
|
||||
lvd_ioctl_layer_v0_t *layers_ptr;
|
||||
} lvd_ioctl_attach_v0_t;
|
||||
|
||||
typedef struct {
|
||||
// Must be zero.
|
||||
uint32_t reserved0;
|
||||
// Target lvd unit id to detach.
|
||||
int32_t device_id;
|
||||
// Reserved padding required by kernel ABI.
|
||||
uint8_t reserved[0x20];
|
||||
} lvd_ioctl_detach_t;
|
||||
|
||||
struct AppDbTitleList {
|
||||
char(*ids)[MAX_TITLE_ID];
|
||||
int count;
|
||||
int capacity;
|
||||
};
|
||||
|
||||
typedef struct scan_candidate {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
bool installed;
|
||||
bool in_app_db;
|
||||
} scan_candidate_t;
|
||||
|
||||
typedef enum {
|
||||
ATTACH_BACKEND_NONE = 0,
|
||||
// /dev/lvdctl -> /dev/lvdN
|
||||
ATTACH_BACKEND_LVD,
|
||||
// /dev/mdctl -> /dev/mdN
|
||||
ATTACH_BACKEND_MD,
|
||||
} attach_backend_t;
|
||||
|
||||
typedef struct runtime_config {
|
||||
bool debug_enabled;
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool recursive_scan;
|
||||
bool backport_fakelib_enabled;
|
||||
bool kstuff_game_auto_toggle;
|
||||
uint32_t scan_interval_us;
|
||||
uint32_t stability_wait_seconds;
|
||||
uint32_t kstuff_pause_delay_image_seconds;
|
||||
uint32_t kstuff_pause_delay_direct_seconds;
|
||||
attach_backend_t exfat_backend;
|
||||
attach_backend_t ufs_backend;
|
||||
uint32_t lvd_sector_exfat;
|
||||
uint32_t lvd_sector_ufs;
|
||||
uint32_t lvd_sector_pfs;
|
||||
uint32_t md_sector_exfat;
|
||||
uint32_t md_sector_ufs;
|
||||
} runtime_config_t;
|
||||
|
||||
typedef enum {
|
||||
IMAGE_FS_UNKNOWN = 0,
|
||||
IMAGE_FS_UFS,
|
||||
IMAGE_FS_EXFAT,
|
||||
IMAGE_FS_PFS,
|
||||
} image_fs_type_t;
|
||||
|
||||
typedef struct sm_error {
|
||||
int code;
|
||||
char subsystem[16];
|
||||
char message[256];
|
||||
char path[MAX_PATH];
|
||||
bool valid;
|
||||
bool notified;
|
||||
} sm_error_t;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/sh
|
||||
# macOS exFAT image builder — requires macOS 10.6.5+ (exFAT and rsync built-in)
|
||||
# Usage: mkexfat_macos.sh <input_dir> [output_file]
|
||||
# (c) @drakmor,@betmoar
|
||||
|
||||
set -e
|
||||
|
||||
# ── argument validation ───────────────────────────────────────────────────────
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <input_dir> [output_file]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_DIR="$1"
|
||||
OUTPUT="${2:-test.exfat}"
|
||||
# newfs_exfat interprets relative paths as device names under /dev/ — resolve to absolute
|
||||
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
|
||||
|
||||
if [ ! -d "$INPUT_DIR" ]; then
|
||||
echo "Error: input directory not found: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
|
||||
echo "Error: eboot.bin not found in source directory: $INPUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── sizing constants ──────────────────────────────────────────────────────────
|
||||
|
||||
CLUSTER_SIZE=32768
|
||||
LARGE_FILE_THRESHOLD=$((1024 * 1024))
|
||||
META_FIXED=$((32 * 1024 * 1024)) # boot region, upcase, root and misc
|
||||
MIN_SLACK=$((64 * 1024 * 1024)) # minimum copy/runtime safety margin
|
||||
SPARE_MIN=$((64 * 1024 * 1024)) # lower bound for dynamic headroom
|
||||
SPARE_MAX=$((512 * 1024 * 1024)) # upper bound for dynamic headroom
|
||||
ENTRY_META_BYTES=256
|
||||
|
||||
# Single filesystem traversal: collect all file sizes into a temp file.
|
||||
# FILE_COUNT and RAW_FILE_BYTES are derived from it in one awk pass.
|
||||
# DIR_COUNT needs a separate traversal (different -type).
|
||||
SIZES_FILE=$(mktemp)
|
||||
MOUNT_POINT=""
|
||||
LOOP_DEVICE=""
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$MOUNT_POINT" ] && mount | grep -q "on $MOUNT_POINT "; then
|
||||
umount "$MOUNT_POINT" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$LOOP_DEVICE" ]; then
|
||||
hdiutil detach "$LOOP_DEVICE" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$MOUNT_POINT" ]; then
|
||||
rmdir "$MOUNT_POINT" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$SIZES_FILE"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
find "$INPUT_DIR" -type f -exec stat -f '%z' {} \; > "$SIZES_FILE"
|
||||
|
||||
read -r FILE_COUNT RAW_FILE_BYTES <<EOF
|
||||
$(awk '{c++; s += $1} END {print c+0, s+0}' "$SIZES_FILE")
|
||||
EOF
|
||||
|
||||
DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# exFAT profile selection:
|
||||
# - large-file sets: 64K cluster
|
||||
# - small/mixed-file sets: 32K cluster
|
||||
if [ "$AVG_FILE_BYTES" -ge "$LARGE_FILE_THRESHOLD" ]; then
|
||||
CLUSTER_SIZE=65536
|
||||
fi
|
||||
|
||||
# DATA_BYTES re-reads the already-collected sizes from the temp file — no second traversal
|
||||
DATA_BYTES=$(awk -v cls="$CLUSTER_SIZE" \
|
||||
'{s += int(($1 + cls - 1) / cls) * cls} END {print s + 0}' "$SIZES_FILE")
|
||||
rm -f "$SIZES_FILE"
|
||||
|
||||
DATA_CLUSTERS=$(( (DATA_BYTES + CLUSTER_SIZE - 1) / CLUSTER_SIZE ))
|
||||
FAT_BYTES=$((DATA_CLUSTERS * 4))
|
||||
BITMAP_BYTES=$(( (DATA_CLUSTERS + 7) / 8 ))
|
||||
ENTRY_BYTES=$(( (FILE_COUNT + DIR_COUNT) * ENTRY_META_BYTES ))
|
||||
|
||||
BASE_TOTAL=$((DATA_BYTES + FAT_BYTES + BITMAP_BYTES + ENTRY_BYTES + META_FIXED))
|
||||
SPARE_BYTES=$((BASE_TOTAL / 200)) # ~0.5%
|
||||
if [ "$SPARE_BYTES" -lt "$SPARE_MIN" ]; then SPARE_BYTES=$SPARE_MIN; fi
|
||||
if [ "$SPARE_BYTES" -gt "$SPARE_MAX" ]; then SPARE_BYTES=$SPARE_MAX; fi
|
||||
TOTAL=$((BASE_TOTAL + SPARE_BYTES))
|
||||
MIN_TOTAL=$((RAW_FILE_BYTES + MIN_SLACK))
|
||||
if [ "$TOTAL" -lt "$MIN_TOTAL" ]; then TOTAL=$MIN_TOTAL; fi
|
||||
|
||||
# Round up to nearest MB
|
||||
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (exFAT alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "exFAT profile: -b ${CLUSTER_SIZE} (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
# ── create, attach, format, mount ────────────────────────────────────────────
|
||||
|
||||
MOUNT_POINT=$(mktemp -d)
|
||||
|
||||
# mkfile -n creates a sparse file without writing zeros (equivalent to Linux truncate)
|
||||
mkfile -n "${MB}m" "$OUTPUT"
|
||||
|
||||
# Attach as a block device first — newfs_exfat requires real sector geometry via ioctl
|
||||
# and cannot format a raw file directly
|
||||
ATTACH_OUTPUT=$(hdiutil attach \
|
||||
-imagekey diskimage-class=CRawDiskImage \
|
||||
-nomount "$OUTPUT")
|
||||
LOOP_DEVICE=$(printf '%s\n' "$ATTACH_OUTPUT" | awk 'NR==1 {print $1; exit}')
|
||||
if [ -z "$LOOP_DEVICE" ]; then
|
||||
echo "Error: failed to determine attached device: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# newfs_exfat -b takes bytes-per-cluster directly
|
||||
newfs_exfat -b "$CLUSTER_SIZE" "$LOOP_DEVICE"
|
||||
|
||||
mount -t exfat "$LOOP_DEVICE" "$MOUNT_POINT"
|
||||
|
||||
rsync -r --progress "$INPUT_DIR"/ "$MOUNT_POINT"/
|
||||
|
||||
umount "$MOUNT_POINT"
|
||||
hdiutil detach "$LOOP_DEVICE"
|
||||
LOOP_DEVICE="" # prevent double-detach in trap
|
||||
|
||||
echo "Created $OUTPUT"
|
||||
@@ -23,14 +23,17 @@ if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
|
||||
fi
|
||||
|
||||
# More accurate sizing for UFS2:
|
||||
# - file payload rounded to fragment size (-f)
|
||||
# - file payload rounded to the actual allocation unit (-f)
|
||||
# - minimal per-directory allocation
|
||||
# - inode table estimate
|
||||
# - fixed filesystem metadata headroom
|
||||
BLOCK_SIZE=32768
|
||||
FRAG_SIZE=4096
|
||||
|
||||
BLOCK_SIZE=65536
|
||||
FRAG_SIZE=65536
|
||||
SECTOR_SIZE=4096
|
||||
MINFREE_PERCENT=0
|
||||
BYTES_PER_INODE=262144
|
||||
DEFAULT_BYTES_PER_INODE=$((4 * FRAG_SIZE))
|
||||
MIN_BYTES_PER_INODE=$FRAG_SIZE
|
||||
INODE_SIZE=256
|
||||
INODE_SPARE=2048
|
||||
META_FIXED=$((64 * 1024 * 1024)) # superblocks, cg metadata, allocator slack
|
||||
@@ -43,18 +46,6 @@ DIR_COUNT=$(find "$INPUT_DIR" -type d | wc -l | tr -d ' ')
|
||||
RAW_FILE_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
|
||||
awk '{s += $1} END {print s + 0}')
|
||||
|
||||
AVG_FILE_BYTES=0
|
||||
if [ "$FILE_COUNT" -gt 0 ]; then
|
||||
AVG_FILE_BYTES=$((RAW_FILE_BYTES / FILE_COUNT))
|
||||
fi
|
||||
|
||||
# UFS profile selection:
|
||||
# - large-file sets: 64K block
|
||||
# - small/mixed-file sets: 32K block (safer/denser default)
|
||||
if [ "$AVG_FILE_BYTES" -ge $((1024 * 1024)) ]; then
|
||||
BLOCK_SIZE=65536
|
||||
fi
|
||||
|
||||
DATA_BYTES=$(find "$INPUT_DIR" -type f -exec stat -f '%z' {} + | \
|
||||
awk -v frag="$FRAG_SIZE" '{s += int(($1 + frag - 1) / frag) * frag} END {print s + 0}')
|
||||
DIR_BYTES=$((DIR_COUNT * FRAG_SIZE))
|
||||
@@ -77,17 +68,33 @@ fi
|
||||
|
||||
# Round up to nearest MB
|
||||
MB=$(( (TOTAL + 1024*1024 - 1) / (1024*1024) ))
|
||||
IMAGE_BYTES=$((MB * 1024 * 1024))
|
||||
|
||||
# Tune inode density for the actual tree size. Keep the documented default
|
||||
# unless the source contains enough files/directories to require denser inodes.
|
||||
BYTES_PER_INODE=$DEFAULT_BYTES_PER_INODE
|
||||
if [ "$INODE_COUNT" -gt 0 ]; then
|
||||
AUTO_BYTES_PER_INODE=$((IMAGE_BYTES / INODE_COUNT))
|
||||
AUTO_BYTES_PER_INODE=$(( (AUTO_BYTES_PER_INODE / SECTOR_SIZE) * SECTOR_SIZE ))
|
||||
if [ "$AUTO_BYTES_PER_INODE" -lt "$MIN_BYTES_PER_INODE" ]; then
|
||||
AUTO_BYTES_PER_INODE=$MIN_BYTES_PER_INODE
|
||||
fi
|
||||
if [ "$AUTO_BYTES_PER_INODE" -lt "$BYTES_PER_INODE" ]; then
|
||||
BYTES_PER_INODE=$AUTO_BYTES_PER_INODE
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Input size (raw files): $RAW_FILE_BYTES bytes"
|
||||
echo "Input size (UFS alloc): $DATA_BYTES bytes"
|
||||
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
|
||||
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -i $BYTES_PER_INODE (avg file=$AVG_FILE_BYTES bytes)"
|
||||
echo "Inode target: $INODE_COUNT"
|
||||
echo "UFS profile: -b $BLOCK_SIZE -f $FRAG_SIZE -m $MINFREE_PERCENT -S $SECTOR_SIZE -i $BYTES_PER_INODE"
|
||||
echo "Image size: ${MB}MB"
|
||||
|
||||
truncate -s "${MB}M" "$OUTPUT"
|
||||
|
||||
MD=$(mdconfig -a -t vnode -f "$(realpath "$OUTPUT")")
|
||||
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -i "$BYTES_PER_INODE" /dev/${MD}
|
||||
newfs -O 2 -b "$BLOCK_SIZE" -f "$FRAG_SIZE" -m "$MINFREE_PERCENT" -S "$SECTOR_SIZE" -i "$BYTES_PER_INODE" /dev/${MD}
|
||||
|
||||
mkdir -p /mnt
|
||||
mount /dev/${MD} /mnt
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+318
-3703
File diff suppressed because it is too large.
Load diff
+268
@@ -0,0 +1,268 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sqlite3 *g_app_db;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0;
|
||||
static struct AppDbTitleList g_app_db_title_cache;
|
||||
static bool g_app_db_title_cache_ready = false;
|
||||
static time_t g_app_db_title_cache_mtime = 0;
|
||||
|
||||
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) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
if (!list)
|
||||
return;
|
||||
free(list->ids);
|
||||
list->ids = NULL;
|
||||
list->count = 0;
|
||||
list->capacity = 0;
|
||||
}
|
||||
|
||||
void shutdown_app_db(void) {
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
close_app_db();
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
if (!g_app_db) {
|
||||
if (sqlite3_open_v2(APP_DB_PATH, &g_app_db, SQLITE_OPEN_READWRITE, NULL) !=
|
||||
SQLITE_OK) {
|
||||
log_debug(" [DB] open failed: %s",
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : APP_DB_PATH));
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(g_app_db, APP_DB_BUSY_TIMEOUT_MS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool app_db_wait_retry(int rc, int attempt, int max_attempts,
|
||||
bool close_before_retry) {
|
||||
if (rc != SQLITE_BUSY && rc != SQLITE_LOCKED)
|
||||
return false;
|
||||
if (attempt + 1 >= max_attempts || should_stop_requested())
|
||||
return false;
|
||||
if (close_before_retry)
|
||||
close_app_db();
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
|
||||
int max_attempts, const char *label) {
|
||||
for (int attempt = 0; attempt < max_attempts; attempt++) {
|
||||
if (!ensure_app_db_open())
|
||||
return -1;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_app_db, sql, -1, stmt_out, NULL);
|
||||
if (rc == SQLITE_OK)
|
||||
return SQLITE_OK;
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, max_attempts, true))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
close_app_db();
|
||||
return rc;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return -1;
|
||||
|
||||
if (!g_app_db_stmt_update_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_update_snd0);
|
||||
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));
|
||||
}
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\0')
|
||||
return true;
|
||||
|
||||
if (list->count >= list->capacity) {
|
||||
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
|
||||
char(*new_ids)[MAX_TITLE_ID] =
|
||||
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
|
||||
if (!new_ids)
|
||||
return false;
|
||||
list->ids = new_ids;
|
||||
list->capacity = new_capacity;
|
||||
}
|
||||
|
||||
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
|
||||
list->count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int compare_title_id_str(const void *a, const void *b) {
|
||||
return strcmp((const char *)a, (const char *)b);
|
||||
}
|
||||
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\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) {
|
||||
if (!list)
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
busy_attempts++;
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
*list_out = NULL;
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_title_cache_ready ||
|
||||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
g_app_db_title_cache = fresh;
|
||||
g_app_db_title_cache_mtime = st.st_mtime;
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
*list_out = &g_app_db_title_cache;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static const char *const k_default_scan_paths[] = SM_DEFAULT_SCAN_PATHS_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
char filename[MAX_PATH];
|
||||
bool mount_read_only;
|
||||
bool valid;
|
||||
} image_mode_rule_t;
|
||||
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t delay_seconds;
|
||||
bool valid;
|
||||
} kstuff_delay_rule_t;
|
||||
|
||||
static runtime_config_t g_runtime_cfg;
|
||||
static bool g_runtime_cfg_ready = false;
|
||||
static const char *g_scan_paths[MAX_SCAN_PATHS + 1];
|
||||
static char g_scan_path_storage[MAX_SCAN_PATHS][MAX_PATH];
|
||||
static int g_scan_path_count = 0;
|
||||
static image_mode_rule_t g_image_mode_rules[MAX_IMAGE_MODE_RULES];
|
||||
static char g_kstuff_no_pause_title_ids[MAX_KSTUFF_TITLE_RULES][MAX_TITLE_ID];
|
||||
static int g_kstuff_no_pause_title_count = 0;
|
||||
static kstuff_delay_rule_t g_kstuff_delay_rules[MAX_KSTUFF_TITLE_RULES];
|
||||
|
||||
static char *trim_ascii(char *s);
|
||||
static bool normalize_title_id_value(const char *value,
|
||||
char out[MAX_TITLE_ID]);
|
||||
|
||||
static attach_backend_t default_exfat_backend(void) {
|
||||
#if EXFAT_ATTACH_USE_MDCTL
|
||||
return ATTACH_BACKEND_MD;
|
||||
#else
|
||||
return ATTACH_BACKEND_LVD;
|
||||
#endif
|
||||
}
|
||||
|
||||
static attach_backend_t default_ufs_backend(void) {
|
||||
#if UFS_ATTACH_USE_MDCTL
|
||||
return ATTACH_BACKEND_MD;
|
||||
#else
|
||||
return ATTACH_BACKEND_LVD;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void clear_runtime_scan_paths(void) {
|
||||
g_scan_path_count = 0;
|
||||
memset(g_scan_paths, 0, sizeof(g_scan_paths));
|
||||
memset(g_scan_path_storage, 0, sizeof(g_scan_path_storage));
|
||||
}
|
||||
|
||||
static bool add_runtime_scan_path(const char *path) {
|
||||
if (!path)
|
||||
return false;
|
||||
|
||||
while (*path && isspace((unsigned char)*path))
|
||||
path++;
|
||||
|
||||
size_t len = strlen(path);
|
||||
while (len > 0 && isspace((unsigned char)path[len - 1]))
|
||||
len--;
|
||||
if (len == 0 || len >= MAX_PATH)
|
||||
return false;
|
||||
|
||||
char normalized[MAX_PATH];
|
||||
memcpy(normalized, path, len);
|
||||
normalized[len] = '\0';
|
||||
while (len > 1 && normalized[len - 1] == '/') {
|
||||
normalized[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_scan_path_count; i++) {
|
||||
if (g_scan_paths[i] && strcmp(g_scan_paths[i], normalized) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_scan_path_count >= MAX_SCAN_PATHS)
|
||||
return false;
|
||||
|
||||
(void)strlcpy(g_scan_path_storage[g_scan_path_count], normalized,
|
||||
sizeof(g_scan_path_storage[g_scan_path_count]));
|
||||
g_scan_paths[g_scan_path_count] = g_scan_path_storage[g_scan_path_count];
|
||||
g_scan_path_count++;
|
||||
g_scan_paths[g_scan_path_count] = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void init_runtime_scan_paths_defaults(void) {
|
||||
clear_runtime_scan_paths();
|
||||
for (int i = 0; k_default_scan_paths[i] != NULL; i++)
|
||||
(void)add_runtime_scan_path(k_default_scan_paths[i]);
|
||||
(void)add_runtime_scan_path(IMAGE_MOUNT_BASE);
|
||||
}
|
||||
|
||||
static void clear_kstuff_title_rules(void) {
|
||||
g_kstuff_no_pause_title_count = 0;
|
||||
memset(g_kstuff_no_pause_title_ids, 0, sizeof(g_kstuff_no_pause_title_ids));
|
||||
memset(g_kstuff_delay_rules, 0, sizeof(g_kstuff_delay_rules));
|
||||
}
|
||||
|
||||
static void init_runtime_config_defaults(void) {
|
||||
g_runtime_cfg.debug_enabled = true;
|
||||
g_runtime_cfg.quiet_mode = false;
|
||||
g_runtime_cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
g_runtime_cfg.force_mount = false;
|
||||
g_runtime_cfg.recursive_scan = false;
|
||||
g_runtime_cfg.backport_fakelib_enabled = true;
|
||||
g_runtime_cfg.kstuff_game_auto_toggle = true;
|
||||
g_runtime_cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
|
||||
g_runtime_cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
|
||||
g_runtime_cfg.kstuff_pause_delay_image_seconds =
|
||||
DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS;
|
||||
g_runtime_cfg.kstuff_pause_delay_direct_seconds =
|
||||
DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS;
|
||||
g_runtime_cfg.exfat_backend = default_exfat_backend();
|
||||
g_runtime_cfg.ufs_backend = default_ufs_backend();
|
||||
g_runtime_cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT;
|
||||
g_runtime_cfg.lvd_sector_ufs = LVD_SECTOR_SIZE_UFS;
|
||||
g_runtime_cfg.lvd_sector_pfs = LVD_SECTOR_SIZE_PFS;
|
||||
g_runtime_cfg.md_sector_exfat = MD_SECTOR_SIZE_EXFAT;
|
||||
g_runtime_cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS;
|
||||
memset(g_image_mode_rules, 0, sizeof(g_image_mode_rules));
|
||||
clear_kstuff_title_rules();
|
||||
init_runtime_scan_paths_defaults();
|
||||
g_runtime_cfg_ready = true;
|
||||
}
|
||||
|
||||
void ensure_runtime_config_ready(void) {
|
||||
if (!g_runtime_cfg_ready)
|
||||
init_runtime_config_defaults();
|
||||
}
|
||||
|
||||
const runtime_config_t *runtime_config(void) {
|
||||
ensure_runtime_config_ready();
|
||||
return &g_runtime_cfg;
|
||||
}
|
||||
|
||||
int get_scan_path_count(void) {
|
||||
ensure_runtime_config_ready();
|
||||
return g_scan_path_count;
|
||||
}
|
||||
|
||||
const char *get_scan_path(int index) {
|
||||
ensure_runtime_config_ready();
|
||||
if (index < 0 || index >= g_scan_path_count)
|
||||
return NULL;
|
||||
return g_scan_paths[index];
|
||||
}
|
||||
|
||||
bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
|
||||
ensure_runtime_config_ready();
|
||||
if (!filename || !mount_read_only_out)
|
||||
return false;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (!g_image_mode_rules[k].valid)
|
||||
continue;
|
||||
if (strcasecmp(g_image_mode_rules[k].filename, filename) != 0)
|
||||
continue;
|
||||
*mount_read_only_out = g_image_mode_rules[k].mount_read_only;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_kstuff_pause_disabled_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;
|
||||
|
||||
for (int i = 0; i < g_kstuff_no_pause_title_count; ++i) {
|
||||
if (strcmp(g_kstuff_no_pause_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();
|
||||
if (!delay_seconds_out)
|
||||
return false;
|
||||
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
|
||||
if (!g_kstuff_delay_rules[i].valid)
|
||||
continue;
|
||||
if (strcmp(g_kstuff_delay_rules[i].title_id, normalized) != 0)
|
||||
continue;
|
||||
*delay_seconds_out = g_kstuff_delay_rules[i].delay_seconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
s++;
|
||||
size_t n = strlen(s);
|
||||
while (n > 0) {
|
||||
char c = s[n - 1];
|
||||
if (c != ' ' && c != '\t' && c != '\r' && c != '\n')
|
||||
break;
|
||||
s[n - 1] = '\0';
|
||||
n--;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool normalize_title_id_value(const char *value,
|
||||
char out[MAX_TITLE_ID]) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
|
||||
char local[MAX_TITLE_ID];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
char *trimmed = trim_ascii(local);
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_TITLE_ID)
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
unsigned char ch = (unsigned char)trimmed[i];
|
||||
if (!isalnum(ch))
|
||||
return false;
|
||||
out[i] = (char)toupper(ch);
|
||||
}
|
||||
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_bool_ini(const char *value, bool *out) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
if (strcmp(value, "1") == 0 || strcasecmp(value, "true") == 0 ||
|
||||
strcasecmp(value, "yes") == 0 || strcasecmp(value, "on") == 0 ||
|
||||
strcasecmp(value, "ro") == 0) {
|
||||
*out = true;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(value, "0") == 0 || strcasecmp(value, "false") == 0 ||
|
||||
strcasecmp(value, "no") == 0 || strcasecmp(value, "off") == 0 ||
|
||||
strcasecmp(value, "rw") == 0) {
|
||||
*out = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parse_backend_ini(const char *value, attach_backend_t *out) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
if (strcasecmp(value, "lvd") == 0) {
|
||||
*out = ATTACH_BACKEND_LVD;
|
||||
return true;
|
||||
}
|
||||
if (strcasecmp(value, "md") == 0 || strcasecmp(value, "mdctl") == 0) {
|
||||
*out = ATTACH_BACKEND_MD;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool parse_u32_ini(const char *value, uint32_t *out) {
|
||||
if (!value || !out)
|
||||
return false;
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long v = strtoul(value, &end, 0);
|
||||
if (errno != 0 || end == value || *end != '\0' || v > UINT32_MAX)
|
||||
return false;
|
||||
*out = (uint32_t)v;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_valid_sector_size(uint32_t size) {
|
||||
if (size < 512u || size > 1024u * 1024u)
|
||||
return false;
|
||||
return (size & (size - 1u)) == 0u;
|
||||
}
|
||||
|
||||
static bool set_image_mode_rule(const char *path, bool mount_read_only) {
|
||||
const char *filename = get_filename_component(path);
|
||||
if (filename[0] == '\0')
|
||||
return false;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (!g_image_mode_rules[k].valid)
|
||||
continue;
|
||||
if (strcasecmp(g_image_mode_rules[k].filename, filename) != 0)
|
||||
continue;
|
||||
g_image_mode_rules[k].mount_read_only = mount_read_only;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (g_image_mode_rules[k].valid)
|
||||
continue;
|
||||
(void)strlcpy(g_image_mode_rules[k].filename, filename,
|
||||
sizeof(g_image_mode_rules[k].filename));
|
||||
g_image_mode_rules[k].mount_read_only = mount_read_only;
|
||||
g_image_mode_rules[k].valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool add_kstuff_no_pause_title_rule(const char *value) {
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(value, normalized))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < g_kstuff_no_pause_title_count; ++i) {
|
||||
if (strcmp(g_kstuff_no_pause_title_ids[i], normalized) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (g_kstuff_no_pause_title_count >= MAX_KSTUFF_TITLE_RULES)
|
||||
return false;
|
||||
|
||||
(void)strlcpy(g_kstuff_no_pause_title_ids[g_kstuff_no_pause_title_count],
|
||||
normalized,
|
||||
sizeof(g_kstuff_no_pause_title_ids[g_kstuff_no_pause_title_count]));
|
||||
g_kstuff_no_pause_title_count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool set_kstuff_pause_delay_override_rule(const char *value) {
|
||||
if (!value)
|
||||
return false;
|
||||
|
||||
char local[128];
|
||||
(void)strlcpy(local, value, sizeof(local));
|
||||
|
||||
char *sep = strchr(local, ':');
|
||||
if (!sep)
|
||||
return false;
|
||||
*sep = '\0';
|
||||
|
||||
char *title_id = trim_ascii(local);
|
||||
char *delay_value = trim_ascii(sep + 1);
|
||||
char normalized[MAX_TITLE_ID];
|
||||
uint32_t delay_seconds = 0;
|
||||
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
if (!parse_u32_ini(delay_value, &delay_seconds) ||
|
||||
delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
|
||||
if (!g_kstuff_delay_rules[i].valid)
|
||||
continue;
|
||||
if (strcmp(g_kstuff_delay_rules[i].title_id, normalized) != 0)
|
||||
continue;
|
||||
g_kstuff_delay_rules[i].delay_seconds = delay_seconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
|
||||
if (g_kstuff_delay_rules[i].valid)
|
||||
continue;
|
||||
(void)strlcpy(g_kstuff_delay_rules[i].title_id, normalized,
|
||||
sizeof(g_kstuff_delay_rules[i].title_id));
|
||||
g_kstuff_delay_rules[i].delay_seconds = delay_seconds;
|
||||
g_kstuff_delay_rules[i].valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool load_runtime_config(void) {
|
||||
init_runtime_config_defaults();
|
||||
|
||||
FILE *f = fopen(CONFIG_FILE, "r");
|
||||
if (!f) {
|
||||
if (errno != ENOENT) {
|
||||
log_debug(" [CFG] open failed: %s (%s)", CONFIG_FILE, strerror(errno));
|
||||
} else {
|
||||
log_debug(" [CFG] not found, using defaults");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[512];
|
||||
int line_no = 0;
|
||||
bool has_custom_scanpaths = false;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
line_no++;
|
||||
char *s = trim_ascii(line);
|
||||
if (s[0] == '\0' || s[0] == '#' || s[0] == ';' || s[0] == '[')
|
||||
continue;
|
||||
|
||||
char *eq = strchr(s, '=');
|
||||
if (!eq) {
|
||||
log_debug(" [CFG] invalid line %d (missing '=')", line_no);
|
||||
continue;
|
||||
}
|
||||
*eq = '\0';
|
||||
char *key = trim_ascii(s);
|
||||
char *value = trim_ascii(eq + 1);
|
||||
|
||||
char *comment = strchr(value, '#');
|
||||
if (comment) {
|
||||
*comment = '\0';
|
||||
value = trim_ascii(value);
|
||||
}
|
||||
comment = strchr(value, ';');
|
||||
if (comment) {
|
||||
*comment = '\0';
|
||||
value = trim_ascii(value);
|
||||
}
|
||||
|
||||
if (key[0] == '\0' || value[0] == '\0')
|
||||
continue;
|
||||
|
||||
bool bval = false;
|
||||
uint32_t u32 = 0;
|
||||
attach_backend_t backend = ATTACH_BACKEND_NONE;
|
||||
|
||||
if (strcasecmp(key, "debug") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.debug_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "quiet_mode") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.quiet_mode = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "mount_read_only") == 0 ||
|
||||
strcasecmp(key, "read_only") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.mount_read_only = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "force_mount") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.force_mount = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "image_ro") == 0 ||
|
||||
strcasecmp(key, "image_rw") == 0) {
|
||||
bool rule_read_only = (strcasecmp(key, "image_ro") == 0);
|
||||
if (!set_image_mode_rule(value, rule_read_only)) {
|
||||
log_debug(" [CFG] invalid image mode rule at line %d: %s=%s", line_no,
|
||||
key, value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "recursive_scan") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.recursive_scan = 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);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.backport_fakelib_enabled = bval;
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.kstuff_game_auto_toggle = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_no_pause") == 0) {
|
||||
if (!add_kstuff_no_pause_title_rule(value)) {
|
||||
log_debug(" [CFG] invalid kstuff no-pause title rule at line %d: "
|
||||
"%s=%s", line_no, key, value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_delay") == 0) {
|
||||
if (!set_kstuff_pause_delay_override_rule(value)) {
|
||||
log_debug(" [CFG] invalid kstuff pause override at line %d: %s=%s "
|
||||
"(format: TITLEID:SECONDS, max: %u)",
|
||||
line_no, key, value,
|
||||
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "scan_interval_seconds") == 0 ||
|
||||
strcasecmp(key, "scan_interval_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 < MIN_SCAN_INTERVAL_SECONDS ||
|
||||
u32 > MAX_SCAN_INTERVAL_SECONDS) {
|
||||
log_debug(" [CFG] invalid scan interval at line %d: %s=%s (range: %u..%u)",
|
||||
line_no, key, value, (unsigned)MIN_SCAN_INTERVAL_SECONDS,
|
||||
(unsigned)MAX_SCAN_INTERVAL_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.scan_interval_us = u32 * 1000000u;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "stability_wait_seconds") == 0 ||
|
||||
strcasecmp(key, "stability_wait_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_STABILITY_WAIT_SECONDS) {
|
||||
log_debug(" [CFG] invalid stability wait at line %d: %s=%s (max: %u)",
|
||||
line_no, key, value, (unsigned)MAX_STABILITY_WAIT_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.stability_wait_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_pause_delay_image_seconds") == 0 ||
|
||||
strcasecmp(key, "kstuff_pause_delay_image_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
log_debug(" [CFG] invalid image kstuff pause delay at line %d: %s=%s "
|
||||
"(max: %u)", line_no, key, value,
|
||||
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.kstuff_pause_delay_image_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_pause_delay_direct_seconds") == 0 ||
|
||||
strcasecmp(key, "kstuff_pause_delay_direct_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
log_debug(" [CFG] invalid direct kstuff pause delay at line %d: %s=%s "
|
||||
"(max: %u)", line_no, key, value,
|
||||
(unsigned)MAX_KSTUFF_PAUSE_DELAY_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.kstuff_pause_delay_direct_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "exfat_backend") == 0) {
|
||||
if (!parse_backend_ini(value, &backend)) {
|
||||
log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.exfat_backend = backend;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "ufs_backend") == 0) {
|
||||
if (!parse_backend_ini(value, &backend)) {
|
||||
log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.ufs_backend = backend;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "scanpath") == 0) {
|
||||
if (!has_custom_scanpaths) {
|
||||
clear_runtime_scan_paths();
|
||||
has_custom_scanpaths = true;
|
||||
}
|
||||
if (!add_runtime_scan_path(value)) {
|
||||
log_debug(" [CFG] invalid scanpath at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_sector_key =
|
||||
(strcasecmp(key, "lvd_exfat_sector_size") == 0) ||
|
||||
(strcasecmp(key, "lvd_ufs_sector_size") == 0) ||
|
||||
(strcasecmp(key, "lvd_pfs_sector_size") == 0) ||
|
||||
(strcasecmp(key, "md_exfat_sector_size") == 0) ||
|
||||
(strcasecmp(key, "md_ufs_sector_size") == 0);
|
||||
|
||||
if (!is_sector_key) {
|
||||
log_debug(" [CFG] unknown key at line %d: %s", line_no, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parse_u32_ini(value, &u32) || !is_valid_sector_size(u32)) {
|
||||
log_debug(" [CFG] invalid sector size at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "lvd_exfat_sector_size") == 0) {
|
||||
g_runtime_cfg.lvd_sector_exfat = u32;
|
||||
} else if (strcasecmp(key, "lvd_ufs_sector_size") == 0) {
|
||||
g_runtime_cfg.lvd_sector_ufs = u32;
|
||||
} else if (strcasecmp(key, "lvd_pfs_sector_size") == 0) {
|
||||
g_runtime_cfg.lvd_sector_pfs = u32;
|
||||
} else if (strcasecmp(key, "md_exfat_sector_size") == 0) {
|
||||
g_runtime_cfg.md_sector_exfat = u32;
|
||||
} else if (strcasecmp(key, "md_ufs_sector_size") == 0) {
|
||||
g_runtime_cfg.md_sector_ufs = u32;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
if (has_custom_scanpaths && g_scan_path_count == 0) {
|
||||
log_debug(" [CFG] no valid scanpath entries, using defaults");
|
||||
init_runtime_scan_paths_defaults();
|
||||
} else {
|
||||
(void)add_runtime_scan_path(IMAGE_MOUNT_BASE);
|
||||
}
|
||||
|
||||
int image_rule_count = 0;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (g_image_mode_rules[k].valid)
|
||||
image_rule_count++;
|
||||
}
|
||||
|
||||
int kstuff_delay_rule_count = 0;
|
||||
for (int k = 0; k < MAX_KSTUFF_TITLE_RULES; k++) {
|
||||
if (g_kstuff_delay_rules[k].valid)
|
||||
kstuff_delay_rule_count++;
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d recursive_scan=%d "
|
||||
"backport_fakelib=%d kstuff_game_auto_toggle=%d "
|
||||
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
"lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) "
|
||||
"scan_interval_s=%u stability_wait_s=%u scan_paths=%d image_rules=%d "
|
||||
"kstuff_no_pause=%d kstuff_delay_rules=%d",
|
||||
g_runtime_cfg.debug_enabled ? 1 : 0,
|
||||
g_runtime_cfg.quiet_mode ? 1 : 0,
|
||||
g_runtime_cfg.mount_read_only ? 1 : 0,
|
||||
g_runtime_cfg.force_mount ? 1 : 0,
|
||||
g_runtime_cfg.recursive_scan ? 1 : 0,
|
||||
g_runtime_cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
g_runtime_cfg.kstuff_game_auto_toggle ? 1 : 0,
|
||||
g_runtime_cfg.kstuff_pause_delay_image_seconds,
|
||||
g_runtime_cfg.kstuff_pause_delay_direct_seconds,
|
||||
attach_backend_name(g_runtime_cfg.exfat_backend),
|
||||
attach_backend_name(g_runtime_cfg.ufs_backend),
|
||||
g_runtime_cfg.lvd_sector_exfat, g_runtime_cfg.lvd_sector_ufs,
|
||||
g_runtime_cfg.lvd_sector_pfs, g_runtime_cfg.md_sector_exfat,
|
||||
g_runtime_cfg.md_sector_ufs,
|
||||
g_runtime_cfg.scan_interval_us / 1000000u,
|
||||
g_runtime_cfg.stability_wait_seconds, g_scan_path_count,
|
||||
image_rule_count, g_kstuff_no_pause_title_count,
|
||||
kstuff_delay_rule_count);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
char mount_path[MAX_PATH];
|
||||
} fakelib_mount_t;
|
||||
|
||||
static fakelib_mount_t g_fakelib_mount;
|
||||
|
||||
bool sm_fakelib_game_feature_enabled(void) {
|
||||
return runtime_config()->backport_fakelib_enabled;
|
||||
}
|
||||
|
||||
static bool mount_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path) {
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
|
||||
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
|
||||
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] mount failed for %s (%s -> %s): %s", title_id,
|
||||
source_path, mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_fakelib_overlay(const char *mount_path) {
|
||||
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] unmount failed for %s: %s", 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]) {
|
||||
char sandbox_id[MAX_TITLE_ID];
|
||||
DIR *d = opendir("/mnt/sandbox");
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
size_t title_len = strlen(title_id);
|
||||
int best_index = -1;
|
||||
bool found = false;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (strncmp(entry->d_name, title_id, title_len) != 0)
|
||||
continue;
|
||||
if (entry->d_name[title_len] != '_')
|
||||
continue;
|
||||
|
||||
const char *suffix = entry->d_name + title_len + 1u;
|
||||
if (!isdigit((unsigned char)suffix[0]))
|
||||
continue;
|
||||
|
||||
int idx = atoi(suffix);
|
||||
if (idx < best_index)
|
||||
continue;
|
||||
|
||||
best_index = idx;
|
||||
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
|
||||
found = true;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
snprintf(source_path, MAX_PATH, "/mnt/sandbox/%s/app0/fakelib", sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
d = opendir(sandbox_root);
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
found = false;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (strcmp(entry->d_name, "app0") == 0)
|
||||
continue;
|
||||
|
||||
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
|
||||
entry->d_name);
|
||||
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
continue;
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (!g_fakelib_mount.active)
|
||||
return true;
|
||||
|
||||
if (!unmount_fakelib_overlay(g_fakelib_mount.mount_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.active && 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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
if (!resolve_sandbox_fakelib_context(title_id, source_path, mount_path))
|
||||
return;
|
||||
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path))
|
||||
return;
|
||||
|
||||
g_fakelib_mount.active = true;
|
||||
g_fakelib_mount.pid = pid;
|
||||
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
|
||||
sizeof(g_fakelib_mount.mount_path));
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exit(pid_t pid) {
|
||||
if (!g_fakelib_mount.active || g_fakelib_mount.pid != pid)
|
||||
return;
|
||||
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
|
||||
g_fakelib_mount.mount_path);
|
||||
cleanup_fakelib_mount();
|
||||
}
|
||||
|
||||
void sm_fakelib_game_shutdown(void) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
// --- FILESYSTEM ---
|
||||
bool is_installed(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "/user/app/%s", title_id);
|
||||
struct stat st;
|
||||
return (stat(path, &st) == 0);
|
||||
}
|
||||
bool is_data_mounted(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json",
|
||||
title_id);
|
||||
return (access(path, F_OK) == 0);
|
||||
}
|
||||
|
||||
static bool read_mount_link_file(const char *lnk_path, char *out,
|
||||
size_t out_size) {
|
||||
if (!lnk_path || out_size == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
|
||||
FILE *f = fopen(lnk_path, "r");
|
||||
if (!f)
|
||||
return false;
|
||||
|
||||
if (!fgets(out, (int)out_size, f)) {
|
||||
fclose(f);
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
size_t len = strcspn(out, "\r\n");
|
||||
out[len] = '\0';
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
static void build_title_link_path(const char *title_id, const char *filename,
|
||||
char out[MAX_PATH]) {
|
||||
snprintf(out, MAX_PATH, "/user/app/%s/%s", title_id, filename);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char mount_link[MAX_PATH];
|
||||
char staged_mount_link[MAX_PATH];
|
||||
char mount_image_link[MAX_PATH];
|
||||
} title_link_paths_t;
|
||||
|
||||
static void build_title_link_paths(const char *title_id,
|
||||
title_link_paths_t *paths) {
|
||||
build_title_link_path(title_id, "mount.lnk", paths->mount_link);
|
||||
build_title_link_path(title_id, "mount.lnk.cleanup",
|
||||
paths->staged_mount_link);
|
||||
build_title_link_path(title_id, "mount_img.lnk", paths->mount_image_link);
|
||||
}
|
||||
|
||||
typedef bool (*title_app_dir_iter_fn)(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx);
|
||||
|
||||
static void for_each_title_app_dir(const char *open_error_context,
|
||||
bool stop_on_signal,
|
||||
title_app_dir_iter_fn fn, void *ctx) {
|
||||
DIR *d = opendir("/user/app");
|
||||
if (!d) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [LINK] open /user/app failed%s: %s",
|
||||
open_error_context ? open_error_context : "", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (stop_on_signal && should_stop_requested())
|
||||
break;
|
||||
if (!resolve_title_app_dir(entry, NULL, 0))
|
||||
continue;
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(entry->d_name, &paths);
|
||||
if (!fn(entry->d_name, &paths, ctx))
|
||||
break;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
if (out_size == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(title_id, &paths);
|
||||
return read_mount_link_file(paths.mount_link, out, out_size);
|
||||
}
|
||||
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size) {
|
||||
if (!entry)
|
||||
return false;
|
||||
if (entry->d_name[0] == '.')
|
||||
return false;
|
||||
if (strlen(entry->d_name) != 9)
|
||||
return false;
|
||||
|
||||
char path_buf[MAX_PATH];
|
||||
char *path = path_buf;
|
||||
size_t path_size = sizeof(path_buf);
|
||||
if (app_dir && app_dir_size > 0) {
|
||||
path = app_dir;
|
||||
path_size = app_dir_size;
|
||||
}
|
||||
|
||||
int written = snprintf(path, path_size, "/user/app/%s", entry->d_name);
|
||||
if (written < 0 || (size_t)written >= path_size) {
|
||||
if (app_dir && app_dir_size > 0)
|
||||
app_dir[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
if (entry->d_type != DT_DIR) {
|
||||
if (entry->d_type != DT_UNKNOWN)
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool source_path_needs_cleanup(const char *source_path,
|
||||
bool *tried_image_recovery) {
|
||||
if (is_under_image_mount_base(source_path)) {
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = resolve_image_source_from_mount_cache(
|
||||
source_path, image_source_path, sizeof(image_source_path));
|
||||
if (has_image_source && access(image_source_path, F_OK) != 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (access(source_path, F_OK) != 0)
|
||||
return true;
|
||||
if (path_matches_root_or_child(source_path, "/system_ex/app"))
|
||||
return true;
|
||||
|
||||
char eboot_path[MAX_PATH];
|
||||
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
|
||||
if (access(eboot_path, F_OK) == 0)
|
||||
return false;
|
||||
|
||||
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
|
||||
cleanup_stale_image_mounts();
|
||||
*tried_image_recovery = true;
|
||||
}
|
||||
|
||||
return access(eboot_path, F_OK) != 0;
|
||||
}
|
||||
|
||||
static bool cleanup_staged_mount_links_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
(void)title_id;
|
||||
(void)ctx;
|
||||
|
||||
struct stat staged_st;
|
||||
if (stat(paths->staged_mount_link, &staged_st) == 0) {
|
||||
struct stat link_st;
|
||||
if (stat(paths->mount_link, &link_st) == 0) {
|
||||
if (unlink(paths->staged_mount_link) == 0) {
|
||||
log_debug(" [LINK] removed stale staged mount link: %s",
|
||||
paths->staged_mount_link);
|
||||
} else if (errno != ENOENT) {
|
||||
log_debug(" [LINK] staged cleanup failed for %s: %s",
|
||||
paths->staged_mount_link, strerror(errno));
|
||||
}
|
||||
} else if (errno != ENOENT) {
|
||||
log_debug(" [LINK] staged cleanup failed for %s: %s",
|
||||
paths->staged_mount_link, strerror(errno));
|
||||
} else if (rename(paths->staged_mount_link, paths->mount_link) == 0) {
|
||||
log_debug(" [LINK] restored interrupted mount link: %s",
|
||||
paths->mount_link);
|
||||
} else {
|
||||
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
}
|
||||
} else if (errno != ENOENT) {
|
||||
log_debug(" [LINK] staged cleanup failed for %s: %s",
|
||||
paths->staged_mount_link, strerror(errno));
|
||||
return true;
|
||||
}
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
|
||||
strerror(errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_under_image_mount_base(source_path)) {
|
||||
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
|
||||
strerror(errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
char image_source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_image_link, image_source_path,
|
||||
sizeof(image_source_path))) {
|
||||
return true;
|
||||
}
|
||||
if (!cache_image_source_mapping(image_source_path, source_path)) {
|
||||
log_debug(" [LINK] image source cache warmup failed: %s -> %s",
|
||||
source_path, image_source_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void cleanup_staged_mount_links(void) {
|
||||
for_each_title_app_dir(" for staged cleanup", false,
|
||||
cleanup_staged_mount_links_entry, NULL);
|
||||
}
|
||||
|
||||
static bool get_top_mount(const char *path, struct statfs *mount_st_out) {
|
||||
if (statfs(path, mount_st_out) == 0 &&
|
||||
strcmp(mount_st_out->f_mntonname, path) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
if (mntcount <= 0 || !mntbuf)
|
||||
return false;
|
||||
|
||||
const struct statfs *best_mount = NULL;
|
||||
for (int i = 0; i < mntcount; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, path) != 0)
|
||||
continue;
|
||||
if (!best_mount ||
|
||||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
|
||||
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
|
||||
best_mount = &mntbuf[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (!best_mount)
|
||||
return false;
|
||||
|
||||
*mount_st_out = *best_mount;
|
||||
return true;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
TITLE_STACK_TOP_NONE = 0,
|
||||
TITLE_STACK_TOP_OTHER,
|
||||
TITLE_STACK_TOP_NULLFS,
|
||||
TITLE_STACK_TOP_BACKPORT,
|
||||
} title_stack_top_kind_t;
|
||||
|
||||
typedef struct {
|
||||
bool mounted;
|
||||
bool has_our_nullfs;
|
||||
bool has_nullfs_from_root;
|
||||
bool has_our_backport;
|
||||
bool top_is_our_nullfs;
|
||||
int our_nullfs_count;
|
||||
int our_backport_count;
|
||||
title_stack_top_kind_t top_kind;
|
||||
char system_ex_path[MAX_PATH];
|
||||
char top_from[MAX_PATH];
|
||||
} title_mount_state_t;
|
||||
|
||||
static bool path_is_managed_backport_for_title(const char *title_id,
|
||||
const char *path) {
|
||||
if (strncmp(path, "<above>:", 8) == 0)
|
||||
path += 8;
|
||||
|
||||
char root[MAX_PATH];
|
||||
int scan_path_count = get_scan_path_count();
|
||||
for (int i = 0; i < scan_path_count; i++) {
|
||||
if (!build_backports_root_path(get_scan_path(i), root))
|
||||
continue;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
continue;
|
||||
if (path[root_len] != '/')
|
||||
continue;
|
||||
if (strcmp(path + root_len + 1u, title_id) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool inspect_title_stack(const char *title_id, const char *source_path,
|
||||
const char *source_root,
|
||||
title_mount_state_t *state_out) {
|
||||
memset(state_out, 0, sizeof(*state_out));
|
||||
snprintf(state_out->system_ex_path, sizeof(state_out->system_ex_path),
|
||||
"/system_ex/app/%s", title_id);
|
||||
|
||||
struct statfs statfs_mount;
|
||||
bool statfs_ok = false;
|
||||
int inspect_errno = 0;
|
||||
int statfs_res = statfs(state_out->system_ex_path, &statfs_mount);
|
||||
if (statfs_res == 0 &&
|
||||
strcmp(statfs_mount.f_mntonname, state_out->system_ex_path) == 0) {
|
||||
statfs_ok = true;
|
||||
} else if (statfs_res != 0) {
|
||||
inspect_errno = errno;
|
||||
}
|
||||
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
const struct statfs *best_mount = NULL;
|
||||
bool found_mount_entry = false;
|
||||
if (mntcount > 0 && mntbuf) {
|
||||
for (int i = 0; i < mntcount; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
|
||||
continue;
|
||||
|
||||
found_mount_entry = true;
|
||||
if (!best_mount ||
|
||||
(strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
|
||||
strcmp(best_mount->f_fstypename, "unionfs") != 0)) {
|
||||
best_mount = &mntbuf[i];
|
||||
}
|
||||
|
||||
if (strcmp(mntbuf[i].f_fstypename, "nullfs") == 0) {
|
||||
if (source_path && source_path[0] != '\0' &&
|
||||
strcmp(mntbuf[i].f_mntfromname, source_path) == 0) {
|
||||
state_out->has_our_nullfs = true;
|
||||
state_out->our_nullfs_count++;
|
||||
}
|
||||
if (source_root && source_root[0] != '\0' &&
|
||||
path_matches_root_or_child(mntbuf[i].f_mntfromname, source_root)) {
|
||||
state_out->has_nullfs_from_root = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(mntbuf[i].f_fstypename, "unionfs") == 0 &&
|
||||
path_is_managed_backport_for_title(title_id,
|
||||
mntbuf[i].f_mntfromname)) {
|
||||
state_out->has_our_backport = true;
|
||||
state_out->our_backport_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const struct statfs *top_mount = NULL;
|
||||
if (statfs_ok) {
|
||||
top_mount = &statfs_mount;
|
||||
} else if (best_mount) {
|
||||
top_mount = best_mount;
|
||||
if (inspect_errno == EEXIST) {
|
||||
log_debug(" [LINK] inspect recovered from statfs(EEXIST) for %s",
|
||||
state_out->system_ex_path);
|
||||
}
|
||||
} else if (inspect_errno != 0 && inspect_errno != ENOENT &&
|
||||
inspect_errno != EINVAL) {
|
||||
log_debug(" [LINK] inspect statfs failed for %s: %s",
|
||||
state_out->system_ex_path, strerror(inspect_errno));
|
||||
if (mntcount <= 0 || !mntbuf) {
|
||||
log_debug(" [LINK] mount table unavailable for %s",
|
||||
state_out->system_ex_path);
|
||||
} else if (!found_mount_entry) {
|
||||
log_debug(" [LINK] no mount entries for %s", state_out->system_ex_path);
|
||||
}
|
||||
for (int i = 0; i < mntcount; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, state_out->system_ex_path) != 0)
|
||||
continue;
|
||||
log_debug(" [LINK] mount entry for %s: type=%s from=%s flags=0x%lX",
|
||||
state_out->system_ex_path, mntbuf[i].f_fstypename,
|
||||
mntbuf[i].f_mntfromname, (unsigned long)mntbuf[i].f_flags);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!top_mount)
|
||||
return true;
|
||||
|
||||
const char *top_from = top_mount->f_mntfromname;
|
||||
if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
|
||||
strncmp(top_from, "<above>:", 8) == 0) {
|
||||
top_from += 8;
|
||||
}
|
||||
|
||||
state_out->mounted = true;
|
||||
(void)strlcpy(state_out->top_from, top_from, sizeof(state_out->top_from));
|
||||
state_out->top_kind = TITLE_STACK_TOP_OTHER;
|
||||
if (strcmp(top_mount->f_fstypename, "nullfs") == 0) {
|
||||
state_out->top_kind = TITLE_STACK_TOP_NULLFS;
|
||||
if (source_path && source_path[0] != '\0' &&
|
||||
strcmp(top_from, source_path) == 0) {
|
||||
state_out->top_is_our_nullfs = true;
|
||||
state_out->has_our_nullfs = true;
|
||||
}
|
||||
} else if (strcmp(top_mount->f_fstypename, "unionfs") == 0 &&
|
||||
path_is_managed_backport_for_title(title_id, top_from)) {
|
||||
state_out->top_kind = TITLE_STACK_TOP_BACKPORT;
|
||||
state_out->has_our_backport = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_top_controlled_layer(const char *path) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
|
||||
bool controlled_top =
|
||||
(strcmp(mount_st.f_fstypename, "unionfs") == 0) ||
|
||||
(strcmp(mount_st.f_fstypename, "nullfs") == 0);
|
||||
if (!controlled_top) {
|
||||
log_debug(" [LINK] mount point busy for %s: type=%s from=%s flags=0x%lX",
|
||||
path, mount_st.f_fstypename, mount_st.f_mntfromname,
|
||||
(unsigned long)mount_st.f_flags);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
|
||||
log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
if (overlay_active_out)
|
||||
*overlay_active_out = false;
|
||||
if (!title_id || title_id[0] == '\0' || !src_path || src_path[0] == '\0' ||
|
||||
!expected_backport_path || expected_backport_path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat backport_st;
|
||||
bool backport_present = stat(expected_backport_path, &backport_st) == 0 &&
|
||||
S_ISDIR(backport_st.st_mode);
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
|
||||
return false;
|
||||
}
|
||||
if (!state.has_our_nullfs)
|
||||
return false;
|
||||
|
||||
if (state.top_is_our_nullfs)
|
||||
return backport_present;
|
||||
|
||||
if (!state.has_our_backport || state.top_kind != TITLE_STACK_TOP_BACKPORT)
|
||||
return false;
|
||||
|
||||
if (strcmp(state.top_from, expected_backport_path) == 0 && backport_present) {
|
||||
if (overlay_active_out)
|
||||
*overlay_active_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!unmount_top_controlled_layer(state.system_ex_path))
|
||||
return false;
|
||||
log_debug(" [IMG] backport overlay removed: %s -> %s", state.top_from,
|
||||
state.system_ex_path);
|
||||
return backport_present;
|
||||
}
|
||||
|
||||
void 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;
|
||||
|
||||
struct statfs mounted_sfs;
|
||||
if (statfs(mount_point, &mounted_sfs) != 0 ||
|
||||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
|
||||
return;
|
||||
}
|
||||
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0)
|
||||
return;
|
||||
|
||||
bool mount_read_only = ((mounted_sfs.f_flags & MNT_RDONLY) != 0);
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
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)};
|
||||
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;
|
||||
}
|
||||
|
||||
int overlay_err = errno;
|
||||
log_debug(" [IMG] backport overlay failed: %s -> %s (%s)", backport_path,
|
||||
mount_point, strerror(overlay_err));
|
||||
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
|
||||
backport_path, (uint32_t)overlay_err);
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
if (!unmount_top_controlled_layer(path))
|
||||
return false;
|
||||
}
|
||||
|
||||
struct statfs mount_st;
|
||||
return !get_top_mount(path, &mount_st);
|
||||
}
|
||||
|
||||
static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
(void)ctx;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path)))
|
||||
return true;
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state))
|
||||
return true;
|
||||
if ((state.our_nullfs_count <= 1 && state.our_backport_count <= 1) ||
|
||||
!state.has_our_nullfs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(
|
||||
" [LINK] duplicate managed mount layers for %s: nullfs=%d backport=%d. "
|
||||
"Resetting stack.",
|
||||
state.system_ex_path, state.our_nullfs_count, state.our_backport_count);
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
log_debug(" [LINK] failed to reset duplicate mount stack for %s",
|
||||
state.system_ex_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void cleanup_duplicate_title_mounts(void) {
|
||||
for_each_title_app_dir(" for duplicate cleanup", false,
|
||||
cleanup_duplicate_title_mounts_entry, NULL);
|
||||
}
|
||||
|
||||
// --- Copy Helpers for Install Action ---
|
||||
static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
FILE *fs = fopen(src, "rb");
|
||||
if (!fs)
|
||||
return -1;
|
||||
|
||||
if (fseek(fs, 0, SEEK_END) != 0) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
long file_size = ftell(fs);
|
||||
if (file_size < 0) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
rewind(fs);
|
||||
|
||||
size_t len = (size_t)file_size;
|
||||
char *buf = (char *)malloc(len + 1);
|
||||
if (!buf) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
if (len > 0 && fread(buf, 1, len, fs) != len) {
|
||||
free(buf);
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
fclose(fs);
|
||||
buf[len] = '\0';
|
||||
|
||||
char *hit = strstr(buf, "upgradable");
|
||||
if (hit) {
|
||||
size_t offset = (size_t)(hit - buf);
|
||||
size_t tail = len - offset - 10u;
|
||||
memcpy(hit, "standard", 8u);
|
||||
memmove(hit + 8u, hit + 10u, tail + 1u);
|
||||
len -= 2u;
|
||||
}
|
||||
|
||||
FILE *fd = fopen(dst, "wb");
|
||||
if (!fd) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
if (len > 0 && fwrite(buf, 1, len, fd) != len)
|
||||
ret = -1;
|
||||
if (fclose(fd) != 0)
|
||||
ret = -1;
|
||||
|
||||
if (ret == 0 && hit)
|
||||
log_debug(" [COPY] param.json patched: %s", dst);
|
||||
|
||||
free(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int copy_file(const char *src, const char *dst) {
|
||||
if (strstr(src, "/sce_sys/param.json")) {
|
||||
return copy_param_json_rewrite(src, dst);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
FILE *fs = fopen(src, "rb");
|
||||
if (!fs)
|
||||
return -1;
|
||||
FILE *fd = fopen(dst, "wb");
|
||||
if (!fd) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
int ret = 0;
|
||||
while (true) {
|
||||
size_t n = fread(buf, 1, sizeof(buf), fs);
|
||||
if (n > 0 && fwrite(buf, 1, n, fd) != n) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (n < sizeof(buf)) {
|
||||
if (ferror(fs))
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fflush(fd) != 0)
|
||||
ret = -1;
|
||||
if (fclose(fd) != 0)
|
||||
ret = -1;
|
||||
if (fclose(fs) != 0)
|
||||
ret = -1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int copy_dir(const char *src, const char *dst) {
|
||||
if (mkdir(dst, 0777) != 0 && errno != EEXIST)
|
||||
return -1;
|
||||
DIR *d = opendir(src);
|
||||
if (!d)
|
||||
return -1;
|
||||
int ret = 0;
|
||||
struct dirent *e;
|
||||
char ss[MAX_PATH], dd[MAX_PATH];
|
||||
struct stat st;
|
||||
struct stat lst;
|
||||
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);
|
||||
if (lstat(ss, &lst) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (S_ISLNK(lst.st_mode)) {
|
||||
if (stat(ss, &st) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
log_debug(" [COPY] refusing symlink directory: %s", ss);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
st = lst;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (copy_dir(ss, dd) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (copy_file(ss, dd) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closedir(d) != 0)
|
||||
ret = -1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int remount_system_ex(void) {
|
||||
struct iovec iov[] = {
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY("/system_ex"),
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
|
||||
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
|
||||
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
|
||||
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL)};
|
||||
return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE);
|
||||
}
|
||||
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
char dst[MAX_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);
|
||||
snprintf(dst_eboot, sizeof(dst_eboot), "%s/eboot.bin", dst);
|
||||
|
||||
struct stat src_st;
|
||||
if (stat(src_path, &src_st) != 0) {
|
||||
log_debug(" [LINK] source path check failed for %s -> %s: %s", title_id,
|
||||
src_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(src_st.st_mode)) {
|
||||
log_debug(" [LINK] source path is not a directory for %s: %s mode=0%o",
|
||||
title_id, src_path, (unsigned)(src_st.st_mode & 077777));
|
||||
return false;
|
||||
}
|
||||
if (access(src_eboot, F_OK) != 0) {
|
||||
bool tried_image_recovery = false;
|
||||
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
|
||||
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
|
||||
src_eboot);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
|
||||
log_debug(" [LINK] inspect failed for %s (%s): %s", title_id, dst,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (state.mounted) {
|
||||
if (state.has_our_nullfs &&
|
||||
(state.top_is_our_nullfs || state.has_our_backport) &&
|
||||
access(dst_eboot, F_OK) == 0) {
|
||||
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
|
||||
return true;
|
||||
}
|
||||
if (!unmount_controlled_mount_stack(dst)) {
|
||||
log_debug(" [LINK] failed to reset mount stack for %s (%s)", title_id,
|
||||
dst);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int mkdir_res = mkdir(dst, 0755);
|
||||
if (mkdir_res != 0) {
|
||||
if (errno != EEXIST) {
|
||||
log_debug(" [LINK] Failed to create mount directory for title %s: %s",
|
||||
title_id, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
struct stat dst_st;
|
||||
if (stat(dst, &dst_st) != 0) {
|
||||
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
|
||||
dst, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(dst_st.st_mode)) {
|
||||
log_debug(" [LINK] mount target is not a directory: %s mode=0%o", dst,
|
||||
(unsigned)(dst_st.st_mode & 077777));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct iovec iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(src_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
|
||||
};
|
||||
|
||||
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 (access(dst_eboot, F_OK) != 0) {
|
||||
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
|
||||
dst_eboot);
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
|
||||
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;
|
||||
} cleanup_mount_links_ctx_t;
|
||||
|
||||
static bool cleanup_mount_links_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx_ptr) {
|
||||
cleanup_mount_links_ctx_t *ctx = (cleanup_mount_links_ctx_t *)ctx_ptr;
|
||||
|
||||
struct stat lst;
|
||||
if (stat(paths->mount_link, &lst) != 0 || !S_ISREG(lst.st_mode))
|
||||
return true;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
bool should_remove = false;
|
||||
bool matches_removed_source = false;
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
should_remove = true;
|
||||
}
|
||||
|
||||
if (!should_remove) {
|
||||
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);
|
||||
should_remove = matches_removed_source;
|
||||
} else {
|
||||
should_remove =
|
||||
source_path_needs_cleanup(source_path, &ctx->tried_image_recovery);
|
||||
}
|
||||
}
|
||||
|
||||
if (!should_remove)
|
||||
return true;
|
||||
|
||||
bool keep_mount_link = false;
|
||||
bool mount_link_staged = false;
|
||||
if (ctx->unmount_system_ex_bind) {
|
||||
title_mount_state_t 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 stack_matches_link = false;
|
||||
if (source_path[0] != '\0') {
|
||||
stack_matches_link = state.has_our_nullfs;
|
||||
} else if (matches_removed_source && ctx->removed_source_root &&
|
||||
ctx->removed_source_root[0] != '\0') {
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
|
||||
if (inspected && stack_matches_link && !top_is_ours) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] keeping mount link for %s: stack still active but "
|
||||
"top layer is not ours", state.system_ex_path);
|
||||
} else if (inspected && top_is_ours && stack_matches_link) {
|
||||
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
|
||||
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
}
|
||||
mount_link_staged = true;
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] failed to unmount mount stack for %s",
|
||||
state.system_ex_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keep_mount_link) {
|
||||
int unlink_res = mount_link_staged ? unlink(paths->staged_mount_link)
|
||||
: unlink(paths->mount_link);
|
||||
if (unlink_res == 0 || errno == ENOENT) {
|
||||
log_debug(" [LINK] removed stale mount link: %s", paths->mount_link);
|
||||
if (unlink(paths->mount_image_link) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", paths->mount_image_link,
|
||||
strerror(errno));
|
||||
}
|
||||
} else {
|
||||
log_debug(" [LINK] remove failed for %s: %s",
|
||||
mount_link_staged ? paths->staged_mount_link : paths->mount_link,
|
||||
strerror(errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
|
||||
log_debug(" [LINK] restored mount link after failed cleanup: %s",
|
||||
paths->mount_link);
|
||||
} else if (mount_link_staged) {
|
||||
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind) {
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = removed_source_root,
|
||||
.unmount_system_ex_bind = unmount_system_ex_bind,
|
||||
.tried_image_recovery = false,
|
||||
};
|
||||
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
|
||||
&ctx);
|
||||
}
|
||||
|
||||
static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
(void)ctx;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
log_debug(" [LINK] failed to unmount shutdown stack for %s",
|
||||
state.system_ex_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown_title_mounts(void) {
|
||||
for_each_title_app_dir(" during shutdown", false,
|
||||
shutdown_title_mounts_entry, NULL);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
struct GameCache {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct GameCache g_game_cache[MAX_PENDING];
|
||||
|
||||
static void clear_game_cache_slot(int index, const char *reason) {
|
||||
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
|
||||
return;
|
||||
|
||||
if (reason && reason[0] != '\0') {
|
||||
if (g_game_cache[index].title_id[0] != '\0')
|
||||
log_debug(" [CACHE] %s: %s (%s)", reason, g_game_cache[index].title_id,
|
||||
g_game_cache[index].path);
|
||||
else
|
||||
log_debug(" [CACHE] %s: %s", reason, g_game_cache[index].path);
|
||||
}
|
||||
|
||||
if (g_game_cache[index].title_id[0] != '\0')
|
||||
clear_duplicate_title_notification(g_game_cache[index].title_id);
|
||||
|
||||
memset(&g_game_cache[index], 0, sizeof(g_game_cache[index]));
|
||||
}
|
||||
|
||||
void cache_game_entry(const char *path, const char *title_id,
|
||||
const char *title_name) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid) {
|
||||
(void)strlcpy(g_game_cache[k].path, path, sizeof(g_game_cache[k].path));
|
||||
(void)strlcpy(g_game_cache[k].title_id, title_id,
|
||||
sizeof(g_game_cache[k].title_id));
|
||||
(void)strlcpy(g_game_cache[k].title_name, title_name,
|
||||
sizeof(g_game_cache[k].title_name));
|
||||
g_game_cache[k].valid = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void prune_game_cache(void) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (access(g_game_cache[k].path, F_OK) == 0)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
}
|
||||
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = NULL;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path && strcmp(g_game_cache[k].path, path) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
return true;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_cached_game(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "removed from duplicate tracking");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/event.h>
|
||||
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
uint64_t exec_time_us;
|
||||
uint64_t deadline_us;
|
||||
} pending_game_launch_t;
|
||||
|
||||
// SysCore can emit multiple child exec events before the actual game title_id
|
||||
// becomes visible, so keep a small raw exec candidate buffer here.
|
||||
static pending_game_launch_t
|
||||
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
|
||||
static pthread_t g_game_lifecycle_thread;
|
||||
static bool g_game_lifecycle_thread_started = false;
|
||||
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
|
||||
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
|
||||
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 bool register_game_exit_watch(int kq, pid_t pid);
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
if (a == 0)
|
||||
return b;
|
||||
if (b == 0)
|
||||
return a;
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
/*
|
||||
static void bytes_to_hex(const void *src, size_t src_len, char *dst,
|
||||
size_t dst_size) {
|
||||
static const char hex_chars[] = "0123456789abcdef";
|
||||
const uint8_t *bytes = (const uint8_t *)src;
|
||||
size_t out_pos = 0;
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
|
||||
for (size_t i = 0; i < src_len && out_pos + 2 < dst_size; ++i) {
|
||||
dst[out_pos++] = hex_chars[(bytes[i] >> 4) & 0x0f];
|
||||
dst[out_pos++] = hex_chars[bytes[i] & 0x0f];
|
||||
}
|
||||
|
||||
dst[out_pos] = '\0';
|
||||
}
|
||||
|
||||
static void log_app_info_response(pid_t pid, int rc, const app_info_t *appinfo) {
|
||||
if (rc != 0) {
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld failed: rc=0x%08X",
|
||||
(long)pid, (unsigned)rc);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appinfo)
|
||||
return;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
size_t title_len = strnlen(appinfo->title_id, sizeof(appinfo->title_id));
|
||||
if (title_len >= sizeof(title_id))
|
||||
title_len = sizeof(title_id) - 1u;
|
||||
memcpy(title_id, appinfo->title_id, title_len);
|
||||
title_id[title_len] = '\0';
|
||||
|
||||
char unknown2_hex[sizeof(appinfo->unknown2) * 2u + 1u];
|
||||
bytes_to_hex(appinfo->unknown2, sizeof(appinfo->unknown2), unknown2_hex,
|
||||
sizeof(unknown2_hex));
|
||||
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld app_id=0x%08X "
|
||||
"unknown1=0x%016llX title_id=\"%s\"",
|
||||
(long)pid, appinfo->app_id,
|
||||
(unsigned long long)appinfo->unknown1, title_id);
|
||||
log_debug(" [GAME] sceKernelGetAppInfo pid=%ld unknown2=%s", (long)pid,
|
||||
unknown2_hex);
|
||||
}
|
||||
*/
|
||||
|
||||
static bool resolve_game_title_id(pid_t pid, char title_id[MAX_TITLE_ID],
|
||||
uint32_t *app_id_out) {
|
||||
app_info_t appinfo;
|
||||
memset(&appinfo, 0, sizeof(appinfo));
|
||||
int rc = sceKernelGetAppInfo(pid, &appinfo);
|
||||
/* log_app_info_response(pid, rc, &appinfo); */
|
||||
if (rc != 0)
|
||||
return false;
|
||||
|
||||
size_t title_len = strnlen(appinfo.title_id, sizeof(appinfo.title_id));
|
||||
if (title_len == 0 || title_len >= MAX_TITLE_ID)
|
||||
return false;
|
||||
|
||||
memcpy(title_id, appinfo.title_id, title_len);
|
||||
title_id[title_len] = '\0';
|
||||
if (app_id_out)
|
||||
*app_id_out = appinfo.app_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_process_alive(pid_t pid) {
|
||||
if (kill(pid, 0) == 0)
|
||||
return true;
|
||||
|
||||
return errno != ESRCH;
|
||||
}
|
||||
|
||||
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
|
||||
title_id, (long)pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
|
||||
sm_fakelib_game_on_exec(pid, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void clear_pending_game_launch(pending_game_launch_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
}
|
||||
|
||||
static void clear_all_pending_game_launches(void) {
|
||||
memset(g_pending_game_launches, 0, sizeof(g_pending_game_launches));
|
||||
}
|
||||
|
||||
static void close_game_lifecycle_wake_pipe(void) {
|
||||
if (g_game_lifecycle_wake_pipe[0] >= 0) {
|
||||
close(g_game_lifecycle_wake_pipe[0]);
|
||||
g_game_lifecycle_wake_pipe[0] = -1;
|
||||
}
|
||||
if (g_game_lifecycle_wake_pipe[1] >= 0) {
|
||||
close(g_game_lifecycle_wake_pipe[1]);
|
||||
g_game_lifecycle_wake_pipe[1] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void set_game_lifecycle_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_success = success;
|
||||
g_game_lifecycle_start_ready = true;
|
||||
pthread_cond_broadcast(&g_game_lifecycle_start_cond);
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
}
|
||||
|
||||
static pending_game_launch_t *find_pending_game_launch(pid_t pid) {
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (entry->active && entry->pid == pid)
|
||||
return entry;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pending_game_launch_t *reserve_pending_game_launch(uint64_t now_us) {
|
||||
pending_game_launch_t *oldest_entry = NULL;
|
||||
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
return entry;
|
||||
|
||||
if (now_us != 0 && entry->deadline_us != 0 && now_us >= entry->deadline_us)
|
||||
return entry;
|
||||
|
||||
if (!oldest_entry ||
|
||||
(entry->deadline_us != 0 &&
|
||||
(oldest_entry->deadline_us == 0 ||
|
||||
entry->deadline_us < oldest_entry->deadline_us))) {
|
||||
oldest_entry = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return oldest_entry;
|
||||
}
|
||||
|
||||
static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
|
||||
uint64_t exec_time_us,
|
||||
uint64_t now_us) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (!entry)
|
||||
entry = reserve_pending_game_launch(now_us);
|
||||
if (!entry)
|
||||
return NULL;
|
||||
|
||||
if (entry->active && entry->pid != pid) {
|
||||
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
|
||||
(long)entry->pid, (long)pid);
|
||||
}
|
||||
|
||||
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
|
||||
entry->active = true;
|
||||
entry->pid = pid;
|
||||
entry->exec_time_us = base_time_us;
|
||||
entry->deadline_us =
|
||||
base_time_us == 0 ? 0 : base_time_us + GAME_APPINFO_LOOKUP_TIMEOUT_US;
|
||||
return entry;
|
||||
}
|
||||
|
||||
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
|
||||
uint64_t now_us,
|
||||
const char *title_id) {
|
||||
clear_all_pending_game_launches();
|
||||
if (!queue_pending_game_launch(pid, exec_time_us, now_us)) {
|
||||
log_debug(" [GAME] failed to defer launch retry for %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
}
|
||||
|
||||
static uint64_t next_pending_game_wake_us(uint64_t now_us) {
|
||||
uint64_t next_wake_us = 0;
|
||||
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
continue;
|
||||
|
||||
if (now_us == 0) {
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t poll_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
uint64_t entry_wake_us = min_nonzero_u64(entry->deadline_us, poll_wake_us);
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, entry_wake_us);
|
||||
}
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
static const struct timespec *compute_game_wait_timeout(
|
||||
struct timespec *timeout_out) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t next_wake_us = 0;
|
||||
|
||||
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));
|
||||
if (next_wake_us == 0)
|
||||
return NULL;
|
||||
|
||||
uint64_t wait_us = GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
if (now_us != 0 && next_wake_us > now_us)
|
||||
wait_us = next_wake_us - now_us;
|
||||
else if (now_us != 0)
|
||||
wait_us = 0;
|
||||
|
||||
timeout_out->tv_sec = (time_t)(wait_us / 1000000ull);
|
||||
timeout_out->tv_nsec = (long)((wait_us % 1000000ull) * 1000ull);
|
||||
return timeout_out;
|
||||
}
|
||||
|
||||
static void poll_game_modules(int kq) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
for (size_t i = 0; i < MAX_PENDING_GAME_EXEC_CANDIDATES; ++i) {
|
||||
pending_game_launch_t *entry = &g_pending_game_launches[i];
|
||||
if (!entry->active)
|
||||
continue;
|
||||
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
|
||||
pid_t pid = entry->pid;
|
||||
uint64_t exec_time_us = entry->exec_time_us;
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
|
||||
clear_pending_game_launch(entry);
|
||||
clear_all_pending_game_launches();
|
||||
break;
|
||||
}
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
|
||||
break;
|
||||
} else {
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
} else if (!is_process_alive(entry->pid)) {
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
|
||||
if (entry->active && now_us != 0 && entry->deadline_us != 0 &&
|
||||
now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] title_id was not available within %uus for pid=%ld",
|
||||
(unsigned)GAME_APPINFO_LOOKUP_TIMEOUT_US, (long)entry->pid);
|
||||
clear_pending_game_launch(entry);
|
||||
}
|
||||
}
|
||||
|
||||
sm_kstuff_game_poll();
|
||||
}
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid) {
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_EXIT, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] failed to register exit watch for pid=%ld: %s",
|
||||
(long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handle_game_exec(int kq, pid_t pid) {
|
||||
if (find_pending_game_launch(pid))
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(pid, title_id, &app_id)) {
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
|
||||
clear_all_pending_game_launches();
|
||||
else
|
||||
defer_confirmed_game_launch_retry(pid, now_us, now_us, title_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_process_alive(pid))
|
||||
return;
|
||||
|
||||
pending_game_launch_t *entry = queue_pending_game_launch(pid, now_us, now_us);
|
||||
if (!entry)
|
||||
return;
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
pid_t syscore_pid = find_pid_by_name("SceSysCore.elf", false);
|
||||
if (syscore_pid <= 0) {
|
||||
log_debug(" [GAME] failed to find SceSysCore.elf");
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int kq = kqueue();
|
||||
if (kq < 0) {
|
||||
log_debug(" [GAME] kqueue failed: %s", strerror(errno));
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct kevent kev;
|
||||
EV_SET(&kev, (uintptr_t)syscore_pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_CLEAR,
|
||||
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] proc watch registration failed: %s",
|
||||
strerror(errno));
|
||||
close(kq);
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EV_SET(&kev, (uintptr_t)g_game_lifecycle_wake_pipe[0], EVFILT_READ,
|
||||
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
|
||||
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
||||
log_debug(" [GAME] wake pipe registration failed: %s", strerror(errno));
|
||||
close(kq);
|
||||
set_game_lifecycle_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
set_game_lifecycle_start_result(true);
|
||||
log_debug(" [GAME] lifecycle watcher started");
|
||||
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
struct kevent event;
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, timeout_ptr);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (g_game_lifecycle_stop_requested)
|
||||
break;
|
||||
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
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));
|
||||
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
|
||||
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
|
||||
} else {
|
||||
if ((event.fflags & NOTE_EXEC) != 0)
|
||||
handle_game_exec(kq, (pid_t)event.ident);
|
||||
if ((event.fflags & NOTE_EXIT) != 0)
|
||||
handle_game_exit((pid_t)event.ident);
|
||||
}
|
||||
}
|
||||
|
||||
poll_game_modules(kq);
|
||||
}
|
||||
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_game_shutdown();
|
||||
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) {
|
||||
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_ready = false;
|
||||
g_game_lifecycle_start_success = false;
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
|
||||
int rc =
|
||||
pthread_create(&g_game_lifecycle_thread, NULL, game_lifecycle_watcher_main,
|
||||
NULL);
|
||||
if (rc != 0) {
|
||||
log_debug(" [GAME] watcher start failed: %s", strerror(rc));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
while (!g_game_lifecycle_start_ready)
|
||||
pthread_cond_wait(&g_game_lifecycle_start_cond, &g_game_lifecycle_start_mutex);
|
||||
bool start_success = g_game_lifecycle_start_success;
|
||||
pthread_mutex_unlock(&g_game_lifecycle_start_mutex);
|
||||
|
||||
if (!start_success) {
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_thread_started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void wake_game_lifecycle_watcher(void) {
|
||||
if (g_game_lifecycle_wake_pipe[1] < 0)
|
||||
return;
|
||||
|
||||
char wake = 'x';
|
||||
ssize_t written = write(g_game_lifecycle_wake_pipe[1], &wake, sizeof(wake));
|
||||
if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
log_debug(" [GAME] wake pipe write failed: %s", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
void stop_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return;
|
||||
|
||||
g_game_lifecycle_stop_requested = 1;
|
||||
wake_game_lifecycle_watcher();
|
||||
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_path_state.h"
|
||||
|
||||
// --- Game Metadata Parsing (param.json) ---
|
||||
static int extract_json_string(const char *json, const char *key, char *out,
|
||||
size_t out_size) {
|
||||
char search[64];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
const char *p = strstr(json, search);
|
||||
if (!p)
|
||||
return -1;
|
||||
p = strchr(p + strlen(search), ':');
|
||||
if (!p)
|
||||
return -2;
|
||||
while (*++p && isspace(*p)) {
|
||||
/* skip */
|
||||
}
|
||||
if (*p != '"')
|
||||
return -3;
|
||||
p++;
|
||||
|
||||
size_t i = 0;
|
||||
while (i < out_size - 1 && p[i] && p[i] != '"') {
|
||||
out[i] = p[i];
|
||||
i++;
|
||||
}
|
||||
out[i] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name) {
|
||||
if (!out_id || !out_name)
|
||||
return false;
|
||||
out_id[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
if (!base_path || !param_st || !S_ISREG(param_st->st_mode))
|
||||
return false;
|
||||
|
||||
bool cached_valid = false;
|
||||
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) {
|
||||
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);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = (size_t)param_st->st_size;
|
||||
char *buf = (char *)malloc(len + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
bool read_ok = (fread(buf, 1, len, f) == len);
|
||||
fclose(f);
|
||||
if (!read_ok) {
|
||||
free(buf);
|
||||
return false;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
|
||||
bool valid = false;
|
||||
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
|
||||
if (res != 0)
|
||||
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
|
||||
if (res == 0) {
|
||||
const char *en_ptr = strstr(buf, "\"en-US\"");
|
||||
const char *search_start = en_ptr ? en_ptr : buf;
|
||||
if (extract_json_string(search_start, "titleName", out_name,
|
||||
MAX_TITLE_NAME) != 0)
|
||||
extract_json_string(buf, "titleName", out_name, MAX_TITLE_NAME);
|
||||
if (out_name[0] == '\0')
|
||||
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
|
||||
valid = true;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
store_cached_game_info(base_path, param_st, valid, out_id, out_name);
|
||||
return valid;
|
||||
}
|
||||
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
|
||||
if (!dir_path || dir_path[0] == '\0')
|
||||
return false;
|
||||
|
||||
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
|
||||
if (dir_fd < 0)
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (fstatat(dir_fd, "sce_sys", &st, 0) == 0 && S_ISDIR(st.st_mode) &&
|
||||
fstatat(dir_fd, "sce_sys/param.json", &st, 0) == 0 &&
|
||||
S_ISREG(st.st_mode)) {
|
||||
if (param_st_out)
|
||||
*param_st_out = st;
|
||||
close(dir_fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
close(dir_fd);
|
||||
return false;
|
||||
}
|
||||
+926
@@ -0,0 +1,926 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
|
||||
if ((access(devname, F_OK) == 0) == should_exist)
|
||||
return true;
|
||||
sceKernelUsleep(LVD_NODE_WAIT_US);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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:
|
||||
return cfg->lvd_sector_pfs;
|
||||
case IMAGE_FS_EXFAT:
|
||||
default:
|
||||
return cfg->lvd_sector_exfat;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t get_lvd_sector_size(const char *path, image_fs_type_t fs_type) {
|
||||
uint32_t fallback = get_lvd_sector_size_fallback(fs_type);
|
||||
if (!path)
|
||||
return fallback;
|
||||
|
||||
struct statfs sfs;
|
||||
if (statfs(path, &sfs) != 0)
|
||||
return fallback;
|
||||
|
||||
uint64_t fs_cluster_size = (uint64_t)sfs.f_bsize;
|
||||
if (fs_cluster_size == 0)
|
||||
fs_cluster_size = (uint64_t)sfs.f_iosize;
|
||||
if (fs_cluster_size == 0 || fs_cluster_size >= (uint64_t)fallback)
|
||||
return fallback;
|
||||
|
||||
return (uint32_t)fs_cluster_size;
|
||||
}
|
||||
|
||||
static uint32_t get_lvd_secondary_unit(const char *path,
|
||||
image_fs_type_t fs_type) {
|
||||
if (fs_type == IMAGE_FS_EXFAT)
|
||||
return LVD_SECONDARY_UNIT_SINGLE_IMAGE;
|
||||
return get_lvd_sector_size(path, fs_type);
|
||||
}
|
||||
|
||||
static uint32_t get_md_sector_size(image_fs_type_t fs_type) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
switch (fs_type) {
|
||||
case IMAGE_FS_UFS:
|
||||
return cfg->md_sector_ufs;
|
||||
case IMAGE_FS_EXFAT:
|
||||
default:
|
||||
return cfg->md_sector_exfat;
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned int get_md_attach_options(bool mount_read_only) {
|
||||
unsigned int options = MD_AUTOUNIT | MD_ASYNC;
|
||||
if (mount_read_only)
|
||||
options |= MD_READONLY;
|
||||
return options;
|
||||
}
|
||||
|
||||
static uint16_t get_lvd_attach_raw_flags(image_fs_type_t fs_type,
|
||||
bool mount_read_only) {
|
||||
if (fs_type == IMAGE_FS_UFS) {
|
||||
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_DD_RO
|
||||
: LVD_ATTACH_RAW_FLAGS_DD_RW;
|
||||
}
|
||||
return mount_read_only ? LVD_ATTACH_RAW_FLAGS_SINGLE_RO
|
||||
: LVD_ATTACH_RAW_FLAGS_SINGLE_RW;
|
||||
}
|
||||
|
||||
static unsigned int get_nmount_flags(image_fs_type_t fs_type,
|
||||
bool mount_read_only,
|
||||
const char **mount_mode_out) {
|
||||
if (fs_type == IMAGE_FS_UFS) {
|
||||
if (mount_mode_out)
|
||||
*mount_mode_out = mount_read_only ? "dd_ro" : "dd_rw";
|
||||
return mount_read_only ? UFS_NMOUNT_FLAG_RO : UFS_NMOUNT_FLAG_RW;
|
||||
}
|
||||
if (mount_mode_out)
|
||||
*mount_mode_out = mount_read_only ? "rdonly" : "rw";
|
||||
return mount_read_only ? MNT_RDONLY : 0;
|
||||
}
|
||||
|
||||
static uint16_t normalize_lvd_raw_flags(uint16_t raw_flags) {
|
||||
if ((raw_flags & 0x800Eu) != 0u) {
|
||||
uint32_t raw = (uint32_t)raw_flags;
|
||||
uint32_t len = (raw & 0xFFFF8000u) + ((raw & 2u) << 6) +
|
||||
(8u * (raw & 1u)) + (2u * ((raw >> 2) & 1u)) +
|
||||
(2u * (raw & 8u)) + 4u;
|
||||
return (uint16_t)len;
|
||||
}
|
||||
return (uint16_t)(8u * ((uint32_t)raw_flags & 1u) + 4u);
|
||||
}
|
||||
|
||||
static uint16_t get_lvd_image_type(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)
|
||||
return LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA;
|
||||
return LVD_ATTACH_IMAGE_TYPE_SINGLE;
|
||||
}
|
||||
|
||||
static uint16_t get_lvd_source_type(const char *path) {
|
||||
if (!path)
|
||||
return LVD_ENTRY_TYPE_FILE;
|
||||
|
||||
if (strncmp(path, "/dev/sbram0", strlen("/dev/sbram0")) == 0)
|
||||
return LVD_ENTRY_TYPE_SPECIAL;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
|
||||
return LVD_ENTRY_TYPE_SPECIAL;
|
||||
}
|
||||
return LVD_ENTRY_TYPE_FILE;
|
||||
}
|
||||
|
||||
// --- Image Path and Naming Helpers ---
|
||||
static image_fs_type_t detect_image_fs_type(const char *name) {
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot)
|
||||
return IMAGE_FS_UNKNOWN;
|
||||
if (strcasecmp(dot, ".ffpkg") == 0)
|
||||
return IMAGE_FS_UFS;
|
||||
if (strcasecmp(dot, ".exfat") == 0)
|
||||
return IMAGE_FS_EXFAT;
|
||||
if (strcasecmp(dot, ".ffpfs") == 0)
|
||||
return IMAGE_FS_PFS;
|
||||
return IMAGE_FS_UNKNOWN;
|
||||
}
|
||||
|
||||
static const char *image_fs_name(image_fs_type_t fs_type) {
|
||||
switch (fs_type) {
|
||||
case IMAGE_FS_UFS:
|
||||
return "ufs";
|
||||
case IMAGE_FS_EXFAT:
|
||||
return "exfatfs";
|
||||
case IMAGE_FS_PFS:
|
||||
return "pfs";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void log_fs_stats(const char *tag, const char *path,
|
||||
const char *type_hint) {
|
||||
struct statfs sfs;
|
||||
if (statfs(path, &sfs) != 0) {
|
||||
log_debug(" [%s] FS stats read failed for %s: %s", tag, path,
|
||||
strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
const char *type_name = type_hint;
|
||||
if (sfs.f_fstypename[0] != '\0')
|
||||
type_name = sfs.f_fstypename;
|
||||
if (!type_name)
|
||||
type_name = "unknown";
|
||||
|
||||
uint64_t bsize = (uint64_t)sfs.f_bsize;
|
||||
uint64_t iosize = (uint64_t)sfs.f_iosize;
|
||||
uint64_t blocks = (uint64_t)sfs.f_blocks;
|
||||
uint64_t bfree = (uint64_t)sfs.f_bfree;
|
||||
uint64_t bavail = (uint64_t)sfs.f_bavail;
|
||||
uint64_t files = (uint64_t)sfs.f_files;
|
||||
uint64_t ffree = (uint64_t)sfs.f_ffree;
|
||||
uint64_t total_bytes = blocks * bsize;
|
||||
uint64_t free_bytes = bfree * bsize;
|
||||
uint64_t avail_bytes = bavail * bsize;
|
||||
|
||||
log_debug(" [%s] FS stats: path=%s type=%s bsize=%llu iosize=%llu "
|
||||
"blocks=%llu bfree=%llu bavail=%llu files=%llu ffree=%llu "
|
||||
"flags=0x%lX total=%lluB free=%lluB avail=%lluB",
|
||||
tag, path, type_name, (unsigned long long)bsize,
|
||||
(unsigned long long)iosize, (unsigned long long)blocks,
|
||||
(unsigned long long)bfree, (unsigned long long)bavail,
|
||||
(unsigned long long)files, (unsigned long long)ffree,
|
||||
(unsigned long)sfs.f_flags, (unsigned long long)total_bytes,
|
||||
(unsigned long long)free_bytes, (unsigned long long)avail_bytes);
|
||||
}
|
||||
|
||||
static void strip_extension(const char *filename, char *out, size_t out_size) {
|
||||
const char *dot = strrchr(filename, '.');
|
||||
size_t len = dot ? (size_t)(dot - filename) : strlen(filename);
|
||||
if (len >= out_size)
|
||||
len = out_size - 1;
|
||||
memcpy(out, filename, len);
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
static void build_image_mount_point(const char *file_path,
|
||||
char mount_point[MAX_PATH]) {
|
||||
const char *filename = get_filename_component(file_path);
|
||||
char base_name[MAX_PATH];
|
||||
char mount_name[MAX_PATH];
|
||||
strip_extension(filename, base_name, sizeof(base_name));
|
||||
|
||||
size_t base_len = strlen(base_name);
|
||||
size_t max_base_len = sizeof(mount_name) - 1u - 9u;
|
||||
if (base_len > max_base_len)
|
||||
base_len = max_base_len;
|
||||
memcpy(mount_name, base_name, base_len);
|
||||
mount_name[base_len] = '\0';
|
||||
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);
|
||||
}
|
||||
|
||||
typedef bool (*image_attach_fn)(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);
|
||||
|
||||
typedef struct {
|
||||
attach_backend_t id;
|
||||
image_attach_fn attach;
|
||||
} image_backend_ops_t;
|
||||
|
||||
static bool attach_md_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) {
|
||||
int md_fd = open(MD_CTRL_PATH, O_RDWR);
|
||||
if (md_fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
struct md_ioctl req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.md_version = MDIOVERSION;
|
||||
req.md_type = MD_VNODE;
|
||||
req.md_file = (char *)file_path;
|
||||
req.md_mediasize = file_size;
|
||||
req.md_sectorsize = get_md_sector_size(fs_type);
|
||||
req.md_options = get_md_attach_options(mount_read_only);
|
||||
|
||||
int last_errno = 0;
|
||||
log_debug(" [IMG][%s] attach try: options=0x%x",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), req.md_options);
|
||||
int ret = ioctl(md_fd, MDIOCATTACH, &req);
|
||||
if (ret != 0)
|
||||
last_errno = errno;
|
||||
close(md_fd);
|
||||
|
||||
if (ret != 0) {
|
||||
errno = last_errno;
|
||||
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), strerror(errno), ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
int unit_id = (int)req.md_unit;
|
||||
if (unit_id < 0) {
|
||||
log_debug(" [IMG][%s] attach returned invalid unit: %d",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
snprintf(devname_out, devname_size, "/dev/md%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname_out, true)) {
|
||||
log_debug(" [IMG][%s] device node did not appear: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), devname_out);
|
||||
(void)detach_attached_unit(ATTACH_BACKEND_MD, unit_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] attach returned unit=%d",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
|
||||
*unit_id_out = unit_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
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) {
|
||||
int lvd_fd = open(LVD_CTRL_PATH, O_RDWR);
|
||||
if (lvd_fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
lvd_ioctl_layer_v0_t layers[LVD_ATTACH_LAYER_COUNT];
|
||||
memset(layers, 0, sizeof(layers));
|
||||
layers[0].source_type = get_lvd_source_type(file_path);
|
||||
layers[0].flags = LVD_ENTRY_FLAG_NO_BITMAP;
|
||||
layers[0].path = file_path;
|
||||
layers[0].offset = 0;
|
||||
layers[0].size = (uint64_t)file_size;
|
||||
|
||||
uint32_t sector_size = get_lvd_sector_size(file_path, fs_type);
|
||||
uint32_t secondary_unit = get_lvd_secondary_unit(file_path, fs_type);
|
||||
uint16_t raw_flags = get_lvd_attach_raw_flags(fs_type, mount_read_only);
|
||||
uint16_t normalized_flags = normalize_lvd_raw_flags(raw_flags);
|
||||
|
||||
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.layer_count = LVD_ATTACH_LAYER_COUNT;
|
||||
req.device_size = (uint64_t)file_size;
|
||||
req.layers_ptr = layers;
|
||||
req.sector_size = sector_size;
|
||||
req.secondary_unit = secondary_unit;
|
||||
req.flags = normalized_flags;
|
||||
req.device_id = -1;
|
||||
|
||||
int last_errno = 0;
|
||||
log_debug(" [IMG][%s] attach try: ver=%u sec=%u sec2=%u raw=0x%x "
|
||||
"flags=0x%x img=%u",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), req.io_version,
|
||||
req.sector_size, req.secondary_unit, raw_flags, req.flags,
|
||||
req.image_type);
|
||||
int ret = ioctl(lvd_fd, SCE_LVD_IOC_ATTACH_V0, &req);
|
||||
if (ret != 0)
|
||||
last_errno = errno;
|
||||
close(lvd_fd);
|
||||
int unit_id = req.device_id;
|
||||
|
||||
if (ret != 0) {
|
||||
errno = last_errno;
|
||||
log_debug(" [IMG][%s] attach failed: %s (ret: 0x%x)",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), strerror(errno), ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (unit_id < 0) {
|
||||
log_debug(" [IMG][%s] attach returned invalid unit: %d",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
|
||||
return false;
|
||||
}
|
||||
log_debug(" [IMG][%s] attach returned unit=%d",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
|
||||
|
||||
snprintf(devname_out, devname_size, "/dev/lvd%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname_out, true)) {
|
||||
log_debug(" [IMG][%s] device node did not appear: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), devname_out);
|
||||
(void)detach_attached_unit(ATTACH_BACKEND_LVD, unit_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
*unit_id_out = unit_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const image_backend_ops_t *get_image_backend_ops(attach_backend_t backend) {
|
||||
static const image_backend_ops_t md_ops = {
|
||||
.id = ATTACH_BACKEND_MD, .attach = attach_md_backend};
|
||||
static const image_backend_ops_t lvd_ops = {
|
||||
.id = ATTACH_BACKEND_LVD, .attach = attach_lvd_backend};
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return &md_ops;
|
||||
if (backend == ATTACH_BACKEND_LVD)
|
||||
return &lvd_ops;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool is_cached_image_path(const char *file_path) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (get_image_cache_entry(k, &cached_entry) &&
|
||||
strcmp(cached_entry.path, file_path) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool directory_has_visible_entries(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
closedir(dir);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool reuse_existing_image_mount(const char *file_path,
|
||||
const char *mount_point,
|
||||
bool *cache_failed_out) {
|
||||
if (cache_failed_out)
|
||||
*cache_failed_out = false;
|
||||
|
||||
struct stat mount_st;
|
||||
if (stat(mount_point, &mount_st) != 0 || !S_ISDIR(mount_st.st_mode))
|
||||
return false;
|
||||
if (!directory_has_visible_entries(mount_point))
|
||||
return false;
|
||||
|
||||
attach_backend_t existing_backend = ATTACH_BACKEND_NONE;
|
||||
int existing_unit = -1;
|
||||
if (resolve_device_from_mount(mount_point, &existing_backend, &existing_unit)) {
|
||||
if (!cache_image_mount(file_path, mount_point, existing_unit,
|
||||
existing_backend)) {
|
||||
sm_error_set("IMG", ENOSPC, file_path,
|
||||
"Image cache full (%u entries), cannot track mount %s",
|
||||
(unsigned)MAX_IMAGE_MOUNTS, mount_point);
|
||||
log_debug(" [IMG] image cache full, refusing unmanaged mount reuse: %s",
|
||||
mount_point);
|
||||
errno = ENOSPC;
|
||||
if (cache_failed_out)
|
||||
*cache_failed_out = true;
|
||||
return false;
|
||||
}
|
||||
log_debug(" [IMG][%s] Already mounted: %s",
|
||||
attach_backend_name(existing_backend), mount_point);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [IMG] Mount point exists and is non-empty but is not an active "
|
||||
"mount, reattaching: %s", mount_point);
|
||||
return false;
|
||||
}
|
||||
|
||||
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));
|
||||
return false;
|
||||
}
|
||||
if (st_out->st_size < 0) {
|
||||
log_debug(" [IMG] invalid file size for %s: %lld", file_path,
|
||||
(long long)st_out->st_size);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensure_mount_dirs(const char *mount_point) {
|
||||
mkdir(IMAGE_MOUNT_BASE, 0777);
|
||||
mkdir(mount_point, 0777);
|
||||
}
|
||||
|
||||
static attach_backend_t select_image_backend(const runtime_config_t *cfg,
|
||||
image_fs_type_t fs_type) {
|
||||
if (fs_type == IMAGE_FS_EXFAT)
|
||||
return cfg->exfat_backend;
|
||||
if (fs_type == IMAGE_FS_UFS)
|
||||
return cfg->ufs_backend;
|
||||
return ATTACH_BACKEND_LVD;
|
||||
}
|
||||
|
||||
static bool attach_image_device(const char *file_path, image_fs_type_t fs_type,
|
||||
bool mount_read_only, off_t file_size,
|
||||
attach_backend_t attach_backend, int *unit_id_out,
|
||||
char *devname_out, size_t devname_size) {
|
||||
const image_backend_ops_t *backend_ops = get_image_backend_ops(attach_backend);
|
||||
if (!backend_ops) {
|
||||
log_debug(" [IMG] unsupported attach backend for %s", file_path);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!backend_ops->attach(file_path, fs_type, mount_read_only, file_size,
|
||||
unit_id_out, devname_out, devname_size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] Attached as %s", attach_backend_name(attach_backend),
|
||||
devname_out);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool perform_image_nmount(const char *file_path, image_fs_type_t fs_type,
|
||||
attach_backend_t attach_backend, int unit_id,
|
||||
const char *devname, const char *mount_point,
|
||||
bool mount_read_only, bool force_mount) {
|
||||
struct iovec *iov = NULL;
|
||||
unsigned int iovlen = 0;
|
||||
char mount_errmsg[256];
|
||||
memset(mount_errmsg, 0, sizeof(mount_errmsg));
|
||||
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 *ekpfs_key = PFS_ZERO_EKPFS_KEY_HEX;
|
||||
|
||||
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("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)};
|
||||
|
||||
struct iovec iov_exfat[] = {
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(devname),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
|
||||
IOVEC_ENTRY("budgetid"), IOVEC_ENTRY(DEVPFS_BUDGET_GAME),
|
||||
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
|
||||
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
|
||||
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("automounted"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("errmsg"), {(void *)mount_errmsg, sizeof(mount_errmsg)},
|
||||
IOVEC_ENTRY("force"), IOVEC_ENTRY(NULL)};
|
||||
|
||||
struct iovec iov_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("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 {
|
||||
log_debug(" [IMG][%s] unsupported fstype=%s",
|
||||
attach_backend_name(attach_backend), image_fs_name(fs_type));
|
||||
(void)detach_attached_unit(attach_backend, unit_id);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *mount_mode = NULL;
|
||||
unsigned int mount_flags =
|
||||
get_nmount_flags(fs_type, mount_read_only, &mount_mode);
|
||||
if (nmount(iov, iovlen, (int)mount_flags) == 0)
|
||||
return true;
|
||||
|
||||
int mount_errno = errno;
|
||||
if (mount_errmsg[0] != '\0') {
|
||||
sm_error_set("IMG", mount_errno, file_path, "%s", mount_errmsg);
|
||||
log_debug(" [IMG][%s] nmount %s errmsg: %s",
|
||||
attach_backend_name(attach_backend), mount_mode, mount_errmsg);
|
||||
}
|
||||
log_debug(" [IMG][%s] nmount %s failed: %s",
|
||||
attach_backend_name(attach_backend), mount_mode,
|
||||
strerror(mount_errno));
|
||||
(void)detach_attached_unit(attach_backend, unit_id);
|
||||
errno = mount_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool validate_mounted_image(const char *file_path, image_fs_type_t fs_type,
|
||||
attach_backend_t attach_backend, int unit_id,
|
||||
const char *devname,
|
||||
const char *mount_point) {
|
||||
struct statfs mounted_sfs;
|
||||
if (statfs(mount_point, &mounted_sfs) != 0) {
|
||||
log_debug(" [IMG][%s] statfs after mount failed for %s: %s",
|
||||
attach_backend_name(attach_backend), mount_point, strerror(errno));
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t min_device_sector =
|
||||
(attach_backend == ATTACH_BACKEND_MD) ? get_md_sector_size(fs_type)
|
||||
: get_lvd_sector_size(file_path,
|
||||
fs_type);
|
||||
uint64_t fs_block_size = (uint64_t)mounted_sfs.f_bsize;
|
||||
if (fs_block_size >= (uint64_t)min_device_sector)
|
||||
return true;
|
||||
|
||||
sm_error_set("IMG", EINVAL, file_path,
|
||||
"Filesystem cluster size (%llu) is smaller than device "
|
||||
"sector size (%u) for %s",
|
||||
(unsigned long long)fs_block_size, min_device_sector, devname);
|
||||
log_debug(" [IMG][%s] %s", attach_backend_name(attach_backend),
|
||||
sm_last_error()->message);
|
||||
notify_system("Image mount rejected:\n%s\nCluster size is too small "
|
||||
"(%llu < %u).\nUse a larger cluster size in the image or "
|
||||
"lower sector size in config.",
|
||||
file_path, (unsigned long long)fs_block_size,
|
||||
min_device_sector);
|
||||
sm_error_mark_notified();
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Image Attach + nmount Pipeline ---
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
sm_error_clear();
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
bool mount_read_only = cfg->mount_read_only;
|
||||
bool mount_mode_overridden = false;
|
||||
bool force_mount = cfg->force_mount;
|
||||
const char *filename = get_filename_component(file_path);
|
||||
if (filename[0] != '\0')
|
||||
mount_mode_overridden =
|
||||
get_image_mode_override(filename, &mount_read_only);
|
||||
|
||||
if (is_cached_image_path(file_path))
|
||||
return true;
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
build_image_mount_point(file_path, mount_point);
|
||||
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;
|
||||
|
||||
log_debug(" [IMG] Mounting image (%s): %s -> %s", image_fs_name(fs_type),
|
||||
file_path, mount_point);
|
||||
if (mount_mode_overridden) {
|
||||
log_debug(" [CFG] Image mode override: %s -> %s", file_path,
|
||||
mount_read_only ? "ro" : "rw");
|
||||
}
|
||||
|
||||
ensure_mount_dirs(mount_point);
|
||||
|
||||
attach_backend_t attach_backend = select_image_backend(cfg, fs_type);
|
||||
log_debug(" [IMG][%s] attach backend selected for %s",
|
||||
attach_backend_name(attach_backend), file_path);
|
||||
|
||||
int unit_id = -1;
|
||||
char devname[64];
|
||||
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))) {
|
||||
return false;
|
||||
}
|
||||
if (!perform_image_nmount(file_path, fs_type, attach_backend, unit_id, devname,
|
||||
mount_point, mount_read_only, force_mount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] Mounted (%s) %s -> %s",
|
||||
attach_backend_name(attach_backend), image_fs_name(fs_type),
|
||||
devname, mount_point);
|
||||
log_fs_stats("IMG", mount_point, image_fs_name(fs_type));
|
||||
|
||||
if (!validate_mounted_image(file_path, fs_type, attach_backend, unit_id,
|
||||
devname, mount_point)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cache_image_mount(file_path, mount_point, unit_id, attach_backend)) {
|
||||
sm_error_set("IMG", ENOSPC, file_path,
|
||||
"Image cache full (%u entries), rolling back mount",
|
||||
(unsigned)MAX_IMAGE_MOUNTS);
|
||||
log_debug(" [IMG] image cache full, rolling back mount: %s -> %s",
|
||||
file_path, mount_point);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
errno = ENOSPC;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
int resolved_unit = unit_id;
|
||||
attach_backend_t resolved_backend = backend;
|
||||
|
||||
if (resolved_unit < 0 || resolved_backend == ATTACH_BACKEND_NONE) {
|
||||
if (!resolve_device_from_mount(mount_point, &resolved_backend,
|
||||
&resolved_unit)) {
|
||||
resolved_backend = ATTACH_BACKEND_NONE;
|
||||
resolved_unit = -1;
|
||||
}
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] unmount start: source=%s mount=%s unit=%d",
|
||||
attach_backend_name(resolved_backend), file_path, mount_point,
|
||||
resolved_unit);
|
||||
|
||||
// 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);
|
||||
clear_cached_game(mount_point);
|
||||
|
||||
// Unmount stacked layers (unionfs over image fs).
|
||||
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS; i++) {
|
||||
if (!is_active_image_mount_point(mount_point))
|
||||
break;
|
||||
if (unmount(mount_point, 0) == 0)
|
||||
continue;
|
||||
if (errno == ENOENT || errno == EINVAL)
|
||||
break;
|
||||
if (unmount(mount_point, MNT_FORCE) != 0 && errno != ENOENT &&
|
||||
errno != EINVAL) {
|
||||
log_debug(" [IMG][%s] unmount failed for %s: %s",
|
||||
attach_backend_name(resolved_backend), mount_point,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_active_image_mount_point(mount_point)) {
|
||||
log_debug(" [IMG][%s] unmount incomplete for %s",
|
||||
attach_backend_name(resolved_backend), mount_point);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool detach_ok = true;
|
||||
if (resolved_backend != ATTACH_BACKEND_NONE && resolved_unit >= 0)
|
||||
detach_ok = detach_attached_unit(resolved_backend, resolved_unit);
|
||||
|
||||
if (rmdir(mount_point) == 0) {
|
||||
log_debug(" [IMG] Removed mount directory: %s", mount_point);
|
||||
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
|
||||
attach_backend_name(resolved_backend), file_path, mount_point,
|
||||
resolved_unit);
|
||||
return detach_ok;
|
||||
}
|
||||
|
||||
int err = errno;
|
||||
if (err == ENOENT) {
|
||||
log_debug(" [IMG][%s] unmount complete: source=%s mount=%s unit=%d",
|
||||
attach_backend_name(resolved_backend), file_path, mount_point,
|
||||
resolved_unit);
|
||||
return detach_ok;
|
||||
}
|
||||
if (err == ENOTEMPTY || err == EBUSY) {
|
||||
log_debug(" [IMG] Mount directory not removed (%s): %s",
|
||||
strerror(err), mount_point);
|
||||
if (detach_ok) {
|
||||
log_debug(" [IMG][%s] unmount complete with leftover dir: source=%s "
|
||||
"mount=%s unit=%d",
|
||||
attach_backend_name(resolved_backend), file_path, mount_point,
|
||||
resolved_unit);
|
||||
}
|
||||
return detach_ok;
|
||||
}
|
||||
log_debug(" [IMG] Failed to remove mount directory %s: %s", mount_point,
|
||||
strerror(err));
|
||||
return detach_ok;
|
||||
}
|
||||
|
||||
void cleanup_stale_image_mounts(void) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
|
||||
if (access(cached_entry.path, F_OK) != 0) {
|
||||
log_debug(" [IMG][%s] Source removed, unmounting: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend))
|
||||
invalidate_image_cache_entry(k);
|
||||
continue;
|
||||
}
|
||||
|
||||
image_fs_type_t fs_type = detect_image_fs_type(cached_entry.path);
|
||||
char mount_point[MAX_PATH];
|
||||
build_image_mount_point(cached_entry.path, mount_point);
|
||||
if (is_active_image_mount_point(mount_point))
|
||||
continue;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
(void)strlcpy(source_path, cached_entry.path, sizeof(source_path));
|
||||
log_debug(" [IMG][%s] mount lost, retrying: %s -> %s",
|
||||
attach_backend_name(cached_entry.backend), source_path,
|
||||
mount_point);
|
||||
|
||||
clear_cached_game(mount_point);
|
||||
clear_missing_param_entry(mount_point);
|
||||
|
||||
invalidate_image_cache_entry(k);
|
||||
if (mount_image(source_path, fs_type)) {
|
||||
clear_image_mount_attempts(source_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
int mount_err = errno;
|
||||
if (bump_image_mount_attempts(source_path) == 1 && !sm_error_notified()) {
|
||||
notify_image_mount_failed(source_path, mount_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup_mount_dirs(void) {
|
||||
DIR *d = opendir(IMAGE_MOUNT_BASE);
|
||||
if (!d) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [IMG] open %s failed: %s", IMAGE_MOUNT_BASE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (should_stop_requested())
|
||||
break;
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
char full_path[MAX_PATH];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", IMAGE_MOUNT_BASE,
|
||||
entry->d_name);
|
||||
|
||||
bool is_dir = false;
|
||||
if (entry->d_type == DT_DIR) {
|
||||
is_dir = true;
|
||||
} else if (entry->d_type == DT_UNKNOWN) {
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0)
|
||||
is_dir = S_ISDIR(st.st_mode);
|
||||
}
|
||||
if (!is_dir)
|
||||
continue;
|
||||
|
||||
if (rmdir(full_path) == 0) {
|
||||
log_debug(" [IMG] removed empty mount dir: %s", full_path);
|
||||
continue;
|
||||
}
|
||||
if (errno == ENOTEMPTY || errno == EBUSY || errno == ENOENT)
|
||||
continue;
|
||||
log_debug(" [IMG] failed to remove mount dir %s: %s", full_path,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
void maybe_mount_image_file(const char *full_path, const char *display_name) {
|
||||
image_fs_type_t fs_type = detect_image_fs_type(display_name);
|
||||
if (fs_type == IMAGE_FS_UNKNOWN)
|
||||
return;
|
||||
if (!is_source_stable_for_mount(full_path, display_name, "IMG"))
|
||||
return;
|
||||
if (is_image_mount_limited(full_path))
|
||||
return;
|
||||
|
||||
if (mount_image(full_path, fs_type)) {
|
||||
clear_image_mount_attempts(full_path);
|
||||
return;
|
||||
}
|
||||
|
||||
int mount_err = errno;
|
||||
if (bump_image_mount_attempts(full_path) == 1 && !sm_error_notified()) {
|
||||
notify_image_mount_failed(full_path, mount_err);
|
||||
}
|
||||
}
|
||||
|
||||
bool shutdown_image_mounts(void) {
|
||||
for (int pass = 0; pass < MAX_LAYERED_UNMOUNT_ATTEMPTS; pass++) {
|
||||
bool any_remaining = false;
|
||||
bool progress = false;
|
||||
|
||||
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 (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
progress = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
any_remaining = true;
|
||||
log_debug(" [IMG] shutdown unmount pending (%d/%d): %s", pass + 1,
|
||||
MAX_LAYERED_UNMOUNT_ATTEMPTS, cached_entry.path);
|
||||
}
|
||||
|
||||
if (!any_remaining)
|
||||
return true;
|
||||
if (!progress)
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct ImageCache {
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
if ((!path || path[0] == '\0') && (!mount_point || mount_point[0] == '\0'))
|
||||
return -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (mount_point && mount_point[0] != '\0' &&
|
||||
strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
return k;
|
||||
}
|
||||
if (path && path[0] != '\0' && strcmp(g_image_cache[k].path, path) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
if (entry_index >= 0) {
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
|
||||
return entry_index;
|
||||
}
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid) {
|
||||
(void)strlcpy(g_image_cache[k].path, path, sizeof(g_image_cache[k].path));
|
||||
(void)strlcpy(g_image_cache[k].mount_point, mount_point,
|
||||
sizeof(g_image_cache[k].mount_point));
|
||||
g_image_cache[k].unit_id = -1;
|
||||
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
|
||||
g_image_cache[k].valid = true;
|
||||
return k;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
return upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0)
|
||||
return false;
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
if (!entry_out || index < 0 || index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_cache[index].valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS)
|
||||
return;
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0)
|
||||
return false;
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0)
|
||||
return false;
|
||||
*backend_out = entry->backend;
|
||||
*unit_out = entry->unit_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0 || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
|
||||
path_out[0] = '\0';
|
||||
(void)strlcpy(path_out, entry->path, path_out_size);
|
||||
return path_out[0] != '\0';
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
if (fprintf(f, "%s", value) < 0)
|
||||
saved_errno = errno;
|
||||
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(" [LINK] write failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
char user_app_dir[MAX_PATH];
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = false;
|
||||
|
||||
snprintf(user_app_dir, sizeof(user_app_dir), "/user/app/%s", title_id);
|
||||
|
||||
if (is_under_image_mount_base(src_path)) {
|
||||
has_image_source = resolve_image_source_from_mount_cache(
|
||||
src_path, image_source_path, sizeof(image_source_path));
|
||||
if (!has_image_source) {
|
||||
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
|
||||
src_path);
|
||||
}
|
||||
}
|
||||
|
||||
// COPY FILES
|
||||
if (!is_remount) {
|
||||
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
|
||||
mkdir(user_app_dir, 0777);
|
||||
mkdir(user_sce_sys, 0777);
|
||||
|
||||
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
|
||||
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
|
||||
log_debug(" [COPY] Failed to copy sce_sys: %s -> %s", src_sce_sys,
|
||||
user_sce_sys);
|
||||
return false;
|
||||
}
|
||||
|
||||
char icon_src[MAX_PATH], icon_dst[MAX_PATH];
|
||||
snprintf(icon_src, sizeof(icon_src), "%s/sce_sys/icon0.png", src_path);
|
||||
snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id);
|
||||
if (copy_file(icon_src, icon_dst) != 0) {
|
||||
log_debug(" [COPY] Failed to copy icon: %s -> %s", icon_src, icon_dst);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
|
||||
}
|
||||
|
||||
// WRITE TRACKER
|
||||
char lnk_path[MAX_PATH];
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path))
|
||||
return false;
|
||||
|
||||
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
|
||||
|
||||
char img_lnk_path[MAX_PATH];
|
||||
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))
|
||||
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)) {
|
||||
log_debug(" [LINK] image source cache update failed: %s -> %s",
|
||||
src_path, image_source_path);
|
||||
}
|
||||
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
log_debug(" [LINK] mount.lnk created but nullfs mount failed: title=%s "
|
||||
"src=%s",
|
||||
title_id, src_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!should_register) {
|
||||
log_debug(" [REG] Skip (already present in app.db)");
|
||||
return true;
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
char src_snd0[MAX_PATH];
|
||||
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
|
||||
bool has_src_snd0 = (access(src_snd0, F_OK) == 0);
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
log_debug(" [REG] Installed NEW!");
|
||||
notify_game_installed_rich(title_id);
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
} else if ((uint32_t)res == 0x80990002u) {
|
||||
invalidate_app_db_title_cache();
|
||||
log_debug(" [REG] Restored.");
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
// Silent on restore/remount to avoid spam
|
||||
} else {
|
||||
log_debug(" [REG] FAIL: 0x%x", res);
|
||||
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
|
||||
(uint32_t)res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Execution (per discovered candidate) ---
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count) {
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
|
||||
const scan_candidate_t *c = &candidates[i];
|
||||
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 (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);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define KSTUFF_SYSENTVEC_TOGGLE_OFFSET 14
|
||||
#define KSTUFF_SYSENTVEC_ENABLED 0xdeb7u
|
||||
#define KSTUFF_SYSENTVEC_DISABLED 0xffffu
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool image_backed;
|
||||
bool pause_applied;
|
||||
bool focus_override_active;
|
||||
pid_t pid;
|
||||
uint32_t app_id;
|
||||
uint64_t pause_deadline_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} kstuff_game_entry_t;
|
||||
|
||||
typedef struct {
|
||||
intptr_t sysentvec_ps5;
|
||||
intptr_t sysentvec_ps4;
|
||||
bool supported;
|
||||
bool restore_on_empty;
|
||||
bool current_app_focus_valid;
|
||||
uint32_t current_app_focus_id;
|
||||
uint64_t restore_retry_us;
|
||||
kstuff_game_entry_t game;
|
||||
} kstuff_state_t;
|
||||
|
||||
static kstuff_state_t g_kstuff;
|
||||
static _Atomic uint32_t g_pending_app_focus_id;
|
||||
static _Atomic bool g_pending_app_focus_valid;
|
||||
|
||||
static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out) {
|
||||
if (!ps5_out || !ps4_out)
|
||||
return false;
|
||||
|
||||
*ps5_out = 0;
|
||||
*ps4_out = 0;
|
||||
|
||||
switch (kernel_get_fw_version() & 0xffff0000u) {
|
||||
case 0x1000000:
|
||||
case 0x1010000:
|
||||
case 0x1020000:
|
||||
case 0x1050000:
|
||||
case 0x1100000:
|
||||
case 0x1110000:
|
||||
case 0x1120000:
|
||||
case 0x1130000:
|
||||
case 0x1140000:
|
||||
case 0x2000000:
|
||||
case 0x2200000:
|
||||
case 0x2250000:
|
||||
case 0x2260000:
|
||||
case 0x2300000:
|
||||
case 0x2500000:
|
||||
case 0x2700000:
|
||||
// Older jailbreak chains usually rely on Byepervisor instead of kstuff.
|
||||
return false;
|
||||
|
||||
case 0x3000000:
|
||||
case 0x3100000:
|
||||
case 0x3200000:
|
||||
case 0x3210000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xca0cd8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xca0e50;
|
||||
return true;
|
||||
|
||||
case 0x4000000:
|
||||
case 0x4020000:
|
||||
case 0x4030000:
|
||||
case 0x4500000:
|
||||
case 0x4510000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xd11bb8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xd11d30;
|
||||
return true;
|
||||
|
||||
case 0x5000000:
|
||||
case 0x5020000:
|
||||
case 0x5100000:
|
||||
case 0x5500000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe00be8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe00d60;
|
||||
return true;
|
||||
|
||||
case 0x6000000:
|
||||
case 0x6020000:
|
||||
case 0x6500000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe210a8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21220;
|
||||
return true;
|
||||
|
||||
case 0x7000000:
|
||||
case 0x7010000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ab8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21c30;
|
||||
return true;
|
||||
|
||||
case 0x7200000:
|
||||
case 0x7400000:
|
||||
case 0x7600000:
|
||||
case 0x7610000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21b78;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21cf0;
|
||||
return true;
|
||||
|
||||
case 0x8000000:
|
||||
case 0x8200000:
|
||||
case 0x8400000:
|
||||
case 0x8600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xe21ca8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xe21e20;
|
||||
return true;
|
||||
|
||||
case 0x9000000:
|
||||
case 0x9050000:
|
||||
case 0x9200000:
|
||||
case 0x9400000:
|
||||
case 0x9600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba648;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba7c0;
|
||||
return true;
|
||||
|
||||
case 0x10000000:
|
||||
case 0x10010000:
|
||||
case 0x10200000:
|
||||
case 0x10400000:
|
||||
case 0x10600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdba6d8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdba850;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool kstuff_sysentvec_is_enabled(intptr_t sysentvec_addr) {
|
||||
return kernel_getshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET) !=
|
||||
KSTUFF_SYSENTVEC_DISABLED;
|
||||
}
|
||||
|
||||
static uint16_t read_kstuff_sysentvec_toggle(intptr_t sysentvec_addr) {
|
||||
return (uint16_t)kernel_getshort(sysentvec_addr +
|
||||
KSTUFF_SYSENTVEC_TOGGLE_OFFSET);
|
||||
}
|
||||
|
||||
static bool kstuff_sysentvec_toggle_is_known(uint16_t value) {
|
||||
return value == KSTUFF_SYSENTVEC_ENABLED ||
|
||||
value == KSTUFF_SYSENTVEC_DISABLED;
|
||||
}
|
||||
|
||||
static void set_kstuff_sysentvec_enabled(intptr_t sysentvec_addr, bool enabled) {
|
||||
(void)kernel_setshort(sysentvec_addr + KSTUFF_SYSENTVEC_TOGGLE_OFFSET,
|
||||
enabled ? KSTUFF_SYSENTVEC_ENABLED
|
||||
: KSTUFF_SYSENTVEC_DISABLED);
|
||||
}
|
||||
|
||||
static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
|
||||
if (!g_kstuff.supported)
|
||||
return false;
|
||||
|
||||
bool ps5_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps5);
|
||||
bool ps4_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps4);
|
||||
if (ps5_out)
|
||||
*ps5_out = ps5_enabled;
|
||||
if (ps4_out)
|
||||
*ps4_out = ps4_enabled;
|
||||
return ps5_enabled && ps4_enabled;
|
||||
}
|
||||
|
||||
static bool kstuff_state_matches(bool enabled, bool ps5_enabled,
|
||||
bool ps4_enabled) {
|
||||
return ps5_enabled == enabled && ps4_enabled == enabled;
|
||||
}
|
||||
|
||||
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
bool *fully_applied_out);
|
||||
|
||||
static void mark_restore_needed(void) {
|
||||
g_kstuff.restore_on_empty = true;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
}
|
||||
|
||||
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
|
||||
bool *image_backed_out) {
|
||||
char source_path[MAX_PATH];
|
||||
bool image_backed =
|
||||
read_mount_link(title_id, source_path, sizeof(source_path)) &&
|
||||
is_under_image_mount_base(source_path);
|
||||
|
||||
if (image_backed_out)
|
||||
*image_backed_out = image_backed;
|
||||
|
||||
uint32_t delay_seconds =
|
||||
image_backed ? runtime_config()->kstuff_pause_delay_image_seconds
|
||||
: runtime_config()->kstuff_pause_delay_direct_seconds;
|
||||
uint32_t override_delay_seconds = 0;
|
||||
if (get_kstuff_pause_delay_override_for_title(title_id,
|
||||
&override_delay_seconds)) {
|
||||
return override_delay_seconds;
|
||||
}
|
||||
|
||||
return delay_seconds;
|
||||
}
|
||||
|
||||
static void restore_kstuff_if_needed(const char *reason) {
|
||||
if (g_kstuff.game.active && g_kstuff.game.pause_applied)
|
||||
return;
|
||||
|
||||
if (!g_kstuff.restore_on_empty)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0 && g_kstuff.restore_retry_us != 0 &&
|
||||
now_us < g_kstuff.restore_retry_us) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.game.pause_deadline_us == 0 ||
|
||||
(now_us != 0 && now_us >= g_kstuff.game.pause_deadline_us)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool enabled = sm_kstuff_is_enabled();
|
||||
if (!enabled)
|
||||
enabled = sm_kstuff_set_enabled(true, true);
|
||||
if (enabled) {
|
||||
log_debug(" [KSTUFF] auto-resumed (%s)%s",
|
||||
reason ? reason : "no reason",
|
||||
g_kstuff.game.active ? ", pending delay for another tracked game"
|
||||
: "");
|
||||
} else {
|
||||
log_debug(" [KSTUFF] failed to auto-resume (%s)",
|
||||
reason ? reason : "no reason");
|
||||
g_kstuff.restore_retry_us =
|
||||
now_us == 0 ? 0 : now_us + KSTUFF_RESTORE_RETRY_US;
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.restore_on_empty = false;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
}
|
||||
|
||||
static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
if (!entry || !entry->active || entry->pause_applied)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0 && now_us < entry->pause_deadline_us)
|
||||
return;
|
||||
|
||||
if (g_kstuff.current_app_focus_valid && entry->app_id != 0 &&
|
||||
g_kstuff.current_app_focus_id != entry->app_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool was_enabled = sm_kstuff_is_enabled();
|
||||
bool fully_disabled = false;
|
||||
(void)apply_kstuff_enabled_state(false, was_enabled, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to auto-pause for %s pid=%ld",
|
||||
entry->title_id, (long)entry->pid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (was_enabled) {
|
||||
mark_restore_needed();
|
||||
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
|
||||
entry->title_id, (long)entry->pid,
|
||||
entry->image_backed ? "image" : "direct");
|
||||
} else {
|
||||
log_debug(" [KSTUFF] pause deadline reached for %s pid=%ld but kstuff "
|
||||
"was already disabled", entry->title_id, (long)entry->pid);
|
||||
}
|
||||
|
||||
entry->pause_applied = true;
|
||||
}
|
||||
|
||||
static void handle_kstuff_app_focus_change(uint32_t focused_app_id) {
|
||||
g_kstuff.current_app_focus_valid = true;
|
||||
g_kstuff.current_app_focus_id = focused_app_id;
|
||||
|
||||
if (!g_kstuff.game.active || g_kstuff.game.app_id == 0)
|
||||
return;
|
||||
|
||||
if (focused_app_id != g_kstuff.game.app_id) {
|
||||
if (!g_kstuff.game.pause_applied || g_kstuff.game.focus_override_active)
|
||||
return;
|
||||
|
||||
bool enabled = sm_kstuff_is_enabled();
|
||||
if (!enabled)
|
||||
enabled = sm_kstuff_set_enabled(true, false);
|
||||
if (!enabled) {
|
||||
log_debug(" [KSTUFF] failed to enable on app focus leave "
|
||||
"(tracked=0x%08X current=0x%08X)",
|
||||
g_kstuff.game.app_id, focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.game.focus_override_active = true;
|
||||
log_debug(" [KSTUFF] temporarily enabled on app focus leave "
|
||||
"(tracked=0x%08X current=0x%08X)",
|
||||
g_kstuff.game.app_id, focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_kstuff.game.focus_override_active)
|
||||
return;
|
||||
|
||||
bool fully_disabled = false;
|
||||
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to restore disabled state on app focus return "
|
||||
"(app_id=0x%08X)",
|
||||
focused_app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.game.focus_override_active = false;
|
||||
log_debug(" [KSTUFF] restored disabled state on app focus return "
|
||||
"(app_id=0x%08X)",
|
||||
focused_app_id);
|
||||
}
|
||||
|
||||
uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us) {
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return 0;
|
||||
|
||||
uint64_t next_wake_us = 0;
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.current_app_focus_valid && g_kstuff.game.app_id != 0 &&
|
||||
g_kstuff.current_app_focus_id != g_kstuff.game.app_id) {
|
||||
next_wake_us = 0;
|
||||
} else if (g_kstuff.game.pause_deadline_us != 0) {
|
||||
next_wake_us = g_kstuff.game.pause_deadline_us;
|
||||
} else if (now_us == 0) {
|
||||
next_wake_us = 1;
|
||||
} else {
|
||||
next_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_kstuff.restore_on_empty) {
|
||||
uint64_t restore_wake_us = g_kstuff.restore_retry_us;
|
||||
if (restore_wake_us == 0) {
|
||||
if (g_kstuff.game.active && !g_kstuff.game.pause_applied) {
|
||||
if (g_kstuff.game.pause_deadline_us != 0) {
|
||||
restore_wake_us = g_kstuff.game.pause_deadline_us;
|
||||
} else if (now_us == 0) {
|
||||
restore_wake_us = 1;
|
||||
} else {
|
||||
restore_wake_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
}
|
||||
} else {
|
||||
restore_wake_us = now_us == 0 ? 1 : now_us;
|
||||
}
|
||||
}
|
||||
|
||||
if (next_wake_us == 0 || restore_wake_us < next_wake_us)
|
||||
next_wake_us = restore_wake_us;
|
||||
}
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_supported(void) {
|
||||
return g_kstuff.supported;
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_enabled(void) {
|
||||
return read_kstuff_enabled_state(NULL, NULL);
|
||||
}
|
||||
|
||||
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
bool *fully_applied_out) {
|
||||
if (!g_kstuff.supported)
|
||||
return false;
|
||||
|
||||
bool ps5_enabled = false;
|
||||
bool ps4_enabled = false;
|
||||
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
|
||||
bool fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
|
||||
if (fully_applied) {
|
||||
if (fully_applied_out)
|
||||
*fully_applied_out = true;
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
if (ps5_enabled != enabled)
|
||||
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps5, enabled);
|
||||
if (ps4_enabled != enabled)
|
||||
set_kstuff_sysentvec_enabled(g_kstuff.sysentvec_ps4, enabled);
|
||||
|
||||
is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
|
||||
fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
|
||||
if (fully_applied_out)
|
||||
*fully_applied_out = fully_applied;
|
||||
|
||||
if (fully_applied) {
|
||||
log_debug(" [KSTUFF] %s both sysentvecs", enabled ? "enabled" : "disabled");
|
||||
if (notify_user) {
|
||||
notify_system_info(enabled ? "KStuff active again"
|
||||
: "KStuff paused while the game is running");
|
||||
}
|
||||
} else {
|
||||
log_debug(" [KSTUFF] %s request incomplete (ps5=%s ps4=%s)",
|
||||
enabled ? "enable" : "disable", ps5_enabled ? "on" : "off",
|
||||
ps4_enabled ? "on" : "off");
|
||||
}
|
||||
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
|
||||
return apply_kstuff_enabled_state(enabled, notify_user, NULL);
|
||||
}
|
||||
|
||||
bool sm_kstuff_game_feature_enabled(void) {
|
||||
return runtime_config()->kstuff_game_auto_toggle && sm_kstuff_is_supported();
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
uint64_t exec_time_us) {
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (g_kstuff.game.active) {
|
||||
if (g_kstuff.game.pid == pid) {
|
||||
log_debug(" [KSTUFF] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("tracked game handoff");
|
||||
}
|
||||
|
||||
if (is_kstuff_pause_disabled_for_title(title_id)) {
|
||||
log_debug(" [KSTUFF] auto-pause disabled by config for %s pid=%ld "
|
||||
"app_id=0x%08X", title_id, (long)pid, app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
bool image_backed = false;
|
||||
uint32_t delay_seconds =
|
||||
get_pause_delay_seconds_for_title(title_id, &image_backed);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
uint64_t base_time_us = exec_time_us != 0 ? exec_time_us : now_us;
|
||||
|
||||
g_kstuff.game.active = true;
|
||||
g_kstuff.game.image_backed = image_backed;
|
||||
g_kstuff.game.pause_applied = false;
|
||||
g_kstuff.game.focus_override_active = false;
|
||||
g_kstuff.game.pid = pid;
|
||||
g_kstuff.game.app_id = app_id;
|
||||
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));
|
||||
|
||||
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
|
||||
"pause_delay=%us", title_id, (long)pid, app_id,
|
||||
image_backed ? "image" : "direct", delay_seconds);
|
||||
}
|
||||
|
||||
void sm_kstuff_note_app_focus(uint32_t app_id) {
|
||||
atomic_store(&g_pending_app_focus_id, app_id);
|
||||
atomic_store(&g_pending_app_focus_valid, true);
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exit(pid_t pid) {
|
||||
if (!g_kstuff.game.active || g_kstuff.game.pid != pid)
|
||||
return;
|
||||
|
||||
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
|
||||
g_kstuff.game.title_id, (long)pid);
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
if (!g_kstuff.game.active)
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("tracked game exit");
|
||||
}
|
||||
|
||||
void sm_kstuff_game_poll(void) {
|
||||
if (!sm_kstuff_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (atomic_exchange(&g_pending_app_focus_valid, false)) {
|
||||
handle_kstuff_app_focus_change(atomic_load(&g_pending_app_focus_id));
|
||||
}
|
||||
|
||||
restore_kstuff_if_needed("pending auto-resume");
|
||||
maybe_apply_kstuff_pause_for_slot(&g_kstuff.game);
|
||||
}
|
||||
|
||||
void sm_kstuff_game_shutdown(void) {
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
restore_kstuff_if_needed("watcher shutdown");
|
||||
}
|
||||
|
||||
void sm_kstuff_init(void) {
|
||||
memset(&g_kstuff, 0, sizeof(g_kstuff));
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
|
||||
if (!resolve_kstuff_sysentvec_addrs(&g_kstuff.sysentvec_ps5,
|
||||
&g_kstuff.sysentvec_ps4)) {
|
||||
log_debug(" [KSTUFF] runtime control unavailable on firmware 0x%08X",
|
||||
kernel_get_fw_version());
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
|
||||
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
|
||||
if (!kstuff_sysentvec_toggle_is_known(ps5_toggle) ||
|
||||
!kstuff_sysentvec_toggle_is_known(ps4_toggle)) {
|
||||
log_debug(" [KSTUFF] runtime control disabled: kstuff markers not "
|
||||
"present (ps5=0x%04X ps4=0x%04X)",
|
||||
ps5_toggle, ps4_toggle);
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.supported = true;
|
||||
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
|
||||
"direct_delay=%u)",
|
||||
runtime_config()->kstuff_pause_delay_image_seconds,
|
||||
runtime_config()->kstuff_pause_delay_direct_seconds);
|
||||
}
|
||||
|
||||
void sm_kstuff_shutdown(void) {
|
||||
sm_kstuff_game_shutdown();
|
||||
atomic_store(&g_pending_app_focus_id, 0);
|
||||
atomic_store(&g_pending_app_focus_valid, false);
|
||||
memset(&g_kstuff, 0, sizeof(g_kstuff));
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sm_error_t g_last_error;
|
||||
static bool g_notifications_initialized = false;
|
||||
|
||||
extern unsigned char smp_icon_png[];
|
||||
extern unsigned int smp_icon_png_len;
|
||||
|
||||
#define SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM 0xFE
|
||||
|
||||
int sceNotificationSend(int userId, bool isLogged, const char *payload);
|
||||
int sceNotificationSendById(int userid, bool logged_in, const char *useCaseId,
|
||||
const char *message);
|
||||
|
||||
static void notify_system_plain_message(const char *message) {
|
||||
notify_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
(void)strlcpy(req.message, message, sizeof(req.message));
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
log_debug("NOTIFY: %s", req.message);
|
||||
}
|
||||
|
||||
static bool ensure_notification_icon_present(void) {
|
||||
struct stat st;
|
||||
if (stat(NOTIFY_ICON_FILE, &st) == 0 && st.st_size > 0)
|
||||
return true;
|
||||
|
||||
(void)mkdir("/user/data", 0777);
|
||||
(void)mkdir(NOTIFY_ICON_DIR, 0777);
|
||||
FILE *fp = fopen(NOTIFY_ICON_FILE, "wb");
|
||||
if (!fp) {
|
||||
log_debug(" [NOTIFY] icon create failed: %s (%s)", NOTIFY_ICON_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written = fwrite(smp_icon_png, 1, smp_icon_png_len, fp);
|
||||
int saved_errno = 0;
|
||||
if (written != smp_icon_png_len)
|
||||
saved_errno = ferror(fp) ? errno : EIO;
|
||||
if (fflush(fp) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(fp) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [NOTIFY] icon write failed: %s (%s)", NOTIFY_ICON_FILE,
|
||||
strerror(errno));
|
||||
(void)unlink(NOTIFY_ICON_FILE);
|
||||
return false;
|
||||
}
|
||||
|
||||
log_debug(" [NOTIFY] icon created: %s", NOTIFY_ICON_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_notifications_init(void) {
|
||||
if (g_notifications_initialized)
|
||||
return;
|
||||
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
}
|
||||
|
||||
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
|
||||
size_t used = strlen(dst);
|
||||
if (used >= dst_size)
|
||||
return;
|
||||
|
||||
for (; *src != '\0' && used + 1 < dst_size; ++src) {
|
||||
const char *escape = NULL;
|
||||
char single[2] = {0};
|
||||
|
||||
switch (*src) {
|
||||
case '\\':
|
||||
escape = "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escape = "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
escape = "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escape = "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escape = "\\t";
|
||||
break;
|
||||
default:
|
||||
single[0] = *src;
|
||||
escape = single;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t escape_len = strlen(escape);
|
||||
if (used + escape_len >= dst_size)
|
||||
break;
|
||||
memcpy(dst + used, escape, escape_len);
|
||||
used += escape_len;
|
||||
dst[used] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static bool build_notification_timestamp(time_t timestamp, char out[32]) {
|
||||
struct tm tm_utc;
|
||||
if (gmtime_r(×tamp, &tm_utc) == NULL)
|
||||
return false;
|
||||
return strftime(out, 32, "%Y-%m-%dT%H:%M:%S.000Z", &tm_utc) != 0;
|
||||
}
|
||||
|
||||
static bool build_notification_metadata(char *created_at,
|
||||
char *notification_id,
|
||||
size_t notification_id_size) {
|
||||
time_t now;
|
||||
|
||||
now = time(NULL);
|
||||
if (!build_notification_timestamp(now, created_at))
|
||||
return false;
|
||||
|
||||
snprintf(notification_id, notification_id_size, "%u",
|
||||
(unsigned)((uint32_t)now ^ (uint32_t)getpid()));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool send_rich_notification(const char *message) {
|
||||
char escaped_message[4096];
|
||||
char escaped_version[128];
|
||||
char payload[8192];
|
||||
char created_at[32];
|
||||
char notification_id[32];
|
||||
|
||||
if (!message || message[0] == '\0')
|
||||
return false;
|
||||
|
||||
sm_notifications_init();
|
||||
if (!g_notifications_initialized)
|
||||
return false;
|
||||
|
||||
escaped_message[0] = '\0';
|
||||
escaped_version[0] = '\0';
|
||||
append_json_escaped(escaped_message, sizeof(escaped_message), message);
|
||||
append_json_escaped(escaped_version, sizeof(escaped_version),
|
||||
SHADOWMOUNT_VERSION);
|
||||
if (!build_notification_metadata(created_at, notification_id,
|
||||
sizeof(notification_id))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int len = snprintf(
|
||||
payload, sizeof(payload),
|
||||
"{\n"
|
||||
" \"rawData\": {\n"
|
||||
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
|
||||
" \"channelType\": \"ServiceFeedback\",\n"
|
||||
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
|
||||
" \"useCaseId\": \"IDC\",\n"
|
||||
" \"soundEffect\": \"none\",\n"
|
||||
" \"toastOverwriteType\": \"InQueue\",\n"
|
||||
" \"isImmediate\": true,\n"
|
||||
" \"priority\": 100,\n"
|
||||
" \"viewData\": {\n"
|
||||
" \"icon\": {\n"
|
||||
" \"type\": \"Url\",\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"url\": \"" NOTIFY_ICON_FILE "\"\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"message\": {\n"
|
||||
" \"body\": \"%s\"\n"
|
||||
" },\n"
|
||||
" \"subMessage\": {\n"
|
||||
" \"body\": \"ShadowMountPlus %s\"\n"
|
||||
" },\n"
|
||||
" \"actions\": [\n"
|
||||
" {\n"
|
||||
" \"actionName\": \"Go to Debug Settings\",\n"
|
||||
" \"actionType\": \"DeepLink\",\n"
|
||||
" \"defaultFocus\": true,\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"actionUrl\": \"pssettings:play?function=debug_settings\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
" },\n"
|
||||
" \"platformViews\": {\n"
|
||||
" \"previewDisabled\": {\n"
|
||||
" \"viewData\": {\n"
|
||||
" \"icon\": {\n"
|
||||
" \"type\": \"Predefined\",\n"
|
||||
" \"parameters\": {\n"
|
||||
" \"icon\": \"community\"\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"message\": {\n"
|
||||
" \"body\": \"%s\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
" \"createdDateTime\": \"%s\",\n"
|
||||
" \"localNotificationId\": \"%s\"\n"
|
||||
"}",
|
||||
escaped_message, escaped_version, escaped_message, created_at,
|
||||
notification_id);
|
||||
if (len < 0 || (size_t)len >= sizeof(payload))
|
||||
return false;
|
||||
|
||||
int rc = sceNotificationSend(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
|
||||
payload);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static bool send_game_installed_rich_notification(const char *title_id) {
|
||||
char escaped_title_id[128];
|
||||
char params[256];
|
||||
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
escaped_title_id[0] = '\0';
|
||||
append_json_escaped(escaped_title_id, sizeof(escaped_title_id), title_id);
|
||||
int len = snprintf(params, sizeof(params),
|
||||
"{\"npTitleId\":\"%s_00\"}", escaped_title_id);
|
||||
if (len < 0 || (size_t)len >= sizeof(params))
|
||||
return false;
|
||||
int rc = sceNotificationSendById(SCE_NOTIFICATION_LOCAL_USER_ID_SYSTEM, true,
|
||||
"NUC240", params);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static void notify_system_plain_v(const char *fmt, va_list args) {
|
||||
char message[3075];
|
||||
vsnprintf(message, sizeof(message), fmt, args);
|
||||
notify_system_plain_message(message);
|
||||
}
|
||||
|
||||
static void log_to_file(const char *fmt, va_list args) {
|
||||
mkdir(LOG_DIR, 0777);
|
||||
FILE *fp = fopen(LOG_FILE, "a");
|
||||
if (fp) {
|
||||
va_list args_file;
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
|
||||
va_copy(args_file, args);
|
||||
fprintf(fp, "[%s] ", buffer);
|
||||
vfprintf(fp, fmt, args_file);
|
||||
fprintf(fp, "\n");
|
||||
va_end(args_file);
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
void log_debug(const char *fmt, ...) {
|
||||
if (runtime_config()->debug_enabled == false)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_list args_copy;
|
||||
va_start(args, fmt);
|
||||
va_copy(args_copy, args);
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
log_to_file(fmt, args_copy);
|
||||
va_end(args_copy);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_system_rich(bool allow_in_quiet_mode, const char *fmt, ...) {
|
||||
char message[3075];
|
||||
va_list args;
|
||||
|
||||
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
|
||||
return;
|
||||
|
||||
va_start(args, fmt);
|
||||
vsnprintf(message, sizeof(message), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (!send_rich_notification(message)) {
|
||||
notify_system_plain_message(message);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("NOTIFY: %s", message);
|
||||
}
|
||||
|
||||
void notify_game_installed_rich(const char *title_id) {
|
||||
if (!send_game_installed_rich_notification(title_id)) {
|
||||
notify_system_info("Installed: %s",
|
||||
(title_id && title_id[0] != '\0') ? title_id : "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("NOTIFY: installed game %s", title_id ? title_id : "unknown");
|
||||
}
|
||||
|
||||
void notify_system_info(const char *fmt, ...) {
|
||||
if (runtime_config()->quiet_mode)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
notify_system_plain_v(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void sm_error_clear(void) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
}
|
||||
|
||||
void sm_error_set(const char *subsystem, int code, const char *path,
|
||||
const char *fmt, ...) {
|
||||
memset(&g_last_error, 0, sizeof(g_last_error));
|
||||
g_last_error.valid = true;
|
||||
g_last_error.code = code;
|
||||
if (subsystem && subsystem[0] != '\0')
|
||||
(void)strlcpy(g_last_error.subsystem, subsystem,
|
||||
sizeof(g_last_error.subsystem));
|
||||
if (path && path[0] != '\0')
|
||||
(void)strlcpy(g_last_error.path, path, sizeof(g_last_error.path));
|
||||
if (fmt && fmt[0] != '\0') {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(g_last_error.message, sizeof(g_last_error.message), fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
||||
const sm_error_t *sm_last_error(void) {
|
||||
return &g_last_error;
|
||||
}
|
||||
|
||||
bool sm_error_notified(void) {
|
||||
return g_last_error.notified;
|
||||
}
|
||||
|
||||
void sm_error_mark_notified(void) {
|
||||
g_last_error.notified = true;
|
||||
}
|
||||
|
||||
void notify_system(const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
notify_system_plain_v(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void notify_image_mount_failed(const char *path, int mount_err) {
|
||||
if (g_last_error.valid && g_last_error.message[0] != '\0') {
|
||||
notify_system("Image mount failed: 0x%08X (%s)\n%s\n%s",
|
||||
(uint32_t)mount_err, strerror(mount_err),
|
||||
g_last_error.message,
|
||||
(g_last_error.path[0] != '\0') ? g_last_error.path : path);
|
||||
g_last_error.notified = true;
|
||||
return;
|
||||
}
|
||||
|
||||
notify_system("Image mount failed: 0x%08X (%s)\n%s", (uint32_t)mount_err,
|
||||
strerror(mount_err), path);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_mount_defs.h"
|
||||
#include "sm_stability.h"
|
||||
|
||||
const char *attach_backend_name(attach_backend_t backend) {
|
||||
if (backend == ATTACH_BACKEND_LVD)
|
||||
return "LVD";
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return "MD";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// --- Device Node Wait and Source Stability ---
|
||||
static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
|
||||
if ((access(devname, F_OK) == 0) == should_exist)
|
||||
return true;
|
||||
sceKernelUsleep(LVD_NODE_WAIT_US);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
const char *tag) {
|
||||
double age = 0.0;
|
||||
int st_err = 0;
|
||||
if (is_path_stable_now(path, &age, &st_err))
|
||||
return true;
|
||||
if (st_err != 0)
|
||||
return false;
|
||||
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Mounted Device Resolution (/dev/lvdN, /dev/mdN) ---
|
||||
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
|
||||
int *unit_out) {
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (!dev_path || strncmp(dev_path, prefix, prefix_len) != 0)
|
||||
return false;
|
||||
|
||||
char *end = NULL;
|
||||
long unit = strtol(dev_path + prefix_len, &end, 10);
|
||||
if (end == dev_path + prefix_len || *end != '\0' || unit < 0 ||
|
||||
unit > INT_MAX)
|
||||
return false;
|
||||
|
||||
*unit_out = (int)unit;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount(const char *mount_point,
|
||||
attach_backend_t *backend_out, int *unit_out) {
|
||||
if (!mount_point || !backend_out || !unit_out)
|
||||
return false;
|
||||
|
||||
*backend_out = ATTACH_BACKEND_NONE;
|
||||
*unit_out = -1;
|
||||
|
||||
if (resolve_device_from_mount_cache(mount_point, backend_out, unit_out))
|
||||
return true;
|
||||
|
||||
struct statfs sfs;
|
||||
if (statfs(mount_point, &sfs) != 0)
|
||||
return false;
|
||||
|
||||
if (strcmp(sfs.f_mntonname, mount_point) != 0)
|
||||
return false;
|
||||
|
||||
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/lvd", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_LVD;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parse_unit_from_dev_path(sfs.f_mntfromname, "/dev/md", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_MD;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
if (mntcount <= 0 || !mntbuf)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < mntcount; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, mount_point) != 0)
|
||||
continue;
|
||||
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/lvd", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_LVD;
|
||||
return true;
|
||||
}
|
||||
if (parse_unit_from_dev_path(mntbuf[i].f_mntfromname, "/dev/md", unit_out)) {
|
||||
*backend_out = ATTACH_BACKEND_MD;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_path_mountpoint(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct statfs sfs;
|
||||
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
|
||||
}
|
||||
|
||||
bool is_active_image_mount_point(const char *path) {
|
||||
return is_path_mountpoint(path);
|
||||
}
|
||||
|
||||
bool wait_for_lvd_release(void) {
|
||||
for (unsigned int waited_us = 0;; waited_us += LVD_RELEASE_WAIT_POLL_US) {
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
bool mounted = false;
|
||||
for (int i = 0; i < mntcount && mntbuf; i++) {
|
||||
if (strcmp(mntbuf[i].f_mntfromname, "/dev/lvd2") != 0)
|
||||
continue;
|
||||
mounted = true;
|
||||
break;
|
||||
}
|
||||
if (!mounted) {
|
||||
if (waited_us != 0)
|
||||
log_debug(" [IMG][LVD] /dev/lvd2 released");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (waited_us == 0) {
|
||||
log_debug(" [IMG][LVD] waiting for /dev/lvd2 to be released...");
|
||||
for (int i = 0; i < mntcount && mntbuf; i++) {
|
||||
if (strncmp(mntbuf[i].f_mntfromname, "/dev/lvd", 8) != 0)
|
||||
continue;
|
||||
log_debug(" [IMG][LVD] mounted: from=%s path=%s type=%s "
|
||||
"bsize=%llu iosize=%llu blocks=%llu bfree=%llu "
|
||||
"bavail=%llu files=%llu ffree=%llu flags=0x%lX",
|
||||
mntbuf[i].f_mntfromname, mntbuf[i].f_mntonname,
|
||||
mntbuf[i].f_fstypename,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bsize,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_iosize,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_blocks,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bfree,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_bavail,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_files,
|
||||
(unsigned long long)(uint64_t)mntbuf[i].f_ffree,
|
||||
(unsigned long)mntbuf[i].f_flags);
|
||||
}
|
||||
}
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
if (waited_us >= LVD_RELEASE_WAIT_MAX_US) {
|
||||
log_debug(" [IMG][LVD] /dev/lvd2 wait timeout reached (%u ms), "
|
||||
"continuing startup", LVD_RELEASE_WAIT_MAX_US / 1000u);
|
||||
return true;
|
||||
}
|
||||
sceKernelUsleep(LVD_RELEASE_WAIT_POLL_US);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Device Detach Helpers ---
|
||||
static bool detach_lvd_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
int fd = open(LVD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s for detach failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), LVD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
lvd_ioctl_detach_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.device_id = unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, SCE_LVD_IOC_DETACH, &req) != 0) {
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id, strerror(errno));
|
||||
ok = false;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/lvd%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/lvd%d",
|
||||
attach_backend_name(ATTACH_BACKEND_LVD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool detach_md_unit(int unit_id) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
int fd = open(MD_CTRL_PATH, O_RDWR);
|
||||
if (fd < 0) {
|
||||
log_debug(" [IMG][%s] open %s for detach failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), MD_CTRL_PATH,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
struct md_ioctl req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.md_version = MDIOVERSION;
|
||||
req.md_unit = (unsigned int)unit_id;
|
||||
|
||||
bool ok = true;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
int err = errno;
|
||||
req.md_options = MD_FORCE;
|
||||
if (ioctl(fd, MDIOCDETACH, &req) != 0) {
|
||||
log_debug(" [IMG][%s] detach %d failed: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(errno));
|
||||
ok = false;
|
||||
} else {
|
||||
log_debug(" [IMG][%s] detach %d forced after error: %s",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id,
|
||||
strerror(err));
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
|
||||
char devname[64];
|
||||
snprintf(devname, sizeof(devname), "/dev/md%d", unit_id);
|
||||
if (!wait_for_dev_node_state(devname, false)) {
|
||||
log_debug(" [IMG][%s] device node still present after detach: /dev/md%d",
|
||||
attach_backend_name(ATTACH_BACKEND_MD), unit_id);
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id) {
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return detach_md_unit(unit_id);
|
||||
else if (backend == ATTACH_BACKEND_LVD)
|
||||
return detach_lvd_unit(unit_id);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
|
||||
struct PathStateEntry {
|
||||
char path[MAX_PATH];
|
||||
uint8_t missing_param_attempts;
|
||||
uint8_t image_mount_attempts;
|
||||
bool missing_param_limit_logged;
|
||||
bool image_mount_limit_logged;
|
||||
bool game_info_cached;
|
||||
bool game_info_valid;
|
||||
time_t game_info_mtime;
|
||||
off_t game_info_size;
|
||||
ino_t game_info_ino;
|
||||
char game_title_id[MAX_TITLE_ID];
|
||||
char game_title_name[MAX_TITLE_NAME];
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct PathStateEntry g_path_state[PATH_STATE_CAPACITY];
|
||||
static uint16_t g_path_state_hash[STATE_HASH_SIZE];
|
||||
|
||||
static void rebuild_path_state_hash(void) {
|
||||
memset(g_path_state_hash, 0, sizeof(g_path_state_hash));
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
uint32_t slot = sm_fnv1a32(g_path_state[k].path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_path_state_hash[slot] == 0) {
|
||||
g_path_state_hash[slot] = (uint16_t)(k + 1);
|
||||
break;
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static struct PathStateEntry *find_path_state(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return NULL;
|
||||
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_path_state_hash[slot];
|
||||
if (idx == 0)
|
||||
return NULL;
|
||||
struct PathStateEntry *entry = &g_path_state[idx - 1u];
|
||||
if (entry->valid && strcmp(entry->path, path) == 0)
|
||||
return entry;
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct PathStateEntry *create_path_state(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid) {
|
||||
slot_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_k < 0) {
|
||||
int evict_k = -1;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid)
|
||||
continue;
|
||||
if (access(g_path_state[k].path, F_OK) != 0) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (evict_k < 0) {
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid)
|
||||
continue;
|
||||
if (g_path_state[k].missing_param_attempts == 0 &&
|
||||
g_path_state[k].image_mount_attempts == 0 &&
|
||||
!g_path_state[k].game_info_cached) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (evict_k < 0)
|
||||
evict_k = 0;
|
||||
memset(&g_path_state[evict_k], 0, sizeof(g_path_state[evict_k]));
|
||||
rebuild_path_state_hash();
|
||||
slot_k = evict_k;
|
||||
}
|
||||
|
||||
memset(&g_path_state[slot_k], 0, sizeof(g_path_state[slot_k]));
|
||||
g_path_state[slot_k].valid = true;
|
||||
(void)strlcpy(g_path_state[slot_k].path, path,
|
||||
sizeof(g_path_state[slot_k].path));
|
||||
|
||||
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_path_state_hash[slot] == 0) {
|
||||
g_path_state_hash[slot] = (uint16_t)(slot_k + 1);
|
||||
return &g_path_state[slot_k];
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
|
||||
g_path_state[slot_k].valid = false;
|
||||
g_path_state[slot_k].path[0] = '\0';
|
||||
rebuild_path_state_hash();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct PathStateEntry *get_or_create_path_state(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry : create_path_state(path);
|
||||
}
|
||||
|
||||
bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
char *out_id, char *out_name, bool *valid_out) {
|
||||
if (valid_out)
|
||||
*valid_out = false;
|
||||
if (!path || !param_st)
|
||||
return false;
|
||||
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry || !entry->game_info_cached ||
|
||||
entry->game_info_mtime != param_st->st_mtime ||
|
||||
entry->game_info_size != param_st->st_size ||
|
||||
entry->game_info_ino != param_st->st_ino) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (valid_out)
|
||||
*valid_out = entry->game_info_valid;
|
||||
if (!entry->game_info_valid)
|
||||
return true;
|
||||
|
||||
if (out_id)
|
||||
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
|
||||
if (out_name)
|
||||
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
|
||||
return true;
|
||||
}
|
||||
|
||||
void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
bool valid, const char *title_id,
|
||||
const char *title_name) {
|
||||
if (!path || !param_st)
|
||||
return;
|
||||
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
entry->game_info_cached = true;
|
||||
entry->game_info_valid = valid;
|
||||
entry->game_info_mtime = param_st->st_mtime;
|
||||
entry->game_info_size = param_st->st_size;
|
||||
entry->game_info_ino = param_st->st_ino;
|
||||
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
|
||||
(void)strlcpy(entry->game_title_name, valid ? title_name : "", MAX_TITLE_NAME);
|
||||
}
|
||||
|
||||
void prune_path_state(void) {
|
||||
bool changed = false;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
if (access(g_path_state[k].path, F_OK) == 0)
|
||||
continue;
|
||||
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
rebuild_path_state_hash();
|
||||
}
|
||||
|
||||
bool is_missing_param_scan_limited(const char *path) {
|
||||
if (!is_under_image_mount_base(path))
|
||||
return false;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return false;
|
||||
return entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS;
|
||||
}
|
||||
|
||||
void record_missing_param_failure(const char *path) {
|
||||
if (!is_under_image_mount_base(path))
|
||||
return;
|
||||
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry) {
|
||||
log_debug(" [SCAN] missing/invalid param.json: %s", path);
|
||||
notify_system("Missing/invalid param.json:\n%s", path);
|
||||
return;
|
||||
}
|
||||
if (entry->missing_param_attempts < UINT8_MAX)
|
||||
entry->missing_param_attempts++;
|
||||
|
||||
log_debug(" [SCAN] missing/invalid param.json: %s", path);
|
||||
if (entry->missing_param_attempts == 1)
|
||||
notify_system("Missing/invalid param.json:\n%s", path);
|
||||
if (entry->missing_param_attempts >= MAX_MISSING_PARAM_SCAN_ATTEMPTS &&
|
||||
!entry->missing_param_limit_logged) {
|
||||
log_debug(" [SCAN] attempt limit reached (%u), skipping path: %s",
|
||||
(unsigned)MAX_MISSING_PARAM_SCAN_ATTEMPTS, path);
|
||||
entry->missing_param_limit_logged = true;
|
||||
}
|
||||
}
|
||||
|
||||
void clear_missing_param_entry(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->missing_param_attempts = 0;
|
||||
entry->missing_param_limit_logged = false;
|
||||
}
|
||||
|
||||
bool is_image_mount_limited(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS
|
||||
: false;
|
||||
}
|
||||
|
||||
uint8_t bump_image_mount_attempts(const char *path) {
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return MAX_IMAGE_MOUNT_ATTEMPTS;
|
||||
if (entry->image_mount_attempts < UINT8_MAX)
|
||||
entry->image_mount_attempts++;
|
||||
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
|
||||
!entry->image_mount_limit_logged) {
|
||||
log_debug(" [IMG] retry limit reached (%u/%u), skipping image: %s",
|
||||
(unsigned)entry->image_mount_attempts,
|
||||
(unsigned)MAX_IMAGE_MOUNT_ATTEMPTS, path);
|
||||
entry->image_mount_limit_logged = true;
|
||||
}
|
||||
return entry->image_mount_attempts;
|
||||
}
|
||||
|
||||
void clear_image_mount_attempts(const char *path) {
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->image_mount_attempts = 0;
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
const char *get_filename_component(const char *path) {
|
||||
if (!path)
|
||||
return "";
|
||||
const char *base = strrchr(path, '/');
|
||||
if (!base)
|
||||
base = strrchr(path, '\\');
|
||||
return base ? base + 1 : path;
|
||||
}
|
||||
|
||||
bool is_under_image_mount_base(const char *path) {
|
||||
if (!path)
|
||||
return false;
|
||||
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
|
||||
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
|
||||
path[image_prefix_len] == '/');
|
||||
}
|
||||
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
|
||||
if (!scan_path || scan_path[0] == '\0')
|
||||
return false;
|
||||
if (is_under_image_mount_base(scan_path))
|
||||
return false;
|
||||
|
||||
snprintf(out, MAX_PATH, "%s/%s", scan_path, DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return true;
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_stability.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image.h"
|
||||
|
||||
static bool is_under_discovered_param_root(
|
||||
const char *path, char discovered_param_roots[][MAX_PATH],
|
||||
int discovered_count) {
|
||||
if (!path)
|
||||
return false;
|
||||
for (int i = 0; i < discovered_count; i++) {
|
||||
const char *root = discovered_param_roots[i];
|
||||
if (root[0] == '\0')
|
||||
continue;
|
||||
size_t root_len = strlen(root);
|
||||
if (root_len == 0)
|
||||
continue;
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
continue;
|
||||
if (path[root_len] == '\0' || path[root_len] == '/')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void classify_scan_entry(const char *full_path, unsigned char d_type,
|
||||
bool *is_dir_out, bool *is_regular_out) {
|
||||
bool is_dir = false;
|
||||
bool is_regular = false;
|
||||
|
||||
if (d_type == DT_DIR) {
|
||||
is_dir = true;
|
||||
} else if (d_type == DT_REG) {
|
||||
is_regular = true;
|
||||
} else if (d_type == DT_UNKNOWN) {
|
||||
struct stat st;
|
||||
if (lstat(full_path, &st) == 0) {
|
||||
is_dir = S_ISDIR(st.st_mode);
|
||||
is_regular = S_ISREG(st.st_mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_dir_out)
|
||||
*is_dir_out = is_dir;
|
||||
if (is_regular_out)
|
||||
*is_regular_out = is_regular;
|
||||
}
|
||||
|
||||
// --- Candidate Discovery ---
|
||||
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) {
|
||||
struct stat param_st;
|
||||
memset(¶m_st, 0, sizeof(param_st));
|
||||
bool has_param_json = false;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
title_id[0] = '\0';
|
||||
title_name[0] = '\0';
|
||||
|
||||
if (is_under_discovered_param_root(full_path, discovered_param_roots,
|
||||
*discovered_param_root_count)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_under_image_mount_base(full_path) && !is_active_image_mount_point(full_path)) {
|
||||
log_debug(" [SKIP] inactive mount path: %s", full_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
has_param_json = directory_has_param_json(full_path, ¶m_st);
|
||||
|
||||
if (!has_param_json) {
|
||||
if (is_missing_param_scan_limited(full_path)) {
|
||||
log_debug(" [SKIP] param.json retry limit reached: %s", full_path);
|
||||
} else {
|
||||
record_missing_param_failure(full_path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!get_game_info(full_path, ¶m_st, title_id, title_name)) {
|
||||
record_missing_param_failure(full_path);
|
||||
log_debug(" [SKIP] game info unavailable: %s", full_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_under_discovered_param_root(full_path, discovered_param_roots,
|
||||
*discovered_param_root_count) &&
|
||||
*discovered_param_root_count < MAX_PENDING) {
|
||||
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
|
||||
MAX_PATH);
|
||||
(*discovered_param_root_count)++;
|
||||
}
|
||||
clear_missing_param_entry(full_path);
|
||||
|
||||
for (int i = 0; i < *candidate_count; i++) {
|
||||
if (strcmp(candidates[i].title_id, title_id) == 0) {
|
||||
notify_duplicate_title_once(title_id, full_path, candidates[i].path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!app_db_titles_ready) {
|
||||
return true;
|
||||
}
|
||||
bool in_app_db = app_db_title_list_contains(app_db_titles, title_id);
|
||||
bool installed = in_app_db && is_installed(title_id);
|
||||
char tracked_path[MAX_PATH];
|
||||
bool link_matches_source =
|
||||
installed && read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
|
||||
strcmp(tracked_path, full_path) == 0;
|
||||
bool mounted = false;
|
||||
if (link_matches_source) {
|
||||
mounted = is_data_mounted(title_id);
|
||||
if (!mounted && mount_title_nullfs(title_id, full_path))
|
||||
mounted = is_data_mounted(title_id);
|
||||
}
|
||||
|
||||
if (in_app_db) {
|
||||
const char *cached_path = NULL;
|
||||
if (find_cached_game(full_path, title_id, &cached_path)) {
|
||||
if (cached_path && strcmp(cached_path, full_path) != 0)
|
||||
notify_duplicate_title_once(title_id, full_path, cached_path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_app_db && was_register_attempted(title_id)) {
|
||||
uint8_t register_attempts = get_register_attempts(title_id);
|
||||
log_debug(" [SKIP] register/install retry limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)register_attempts, (unsigned)MAX_REGISTER_ATTEMPTS,
|
||||
title_name, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Installed status requires both app files and app.db presence.
|
||||
if (link_matches_source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t failed_attempts = get_failed_mount_attempts(title_id);
|
||||
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
|
||||
title_name, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!wait_for_stability_fast(full_path, title_name)) {
|
||||
log_debug(" [SKIP] source not stable yet: %s (%s)", title_name, full_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (*candidate_count >= max_candidates) {
|
||||
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
|
||||
title_name, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
(void)strlcpy(candidates[*candidate_count].path, full_path,
|
||||
sizeof(candidates[*candidate_count].path));
|
||||
(void)strlcpy(candidates[*candidate_count].title_id, title_id,
|
||||
sizeof(candidates[*candidate_count].title_id));
|
||||
(void)strlcpy(candidates[*candidate_count].title_name, title_name,
|
||||
sizeof(candidates[*candidate_count].title_name));
|
||||
candidates[*candidate_count].installed = installed;
|
||||
candidates[*candidate_count].in_app_db = in_app_db;
|
||||
(*candidate_count)++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void collect_candidates_recursively(
|
||||
const char *dir_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) {
|
||||
if (should_stop_requested() || !dir_path || dir_path[0] == '\0')
|
||||
return;
|
||||
|
||||
// Once a directory has sce_sys/param.json (valid or not),
|
||||
// it is treated as a terminal game root and descendants are skipped.
|
||||
if (try_collect_candidate_for_directory(
|
||||
dir_path, candidates, max_candidates, candidate_count, app_db_titles,
|
||||
app_db_titles_ready, discovered_param_roots,
|
||||
discovered_param_root_count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DIR *d = opendir(dir_path);
|
||||
if (!d)
|
||||
return;
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (should_stop_requested())
|
||||
break;
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
char full_path[MAX_PATH];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
|
||||
bool is_dir = false;
|
||||
classify_scan_entry(full_path, entry->d_type, &is_dir, NULL);
|
||||
|
||||
if (!is_dir)
|
||||
continue;
|
||||
|
||||
collect_candidates_recursively(
|
||||
full_path, candidates, max_candidates, candidate_count, app_db_titles,
|
||||
app_db_titles_ready, discovered_param_roots, discovered_param_root_count);
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
static bool resolve_backport_mount_context(const char *title_id,
|
||||
const char *source_path,
|
||||
int scan_path_count,
|
||||
char backport_path[MAX_PATH],
|
||||
char system_ex_path[MAX_PATH]) {
|
||||
char backport_source_path[MAX_PATH];
|
||||
if (is_under_image_mount_base(source_path)) {
|
||||
if (!resolve_image_source_from_mount_cache(source_path,
|
||||
backport_source_path,
|
||||
sizeof(backport_source_path))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
(void)strlcpy(backport_source_path, source_path,
|
||||
sizeof(backport_source_path));
|
||||
}
|
||||
|
||||
const char *owning_scan_path = NULL;
|
||||
size_t owning_scan_path_len = 0;
|
||||
for (int i = 0; i < scan_path_count; i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (!path_matches_root_or_child(backport_source_path, scan_path))
|
||||
continue;
|
||||
|
||||
size_t scan_path_len = strlen(scan_path);
|
||||
if (owning_scan_path && scan_path_len <= owning_scan_path_len)
|
||||
continue;
|
||||
|
||||
owning_scan_path = scan_path;
|
||||
owning_scan_path_len = scan_path_len;
|
||||
}
|
||||
if (!owning_scan_path)
|
||||
return false;
|
||||
|
||||
char backport_root[MAX_PATH];
|
||||
if (!build_backports_root_path(owning_scan_path, backport_root))
|
||||
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;
|
||||
}
|
||||
|
||||
void mount_backport_overlays(void) {
|
||||
int scan_path_count = get_scan_path_count();
|
||||
if (scan_path_count == 0)
|
||||
return;
|
||||
|
||||
DIR *d = opendir("/user/app");
|
||||
if (!d) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [BKP] open /user/app failed: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (should_stop_requested())
|
||||
break;
|
||||
if (!resolve_title_app_dir(entry, NULL, 0))
|
||||
continue;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link(entry->d_name, source_path, sizeof(source_path)))
|
||||
continue;
|
||||
|
||||
char backport_path[MAX_PATH];
|
||||
char system_ex_path[MAX_PATH];
|
||||
if (!resolve_backport_mount_context(entry->d_name, source_path,
|
||||
scan_path_count, backport_path,
|
||||
system_ex_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(entry->d_name, source_path,
|
||||
backport_path, &overlay_active) ||
|
||||
overlay_active) {
|
||||
continue;
|
||||
}
|
||||
if (!wait_for_stability_fast(backport_path, "BKP"))
|
||||
continue;
|
||||
overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(entry->d_name, source_path,
|
||||
backport_path, &overlay_active) ||
|
||||
overlay_active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mount_backport_overlay(system_ex_path, backport_path, entry->d_name);
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
void cleanup_lost_sources_before_scan(void) {
|
||||
// 1) Drop stale game cache entries for deleted sources.
|
||||
prune_game_cache();
|
||||
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
|
||||
cleanup_mount_links(NULL, true);
|
||||
// 3) Unmount stale image mounts for deleted image files.
|
||||
cleanup_stale_image_mounts();
|
||||
// 4) Drop stale path-state entries.
|
||||
prune_path_state();
|
||||
}
|
||||
|
||||
int collect_scan_candidates(scan_candidate_t *candidates,
|
||||
int max_candidates,
|
||||
int *total_found_out) {
|
||||
int candidate_count = 0;
|
||||
const struct AppDbTitleList *app_db_titles = NULL;
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
char discovered_param_roots[MAX_PENDING][MAX_PATH];
|
||||
int discovered_param_root_count = 0;
|
||||
memset(discovered_param_roots, 0, sizeof(discovered_param_roots));
|
||||
|
||||
if (!app_db_titles_ready) {
|
||||
log_debug(" [DB] app.db title list unavailable for this scan cycle");
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (should_stop_requested())
|
||||
goto done;
|
||||
|
||||
bool has_backports_root = !is_under_image_mount_base(scan_path);
|
||||
|
||||
DIR *d = opendir(scan_path);
|
||||
if (!d)
|
||||
continue;
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (should_stop_requested()) {
|
||||
closedir(d);
|
||||
goto done;
|
||||
}
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
if (has_backports_root &&
|
||||
strcmp(entry->d_name, DEFAULT_BACKPORTS_DIR_NAME) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char full_path[MAX_PATH];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", scan_path, entry->d_name);
|
||||
|
||||
bool is_dir = false;
|
||||
bool is_regular = false;
|
||||
classify_scan_entry(full_path, entry->d_type, &is_dir, &is_regular);
|
||||
|
||||
if (!path_matches_root_or_child(scan_path, IMAGE_MOUNT_BASE) && is_regular)
|
||||
maybe_mount_image_file(full_path, entry->d_name);
|
||||
if (!is_dir)
|
||||
continue;
|
||||
|
||||
if (runtime_config()->recursive_scan) {
|
||||
collect_candidates_recursively(
|
||||
full_path, candidates, max_candidates, &candidate_count,
|
||||
app_db_titles, app_db_titles_ready, discovered_param_roots,
|
||||
&discovered_param_root_count);
|
||||
} else {
|
||||
(void)try_collect_candidate_for_directory(
|
||||
full_path, candidates, max_candidates, &candidate_count,
|
||||
app_db_titles, app_db_titles_ready, discovered_param_roots,
|
||||
&discovered_param_root_count);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
done:
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
return candidate_count;
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
|
||||
#define SHELLCORE_FLAG_WAITMODE_OR 2u
|
||||
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
|
||||
|
||||
typedef intptr_t sm_kernel_event_flag_t;
|
||||
|
||||
int sceKernelOpenEventFlag(sm_kernel_event_flag_t *ef, const char *name);
|
||||
int sceKernelPollEventFlag(sm_kernel_event_flag_t ef, uint64_t bit_pattern,
|
||||
unsigned int wait_mode, uint64_t *result_pattern);
|
||||
int sceKernelCloseEventFlag(sm_kernel_event_flag_t ef);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
sm_kernel_event_flag_t handle;
|
||||
uint64_t last_pattern;
|
||||
int last_rc;
|
||||
bool is_open;
|
||||
bool has_last_pattern;
|
||||
bool has_last_rc;
|
||||
} shellcore_flag_monitor_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t mask;
|
||||
const char *name;
|
||||
} shellcore_flag_bit_desc_t;
|
||||
|
||||
static shellcore_flag_monitor_t g_shellcore_flags[] = {
|
||||
{.name = "SceShellCoreUtilAppFocus", .handle = -1},
|
||||
{.name = "SceSystemStateMgrInfo", .handle = -1},
|
||||
/* {.name = "SceSystemStateMgrStatus", .handle = -1},
|
||||
{.name = "SceSysCoreSuspend", .handle = -1},
|
||||
{.name = "SceLncUtilSystemStatus", .handle = -1},
|
||||
{.name = "SceShellCoreUtilCtrlFocus", .handle = -1},
|
||||
{.name = "SceShellCoreUtilPowerControl", .handle = -1},
|
||||
{.name = "SceShellCoreUtilRunLevel", .handle = -1},
|
||||
{.name = "SceShellCoreUtilUIStatus", .handle = -1},
|
||||
{.name = "SceAutoMountUsbMass", .handle = -1},
|
||||
{.name = "SceVshctlInternalUserFlag", .handle = -1},
|
||||
{.name = "SceBootStatusFlags", .handle = -1},
|
||||
{.name = "SceSysCoreReboot", .handle = -1},
|
||||
*/
|
||||
};
|
||||
|
||||
static pthread_t g_shellcore_flag_thread;
|
||||
static pthread_mutex_t g_shellcore_flag_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_shellcore_flag_thread_started = false;
|
||||
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;
|
||||
|
||||
#if 0
|
||||
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
|
||||
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
|
||||
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
|
||||
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
|
||||
{0x0000000000040000ULL, "POWER_TRANSITION"},
|
||||
{0x0000000000080000ULL, "POWER_LOCK"},
|
||||
{0x0000000000100000ULL, "SOCIAL_SCREEN_STATUS"},
|
||||
{0x0000000000200000ULL, "REQUEST_CAMERA_CALIBRATION"},
|
||||
{0x0000000002000000ULL, "USER_INPUT_PULSE"},
|
||||
{0x0000000004000000ULL, "IMPOSE_OVERLAY"},
|
||||
{0x0000000008000000ULL, "AUTO_POWER_DOWN_TIMER_RESET"},
|
||||
{0x0000000010000000ULL, "REMOTE_PLAY_STATUS"},
|
||||
{0x0000000020000000ULL, "SHARE_PLAY_STATUS"},
|
||||
{0x0000000040000000ULL, "GAME_LIVE_STREAMING"},
|
||||
{0x0000000080000000ULL, "BGM_CPU_BUDGET"},
|
||||
{0x0000000100000000ULL, "PRENOTIFY_REMOTE_PLAY"},
|
||||
{0x0000000200000000ULL, "PRENOTIFY_SHARE_PLAY"},
|
||||
{0x0000000400000000ULL, "PRENOTIFY_GAME_LIVE_STREAMING"},
|
||||
{0x0000000800000000ULL, "REMOTE_PLAY_CPU_BUDGET"},
|
||||
{0x0000001000000000ULL, "SHARE_PLAY_CPU_BUDGET"},
|
||||
{0x0000100000000000ULL, "EXT_STORAGE_APP_MOVE_IN_PROGRESS"},
|
||||
{0x0000200000000000ULL, "STATE_0x2000000000000"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_shellcore_ui_status_bits[] = {
|
||||
{0x0000000000000001ULL, "LOW_BIT_0x1"},
|
||||
{0x0000000000000010ULL, "LOW_BIT_0x10"},
|
||||
{0x0000000000018000ULL, "BASIC_PRODUCT_SHAPE_FIELD"},
|
||||
{0x0000000000020000ULL, "EXT_HDD_MOUNT_UNBLOCK"},
|
||||
{0x0000000400000000ULL, "HIGH32_BIT2"},
|
||||
};
|
||||
|
||||
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"},
|
||||
{0x0000000000000040ULL, "BIT_0x40"},
|
||||
{0x0000000000000080ULL, "BIT_0x80"},
|
||||
{0x0000000000000200ULL, "BIT_0x200"},
|
||||
{0x0000000000004000ULL, "BIT_0x4000"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_syscore_reboot_bits[] = {
|
||||
{0x0000000000000040ULL, "BIT_0x40"},
|
||||
{0x0000000000004000ULL, "BIT_0x4000"},
|
||||
{0x0000000100000000ULL, "REBOOT_FLAG"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_syscore_suspend_bits[] = {
|
||||
{0x0000000000000001ULL, "SUSPEND_ACK"},
|
||||
{0x0000000000000002ULL, "SUSPEND_REQ_BIT1"},
|
||||
{0x0000000000000004ULL, "SUSPEND_REQ_BIT2"},
|
||||
{0x0000000000000008ULL, "SUSPEND_REQ_BIT3"},
|
||||
};
|
||||
#endif
|
||||
|
||||
enum {
|
||||
SYSTEM_STATE_MGR_STATE_INVALID = 0u,
|
||||
SYSTEM_STATE_MGR_STATE_INITIALIZING = 10u,
|
||||
SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING = 100u,
|
||||
SYSTEM_STATE_MGR_STATE_POWER_SAVING = 200u,
|
||||
SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING = 300u,
|
||||
SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY = 500u,
|
||||
SYSTEM_STATE_MGR_STATE_WORKING = 1000u,
|
||||
};
|
||||
|
||||
enum {
|
||||
SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE = 0u,
|
||||
SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF = 1u,
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
const shellcore_flag_monitor_t *flag, size_t *count_out) {
|
||||
if (count_out)
|
||||
*count_out = 0;
|
||||
if (!flag || !flag->name)
|
||||
return NULL;
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_lnc_util_system_status_bits) /
|
||||
sizeof(g_lnc_util_system_status_bits[0]);
|
||||
}
|
||||
return g_lnc_util_system_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
|
||||
return g_power_control_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_shellcore_ui_status_bits) /
|
||||
sizeof(g_shellcore_ui_status_bits[0]);
|
||||
}
|
||||
return g_shellcore_ui_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceVshctlInternalUserFlag") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_vshctl_internal_user_bits) /
|
||||
sizeof(g_vshctl_internal_user_bits[0]);
|
||||
}
|
||||
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)
|
||||
*count_out = sizeof(g_boot_status_bits) / sizeof(g_boot_status_bits[0]);
|
||||
return g_boot_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSysCoreReboot") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_syscore_reboot_bits) / sizeof(g_syscore_reboot_bits[0]);
|
||||
return g_syscore_reboot_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSysCoreSuspend") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_syscore_suspend_bits) /
|
||||
sizeof(g_syscore_suspend_bits[0]);
|
||||
return g_syscore_suspend_bits;
|
||||
}
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char *get_system_state_mgr_state_name(unsigned state) {
|
||||
switch (state) {
|
||||
case SYSTEM_STATE_MGR_STATE_INVALID:
|
||||
return "INVALID";
|
||||
case SYSTEM_STATE_MGR_STATE_INITIALIZING:
|
||||
return "INITIALIZING";
|
||||
case SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING:
|
||||
return "SHUTDOWN_ON_GOING";
|
||||
case SYSTEM_STATE_MGR_STATE_POWER_SAVING:
|
||||
return "POWER_SAVING";
|
||||
case SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING:
|
||||
return "SUSPEND_ON_GOING";
|
||||
case SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY:
|
||||
return "MAIN_ON_STANDBY";
|
||||
case SYSTEM_STATE_MGR_STATE_WORKING:
|
||||
return "WORKING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_system_state_mgr_reboot_cause_name(unsigned cause) {
|
||||
switch (cause) {
|
||||
case SYSTEM_STATE_MGR_REBOOT_CAUSE_NONE:
|
||||
return "NONE";
|
||||
case SYSTEM_STATE_MGR_REBOOT_CAUSE_CFF:
|
||||
return "CFF";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void append_shellcore_flag_token(char *dst, size_t dst_size,
|
||||
const char *token) {
|
||||
size_t used;
|
||||
|
||||
if (!dst || dst_size == 0 || !token || token[0] == '\0')
|
||||
return;
|
||||
|
||||
used = strnlen(dst, dst_size);
|
||||
if (used >= dst_size - 1)
|
||||
return;
|
||||
|
||||
(void)snprintf(dst + used, dst_size - used, "%s%s", used == 0 ? "" : "|",
|
||||
token);
|
||||
}
|
||||
|
||||
static bool format_shellcore_flag_special_known_masks(
|
||||
const shellcore_flag_monitor_t *flag, char *dst, size_t dst_size) {
|
||||
if (!flag || !flag->name || !dst || dst_size == 0)
|
||||
return false;
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "RUN_LEVEL_NUMERIC");
|
||||
append_shellcore_flag_token(dst, dst_size, "POLL_MASK=0xFFFFFFF");
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL=10");
|
||||
append_shellcore_flag_token(dst, dst_size, "READY=100");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "USB_MOUNT_BITS=0xFFFFFFF");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"CURRENT_STATE=bits[0..15]");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"TRIGGER_CODE=bits[32..47]");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"REBOOT_CAUSE=bits[48..63]");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_INVALID=0");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_INITIALIZING=10");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_SHUTDOWN_ON_GOING=100");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_POWER_SAVING=200");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_SUSPEND_ON_GOING=300");
|
||||
append_shellcore_flag_token(dst, dst_size,
|
||||
"STATE_MAIN_ON_STANDBY=500");
|
||||
append_shellcore_flag_token(dst, dst_size, "STATE_WORKING=1000");
|
||||
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_NONE=0");
|
||||
append_shellcore_flag_token(dst, dst_size, "REBOOT_CAUSE_CFF=1");
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL=0x10000000A");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void format_shellcore_flag_known_masks(const shellcore_flag_monitor_t *flag,
|
||||
char *dst, size_t dst_size) {
|
||||
size_t bit_count = 0;
|
||||
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
dst[0] = '\0';
|
||||
|
||||
(void)format_shellcore_flag_special_known_masks(flag, dst, dst_size);
|
||||
|
||||
for (size_t i = 0; i < bit_count; ++i) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "%s=0x%llX", bits[i].name,
|
||||
(unsigned long long)bits[i].mask);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
append_shellcore_flag_token(dst, dst_size, "APP_ID");
|
||||
}
|
||||
}
|
||||
|
||||
static bool format_shellcore_flag_special_value(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern, char *dst,
|
||||
size_t dst_size) {
|
||||
if (!flag || !flag->name || !dst || dst_size == 0)
|
||||
return false;
|
||||
|
||||
#if 0
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilRunLevel") == 0) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "RUN_LEVEL=%u",
|
||||
(unsigned)(pattern & 0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
if ((pattern & 0x0FFFFFFFULL) == 10u) {
|
||||
append_shellcore_flag_token(dst, dst_size, "INITIAL");
|
||||
} else if ((pattern & 0x0FFFFFFFULL) == 100u) {
|
||||
append_shellcore_flag_token(dst, dst_size, "READY");
|
||||
}
|
||||
if ((pattern & ~0x0FFFFFFFULL) != 0) {
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceAutoMountUsbMass") == 0) {
|
||||
uint64_t usb_bits = pattern & 0x0FFFFFFFULL;
|
||||
for (unsigned i = 0; i < 28; ++i) {
|
||||
if ((usb_bits & (1ULL << i)) == 0)
|
||||
continue;
|
||||
char token[32];
|
||||
(void)snprintf(token, sizeof(token), "USB%u", i);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if ((pattern & ~0x0FFFFFFFULL) != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~0x0FFFFFFFULL));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrInfo") == 0) {
|
||||
unsigned current_state = (unsigned)(pattern & 0xFFFFu);
|
||||
unsigned mid16 = (unsigned)((pattern >> 16) & 0xFFFFu);
|
||||
unsigned trigger_code = (unsigned)((pattern >> 32) & 0xFFFFu);
|
||||
unsigned reboot_cause = (unsigned)((pattern >> 48) & 0xFFFFu);
|
||||
char token[128];
|
||||
|
||||
(void)snprintf(token, sizeof(token), "STATE=%s(0x%04X)",
|
||||
get_system_state_mgr_state_name(current_state), current_state);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
(void)snprintf(token, sizeof(token), "TRIGGER=0x%04X", trigger_code);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
(void)snprintf(token, sizeof(token), "REBOOT_CAUSE=%s(0x%04X)",
|
||||
get_system_state_mgr_reboot_cause_name(reboot_cause),
|
||||
reboot_cause);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
|
||||
if (mid16 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "MID16=0x%04X", mid16);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
unsigned low32 = (unsigned)(pattern & 0xFFFFFFFFu);
|
||||
unsigned high32 = (unsigned)(pattern >> 32);
|
||||
char token[96];
|
||||
|
||||
if (low32 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", low32);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (high32 != 0) {
|
||||
(void)snprintf(token, sizeof(token), "HIGH32=0x%08X", high32);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
return true;
|
||||
}
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilUIStatus") == 0) {
|
||||
unsigned shape_field = (unsigned)((pattern >> 15) & 0x3u);
|
||||
if (shape_field != 0) {
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "BASIC_PRODUCT_SHAPE=%u",
|
||||
shape_field - 1u);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void format_shellcore_flag_set_bits(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern, char *dst,
|
||||
size_t dst_size) {
|
||||
size_t bit_count = 0;
|
||||
uint64_t known_mask = 0;
|
||||
const shellcore_flag_bit_desc_t *bits = get_shellcore_flag_bits(flag, &bit_count);
|
||||
|
||||
if (!dst || dst_size == 0)
|
||||
return;
|
||||
dst[0] = '\0';
|
||||
|
||||
if (format_shellcore_flag_special_value(flag, pattern, dst, dst_size) &&
|
||||
dst[0] != '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < bit_count; ++i) {
|
||||
known_mask |= bits[i].mask;
|
||||
if ((pattern & bits[i].mask) == 0)
|
||||
continue;
|
||||
|
||||
char token[96];
|
||||
(void)snprintf(token, sizeof(token), "%s", bits[i].name);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0 &&
|
||||
(pattern >> 32) == 0 && pattern != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "APP_ID=0x%08X", (unsigned)pattern);
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if ((pattern & ~known_mask) != 0) {
|
||||
char token[64];
|
||||
(void)snprintf(token, sizeof(token), "UNKNOWN=0x%llX",
|
||||
(unsigned long long)(pattern & ~known_mask));
|
||||
append_shellcore_flag_token(dst, dst_size, token);
|
||||
}
|
||||
|
||||
if (dst[0] == '\0')
|
||||
(void)snprintf(dst, dst_size, "0");
|
||||
}
|
||||
|
||||
static void set_shellcore_flag_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_shellcore_flag_mutex);
|
||||
g_shellcore_flag_start_success = success;
|
||||
g_shellcore_flag_start_ready = true;
|
||||
pthread_cond_broadcast(&g_shellcore_flag_cond);
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
if (!flag)
|
||||
return;
|
||||
|
||||
flag->handle = -1;
|
||||
flag->is_open = false;
|
||||
flag->has_last_pattern = false;
|
||||
flag->has_last_rc = false;
|
||||
flag->last_pattern = 0;
|
||||
flag->last_rc = 0;
|
||||
}
|
||||
|
||||
static void log_shellcore_flag_pattern(const shellcore_flag_monitor_t *flag,
|
||||
uint64_t pattern) {
|
||||
char decoded[512];
|
||||
|
||||
if (!flag)
|
||||
return;
|
||||
|
||||
format_shellcore_flag_set_bits(flag, pattern, decoded, sizeof(decoded));
|
||||
|
||||
if (!flag->has_last_pattern) {
|
||||
log_debug(" [SHELLFLAG] %s initial=0x%016llX low32=0x%08X high32=0x%08X "
|
||||
"decoded=%s",
|
||||
flag->name, (unsigned long long)pattern, (unsigned)(pattern),
|
||||
(unsigned)(pattern >> 32), decoded);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [SHELLFLAG] %s value=0x%016llX delta=0x%016llX low32=0x%08X "
|
||||
"high32=0x%08X decoded=%s",
|
||||
flag->name, (unsigned long long)pattern,
|
||||
(unsigned long long)(flag->last_pattern ^ pattern),
|
||||
(unsigned)(pattern), (unsigned)(pattern >> 32), decoded);
|
||||
}
|
||||
|
||||
static void close_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
if (!flag || !flag->is_open)
|
||||
return;
|
||||
|
||||
int rc = sceKernelCloseEventFlag(flag->handle);
|
||||
if (rc < 0) {
|
||||
log_debug(" [SHELLFLAG] close failed: %s handle=0x%016llX rc=0x%08X",
|
||||
flag->name, (unsigned long long)flag->handle, (unsigned)rc);
|
||||
}
|
||||
|
||||
reset_shellcore_flag_state(flag);
|
||||
}
|
||||
|
||||
static void close_shellcore_flags(void) {
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
close_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static size_t open_shellcore_flags(void) {
|
||||
size_t opened_count = 0;
|
||||
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
shellcore_flag_monitor_t *flag = &g_shellcore_flags[i];
|
||||
sm_kernel_event_flag_t handle = -1;
|
||||
int rc = sceKernelOpenEventFlag(&handle, flag->name);
|
||||
if (rc < 0) {
|
||||
log_debug(" [SHELLFLAG] open failed: %s rc=0x%08X", flag->name,
|
||||
(unsigned)rc);
|
||||
reset_shellcore_flag_state(flag);
|
||||
continue;
|
||||
}
|
||||
|
||||
flag->handle = handle;
|
||||
flag->is_open = true;
|
||||
log_debug(" [SHELLFLAG] opened %s handle=0x%016llX", flag->name,
|
||||
(unsigned long long)flag->handle);
|
||||
|
||||
{
|
||||
char known_masks[1024];
|
||||
format_shellcore_flag_known_masks(flag, known_masks, sizeof(known_masks));
|
||||
if (known_masks[0] != '\0') {
|
||||
log_debug(" [SHELLFLAG] known %s masks: %s", flag->name, known_masks);
|
||||
}
|
||||
}
|
||||
|
||||
opened_count++;
|
||||
}
|
||||
|
||||
return opened_count;
|
||||
}
|
||||
|
||||
static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
uint64_t result_pattern = 0;
|
||||
int rc;
|
||||
bool changed = false;
|
||||
bool entered_shutdown_on_going = false;
|
||||
bool entered_resume_working = false;
|
||||
bool is_system_state_mgr_info = false;
|
||||
unsigned current_state = 0;
|
||||
unsigned previous_state = 0;
|
||||
|
||||
if (!flag || !flag->is_open)
|
||||
return;
|
||||
|
||||
rc = sceKernelPollEventFlag(flag->handle, SHELLCORE_FLAG_ALL_BITS,
|
||||
SHELLCORE_FLAG_WAITMODE_OR, &result_pattern);
|
||||
if (rc < 0) {
|
||||
if (!flag->has_last_rc || flag->last_rc != rc) {
|
||||
log_debug(" [SHELLFLAG] %s poll rc=0x%08X", flag->name,
|
||||
(unsigned)rc);
|
||||
}
|
||||
flag->last_rc = rc;
|
||||
flag->has_last_rc = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flag->has_last_rc || flag->last_rc != 0 || !flag->has_last_pattern ||
|
||||
flag->last_pattern != result_pattern) {
|
||||
log_shellcore_flag_pattern(flag, result_pattern);
|
||||
changed = flag->has_last_pattern && flag->last_pattern != result_pattern;
|
||||
}
|
||||
|
||||
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
|
||||
if (is_system_state_mgr_info) {
|
||||
current_state = (unsigned)(result_pattern & 0xFFFFu);
|
||||
previous_state =
|
||||
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
|
||||
|
||||
entered_shutdown_on_going =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
|
||||
entered_resume_working =
|
||||
flag->has_last_pattern &&
|
||||
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
|
||||
}
|
||||
|
||||
flag->last_pattern = result_pattern;
|
||||
flag->has_last_pattern = true;
|
||||
flag->last_rc = 0;
|
||||
flag->has_last_rc = true;
|
||||
|
||||
if (changed && strcmp(flag->name, "SceShellCoreUtilAppFocus") == 0) {
|
||||
sm_kstuff_note_app_focus((uint32_t)result_pattern);
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
if (entered_shutdown_on_going) {
|
||||
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
|
||||
}
|
||||
if (entered_resume_working) {
|
||||
request_scan_now("SceSystemStateMgrInfo=WORKING");
|
||||
}
|
||||
}
|
||||
|
||||
static void *shellcore_flag_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
size_t opened_count = open_shellcore_flags();
|
||||
// The active monitor currently depends on both configured flags opening:
|
||||
// AppFocus plus SceSystemStateMgrInfo.
|
||||
if (opened_count < 2) {
|
||||
if (opened_count != 0)
|
||||
close_shellcore_flags();
|
||||
set_shellcore_flag_start_result(false);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
poll_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
|
||||
log_debug(" [SHELLFLAG] monitor started (%zu/%zu flags opened)", opened_count,
|
||||
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
|
||||
set_shellcore_flag_start_result(true);
|
||||
|
||||
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
poll_shellcore_flag(&g_shellcore_flags[i]);
|
||||
}
|
||||
|
||||
sceKernelUsleep(SHELLCORE_FLAG_POLL_INTERVAL_US);
|
||||
}
|
||||
|
||||
close_shellcore_flags();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_shellcore_flags_start(void) {
|
||||
int rc;
|
||||
|
||||
if (g_shellcore_flag_thread_started)
|
||||
return g_shellcore_flag_start_success;
|
||||
|
||||
g_shellcore_flag_stop_requested = 0;
|
||||
g_shellcore_flag_start_ready = false;
|
||||
g_shellcore_flag_start_success = false;
|
||||
close_shellcore_flags();
|
||||
|
||||
rc = pthread_create(&g_shellcore_flag_thread, NULL, shellcore_flag_thread_main,
|
||||
NULL);
|
||||
if (rc != 0) {
|
||||
log_debug(" [SHELLFLAG] pthread_create failed: rc=%d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_shellcore_flag_mutex);
|
||||
while (!g_shellcore_flag_start_ready)
|
||||
pthread_cond_wait(&g_shellcore_flag_cond, &g_shellcore_flag_mutex);
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
|
||||
if (!g_shellcore_flag_start_success) {
|
||||
pthread_join(g_shellcore_flag_thread, NULL);
|
||||
close_shellcore_flags();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_shellcore_flag_thread_started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_flags_stop(void) {
|
||||
if (!g_shellcore_flag_thread_started)
|
||||
return;
|
||||
|
||||
g_shellcore_flag_stop_requested = 1;
|
||||
pthread_join(g_shellcore_flag_thread, NULL);
|
||||
g_shellcore_flag_thread_started = false;
|
||||
g_shellcore_flag_start_ready = false;
|
||||
g_shellcore_flag_start_success = false;
|
||||
close_shellcore_flags();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_stability.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
int *stat_errno_out) {
|
||||
struct stat st;
|
||||
time_t now = time(NULL);
|
||||
ensure_runtime_config_ready();
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = 0;
|
||||
|
||||
if (stat(path, &st) != 0) {
|
||||
if (root_diff_out)
|
||||
*root_diff_out = -1.0;
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
double root_diff = difftime(now, st.st_mtime);
|
||||
if (root_diff_out)
|
||||
*root_diff_out = root_diff;
|
||||
if (root_diff < 0.0)
|
||||
return true;
|
||||
return root_diff > (double)runtime_config()->stability_wait_seconds;
|
||||
}
|
||||
|
||||
bool wait_for_stability_fast(const char *path, const char *name) {
|
||||
double diff = 0.0;
|
||||
int st_err = 0;
|
||||
if (is_path_stable_now(path, &diff, &st_err))
|
||||
return true;
|
||||
|
||||
if (st_err != 0)
|
||||
log_debug(" [WAIT] %s stat failed for %s: %s", name, path,
|
||||
strerror(st_err));
|
||||
else
|
||||
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_time.h"
|
||||
|
||||
uint64_t monotonic_time_us(void) {
|
||||
struct timespec ts;
|
||||
memset(&ts, 0, sizeof(ts));
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
|
||||
return 0;
|
||||
|
||||
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)ts.tv_nsec / 1000ull;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
|
||||
struct TitleStateEntry {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint8_t mount_reg_attempts;
|
||||
uint8_t register_attempts;
|
||||
bool duplicate_notified_once;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
|
||||
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
|
||||
|
||||
static void rebuild_title_state_hash(void) {
|
||||
memset(g_title_state_hash, 0, sizeof(g_title_state_hash));
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid || g_title_state[k].title_id[0] == '\0')
|
||||
continue;
|
||||
uint32_t slot =
|
||||
sm_fnv1a32(g_title_state[k].title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_title_state_hash[slot] == 0) {
|
||||
g_title_state_hash[slot] = (uint16_t)(k + 1);
|
||||
break;
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *find_title_state(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_title_state_hash[slot];
|
||||
if (idx == 0)
|
||||
return NULL;
|
||||
struct TitleStateEntry *entry = &g_title_state[idx - 1u];
|
||||
if (entry->valid && strcmp(entry->title_id, title_id) == 0)
|
||||
return entry;
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *create_title_state(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid) {
|
||||
slot_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot_k < 0) {
|
||||
int evict_k = -1;
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid)
|
||||
continue;
|
||||
if (g_title_state[k].mount_reg_attempts == 0 &&
|
||||
g_title_state[k].register_attempts == 0) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (evict_k < 0) {
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid)
|
||||
continue;
|
||||
if (g_title_state[k].mount_reg_attempts == 0) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (evict_k < 0)
|
||||
evict_k = 0;
|
||||
memset(&g_title_state[evict_k], 0, sizeof(g_title_state[evict_k]));
|
||||
rebuild_title_state_hash();
|
||||
slot_k = evict_k;
|
||||
}
|
||||
|
||||
memset(&g_title_state[slot_k], 0, sizeof(g_title_state[slot_k]));
|
||||
g_title_state[slot_k].valid = true;
|
||||
(void)strlcpy(g_title_state[slot_k].title_id, title_id,
|
||||
sizeof(g_title_state[slot_k].title_id));
|
||||
|
||||
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
if (g_title_state_hash[slot] == 0) {
|
||||
g_title_state_hash[slot] = (uint16_t)(slot_k + 1);
|
||||
return &g_title_state[slot_k];
|
||||
}
|
||||
slot = (slot + 1u) & (STATE_HASH_SIZE - 1u);
|
||||
}
|
||||
|
||||
g_title_state[slot_k].valid = false;
|
||||
g_title_state[slot_k].title_id[0] = '\0';
|
||||
rebuild_title_state_hash();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *get_or_create_title_state(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry : create_title_state(title_id);
|
||||
}
|
||||
|
||||
bool was_register_attempted(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? (entry->register_attempts >= MAX_REGISTER_ATTEMPTS) : false;
|
||||
}
|
||||
|
||||
uint8_t get_register_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry->register_attempts : 0;
|
||||
}
|
||||
|
||||
void mark_register_attempted(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (entry && entry->register_attempts < UINT8_MAX)
|
||||
entry->register_attempts++;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!entry || entry->duplicate_notified_once)
|
||||
return;
|
||||
entry->duplicate_notified_once = true;
|
||||
notify_system("Duplicate %s detected:\n%s\n%s", title_id, path_a, path_b);
|
||||
}
|
||||
|
||||
void clear_duplicate_title_notification(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
if (entry)
|
||||
entry->duplicate_notified_once = false;
|
||||
}
|
||||
|
||||
uint8_t get_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
return entry ? entry->mount_reg_attempts : 0;
|
||||
}
|
||||
|
||||
void clear_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = find_title_state(title_id);
|
||||
if (entry)
|
||||
entry->mount_reg_attempts = 0;
|
||||
}
|
||||
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (!entry)
|
||||
return MAX_FAILED_MOUNT_ATTEMPTS;
|
||||
if (entry->mount_reg_attempts < UINT8_MAX)
|
||||
entry->mount_reg_attempts++;
|
||||
return entry->mount_reg_attempts;
|
||||
}
|
||||
Reference in new issue
Block a user