Compare commits

...
99 Commits
Author SHA1 Message Date
Roman Tarasov e0b41b3ca6 Fix cleanup for non-uninstallable app.db entries 2026-05-30 17:00:11 +03:00
Roman Tarasov 5c4470cdcd Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-05-30 11:13:24 +03:00
Roman Tarasov 5b6be76272 Fix same-session manual reinstalls by clearing stale install/register state and refreshing app.db before manual scans 2026-05-30 11:12:44 +03:00
Roman Tarasov c66bff6d2a Prevent kstuff auto-resume during shutdown 2026-05-30 10:59:17 +03:00
Drakmor 2df61d7e2d Update README.md 2026-05-25 22:51:30 +03:00
Drakmor e960fc8079 Update README.md 2026-05-25 22:51:03 +03:00
Drakmor d1c6978e78 Update README.md 2026-05-25 22:44:36 +03:00
Roman Tarasov 4eadb06f57 Add manual install list support 2026-05-25 18:00:14 +03:00
Drakmor 78f536b41a maximum mounted images 64->256 2026-05-23 18:47:58 +03:00
Drakmor 99e000eed9 Update README.md
kstuff link
2026-05-23 16:17:25 +03:00
Drakmor 8622d8757f Update README.md 2026-05-14 11:55:28 +03:00
Roman Tarasov bcffa18997 Redice poll STOP file 2026-05-13 05:24:52 +03:00
Roman Tarasov e00dda0075 Lower default scan interval to 15 seconds 2026-05-13 05:21:47 +03:00
Roman Tarasov 315265f5db Code review 2026-05-13 05:19:22 +03:00
Drakmor 30b04358f4 Update README.md 2026-05-10 19:51:33 +03:00
Roman Tarasov a6ad4922a9 Support link 2026-05-10 19:50:17 +03:00
Roman Tarasov ebd649e0ea Add more detailed diagnostics when entering sleep 2026-05-10 19:45:07 +03:00
Roman Tarasov 4072ced15d Pause kstuff during sleep 2026-05-10 19:44:53 +03:00
Roman Tarasov fee1029b0a Enter sleep mode earlier 2026-05-10 19:18:32 +03:00
Roman Tarasov cbd0fce4f9 Fix snd0Info paths 2026-05-10 18:24:29 +03:00
Roman Tarasov 9fa6ad94de Handle suspend sleep mode and USB cleanup. Thx to @hadobedo 2026-05-10 16:01:59 +03:00
Roman TarasovandCopilot d4c4cda739 kstuff autopause 12.70-13.20
Co-authored-by: Copilot <copilot@github.com>
2026-05-10 12:31:15 +03:00
Roman Tarasov 954b3a7d17 Fix discovery after removable drive changes 2026-05-10 12:21:09 +03:00
Roman Tarasov 3652f506c0 Add global fakelib overlay support 2026-05-10 11:50:18 +03:00
Roman Tarasov 6c188a03bb typo 2026-05-10 10:09:47 +03:00
Roman Tarasov 2680ed28d7 ci/cd sdk 2026-04-17 02:44:55 +03:00
Roman Tarasov f401660113 stub path 2026-04-17 02:39:45 +03:00
Roman Tarasov 11a135cf1c typo 2026-04-17 02:35:15 +03:00
Roman Tarasov 146942c138 mdbg: simplify crash monitor and log snapshot handling 2026-04-17 02:30:07 +03:00
Roman Tarasov a8f1a216bd mdbg: switch klog monitoring to debug log snapshots 2026-04-17 01:51:45 +03:00
Roman Tarasov c37088543d /dev/klog reopen 2026-04-16 02:36:49 +03:00
Roman Tarasov 059f8b40f1 Rework crash detection with sm_mdbg and post-pause autotune 2026-04-16 01:50:51 +03:00
Roman Tarasov 6f61e8d29e Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-15 00:09:45 +03:00
Roman Tarasov a5471da456 Stop background full scans while a game is running 2026-04-15 00:09:39 +03:00
Roman Tarasov 0abfecd3e7 Rework backport overlay resolution and mount handling 2026-04-14 23:52:09 +03:00
Drakmor 9f1786905c Use SDK ps5-payload-dev/pacbrew-repo 2026-04-05 10:50:53 +03:00
Roman Tarasov fa714f868f Dynamically load AppInstallTitleDir on FW below 12 2026-04-05 10:40:50 +03:00
Roman Tarasov 17bb561e8d Only log pre-scan cleanup during the initial startup scan 2026-04-04 23:23:47 +03:00
Roman Tarasov 0efeec16fa Merge branch 'main' of https://github.com/drakmor/ShadowMountPlus 2026-04-04 22:55:09 +03:00
Roman Tarasov a584121d76 Create config.ini from bundled template when missing 2026-04-04 22:55:03 +03:00
Roman Tarasov 45da514549 Increase default scan interval to 60 seconds and clarify config example 2026-04-04 22:42:32 +03:00
Roman Tarasov e272c9736c Remove kstuff noautomount sentinel files on shutdown 2026-04-04 21:57:48 +03:00
Roman Tarasov 902a6917df Removed the extra log messages 2026-04-04 21:55:07 +03:00
Roman Tarasov 6df8a472e3 Add optional AppInstallAll mode with FW 12+ override 2026-04-04 21:53:27 +03:00
Roman Tarasov 4e9afa7885 Fast shutdown 2026-04-04 21:27:10 +03:00
Drakmor 40967215b4 Update README.md 2026-03-30 01:39:04 +03:00
Roman Tarasov a23822a26f Disabled crash detection by default, increased kstuff pause timeouts 2026-03-30 01:25:26 +03:00
Roman Tarasov 97ffedf0df Create target /system_ex/app mount directory before nullfs mount 2026-03-29 21:38:09 +03:00
Roman Tarasov bdd3083d65 Temporary use of the EchoStretch repository 2026-03-29 20:15:50 +03:00
Roman Tarasov bcb906e5b6 Add per-image sector-size overrides with autotune on cluster-size mount errors 2026-03-29 16:32:28 +03:00
Roman Tarasov 47a033b50b Make kstuff crash detection optional and reset heuristic state on config reload 2026-03-29 16:05:33 +03:00
Roman Tarasov 47bded61da Restore missing appmeta on remount and replace access checks with stat 2026-03-29 15:49:03 +03:00
Roman Tarasov 5d0ca6d5d1 Add kstuff crash autotune with autotune.ini pause-delay overrides 2026-03-29 03:27:43 +03:00
Roman Tarasov ebfe0376c6 Detect early post-kstuff game crashes 2026-03-29 03:01:38 +03:00
Roman Tarasov a6a5437a42 Reduce install retry log spam 2026-03-29 00:08:31 +03:00
Roman Tarasov 8c1ebbdf28 Wait for sce_sys stabilization before folder install 2026-03-28 23:19:54 +03:00
Roman Tarasov 853ff28b85 Typo and move release archive contents to a manifest file 2026-03-28 01:42:38 +03:00
Roman Tarasov e9098631c1 Add GitHub Actions workflow for PS5 build 2026-03-28 01:34:06 +03:00
Roman Tarasov bf24df419b Added 10.20 to 12.00 kstuff pause support 2026-03-28 01:04:03 +03:00
Roman Tarasov ff70cb1bc8 Deduplicate device node wait helper across image and mount code 2026-03-24 00:23:52 +03:00
Roman Tarasov 9254525a6c Tune release build flags 2026-03-23 23:54:29 +03:00
Roman Tarasov 26ba93c9aa Streamline sm_log synchronization and reuse the open log file 2026-03-23 23:50:40 +03:00
Roman Tarasov 43d61dce37 Move scan work buffers off the stack into reusable scanner workspace 2026-03-23 23:35:49 +03:00
Roman Tarasov 7759b83293 Fix scan-now request synchronization and simplify pending-state handling 2026-03-23 23:14:33 +03:00
Roman Tarasov 0eca3fa138 Fix kstuff auto-pause focus handling for initially-disabled state 2026-03-23 22:47:45 +03:00
Roman Tarasov e30a296a53 Add autopause.txt per-game delay overrides for kstuff auto-pause 2026-03-22 20:49:28 +03:00
Roman Tarasov b374c3163e Add runtime config hot-reload 2026-03-22 20:25:08 +03:00
Roman Tarasov a71a03d0db Probe kstuff availability at auto-pause time instead of startup 2026-03-22 19:20:21 +03:00
Roman Tarasov 56e8ba2185 Prefer valid installed sources when resolving duplicate title IDs 2026-03-22 19:04:55 +03:00
Roman Tarasov 18a97ecca9 Refactor directory candidate collection into focused scan helpers 2026-03-22 18:43:53 +03:00
Roman Tarasov 270264cc75 Detect unreadable image mounts and report them as damaged images 2026-03-22 18:28:36 +03:00
Roman Tarasov ef814ad56b Unify scan and watcher tree traversal with a shared scan walker 2026-03-22 17:54:58 +03:00
Roman Tarasov cec94656c6 Cache owning scan roots for game entries and simplify targeted backport reconciliation 2026-03-22 17:29:47 +03:00
Roman Tarasov 4ff9a3dffe Optimize scanner watcher registry with fd indexing and per-root watch lists 2026-03-22 17:21:15 +03:00
Roman Tarasov fe7dad099c Optimize scanner watcher rebuilds to refresh only affected subtrees 2026-03-22 16:20:26 +03:00
Roman Tarasov c02f237ddf Cache mounted games when app.db title list is temporarily unavailable 2026-03-22 15:53:02 +03:00
Roman Tarasov d318b5e607 Refactor scanning to use kevent-driven dirty roots, debounce filesystem events, run targeted per-root cleanup and discovery, and support bounded scan_depth traversal for nested directories and image files 2026-03-22 15:39:31 +03:00
Roman Tarasov 6f97cde602 Remove redundant internal guard checks 2026-03-22 13:56:09 +03:00
Roman Tarasov edcac6bdd7 Refactor scanning into a scanner service, replace main loop sleep polling with kevent/wake-pipe waiting, and keep polling as a runtime fallback 2026-03-22 13:13:52 +03:00
Roman Tarasov 27feba2085 Add bounded scan_depth with recursive_scan compatibility remap
Add scan_depth config, remap legacy recursive_scan=1 to scan_depth=2, and avoid duplicate traversal of nested scan roots
2026-03-22 12:43:55 +03:00
Roman Tarasov 82603fce7d Fixed waiting for images to stabilize 2026-03-22 12:09:05 +03:00
Roman Tarasov 01b64346c8 Strip all from elf 2026-03-22 12:07:43 +03:00
Roman Tarasov 7e48103149 Updated readme 2026-03-16 22:04:49 +03:00
Roman Tarasov 124e757199 Disable kstuff auto-pause when kstuff is not installed 2026-03-16 20:44:34 +03:00
Roman Tarasov 0fcc970a20 Create /data/.kstuff_noautomount 2026-03-16 01:18:56 +03:00
Roman Tarasov 7684e8431d Increased block size to 64kb for UFS 2026-03-15 14:28:24 +03:00
Roman Tarasov 3166434db1 The start notification has been fixed and its sound has been disabled. 2026-03-15 12:27:11 +03:00
Roman Tarasov c785daaddf Changed image mount directory to /mnt/shadowmnt 2026-03-14 23:41:28 +03:00
Roman Tarasov 2db6b49439 Fix /dev/lvd attach 2026-03-14 23:18:19 +03:00
Roman Tarasov aff2fc0e6c Remove stale duplicate-tracking entries when sources disappear 2026-03-14 20:08:29 +03:00
Roman Tarasov ae36dd5871 unionfs options 2026-03-13 02:57:57 +03:00
Roman Tarasov cc3e07d201 Update compatibility note in README.md 2026-03-10 10:24:30 +03:00
Roman Tarasov 2c03ede25c Update README.md 2026-03-10 02:43:18 +03:00
Roman Tarasov a212a883b3 1.6b4 2026-03-10 02:24:03 +03:00
Roman Tarasov 99a2956c28 upd .gitingore 2026-03-10 02:23:13 +03:00
Roman Tarasov 93071924b5 wait /dev/lvd1 -> /dev/lvd2 2026-03-04 23:47:40 +03:00
Roman Tarasov 47e47477d1 Add macOS exFAT image builder and improve LVD startup wait handling 2026-02-28 20:21:46 +03:00
Roman Tarasov 31c28e1896 Block mounting of images with too small a cluster size. 2026-02-27 22:20:16 +03:00
Roman Tarasov c1f34ffa22 Version bump 2026-02-27 01:09:34 +03:00
69 changed files with 19093 additions and 3811 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Files included in the release archive.
# One path per line, relative to the repository root.
shadowmountplus.elf
config.ini.example
LICENSE
make_image.bat
mkexfat.sh
mkexfat_macos.sh
mkufs2.sh
New-OsfExfatImage.ps1
README.md
+127
View File
@@ -0,0 +1,127 @@
name: PS5 Build and Release
on:
push:
paths-ignore:
- "**/*.md"
pull_request:
paths-ignore:
- "**/*.md"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
jobs:
build:
outputs:
archive_name: ${{ steps.package.outputs.archive_name }}
runs-on: ubuntu-latest
steps:
- name: Checkout Payload
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout pacbrew-repo
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/pacbrew-repo
path: pacbrew-repo
- name: Checkout SDK stubs
uses: actions/checkout@v4
with:
repository: ps5-payload-dev/sdk
path: ps5-sdk
- name: Setup build environment
run: |
sudo apt update
sudo apt install -y \
autoconf \
automake \
build-essential \
clang-18 \
curl \
git \
libarchive-tools \
libtool \
lld-18 \
makepkg \
meson \
pacman-package-manager \
pkg-config \
xxd \
zip
- name: Build PS5 payload SDK
working-directory: pacbrew-repo/sdk
run: |
makepkg -c -f
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
- name: Build PS5 sqlite package
working-directory: pacbrew-repo/sqlite
run: |
makepkg -c -f
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
- name: Build payload
run: |
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
- name: Create release archive
id: package
shell: bash
run: |
set -euo pipefail
version_tag="$(git describe --abbrev=6 --dirty --always --tags 2>/dev/null || echo "${GITHUB_SHA::7}")"
archive_name="ShadowMountPlus_${version_tag}.zip"
manifest_file=".github/release-files.txt"
mapfile -t release_files < <(grep -Ev '^[[:space:]]*(#|$)' "${manifest_file}")
if [ "${#release_files[@]}" -eq 0 ]; then
echo "Release manifest is empty: ${manifest_file}" >&2
exit 1
fi
for release_file in "${release_files[@]}"; do
if [ ! -f "${release_file}" ]; then
echo "Missing release file: ${release_file}" >&2
exit 1
fi
done
zip -9 "${archive_name}" "${release_files[@]}"
echo "archive_name=${archive_name}" >> "$GITHUB_OUTPUT"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.archive_name }}
path: ${{ steps.package.outputs.archive_name }}
if-no-files-found: error
release:
needs: build
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.archive_name }}
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: dist/${{ needs.build.outputs.archive_name }}
+3
View File
@@ -3,3 +3,6 @@
daemon.lock
debug.log
notify.txt
src/notify_icon_asset.c
refs/*
docs/*
+32 -7
View File
@@ -1,22 +1,47 @@
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 := -O3 -flto=thin -DNDEBUG -ffunction-sections -fdata-sections -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 :=
LDFLAGS := -flto=thin -Wl,--gc-sections
# Standard Libraries Only
LIBS := -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_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)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
$(CC) -shared -Wl,-soname=libkernel_sys.sprx -o $@ $^
src/notify_icon_asset.c: smp_icon.png
xxd -i $< > $@
src/config_ini_example_asset.c: config.ini.example
xxd -i $< > $@
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f shadowmountplus.elf kill.elf src/*.o
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
+158 -44
View File
@@ -1,16 +1,26 @@
# ShadowMountPlus (PS5)
**Version:** `1.6beta1`
**Repository:** https://github.com/drakmor/shadowMountPlus
**Discord:** https://discord.gg/hVnfFMHK
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.7**.
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.05+](https://github.com/EchoStretch/kstuff-lite)**.
## 💜 Support Development
If you want to support this project, you can donate
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
## Current image support
@@ -19,36 +29,57 @@
| 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`)
This file is optional. If it does not exist, built-in defaults are used.
This file is optional. If it does not exist, ShadowMountPlus creates it from the bundled `config.ini.example` template on startup and uses built-in defaults until you uncomment overrides.
Supported keys (all optional):
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
- `quiet_mode=1|0` (`1` suppresses plain informational popups but keeps rich toasts; default is `0`)
- `mount_read_only=1|0` (default: `1`)
- `force_mount=1|0` (mounting even damaged file systems; default: `0`)
- `app_install_all=1|0` (`1` stages new titles and submits them through `sceAppInstUtilAppInstallAll`; default: `0` on FW below `12.00`, forced `1` on FW `12.00+`)
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
- `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`)
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
- `scan_depth=<1..2>` (`1` = scan only first-level subfolders, `2` = also scan one additional nested level; default: `1`)
- `recursive_scan=1|0` (deprecated compatibility key; `1` forces `scan_depth=2`)
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `15`)
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
- `exfat_backend=lvd|md` (default: `lvd`)
- `ufs_backend=lvd|md` (default: `lvd`)
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
- `kstuff_game_auto_toggle=1|0` (`1` pauses kstuff after tracked game launches and resumes it on stop; default: `1`)
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
- `/data/shadowmount/autotune.ini` may also provide per-title pause-delay overrides with highest priority:
- `kstuff_delay=<TITLE_ID>:<0..3600>`
- `<TITLE_ID>=<0..3600>`
- `image_sector=<image_filename>:<sector_size>`
- `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`)
@@ -67,19 +98,58 @@ mount_read_only=1
image_rw=PPSA1234-my-image.ffpfs
image_rw=MYGame 123.exfat
image_ro=legacy_dump.ffpkg
image_sector=MYGame 123.exfat:65536
```
Per-image sector override behavior:
- Match is done by image file name (without path).
- `image_sector` in `/data/shadowmount/autotune.ini` has the highest priority for matching image files.
- If no per-image rule matches, the backend-specific global sector size defaults are used.
- When image validation fails because the mounted file-system cluster size is smaller than the selected device sector size, ShadowMountPlus writes `image_sector=<image_filename>:<cluster_size>` into `/data/shadowmount/autotune.ini` and asks you to try mounting again.
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.
- 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`).
- `/mnt/shadowmnt` is always added automatically, even with custom paths.
- With `scan_depth=1` (default), only first-level subfolders are checked.
- With `scan_depth=2`, one additional nested level is checked.
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
- Full scan loop runs every `scan_interval_seconds` (default: `15`).
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
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 any configured scan path.
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
- `global_fakelib_priority=global` reverses that priority.
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib.
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
Kstuff game lifecycle behavior:
- When `kstuff_game_auto_toggle=1`, ShadowMount watches game `exec/exit` events in the background.
- Image-backed launches use `kstuff_pause_delay_image_seconds`; direct/non-image launches use `kstuff_pause_delay_direct_seconds`.
- `kstuff_crash_detection=0` disables crash monitoring and the automatic pause-delay tuning logic, while leaving normal kstuff auto-pause/auto-resume behavior intact.
- `kstuff_no_pause` skips auto-pause entirely for matching title IDs.
- `kstuff_delay` overrides the pause delay for matching title IDs, regardless of image/direct launch type.
- `/data/shadowmount/autotune.ini` overrides both `config.ini` and `autopause.txt` for matching title IDs.
- `/data/shadowmount/autotune.ini` also overrides `image_sector` rules from `config.ini` for matching image file names.
- A game source folder may optionally contain `autopause.txt`; it is read once at launch time.
- Priority order is: `autotune.ini` -> `kstuff_delay` from `config.ini` -> `autopause.txt` -> global direct/image default delay.
- If `autopause.txt` contains only a number, that value is used for direct launches and doubled for image-backed launches.
- `autopause.txt` may also use:
- `direct=<seconds>`
- `image=<seconds>`
- If both kinds of rule target the same title, `kstuff_no_pause` takes priority.
- When crash monitoring detects an app crash before kstuff was paused, ShadowMountPlus only notifies that the app crashed and kstuff is not to blame.
- When crash monitoring detects an app crash within 2 minutes after kstuff auto-pause, ShadowMountPlus doubles the applied pause delay for that title and upserts it into `/data/shadowmount/autotune.ini` (up to `3600` seconds), then prompts you to launch the game again.
- When the last tracked game stops, ShadowMount immediately enables kstuff again if it was the component that disabled it.
Validation:
- See `config.ini.example` for a ready-to-use template.
@@ -88,9 +158,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,22 +180,65 @@ 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`.
## Manual install list
For games that should not live under the normal scan paths, ShadowMountPlus also
checks:
`/data/shadowmount/manual.lst`
Add one source per line:
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
- Path to a supported image file: `.ffpkg`, `.exfat`, or `.ffpfs`.
- Empty lines and lines starting with `#` are ignored.
Example:
```text
/mnt/usb0/MyGames/PPSA12345
/mnt/usb0/images/PPSA54321.ffpkg
# /mnt/usb0/disabled/PPSA00000
```
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
rescans shortly after the write, mounts images when needed, and installs the
game through the same pipeline as normal scan path discoveries.
Manual install state is tracked in:
`/data/shadowmount/manual.status`
This status file is managed by ShadowMountPlus. If a manually installed game is
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
as deleted in `manual.status` and removes the matching source line from
`manual.lst` so it is not installed again automatically. If you later add the
same source path back to `manual.lst`, it will be processed again and the status
will be updated after installation.
You can use **Dump Installer** to install or prepare game dumps for this manual
workflow, then add the resulting game folder path or image file path to
`manual.lst`.
Recommended folder structure:
- Default mode (`recursive_scan=0`):
- Default mode (`scan_depth=1`):
- `/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`):
- Nested mode (`scan_depth=2`):
- `/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 +250,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 +261,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 +283,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 +305,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,15 +327,16 @@ 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.
- `scan_depth=1` scans only first-level subfolders;
- `scan_depth=2` scans one additional nested level;
- `recursive_scan=1` is treated as deprecated compatibility mode and forces `scan_depth=2`.
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
- Verify game structure:
- folder game: `<GAME_DIR>/sce_sys/param.json`;
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
- 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 +353,11 @@ If a game is mounted but does not start:
* **Drakmor** - Evolution of ShadowMount to ShadowMountPlus
* **Special Thanks:**
* VoidWhisper for ShadowMount [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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
+3583
View File
File diff suppressed because it is too large. Load diff
+112 -27
View File
@@ -1,53 +1,117 @@
# ShadowMount runtime config example
# Place as: /data/shadowmount/config.ini
#
# All keys are optional.
# If a key is missing, built-in defaults are used.
# This file is a template.
# Nothing below is active until you remove the leading '#'.
#
# How to use this file:
# 1. Uncomment only the parameters you really want to override.
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
# Debug logging:
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
# 0/false/no/off -> disable log_debug output
debug=1
# Default: 1
# debug=1
# Quiet mode:
# 1/true/yes/on -> only show the startup version popup and runtime errors
# 0/false/no/off -> also show normal informational popups
# Default: 0
# quiet_mode=0
# Global mount mode:
# 1/true/yes/on/ro -> read-only mounts
# 0/false/no/off/rw -> read-write mounts
mount_read_only=1
# Default: 1
# mount_read_only=1
# Mounting even damaged file systems
# Mounting even damaged file systems:
# 1/true/yes/on -> include force option
# 0/false/no/off -> default mount options (default)
force_mount=0
# 0/false/no/off -> default mount options
# Default: 0
# force_mount=0
# Per-image mount mode override by file name (can be repeated many times):
# image_ro=<image_filename>
# image_rw=<image_filename>
# image_ro=PPSA12345.ffpfs
# image_rw=My Game Dump.exfat
# Use batch registration for newly staged titles:
# 1/true/yes/on -> submit queued installs through sceAppInstUtilAppInstallAll
# 0/false/no/off -> register each title directly
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
# app_install_all=0
# Recursive scan mode:
# 1/true/yes/on -> scan subdirectories recursively (no depth limit)
# 0/false/no/off -> scan only first-level subdirectories (default)
recursive_scan=0
# Scan depth relative to each scan root:
# 1 -> scan only first-level subdirectories
# 2 -> also scan one additional nested level
# Default: 1
# scan_depth=1
# Optional backport overlay root:
# If "<backports_path>/<TITLE_ID>" exists, it is mounted over image root via unionfs.
backports_path=/data/backports
# Legacy compatibility:
# recursive_scan=1 forces scan_depth=2
# Default: disabled
# recursive_scan=0
# Full scan loop interval (seconds), range: 1..3600
scan_interval_seconds=10
# Default: 15
# scan_interval_seconds=15
# Source stabilization wait window (seconds), range: 0..3600
# If source mtime is newer than this window, it will be deferred to next scan.
stability_wait_seconds=10
# If source mtime/ctime is newer than this window, it will be deferred to next scan.
# Default: 10
# stability_wait_seconds=10
# Backend selection per filesystem:
# lvd -> /dev/lvdctl -> /dev/lvdN
# md -> /dev/mdctl -> /dev/mdN
exfat_backend=lvd
ufs_backend=lvd
# Defaults: exfat=lvd, ufs=lvd
# exfat_backend=lvd
# ufs_backend=lvd
# Backend-specific overrides (optional):
# 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
# Default: 1
# backport_fakelib=1
# Global fakelib overlay:
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
# 0/false/no/off -> do not use the global fakelib folder
# Default: 1
# global_fakelib=1
#
# Default global fakelib folder:
# global_fakelib_path=/data/shadowmount/fakelib
#
# Priority when both global and per-game fakelib exist:
# game -> mount global first, then game-specific fakelib (game wins)
# global -> mount game-specific fakelib first, then global fakelib (global wins)
# Default: game
# global_fakelib_priority=game
#
## Exclude specific title IDs from the global fakelib overlay (repeatable):
## global_fakelib_exclude=PPSA12345
# Auto-manage kstuff around game launches:
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
# 0/false/no/off -> leave kstuff state untouched
# Default: 1
# kstuff_game_auto_toggle=1
# Detect crash-candidate state and autotune pause delay:
# 1/true/yes/on -> enable crash monitoring and autotune updates
# 0/false/no/off -> disable crash detection/autotune logic
# Default: 1
# kstuff_crash_detection=1
# Delay before pausing kstuff for image-backed titles.
# Default: 25
# kstuff_pause_delay_image_seconds=25
# Delay before pausing kstuff for direct/non-image launches.
# Default: 15
# kstuff_pause_delay_direct_seconds=15
# Backend-specific sector size overrides:
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
# lvd_exfat_sector_size=512
# lvd_ufs_sector_size=4096
# lvd_pfs_sector_size=32768
@@ -56,6 +120,27 @@ ufs_backend=lvd
# Optional custom scan roots (can be repeated):
# If at least one scanpath=... is present, only these paths are used.
# "/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}" are always added automatically.
# "/mnt/shadowmnt" is always added automatically.
# Default: use built-in scan path list
# scanpath=/data/homebrew
# scanpath=/data/etaHEN/games
## Per-image mount mode override by file name (repeatable):
## image_ro=<image_filename>
## image_rw=<image_filename>
## image_sector=<image_filename>:<sector_size>
##
## Examples:
## image_ro=PPSA12345.ffpfs
## image_rw=My Game Dump.exfat
## image_sector=My Game Dump.exfat:65536
## Keep kstuff enabled for specific title IDs (repeatable):
## kstuff_no_pause=PPSA12345
## Override pause delay for specific title IDs (repeatable):
## Format: TITLEID:SECONDS
## kstuff_delay=PPSA12345:30
## More custom scan root examples:
## scanpath=/data/homebrew
## scanpath=/data/etaHEN/games
+28
View File
@@ -0,0 +1,28 @@
#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);
// Normalize existing snd0info rows during process startup.
bool app_db_run_startup_maintenance(void);
// Update snd0 metadata for a registered title.
int update_snd0info(const char *title_id);
// Normalize an existing snd0Info path for one title.
int normalize_snd0info_for_title(const char *title_id);
// Check whether a title ID exists in a cached app.db title list.
bool app_db_title_list_contains(const struct AppDbTitleList *list,
const char *title_id);
// Release a title-list snapshot returned by get_app_db_title_list_cached().
void free_app_db_title_list(struct AppDbTitleList *list);
// Drop the cached app.db title list so it is reloaded on next access.
void invalidate_app_db_title_cache(void);
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
// Copy the cached PPSA titles that app.db marks as not uninstallable.
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
#endif
+45
View File
@@ -0,0 +1,45 @@
#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);
// Reload runtime configuration from disk when config.ini changed.
bool reload_runtime_config_if_changed(bool *reloaded_out);
// 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);
// Resolve a per-image sector-size override from autotune.ini or config.ini.
bool get_image_sector_size_override(const char *filename,
uint32_t *sector_size_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);
// Resolve a per-title autotuned kstuff pause-delay override in seconds.
bool get_kstuff_autotune_pause_delay_for_title(const char *title_id,
uint32_t *delay_seconds_out);
// Upsert an autotuned pause-delay override for the title.
bool upsert_kstuff_autotune_pause_delay(const char *title_id,
uint32_t current_delay_seconds,
uint32_t *delay_seconds_out);
// Upsert an autotuned per-image sector-size override.
bool upsert_image_sector_size_autotune(const char *filename,
uint32_t sector_size,
uint32_t *sector_size_out);
// Return true when the global fakelib overlay is disabled for this title.
bool is_global_fakelib_excluded_for_title(const char *title_id);
#endif
+17
View File
@@ -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
+52
View File
@@ -0,0 +1,52 @@
#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 /user/appmeta/<TITLE_ID>/param.json exists.
bool has_appmeta_data(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 a title tracked under /user/app.
bool read_mount_link(const char *title_id, char *out, size_t out_size);
// Validate a /user/app/<TITLE_ID> tracker 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);
// Roll back a just-mounted title nullfs layer when publication is aborted.
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
// Reconcile the title mount stack against the expected source/backport state.
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
const char *expected_backport_path,
bool *overlay_active_out);
// Mount a prepared backport overlay on top of an already mounted title.
// Returns false only when nmount() itself fails.
bool mount_backport_overlay(const char *mount_point,
const char *backport_path,
const char *title_id);
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
void shutdown_title_mounts(void);
// Remove stale mount links and optionally restore image-backed mounts.
void cleanup_mount_links(const char *removed_source_root,
bool unmount_system_ex_bind);
// Unmount and remove title links that point under a source root.
void cleanup_mount_links_for_source_unmount(const char *source_root);
// Unmount and remove title links backed by USB sources or USB-backed images.
void cleanup_usb_mount_links_for_suspend(void);
// Recursively copy a directory tree.
int copy_dir(const char *src, const char *dst);
// Copy a single file.
int copy_file(const char *src, const char *dst);
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_GAME_CACHE_H
#define SM_GAME_CACHE_H
#include <stdbool.h>
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
const char *title_name,
const char *owning_scan_root, void *ctx);
// 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);
// Drop invalid or stale entries that belong to a specific scan root.
void prune_game_cache_for_root(const char *root);
// 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);
// Visit cached games, optionally limited to one source root.
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
void *ctx);
// Remove a game cache entry by path.
void clear_cached_game(const char *path);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_GAME_LIFECYCLE_H
#define SM_GAME_LIFECYCLE_H
#include <stdbool.h>
// Start the shared game lifecycle watcher used by runtime game features.
bool start_game_lifecycle_watcher(void);
// Wake the shared watcher so pending lifecycle work is processed immediately.
void wake_game_lifecycle_watcher(void);
// Stop the shared watcher and let lifecycle modules clean up tracked state.
void stop_game_lifecycle_watcher(void);
// Return true when a supported game is currently being tracked as running.
bool sm_game_lifecycle_has_active_game(void);
// Ensure the shared watcher is running for runtime game-state tracking.
bool refresh_game_lifecycle_watcher(void);
#endif
+13
View File
@@ -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
+16
View File
@@ -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
+33
View File
@@ -0,0 +1,33 @@
#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);
// Reconcile cached image mounts that belong to a specific scan root.
void cleanup_stale_image_mounts_for_root(const char *root);
// Unmount cached image mounts backed by USB storage during suspend.
bool unmount_usb_image_mounts_for_suspend(void);
// Unmount every cached image mount during shutdown.
bool shutdown_image_mounts(void);
// Remove empty directories left under the image mount root.
void cleanup_mount_dirs(void);
// Mount an image file if it is stable and not currently rate-limited.
bool maybe_mount_image_file(const char *full_path, const char *name,
bool *unstable_out);
// Build the deterministic runtime mount point for an image source path.
void get_image_mount_point_for_source(const char *file_path,
char mount_point[MAX_PATH]);
// Return true when the filename has a supported image extension.
bool is_supported_image_file_name(const char *name);
#endif
+34
View File
@@ -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
+10
View File
@@ -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
+28
View File
@@ -0,0 +1,28 @@
#ifndef SM_INSTALL_QUEUE_H
#define SM_INSTALL_QUEUE_H
#include <stdbool.h>
#include <stdint.h>
typedef struct scan_candidate scan_candidate_t;
// Return whether a title is already waiting for async batch installation.
bool is_title_install_pending(const char *title_id);
// Drop queued/submitted async install state for a title after confirmed removal.
void sm_install_forget_pending_title(const char *title_id);
// Return the next pending-install wake deadline in monotonic microseconds.
uint64_t sm_install_next_wake_us(uint64_t now_us);
// Poll async batch installs for success/timeout completion only.
void sm_install_poll_pending(void);
// Poll async batch installs for success/timeout completion and auto-submit
// queued installs when possible.
void sm_install_service_pending(void);
// Queue a staged title for the next batch install request.
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0);
// Submit all queued titles when no batch install is currently pending.
bool sm_install_submit_queued(void);
// Record retry state for every queued title after a batch submit failure.
void sm_install_note_submit_failure(void);
#endif
+41
View File
@@ -0,0 +1,41 @@
#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);
// Pause kstuff for runtime sleep without restoring game-driven pauses.
void sm_kstuff_sleep_enter(void);
// Restore kstuff after runtime sleep when sleep entry disabled it.
void sm_kstuff_sleep_leave(void);
// Re-apply runtime config changes to the currently tracked game, if any.
void sm_kstuff_on_config_reload(void);
#endif
+50
View File
@@ -0,0 +1,50 @@
#ifndef SM_LIMITS_H
#define SM_LIMITS_H
#define DEFAULT_SCAN_INTERVAL_US 15000000u
#define DEFAULT_SCAN_DEPTH 1u
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
#define MAX_PENDING 512
#define MAX_IMAGE_MOUNTS 256
#define MAX_IMAGE_MODE_RULES 128
#define MAX_KSTUFF_TITLE_RULES 128
#define MAX_FAKELIB_EXCLUDE_RULES 128
#define PATH_STATE_CAPACITY MAX_PENDING
#define TITLE_STATE_CAPACITY MAX_PENDING
#define STATE_HASH_SIZE 1024u
#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_DEPTH 1u
#define MAX_SCAN_DEPTH 2u
#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
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
#endif
+36
View File
@@ -0,0 +1,36 @@
#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);
// Flush and close persistent log resources.
void sm_log_shutdown(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
+20
View File
@@ -0,0 +1,20 @@
#ifndef SM_MANUAL_H
#define SM_MANUAL_H
#include <stdbool.h>
struct AppDbTitleList;
// Visit each manual source path from manual.lst.
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx);
// Mark previously installed manual titles as deleted when they disappear from
// app.db, and remove their source lines from manual.lst.
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
bool app_db_titles_ready);
// Record that a manual source is installed and associated with a title.
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name);
#endif
+26
View File
@@ -0,0 +1,26 @@
#ifndef SM_MDBG_H
#define SM_MDBG_H
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
// Initialize mdbg-backed crash monitoring state.
void sm_mdbg_init(void);
// Shut down mdbg-backed crash monitoring state.
void sm_mdbg_shutdown(void);
// Start monitoring the tracked game process for crash-candidate state.
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id);
// Record that kstuff auto-pause was actually applied to the tracked game.
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
uint32_t pause_delay_seconds);
// Stop monitoring the tracked game process on exit.
void sm_mdbg_game_on_exit(pid_t pid);
// Clear tracked game monitoring state unconditionally.
void sm_mdbg_game_shutdown(void);
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
// Poll the tracked process for mdbg crash-candidate state.
void sm_mdbg_poll(void);
#endif
+80
View File
@@ -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
+25
View File
@@ -0,0 +1,25 @@
#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);
// Wait until a device node appears or disappears.
bool wait_for_dev_node_state(const char *devname, bool should_exist);
// 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
+42
View File
@@ -0,0 +1,42 @@
#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);
// Drop expired entries that belong to a specific scan root.
void prune_path_state_for_root(const char *root);
// 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);
// Return whether backport overlay remounts are suppressed for a path.
bool is_backport_mount_blocked(const char *path);
// Record the first failed backport overlay mount for a path.
void note_backport_mount_failure(const char *path);
// Clear failed backport overlay mount state for a path.
void clear_backport_mount_failure(const char *path);
// Return true once for a missing manual source until the path appears again.
bool note_manual_missing_source_once(const char *path);
// Allow a manual source to be reported missing again after it becomes visible.
void clear_manual_missing_source(const char *path);
#endif
+22
View File
@@ -0,0 +1,22 @@
#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 stat() can see the path.
bool path_exists(const char *path);
// Return true when a path lives under the image mount root.
bool is_under_image_mount_base(const char *path);
// Return true when path is equal to root or is under it.
bool path_matches_root_or_child(const char *path, const char *root);
// Return true when a path lives on USB-backed storage.
bool is_usb_storage_path(const char *path);
// Build "<scan_path>/backports" for a managed scan root.
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
#endif
+46
View File
@@ -0,0 +1,46 @@
#ifndef SM_PATHS_H
#define SM_PATHS_H
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
#define LOG_DIR "/data/shadowmount"
#define LOG_FILE "/data/shadowmount/debug.log"
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
#define CONFIG_FILE "/data/shadowmount/config.ini"
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
#define APPMETA_BASE "/user/appmeta"
#define APP_BASE "/user/app"
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
#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
+65
View File
@@ -0,0 +1,65 @@
#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;
static inline uint32_t sm_firmware_major_version(void) {
uint32_t fw = kernel_get_fw_version();
uint32_t major_bcd = (fw >> 24) & 0xFFu;
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
}
// --- SDK Imports ---
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void);
int sceAppInstUtilAppUnInstall(const char *title_id);
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
int sceKernelUsleep(unsigned int microseconds);
int sceUserServiceInitialize(void *);
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
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_RUNTIME_H
#define SM_RUNTIME_H
#include <stdbool.h>
#include <stddef.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);
// Return true when the payload is paused for system suspend/resume.
bool runtime_sleep_mode_active(void);
// Enter or leave sleep mode with a descriptive source string.
// Returns true when the runtime sleep state changed.
bool request_runtime_sleep_mode(bool active, const char *reason);
// Serialize mount/link/cache publication against suspend cleanup.
void runtime_mount_state_lock(void);
void runtime_mount_state_unlock(void);
// Request an immediate scan cycle with a descriptive source string.
void request_scan_now(const char *reason);
// Consume a pending immediate scan request and copy its reason into caller storage.
bool consume_scan_now_request(char *reason_out, size_t reason_out_size);
// Sleep in chunks and stop early if shutdown was requested.
bool sleep_with_stop_check(unsigned int total_us);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SM_SCAN_H
#define SM_SCAN_H
#include <stdbool.h>
typedef struct scan_candidate scan_candidate_t;
// Unmount and clean up mounts whose backing sources disappeared.
void cleanup_lost_sources_before_scan(void);
// Unmount and clean up mounts whose backing sources disappeared under one root.
void cleanup_lost_sources_for_scan_root(const char *scan_root);
// Immediately unmount runtime mounts backed by USB storage for suspend.
void unmount_usb_sources_for_suspend(void);
// Scan configured roots and collect install candidates.
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out);
// Scan a single configured root and collect install candidates.
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
int *total_found_out,
bool *unstable_found_out);
// Mount stable backport overlays for already mounted titles.
void mount_backport_overlays(bool *unstable_found_out);
#endif
+31
View File
@@ -0,0 +1,31 @@
#ifndef SM_SCAN_TREE_H
#define SM_SCAN_TREE_H
#include <stdbool.h>
typedef enum {
SM_SCAN_TREE_DIR_DESCEND = 0,
SM_SCAN_TREE_DIR_SKIP_DESCEND,
SM_SCAN_TREE_DIR_ABORT,
} sm_scan_tree_dir_visit_t;
typedef sm_scan_tree_dir_visit_t (*sm_scan_tree_dir_fn)(const char *dir_path,
unsigned int depth_from_root,
void *ctx);
typedef bool (*sm_scan_tree_image_fn)(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx);
typedef struct {
sm_scan_tree_dir_fn on_directory;
sm_scan_tree_image_fn on_image_file;
} sm_scan_tree_callbacks_t;
// Walk a managed scan subtree using the shared scan-depth and path filtering rules.
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef SM_SCANNER_H
#define SM_SCANNER_H
#include <stdbool.h>
// Initialize scanner service resources such as wake pipe and file watches.
bool sm_scanner_init(void);
// Wake a blocked scanner wait. Safe to call from signal context.
void sm_scanner_wake(void);
// Run the initial full synchronization cycle.
bool sm_scanner_run_startup_sync(void);
// Run the steady-state scanner wait/scan loop until shutdown is requested.
void sm_scanner_run_loop(void);
// Release scanner service resources.
void sm_scanner_shutdown(void);
#endif
+9
View File
@@ -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
+12
View File
@@ -0,0 +1,12 @@
#ifndef SM_STABILITY_H
#define SM_STABILITY_H
#include <stdbool.h>
// Check whether a path is old enough since its latest mtime/ctime change.
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
+9
View File
@@ -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
+27
View File
@@ -0,0 +1,27 @@
#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);
// Clear registration attempt state after success or confirmed user removal.
void clear_register_attempts(const char *title_id);
// Notify once about duplicate title IDs from different paths.
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b);
// 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
+152
View File
@@ -0,0 +1,152 @@
#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];
char manual_source_path[MAX_PATH];
bool installed;
bool in_app_db;
bool manual;
} 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 app_install_all_enabled;
bool app_install_all_forced;
bool backport_fakelib_enabled;
bool global_fakelib_enabled;
bool global_fakelib_mount_first;
bool kstuff_game_auto_toggle;
bool kstuff_crash_detection_enabled;
bool legacy_recursive_scan_forced;
char global_fakelib_path[MAX_PATH];
uint32_t global_fakelib_exclude_title_count;
char global_fakelib_exclude_title_ids[MAX_FAKELIB_EXCLUDE_RULES][MAX_TITLE_ID];
uint32_t scan_depth;
uint32_t scan_interval_us;
uint32_t stability_wait_seconds;
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
+137
View File
@@ -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"
+25 -18
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
asm(".global \"C49jelxiaVE\"\n"
".type \"C49jelxiaVE\" @function\n"
"\"C49jelxiaVE\":\n");
+444 -3715
View File
File diff suppressed because it is too large. Load diff
+538
View File
@@ -0,0 +1,538 @@
#include "sm_platform.h"
#include <pthread.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 sqlite3_stmt *g_app_db_stmt_normalize_snd0;
static struct AppDbTitleList g_app_db_title_cache;
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
static bool g_app_db_title_cache_ready = false;
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
static time_t g_app_db_title_cache_mtime = 0;
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
static void close_app_db(void) {
if (g_app_db_stmt_update_snd0) {
sqlite3_finalize(g_app_db_stmt_update_snd0);
g_app_db_stmt_update_snd0 = NULL;
}
if (g_app_db_stmt_normalize_snd0) {
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
g_app_db_stmt_normalize_snd0 = NULL;
}
if (g_app_db) {
sqlite3_close(g_app_db);
g_app_db = NULL;
}
}
void free_app_db_title_list(struct AppDbTitleList *list) {
free(list->ids);
list->ids = NULL;
list->count = 0;
list->capacity = 0;
}
void shutdown_app_db(void) {
pthread_mutex_lock(&g_app_db_mutex);
free_app_db_title_list(&g_app_db_title_cache);
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
close_app_db();
pthread_mutex_unlock(&g_app_db_mutex);
}
static bool ensure_app_db_open(void) {
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;
}
static bool app_db_exec_with_retry(const char *sql, int max_attempts,
const char *label, int *changes_out) {
if (changes_out)
*changes_out = 0;
for (int attempt = 0; attempt < max_attempts; attempt++) {
if (!ensure_app_db_open())
return false;
char *err_msg = NULL;
int rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err_msg);
if (rc == SQLITE_OK) {
if (changes_out)
*changes_out = sqlite3_changes(g_app_db);
sqlite3_free(err_msg);
return true;
}
if (app_db_wait_retry(rc, attempt, max_attempts, false)) {
sqlite3_free(err_msg);
continue;
}
if (err_msg) {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc, err_msg);
} else {
log_debug(" [DB] exec failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
}
sqlite3_free(err_msg);
close_app_db();
return false;
}
close_app_db();
return false;
}
static int app_db_query_int(const char *sql, const char *label) {
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
label);
if (prep_rc != SQLITE_OK)
return -1;
int result = -1;
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
result = sqlite3_column_int(stmt, 0);
break;
}
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
continue;
log_debug(" [DB] query failed for %s: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
break;
}
sqlite3_finalize(stmt);
return result;
}
bool app_db_run_startup_maintenance(void) {
bool ok = false;
pthread_mutex_lock(&g_app_db_mutex);
if (!ensure_app_db_open())
goto out;
(void)sqlite3_busy_timeout(g_app_db,
APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS);
const char *backfill_count_sql =
"SELECT COUNT(*) FROM tbl_contentinfo "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
int pending_backfill =
app_db_query_int(backfill_count_sql, "snd0info normalize backfill check");
if (pending_backfill < 0) {
close_app_db();
goto out;
}
int changes = 0;
if (pending_backfill > 0) {
const char *backfill_sql =
"UPDATE tbl_contentinfo "
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
"'/user/appmeta/'), '/sce_sys', '') "
"WHERE instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0;";
if (!app_db_exec_with_retry(backfill_sql,
APP_DB_STARTUP_MAINTENANCE_RETRIES,
"snd0info startup normalize", &changes))
goto out;
}
log_debug(" [DB] snd0info startup maintenance done rows=%d pending=%d",
changes, pending_backfill);
close_app_db();
ok = true;
out:
pthread_mutex_unlock(&g_app_db_mutex);
return ok;
}
int update_snd0info(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
if (!g_app_db_stmt_update_snd0) {
const char *sql =
"UPDATE tbl_contentinfo "
"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)
goto out;
}
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) {
log_debug(" [DB] bind failed for snd0info update: %s",
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
if (rc == SQLITE_DONE) {
int changes = sqlite3_changes(g_app_db);
close_app_db();
result = changes;
goto out;
}
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
continue;
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
close_app_db();
out:
pthread_mutex_unlock(&g_app_db_mutex);
return result;
}
int normalize_snd0info_for_title(const char *title_id) {
int result = -1;
pthread_mutex_lock(&g_app_db_mutex);
if (!g_app_db_stmt_normalize_snd0) {
const char *sql =
"UPDATE tbl_contentinfo "
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
"'/user/appmeta/'), '/sce_sys', '') "
"WHERE titleId = ?1 "
"AND (instr(snd0Info, '/user/app/') > 0 "
"OR instr(snd0Info, '/sce_sys') > 0);";
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
APP_DB_PREPARE_BUSY_RETRIES,
"snd0info normalize");
if (prep_rc != SQLITE_OK)
goto out;
}
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
sqlite3_reset(g_app_db_stmt_normalize_snd0);
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
log_debug(" [DB] bind failed for snd0info normalize: %s",
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
if (rc == SQLITE_DONE) {
int changes = sqlite3_changes(g_app_db);
close_app_db();
result = changes;
goto out;
}
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
continue;
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
sqlite3_errmsg(g_app_db));
close_app_db();
goto out;
}
close_app_db();
out:
pthread_mutex_unlock(&g_app_db_mutex);
return result;
}
static bool append_app_db_title(struct AppDbTitleList *list,
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->count <= 0)
return false;
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
compare_title_id_str) != NULL;
}
static bool step_app_db_title_query(struct AppDbTitleList *list,
sqlite3_stmt *stmt,
const char *label) {
int busy_attempts = 0;
while (!should_stop_requested()) {
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
if (!append_app_db_title(list, title_id)) {
log_debug(" [DB] %s allocation failed", label);
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
continue;
}
if (rc == SQLITE_DONE) {
sqlite3_finalize(stmt);
close_app_db();
return true;
}
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
busy_attempts++;
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
continue;
}
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
sqlite3_errmsg(g_app_db));
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
sqlite3_finalize(stmt);
close_app_db();
free_app_db_title_list(list);
return false;
}
static bool load_app_db_title_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId != '' "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
APP_DB_PREPARE_BUSY_RETRIES,
"title list query");
if (prep_rc != SQLITE_OK)
return false;
bool loaded = step_app_db_title_query(list, stmt, "title list");
if (loaded)
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
return loaded;
}
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
free_app_db_title_list(list);
const char *sql =
"SELECT DISTINCT titleId "
"FROM tbl_contentinfo "
"WHERE titleId LIKE 'PPSA%' "
"AND uninstallable = 0 "
"ORDER BY titleId;";
sqlite3_stmt *stmt = NULL;
int prep_rc =
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
"blocked PPSA uninstallable query");
if (prep_rc != SQLITE_OK)
return false;
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
if (loaded)
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
list->count);
return loaded;
}
void invalidate_app_db_title_cache(void) {
pthread_mutex_lock(&g_app_db_mutex);
g_app_db_title_cache_ready = false;
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
g_app_db_title_cache_mtime = 0;
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
pthread_mutex_unlock(&g_app_db_mutex);
}
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
const struct AppDbTitleList *src) {
if (!dst || !src)
return false;
free_app_db_title_list(dst);
if (src->count <= 0)
return true;
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
if (!ids)
return false;
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
dst->ids = ids;
dst->count = src->count;
dst->capacity = src->count;
return true;
}
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
free_app_db_title_list(list_out);
pthread_mutex_lock(&g_app_db_mutex);
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_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) {
pthread_mutex_unlock(&g_app_db_mutex);
return false;
}
}
}
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return copied;
}
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
if (!list_out)
return false;
free_app_db_title_list(list_out);
pthread_mutex_lock(&g_app_db_mutex);
struct stat st;
int app_db_stat_rc = stat(APP_DB_PATH, &st);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
(app_db_stat_rc == 0 &&
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
struct AppDbTitleList fresh = {0};
if (app_db_stat_rc == 0 &&
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
g_app_db_blocked_uninstall_ppsa_cache = fresh;
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
} else {
free_app_db_title_list(&fresh);
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
pthread_mutex_unlock(&g_app_db_mutex);
return false;
}
}
}
bool loaded =
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
pthread_mutex_unlock(&g_app_db_mutex);
return loaded;
}
File diff suppressed because it is too large. Load diff
+305
View File
@@ -0,0 +1,305 @@
#include "sm_platform.h"
#include "sm_fakelib.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
#include <pthread.h>
typedef struct {
char source_path[MAX_PATH];
char mount_path[MAX_PATH];
const char *label;
} fakelib_layer_t;
typedef struct {
pid_t pid;
char mount_path[MAX_PATH];
fakelib_layer_t layers[2];
size_t layer_count;
} fakelib_session_t;
static fakelib_session_t g_fakelib_mount;
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
bool sm_fakelib_game_feature_enabled(void) {
return runtime_config()->backport_fakelib_enabled;
}
static bool mount_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
struct iovec overlay_iov[] = {
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
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) {
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
title_id, source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
title_id, source_path, mount_path, strerror(errno));
return false;
}
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
const char *mount_path = layer->mount_path;
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
errno == EINVAL) {
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
layer->source_path, mount_path);
return true;
}
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
mount_path, strerror(errno));
return false;
}
static bool track_fakelib_overlay(const char *title_id,
const char *source_path,
const char *mount_path,
const char *label) {
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
return false;
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
layer->label = label;
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
return true;
}
static bool resolve_sandbox_context(const char *title_id,
char game_source_path[MAX_PATH],
char mount_path[MAX_PATH]) {
game_source_path[0] = '\0';
mount_path[0] = '\0';
char sandbox_id[MAX_TITLE_ID];
DIR *d = opendir("/mnt/sandbox");
if (!d)
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;
if (strlen(entry->d_name) >= sizeof(sandbox_id))
continue;
errno = 0;
long idx_long = strtol(suffix, NULL, 10);
if (errno == ERANGE || idx_long > INT_MAX)
continue;
int idx = (int)idx_long;
if (idx < best_index)
continue;
best_index = idx;
(void)strlcpy(sandbox_id, entry->d_name, sizeof(sandbox_id));
found = true;
}
closedir(d);
if (!found)
return false;
char source_path[MAX_PATH];
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
sandbox_id);
struct stat st;
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
(void)strlcpy(game_source_path, source_path, MAX_PATH);
char sandbox_root[MAX_PATH];
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
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 resolve_global_fakelib_source(const char *title_id,
char source_path[MAX_PATH]) {
source_path[0] = '\0';
const runtime_config_t *cfg = runtime_config();
if (!cfg->global_fakelib_enabled)
return false;
if (is_global_fakelib_excluded_for_title(title_id))
return false;
struct stat st;
if (stat(cfg->global_fakelib_path, &st) != 0) {
if (errno != ENOENT)
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
title_id, cfg->global_fakelib_path, strerror(errno));
return false;
}
if (!S_ISDIR(st.st_mode)) {
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
title_id, cfg->global_fakelib_path);
return false;
}
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
return true;
}
static bool cleanup_fakelib_mount(void) {
if (g_fakelib_mount.layer_count == 0) {
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
while (g_fakelib_mount.layer_count > 0) {
fakelib_layer_t *layer =
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
if (!unmount_fakelib_overlay(layer))
return false;
memset(layer, 0, sizeof(*layer));
g_fakelib_mount.layer_count--;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
return true;
}
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
pthread_mutex_lock(&g_fakelib_mutex);
if (!sm_fakelib_game_feature_enabled()) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
(long)g_fakelib_mount.pid, (long)pid, title_id);
if (!cleanup_fakelib_mount()) {
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
(long)g_fakelib_mount.pid, title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
char game_source_path[MAX_PATH];
char global_source_path[MAX_PATH];
char mount_path[MAX_PATH];
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
bool has_global =
resolve_global_fakelib_source(title_id, global_source_path);
bool has_game = (game_source_path[0] != '\0');
if (!has_global && !has_game) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
g_fakelib_mount.pid = pid;
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
sizeof(g_fakelib_mount.mount_path));
bool global_first = runtime_config()->global_fakelib_mount_first;
bool same_source =
has_global && has_game &&
strcmp(global_source_path, game_source_path) == 0;
if (global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game) {
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
"game")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (!global_first && has_global && !same_source) {
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
"global")) {
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
}
if (has_game)
notify_system_info("Game backported: %s", title_id);
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_on_exit(pid_t pid) {
pthread_mutex_lock(&g_fakelib_mutex);
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
pthread_mutex_unlock(&g_fakelib_mutex);
return;
}
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
g_fakelib_mount.mount_path);
cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
void sm_fakelib_game_shutdown(void) {
pthread_mutex_lock(&g_fakelib_mutex);
(void)cleanup_fakelib_mount();
pthread_mutex_unlock(&g_fakelib_mutex);
}
+1109
View File
File diff suppressed because it is too large. Load diff
+198
View File
@@ -0,0 +1,198 @@
#include "sm_platform.h"
#include "sm_game_cache.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_path_utils.h"
#include "sm_title_state.h"
struct GameCache {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char owning_scan_root[MAX_PATH];
bool valid;
};
static struct GameCache g_game_cache[MAX_PENDING];
static bool resolve_game_cache_owning_scan_root(const char *path,
char owning_scan_root[MAX_PATH]) {
char resolved_source_path[MAX_PATH];
const char *match_path = path;
owning_scan_root[0] = '\0';
if (is_under_image_mount_base(path) &&
resolve_image_source_from_mount_cache(path, resolved_source_path,
sizeof(resolved_source_path))) {
match_path = resolved_source_path;
}
size_t best_match_len = 0;
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (!path_matches_root_or_child(match_path, scan_path))
continue;
size_t scan_path_len = strlen(scan_path);
if (scan_path_len <= best_match_len)
continue;
(void)strlcpy(owning_scan_root, scan_path, MAX_PATH);
best_match_len = scan_path_len;
}
return owning_scan_root[0] != '\0';
}
static void write_game_cache_slot(struct GameCache *entry, const char *path,
const char *title_id,
const char *title_name,
const char *owning_scan_root) {
(void)strlcpy(entry->path, path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
(void)strlcpy(entry->owning_scan_root, owning_scan_root,
sizeof(entry->owning_scan_root));
entry->valid = true;
}
static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
if (entry->owning_scan_root[0] != '\0')
return true;
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
}
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) {
char owning_scan_root[MAX_PATH];
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (strcmp(g_game_cache[k].path, path) != 0 &&
strcmp(g_game_cache[k].title_id, title_id) != 0) {
continue;
}
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid) {
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
owning_scan_root);
return;
}
}
}
void prune_game_cache(void) {
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
if (path_exists(g_game_cache[k].path))
continue;
clear_game_cache_slot(k, "source removed");
}
}
void prune_game_cache_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_game_cache();
return;
}
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
const char *entry_root =
ensure_game_cache_owning_scan_root(&g_game_cache[k])
? g_game_cache[k].owning_scan_root
: g_game_cache[k].path;
if (!path_matches_root_or_child(entry_root, root))
continue;
if (path_exists(g_game_cache[k].path))
continue;
clear_game_cache_slot(k, "source removed");
}
}
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
void *ctx) {
if (!fn)
return;
for (int k = 0; k < MAX_PENDING; k++) {
if (!g_game_cache[k].valid)
continue;
bool has_owning_scan_root = ensure_game_cache_owning_scan_root(&g_game_cache[k]);
const char *entry_root =
has_owning_scan_root ? g_game_cache[k].owning_scan_root : g_game_cache[k].path;
if (root && root[0] != '\0' && strcmp(entry_root, root) != 0) {
continue;
}
if (!fn(g_game_cache[k].path, g_game_cache[k].title_id,
g_game_cache[k].title_name,
has_owning_scan_root ? g_game_cache[k].owning_scan_root : NULL, ctx)) {
break;
}
}
}
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) {
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");
}
}
+655
View File
@@ -0,0 +1,655 @@
#include "sm_platform.h"
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <sys/event.h>
#include <sys/select.h>
#include "sm_appdb.h"
#include "sm_fakelib.h"
#include "sm_game_lifecycle.h"
#include "sm_kstuff.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_runtime.h"
#include "sm_scanner.h"
#include "sm_time.h"
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
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 _Atomic pid_t g_active_game_pid = 0;
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
static char g_active_game_title_id[MAX_TITLE_ID];
static bool register_game_exit_watch(int kq, pid_t pid);
static void publish_active_game_pid(pid_t pid) {
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
if (previous_pid == pid)
return;
sm_scanner_wake();
}
static void publish_active_game(pid_t pid, const char *title_id) {
pthread_mutex_lock(&g_active_game_mutex);
if (pid > 0 && title_id && title_id[0] != '\0')
(void)strlcpy(g_active_game_title_id, title_id,
sizeof(g_active_game_title_id));
else
g_active_game_title_id[0] = '\0';
pthread_mutex_unlock(&g_active_game_mutex);
publish_active_game_pid(pid);
}
static bool consume_active_game_title(pid_t pid,
char title_id_out[MAX_TITLE_ID]) {
bool matched = false;
pthread_mutex_lock(&g_active_game_mutex);
if (pid > 0 && atomic_load(&g_active_game_pid) == pid &&
g_active_game_title_id[0] != '\0') {
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
matched = true;
}
if (matched)
g_active_game_title_id[0] = '\0';
pthread_mutex_unlock(&g_active_game_mutex);
return matched;
}
static bool is_supported_title_id(const char *title_id) {
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
}
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);
publish_active_game(pid, title_id);
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
sm_fakelib_game_on_exec(pid, title_id);
return true;
}
static void clear_pending_game_launch(pending_game_launch_t *entry) {
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 void drain_game_lifecycle_wake_pipe(void) {
char drain_buf[32];
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
}
}
static bool discard_game_lifecycle_events_nowait(int kq) {
struct kevent events[16];
struct timespec timeout;
memset(&timeout, 0, sizeof(timeout));
while (true) {
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
&timeout);
if (nev < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
return false;
}
if (nev == 0)
return true;
}
}
static bool set_fd_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
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->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();
queue_pending_game_launch(pid, exec_time_us, now_us);
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));
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_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();
sm_mdbg_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;
queue_pending_game_launch(pid, now_us, now_us);
}
static void handle_game_exit(pid_t pid) {
char title_id[MAX_TITLE_ID] = {0};
bool had_active_title = consume_active_game_title(pid, title_id);
pending_game_launch_t *entry = find_pending_game_launch(pid);
if (entry)
clear_pending_game_launch(entry);
if (atomic_load(&g_active_game_pid) == pid)
publish_active_game_pid(0);
sm_fakelib_game_on_exit(pid);
sm_kstuff_game_on_exit(pid);
if (had_active_title) {
int snd0_updates = normalize_snd0info_for_title(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info normalized after game exit rows=%d title=%s",
snd0_updates, title_id);
}
}
static void restore_suspended_game_if_alive(int kq, pid_t pid) {
if (pid <= 0 || !is_process_alive(pid))
return;
char title_id[MAX_TITLE_ID];
uint32_t app_id = 0;
if (!resolve_game_title_id(pid, title_id, &app_id))
return;
if (!is_supported_title_id(title_id))
return;
uint64_t now_us = monotonic_time_us();
log_debug(" [GAME] resumed: %s pid=%ld app_id=0x%08X", title_id,
(long)pid, app_id);
(void)dispatch_game_launch(kq, pid, now_us, title_id, app_id);
}
static void *game_lifecycle_watcher_main(void *arg) {
(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");
bool sleep_cleanup_done = false;
pid_t suspended_game_pid = 0;
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
if (runtime_sleep_mode_active()) {
if (!sleep_cleanup_done) {
suspended_game_pid = atomic_load(&g_active_game_pid);
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
sm_kstuff_sleep_enter();
publish_active_game(0, NULL);
sleep_cleanup_done = true;
}
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
NULL);
if (rc < 0) {
if (errno == EINTR)
continue;
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
break;
}
drain_game_lifecycle_wake_pipe();
continue;
}
if (sleep_cleanup_done) {
if (!discard_game_lifecycle_events_nowait(kq))
break;
sleep_cleanup_done = false;
restore_suspended_game_if_alive(kq, suspended_game_pid);
sm_kstuff_sleep_leave();
suspended_game_pid = 0;
}
struct kevent event;
struct timespec timeout;
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
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]) {
drain_game_lifecycle_wake_pipe();
} 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);
}
}
if (runtime_sleep_mode_active())
continue;
poll_game_modules(kq);
}
clear_all_pending_game_launches();
sm_fakelib_game_shutdown();
sm_kstuff_game_shutdown();
publish_active_game(0, NULL);
close(kq);
log_debug(" [GAME] lifecycle watcher stopped");
return NULL;
}
bool start_game_lifecycle_watcher(void) {
if (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;
publish_active_game(0, NULL);
close_game_lifecycle_wake_pipe();
}
bool sm_game_lifecycle_has_active_game(void) {
return atomic_load(&g_active_game_pid) > 0;
}
bool refresh_game_lifecycle_watcher(void) {
if (!g_game_lifecycle_thread_started)
return start_game_lifecycle_watcher();
wake_game_lifecycle_watcher();
return true;
}
+113
View File
@@ -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) {
out_id[0] = '\0';
out_name[0] = '\0';
if (!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 > MAX_PARAM_JSON_SIZE) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
char path[MAX_PATH];
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
base_path);
if (written < 0 || (size_t)written >= sizeof(path)) {
store_cached_game_info(base_path, param_st, false, "", "");
return false;
}
FILE *f = fopen(path, "rb");
if (!f) {
store_cached_game_info(base_path, param_st, false, "", "");
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) {
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;
}
+1171
View File
File diff suppressed because it is too large. Load diff
+142
View File
@@ -0,0 +1,142 @@
#include "sm_platform.h"
#include <pthread.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 pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
static int find_cache_index(const char *path, const char *mount_point) {
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
if (!g_image_cache[k].valid)
continue;
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
return k;
}
if (path && 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) {
pthread_mutex_lock(&g_image_cache_mutex);
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
pthread_mutex_unlock(&g_image_cache_mutex);
return ok;
}
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
attach_backend_t backend) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = upsert_image_source_mapping(path, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
struct ImageCache *entry = &g_image_cache[entry_index];
entry->unit_id = unit_id;
entry->backend = backend;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
pthread_mutex_lock(&g_image_cache_mutex);
if (index < 0 || index >= MAX_IMAGE_MOUNTS || !g_image_cache[index].valid) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
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;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
void invalidate_image_cache_entry(int index) {
pthread_mutex_lock(&g_image_cache_mutex);
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
pthread_mutex_unlock(&g_image_cache_mutex);
return;
}
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
pthread_mutex_unlock(&g_image_cache_mutex);
}
bool resolve_device_from_mount_cache(const char *mount_point,
attach_backend_t *backend_out,
int *unit_out) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
*backend_out = entry->backend;
*unit_out = entry->unit_id;
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
bool resolve_image_source_from_mount_cache(const char *mount_point,
char *path_out,
size_t path_out_size) {
pthread_mutex_lock(&g_image_cache_mutex);
int entry_index = find_cache_index(NULL, mount_point);
if (entry_index < 0) {
pthread_mutex_unlock(&g_image_cache_mutex);
return false;
}
const struct ImageCache *entry = &g_image_cache[entry_index];
path_out[0] = '\0';
(void)strlcpy(path_out, entry->path, path_out_size);
pthread_mutex_unlock(&g_image_cache_mutex);
return true;
}
+415
View File
@@ -0,0 +1,415 @@
#include "sm_platform.h"
#include "sm_config_mount.h"
#include "sm_runtime.h"
#include "sm_install.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_game_cache.h"
#include "sm_log.h"
#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"
#include "sm_paths.h"
#include "sm_manual.h"
#include <dlfcn.h>
typedef int (*app_install_title_dir_fn_t)(const char *title_id,
const char *install_path,
void *reserved);
static const char *const k_app_inst_util_sprx_path =
"/system/common/lib/libSceAppInstUtil.sprx";
static const char *const k_app_install_title_dir_symbol =
"sceAppInstUtilAppInstallTitleDir";
static app_install_title_dir_fn_t g_app_install_title_dir_fn = NULL;
static bool g_app_install_title_dir_resolve_attempted = false;
static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
if (g_app_install_title_dir_resolve_attempted)
return NULL;
if (sm_firmware_major_version() >= 12u) {
g_app_install_title_dir_resolve_attempted = true;
return NULL;
}
g_app_install_title_dir_resolve_attempted = true;
void *app_inst_util_handle = dlopen(k_app_inst_util_sprx_path, RTLD_LAZY);
if (!app_inst_util_handle) {
const char *error = dlerror();
log_debug(" [REG] dlopen(%s) failed: %s", k_app_inst_util_sprx_path,
error ? error : "unknown");
return NULL;
}
dlerror();
g_app_install_title_dir_fn = (app_install_title_dir_fn_t)dlsym(
app_inst_util_handle, k_app_install_title_dir_symbol);
if (g_app_install_title_dir_fn)
return g_app_install_title_dir_fn;
const char *error = dlerror();
log_debug(" [REG] %s lookup in %s failed: %s",
k_app_install_title_dir_symbol, k_app_inst_util_sprx_path,
error ? error : "unknown");
return NULL;
}
static bool write_link_file(const char *path, const char *value) {
FILE *f = fopen(path, "w");
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;
}
static bool is_appmeta_file(const char *name) {
if (!name)
return false;
if (strcasecmp(name, "param.json") == 0 || strcasecmp(name, "param.sfo") == 0)
return true;
const char *ext = strrchr(name, '.');
if (!ext)
return false;
return (strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".dds") == 0 ||
strcasecmp(ext, ".at9") == 0);
}
static bool copy_sce_sys_to_appmeta(const char *src_sce_sys,
const char *user_appmeta_dir) {
DIR *d = opendir(src_sce_sys);
if (!d)
return false;
bool ok = true;
struct dirent *e;
char src_path[MAX_PATH];
char dst_path[MAX_PATH];
struct stat st;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
if (!is_appmeta_file(e->d_name))
continue;
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, e->d_name);
if (stat(src_path, &st) != 0 || !S_ISREG(st.st_mode))
continue;
snprintf(dst_path, sizeof(dst_path), "%s/%s", user_appmeta_dir, e->d_name);
if (copy_file(src_path, dst_path) != 0) {
ok = false;
break;
}
}
if (closedir(d) != 0)
ok = false;
return ok;
}
// --- Install/Remount Action ---
static bool mount_and_install(const char *src_path, const char *title_id,
const char *title_name, bool is_remount,
bool should_register,
bool use_app_install_all,
bool *has_src_snd0_out) {
char user_appmeta_dir[MAX_PATH];
char user_app_dir[MAX_PATH];
char user_sce_sys[MAX_PATH];
char src_sce_sys[MAX_PATH];
char src_snd0[MAX_PATH];
char image_source_path[MAX_PATH];
bool has_image_source = false;
bool appmeta_missing = false;
bool metadata_restaged = false;
bool restage_staging = false;
bool restage_appmeta = false;
bool has_src_snd0 = false;
snprintf(user_appmeta_dir, sizeof(user_appmeta_dir), "%s/%s", APPMETA_BASE,
title_id);
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, 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);
}
}
appmeta_missing = !has_appmeta_data(title_id);
if (is_remount && appmeta_missing) {
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
}
restage_staging = (!is_remount || should_register);
restage_appmeta = (!is_remount || appmeta_missing);
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
// COPY FILES
if (restage_staging || restage_appmeta) {
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
has_src_snd0 = path_exists(src_snd0);
} else {
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
}
if (restage_staging) {
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
mkdir(user_sce_sys, 0777);
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
user_sce_sys);
return false;
}
char icon_src[MAX_PATH];
char icon_dst[MAX_PATH];
snprintf(icon_src, sizeof(icon_src), "%s/icon0.png", src_sce_sys);
snprintf(icon_dst, sizeof(icon_dst), "%s/icon0.png", user_app_dir);
if (copy_file(icon_src, icon_dst) != 0) {
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
icon_dst);
return false;
}
}
if (restage_appmeta) {
mkdir(APPMETA_BASE, 0777);
mkdir(user_appmeta_dir, 0777);
if (!copy_sce_sys_to_appmeta(src_sce_sys, user_appmeta_dir)) {
log_debug(" [COPY] Failed to copy appmeta files: %s -> %s", src_sce_sys,
user_appmeta_dir);
return false;
}
metadata_restaged = true;
}
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (!mount_title_nullfs(title_id, src_path)) {
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
src_path);
return false;
}
// WRITE TRACKER
char lnk_path[MAX_PATH];
runtime_mount_state_lock();
if (runtime_sleep_mode_active()) {
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
mkdir(APP_BASE, 0777);
mkdir(user_app_dir, 0777);
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
if (!write_link_file(lnk_path, src_path)) {
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
log_debug(" [LINK] mount.lnk created: %s -> %s", lnk_path, src_path);
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)) {
(void)unlink(lnk_path);
(void)rollback_title_nullfs_mount(title_id, src_path);
runtime_mount_state_unlock();
return false;
}
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
image_source_path);
if (!cache_image_source_mapping(image_source_path, src_path)) {
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));
}
bool sleep_started = runtime_sleep_mode_active();
runtime_mount_state_unlock();
if (sleep_started || runtime_sleep_mode_active())
return true;
if (!should_register) {
if (metadata_restaged && has_src_snd0) {
int snd0_updates = update_snd0info(title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info force-updated after appmeta refresh rows=%d",
snd0_updates);
}
log_debug(" [REG] Skip (already present in app.db)");
return true;
}
if (!metadata_restaged) {
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
has_src_snd0 = path_exists(src_snd0);
}
if (use_app_install_all) {
if (has_src_snd0_out)
*has_src_snd0_out = has_src_snd0;
log_debug(" [REG] Prepared for batch install: %s (%s)", title_name,
title_id);
return true;
}
// REGISTER
app_install_title_dir_fn_t app_install_title_dir_fn =
resolve_app_install_title_dir();
if (!app_install_title_dir_fn) {
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
title_name, title_id);
return false;
}
mark_register_attempted(title_id);
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
sceKernelUsleep(200000);
if (res == 0) {
invalidate_app_db_title_cache();
clear_register_attempts(title_id);
log_debug(" [REG] Installed NEW!");
notify_game_installed_rich(title_id);
if (has_src_snd0) {
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();
clear_register_attempts(title_id);
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) {
if (runtime_sleep_mode_active())
return;
sm_install_poll_pending();
for (int i = 0; i < candidate_count; i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
const scan_candidate_t *c = &candidates[i];
bool has_src_snd0 = false;
bool use_app_install_all =
runtime_config()->app_install_all_enabled && !c->in_app_db;
if (c->installed) {
log_debug(" [ACTION] Remounting: %s", c->title_name);
} else {
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
if (!use_app_install_all) {
notify_system_info("Installing: %s (%s)...", c->title_name,
c->title_id);
}
}
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
c->installed, !c->in_app_db,
use_app_install_all, &has_src_snd0);
if (runtime_sleep_mode_active())
return;
if (mounted) {
if (c->manual && !use_app_install_all) {
sm_manual_note_installed(c->manual_source_path, c->title_id,
c->title_name);
}
if (use_app_install_all) {
if (!sm_install_queue_candidate(c, has_src_snd0)) {
log_debug(" [REG] Failed to queue staged install: %s (%s)",
c->title_name, c->title_id);
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
continue;
}
clear_failed_mount_attempts(c->title_id);
} else {
clear_failed_mount_attempts(c->title_id);
cache_game_entry(c->path, c->title_id, c->title_name);
}
} else {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
c->title_id);
}
}
}
if (!sm_install_submit_queued())
sm_install_note_submit_failure();
}
+466
View File
@@ -0,0 +1,466 @@
#include "sm_platform.h"
#include "sm_install_queue.h"
#include "sm_types.h"
#include "sm_appdb.h"
#include "sm_config_mount.h"
#include "sm_game_cache.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_manual.h"
#include "sm_runtime.h"
#include "sm_time.h"
#include "sm_title_state.h"
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
typedef enum {
INSTALL_TRACK_NONE = 0,
INSTALL_TRACK_QUEUED,
INSTALL_TRACK_SUBMITTED,
} install_track_state_t;
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
char source_path[MAX_PATH];
char manual_source_path[MAX_PATH];
uint64_t requested_at_us;
bool has_src_snd0;
bool manual;
install_track_state_t state;
} pending_install_entry_t;
static pending_install_entry_t g_pending_installs[MAX_PENDING];
static uint64_t g_pending_install_poll_due_us = 0;
static uint64_t g_queued_install_submit_due_us = 0;
static int g_tracked_install_count = 0;
static int g_submitted_install_count = 0;
static bool g_queued_install_batch_announced = false;
static bool g_queued_install_submit_failure_notified = false;
static int count_queued_installs(void);
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
static void drop_queued_install_entry(pending_install_entry_t *entry);
static bool install_queue_active(void) {
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
}
static pending_install_entry_t *find_pending_install_entry(
const char *title_id) {
if (!title_id || title_id[0] == '\0')
return NULL;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
continue;
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
return &g_pending_installs[i];
}
return NULL;
}
static pending_install_entry_t *reserve_pending_install_entry(
const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
return entry;
for (int i = 0; i < MAX_PENDING; i++) {
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
return &g_pending_installs[i];
}
return NULL;
}
static uint64_t next_pending_install_timeout_us(void) {
if (g_submitted_install_count <= 0)
return 0;
uint64_t next_deadline = 0;
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
continue;
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
if (next_deadline == 0 || deadline_us < next_deadline)
next_deadline = deadline_us;
}
return next_deadline;
}
static void schedule_pending_install_poll(uint64_t now_us) {
if (g_submitted_install_count <= 0) {
g_pending_install_poll_due_us = 0;
return;
}
g_pending_install_poll_due_us =
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
}
static void schedule_queued_install_submit(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
}
static void schedule_queued_install_submit_retry(uint64_t now_us) {
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
g_queued_install_submit_due_us = 0;
return;
}
g_queued_install_submit_due_us =
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
}
static void clear_pending_install_entry(pending_install_entry_t *entry) {
if (!entry)
return;
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
g_submitted_install_count--;
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
g_tracked_install_count--;
memset(entry, 0, sizeof(*entry));
if (g_submitted_install_count <= 0)
g_pending_install_poll_due_us = 0;
if (g_tracked_install_count <= 0) {
g_queued_install_submit_due_us = 0;
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
}
bool is_title_install_pending(const char *title_id) {
return find_pending_install_entry(title_id) != NULL;
}
void sm_install_forget_pending_title(const char *title_id) {
pending_install_entry_t *entry = find_pending_install_entry(title_id);
if (entry)
clear_pending_install_entry(entry);
}
static void append_batch_install_line(char *message, size_t message_size,
const char *title_name,
const char *title_id, int *shown_count) {
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
return;
size_t used = strlen(message);
if (used >= message_size)
return;
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
title_name, title_id);
if (written < 0 || (size_t)written >= message_size - used)
return;
(*shown_count)++;
}
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
bool has_src_snd0) {
if (!candidate || candidate->title_id[0] == '\0' ||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
return false;
}
if (!runtime_config()->app_install_all_enabled)
return false;
bool was_queue_empty = (count_queued_installs() <= 0);
pending_install_entry_t *entry =
reserve_pending_install_entry(candidate->title_id);
if (!entry) {
log_debug(" [REG] install queue full, cannot queue %s (%s)",
candidate->title_name, candidate->title_id);
return false;
}
install_track_state_t previous_state = entry->state;
if (previous_state == INSTALL_TRACK_SUBMITTED)
return true;
memset(entry, 0, sizeof(*entry));
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
(void)strlcpy(entry->title_name, candidate->title_name,
sizeof(entry->title_name));
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
sizeof(entry->manual_source_path));
entry->manual = candidate->manual;
entry->has_src_snd0 = has_src_snd0;
entry->state = INSTALL_TRACK_QUEUED;
if (previous_state == INSTALL_TRACK_NONE)
g_tracked_install_count++;
if (was_queue_empty) {
g_queued_install_batch_announced = false;
g_queued_install_submit_failure_notified = false;
}
schedule_queued_install_submit(monotonic_time_us());
return true;
}
static void finalize_pending_install_success(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
notify_game_installed_rich(entry->title_id);
if (entry->has_src_snd0) {
int snd0_updates = update_snd0info(entry->title_id);
if (snd0_updates >= 0)
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
}
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
if (entry->manual)
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
entry->title_name);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
clear_pending_install_entry(entry);
}
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
if (!entry)
return;
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
entry->title_name, entry->title_id);
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
entry->title_name, entry->title_id);
(void)note_pending_install_failure(entry);
clear_pending_install_entry(entry);
}
static void poll_pending_installs(void) {
if (g_submitted_install_count <= 0)
return;
invalidate_app_db_title_cache();
struct AppDbTitleList app_db_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
uint64_t now_us = monotonic_time_us();
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable while polling installs");
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_SUBMITTED)
continue;
if (app_db_titles_ready &&
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
finalize_pending_install_success(entry);
continue;
}
if (now_us != 0 && entry->requested_at_us != 0 &&
now_us >= entry->requested_at_us &&
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
finalize_pending_install_timeout(entry);
}
}
schedule_pending_install_poll(now_us);
if (g_submitted_install_count == 0)
schedule_queued_install_submit(now_us);
free_app_db_title_list(&app_db_titles);
}
uint64_t sm_install_next_wake_us(uint64_t now_us) {
if (runtime_sleep_mode_active())
return 0;
if (!install_queue_active())
return 0;
if (g_submitted_install_count <= 0)
return g_queued_install_submit_due_us;
uint64_t next_wake_us = next_pending_install_timeout_us();
if (g_pending_install_poll_due_us != 0 &&
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
next_wake_us = g_pending_install_poll_due_us;
}
if (g_queued_install_submit_due_us != 0 &&
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
next_wake_us = g_queued_install_submit_due_us;
}
if (next_wake_us == 0)
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
return next_wake_us;
}
void sm_install_poll_pending(void) {
if (runtime_sleep_mode_active())
return;
poll_pending_installs();
}
void sm_install_service_pending(void) {
if (runtime_sleep_mode_active())
return;
if (!install_queue_active())
return;
sm_install_poll_pending();
if (count_queued_installs() > 0 && !sm_install_submit_queued())
sm_install_note_submit_failure();
}
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts,
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
entry->title_id);
}
}
static int count_queued_installs(void) {
int queued_count = g_tracked_install_count - g_submitted_install_count;
return queued_count > 0 ? queued_count : 0;
}
static void log_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0)
return;
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
}
}
static void notify_queued_install_batch(void) {
int queued_count = count_queued_installs();
if (queued_count <= 0 || g_queued_install_batch_announced)
return;
char message[3075];
int shown_count = 0;
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
for (int i = 0; i < MAX_PENDING; i++) {
const pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
append_batch_install_line(message, sizeof(message), entry->title_name,
entry->title_id, &shown_count);
}
if (shown_count < queued_count) {
size_t used = strlen(message);
if (used < sizeof(message)) {
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
queued_count - shown_count);
}
}
notify_system_info("%s", message);
g_queued_install_batch_announced = true;
}
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
if (!entry)
return 0;
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
log_batch_submit_retry_limit(entry);
return failed_attempts;
}
static void drop_queued_install_entry(pending_install_entry_t *entry) {
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
return;
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
entry->title_name, entry->title_id);
clear_pending_install_entry(entry);
}
bool sm_install_submit_queued(void) {
if (runtime_sleep_mode_active())
return true;
int queued_count = count_queued_installs();
if (queued_count <= 0)
return true;
if (g_submitted_install_count > 0)
return true;
uint64_t now_us = monotonic_time_us();
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
now_us < g_queued_install_submit_due_us) {
return true;
}
log_queued_install_batch();
notify_queued_install_batch();
int res = sceAppInstUtilAppInstallAll();
if (res != 0) {
log_debug(" [REG] Batch install request failed: 0x%x", res);
if (!g_queued_install_submit_failure_notified) {
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
queued_count, (uint32_t)res);
g_queued_install_submit_failure_notified = true;
}
schedule_queued_install_submit_retry(now_us);
return false;
}
g_queued_install_submit_due_us = 0;
g_queued_install_submit_failure_notified = false;
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
entry->state = INSTALL_TRACK_SUBMITTED;
entry->requested_at_us = now_us;
g_submitted_install_count++;
clear_failed_mount_attempts(entry->title_id);
}
invalidate_app_db_title_cache();
schedule_pending_install_poll(now_us);
return true;
}
void sm_install_note_submit_failure(void) {
for (int i = 0; i < MAX_PENDING; i++) {
pending_install_entry_t *entry = &g_pending_installs[i];
if (entry->state != INSTALL_TRACK_QUEUED)
continue;
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
drop_queued_install_entry(entry);
}
}
+920
View File
@@ -0,0 +1,920 @@
#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_mdbg.h"
#include "sm_path_utils.h"
#include "sm_runtime.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 autopause_delay_valid;
bool pause_applied;
bool focus_override_active;
pid_t pid;
uint32_t app_id;
uint32_t autopause_delay_seconds;
uint64_t launch_time_us;
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 probe_available;
bool supported;
bool restore_on_empty;
bool sleep_pause_restore;
bool current_app_focus_valid;
uint32_t current_app_focus_id;
uint64_t restore_retry_us;
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 _Atomic bool g_pending_config_reload;
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry);
static bool refresh_kstuff_support_state(void);
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
bool *image_backed_out,
uint32_t *autopause_delay_out,
bool *autopause_delay_valid_out);
static void restore_kstuff_if_needed(const char *reason);
static char *trim_ascii_inplace(char *s) {
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
s++;
size_t len = strlen(s);
while (len > 0) {
char ch = s[len - 1u];
if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n')
break;
s[len - 1u] = '\0';
len--;
}
return s;
}
static char *strip_inline_comment(char *s) {
char *comment = strpbrk(s, "#;");
if (comment)
*comment = '\0';
return trim_ascii_inplace(s);
}
static bool parse_pause_delay_seconds(const char *value,
uint32_t *delay_seconds_out) {
if (!value || !delay_seconds_out)
return false;
errno = 0;
char *end = NULL;
unsigned long parsed = strtoul(value, &end, 10);
if (errno != 0 || end == value || *end != '\0' ||
parsed > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
return false;
}
*delay_seconds_out = (uint32_t)parsed;
return true;
}
static bool get_autopause_delay_for_source(const char *source_path,
bool image_backed,
uint32_t *delay_seconds_out) {
if (!source_path || source_path[0] == '\0' || !delay_seconds_out)
return false;
char autopause_path[MAX_PATH];
int written = snprintf(autopause_path, sizeof(autopause_path), "%s/%s",
source_path, "autopause.txt");
if (written <= 0 || (size_t)written >= sizeof(autopause_path))
return false;
FILE *f = fopen(autopause_path, "r");
if (!f)
return false;
char line[128];
bool have_scalar = false;
bool have_direct = false;
bool have_image = false;
uint32_t scalar_delay = 0;
uint32_t direct_delay = 0;
uint32_t image_delay = 0;
while (fgets(line, sizeof(line), f)) {
char *value = strip_inline_comment(line);
if (value[0] == '\0')
continue;
char *eq = strchr(value, '=');
if (!eq) {
if (!have_scalar && parse_pause_delay_seconds(value, &scalar_delay))
have_scalar = true;
continue;
}
*eq = '\0';
char *key = trim_ascii_inplace(value);
char *arg = trim_ascii_inplace(eq + 1);
uint32_t parsed_delay = 0;
if (!parse_pause_delay_seconds(arg, &parsed_delay))
continue;
if (strcasecmp(key, "direct") == 0) {
direct_delay = parsed_delay;
have_direct = true;
} else if (strcasecmp(key, "image") == 0) {
image_delay = parsed_delay;
have_image = true;
}
}
fclose(f);
if (image_backed && have_image) {
*delay_seconds_out = image_delay;
return true;
}
if (!image_backed && have_direct) {
*delay_seconds_out = direct_delay;
return true;
}
if (have_scalar) {
uint64_t delay_seconds = scalar_delay;
if (image_backed)
delay_seconds *= 2u;
if (delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS)
delay_seconds = MAX_KSTUFF_PAUSE_DELAY_SECONDS;
*delay_seconds_out = (uint32_t)delay_seconds;
return true;
}
return false;
}
static bool resolve_title_pause_source(const char *title_id,
char source_path[MAX_PATH],
bool *image_backed_out) {
source_path[0] = '\0';
*image_backed_out = false;
if (!title_id)
return false;
if (!read_mount_link(title_id, source_path, MAX_PATH))
return false;
*image_backed_out = is_under_image_mount_base(source_path);
return true;
}
static uint32_t get_configured_pause_delay_seconds(const char *title_id,
bool image_backed,
bool *override_applied_out) {
const runtime_config_t *cfg = runtime_config();
uint32_t delay_seconds =
image_backed ? cfg->kstuff_pause_delay_image_seconds
: cfg->kstuff_pause_delay_direct_seconds;
uint32_t override_delay_seconds = 0;
if (get_kstuff_pause_delay_override_for_title(title_id,
&override_delay_seconds)) {
*override_applied_out = true;
return override_delay_seconds;
}
*override_applied_out = false;
return delay_seconds;
}
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;
case 0x11000000:
case 0x11200000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc78;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbdf0;
return true;
case 0x11400000:
case 0x11600000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbc98;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdcbe10;
return true;
case 0x12000000:
case 0x12020000:
case 0x12200000:
case 0x12400000:
case 0x12600000:
case 0x12700000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xdcc978;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xdccaf0;
return true;
case 0x13000000:
case 0x13200000:
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8a8;
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda20;
return true;
default:
return false;
}
}
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 (!refresh_kstuff_support_state())
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 refresh_kstuff_support_state(void) {
if (!g_kstuff.probe_available) {
g_kstuff.supported = false;
return false;
}
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
g_kstuff.supported = kstuff_sysentvec_toggle_is_known(ps5_toggle) &&
kstuff_sysentvec_toggle_is_known(ps4_toggle);
return g_kstuff.supported;
}
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 bool tracked_game_requires_restore(void) {
return g_kstuff.restore_on_empty;
}
static void finish_tracked_game_clear(const char *reason) {
if (!g_kstuff.restore_on_empty)
return;
mark_restore_needed();
restore_kstuff_if_needed(reason);
}
static void apply_kstuff_config_reload(void) {
if (!runtime_config()->kstuff_crash_detection_enabled)
sm_mdbg_game_shutdown();
if (!runtime_config()->kstuff_game_auto_toggle) {
sm_kstuff_game_shutdown();
return;
}
if (!g_kstuff.game.active)
return;
if (is_kstuff_pause_disabled_for_title(g_kstuff.game.title_id)) {
bool restore_needed = tracked_game_requires_restore();
log_debug(" [KSTUFF] cleared tracked game by config reload: %s",
g_kstuff.game.title_id);
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("config reload");
return;
}
if (g_kstuff.game.pause_applied)
return;
bool image_backed = g_kstuff.game.image_backed;
bool autopause_delay_valid = false;
uint32_t autopause_delay_seconds = 0;
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
g_kstuff.game.title_id, &image_backed, &autopause_delay_seconds,
&autopause_delay_valid);
g_kstuff.game.image_backed = image_backed;
g_kstuff.game.autopause_delay_valid = autopause_delay_valid;
g_kstuff.game.autopause_delay_seconds = autopause_delay_seconds;
g_kstuff.game.pause_deadline_us =
g_kstuff.game.launch_time_us == 0
? 0
: g_kstuff.game.launch_time_us + (uint64_t)delay_seconds * 1000000ull;
log_debug(" [KSTUFF] updated tracked game config: %s source=%s pause_delay=%us",
g_kstuff.game.title_id,
g_kstuff.game.image_backed ? "image" : "direct",
delay_seconds);
}
static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
bool *image_backed_out,
uint32_t *autopause_delay_out,
bool *autopause_delay_valid_out) {
char source_path[MAX_PATH];
bool image_backed = false;
(void)resolve_title_pause_source(title_id, source_path, &image_backed);
*image_backed_out = image_backed;
*autopause_delay_out = 0;
*autopause_delay_valid_out = false;
uint32_t autotuned_delay_seconds = 0;
if (get_kstuff_autotune_pause_delay_for_title(title_id,
&autotuned_delay_seconds)) {
return autotuned_delay_seconds;
}
bool override_applied = false;
uint32_t delay_seconds =
get_configured_pause_delay_seconds(title_id, image_backed,
&override_applied);
uint32_t autopause_delay_seconds = 0;
if (get_autopause_delay_for_source(source_path, image_backed,
&autopause_delay_seconds)) {
*autopause_delay_out = autopause_delay_seconds;
*autopause_delay_valid_out = true;
if (!override_applied)
return autopause_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;
}
}
if (!sm_kstuff_is_supported()) {
g_kstuff.restore_retry_us =
now_us == 0 ? 1 : now_us + GAME_LIFECYCLE_POLL_INTERVAL_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->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;
}
if (!sm_kstuff_is_supported()) {
log_debug(" [KSTUFF] skipping auto-pause for %s pid=%ld: kstuff not "
"available",
entry->title_id, (long)entry->pid);
sm_mdbg_game_shutdown();
memset(entry, 0, sizeof(*entry));
return;
}
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();
sm_mdbg_game_on_kstuff_pause(entry->pid, now_us,
get_effective_pause_delay_seconds(entry));
log_debug(" [KSTUFF] auto-paused for %s pid=%ld (%s launch)",
entry->title_id, (long)entry->pid,
entry->image_backed ? "image" : "direct");
} 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)
return;
if (!g_kstuff.restore_on_empty)
return;
if (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 refresh_kstuff_support_state();
}
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) {
bool ps5_enabled = false;
bool ps4_enabled = false;
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
if (!g_kstuff.supported)
return false;
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 && g_kstuff.probe_available;
}
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;
}
bool restore_needed = tracked_game_requires_restore();
log_debug(" [KSTUFF] handoff tracked game pid=%ld (%s) -> pid=%ld (%s)",
(long)g_kstuff.game.pid, g_kstuff.game.title_id, (long)pid,
title_id);
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("tracked game handoff");
}
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;
bool autopause_delay_valid = false;
uint32_t autopause_delay_seconds = 0;
uint32_t delay_seconds = get_pause_delay_seconds_for_title(
title_id, &image_backed, &autopause_delay_seconds,
&autopause_delay_valid);
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.autopause_delay_valid = autopause_delay_valid;
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.autopause_delay_seconds = autopause_delay_seconds;
g_kstuff.game.launch_time_us = base_time_us;
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));
sm_mdbg_game_on_exec(pid, title_id, app_id);
log_debug(" [KSTUFF] tracking game launch: %s pid=%ld app_id=0x%08X source=%s "
"pause_delay=%us", title_id, (long)pid, app_id,
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;
bool restore_needed = tracked_game_requires_restore();
sm_mdbg_game_on_exit(pid);
log_debug(" [KSTUFF] game stopped: %s pid=%ld",
g_kstuff.game.title_id, (long)pid);
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed)
finish_tracked_game_clear("tracked game exit");
}
void sm_kstuff_game_poll(void) {
if (atomic_exchange(&g_pending_config_reload, false))
apply_kstuff_config_reload();
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) {
bool restore_needed = tracked_game_requires_restore();
if (restore_needed && !g_kstuff.sleep_pause_restore &&
runtime_sleep_mode_active() && should_stop_requested()) {
restore_needed = false;
g_kstuff.restore_on_empty = false;
g_kstuff.restore_retry_us = 0;
} else if (g_kstuff.sleep_pause_restore) {
restore_needed = true;
g_kstuff.sleep_pause_restore = false;
}
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
if (restore_needed) {
mark_restore_needed();
restore_kstuff_if_needed("watcher shutdown");
}
}
void sm_kstuff_sleep_enter(void) {
bool was_enabled = false;
bool fully_disabled = false;
sm_mdbg_game_shutdown();
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
g_kstuff.sleep_pause_restore = false;
if (!sm_kstuff_is_supported())
return;
was_enabled = sm_kstuff_is_enabled();
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
if (!fully_disabled) {
log_debug(" [KSTUFF] failed to pause for system sleep");
return;
}
g_kstuff.sleep_pause_restore = was_enabled;
if (was_enabled)
log_debug(" [KSTUFF] paused for system sleep");
}
void sm_kstuff_sleep_leave(void) {
if (!g_kstuff.sleep_pause_restore)
return;
g_kstuff.sleep_pause_restore = false;
mark_restore_needed();
restore_kstuff_if_needed("system sleep");
}
void sm_kstuff_on_config_reload(void) {
atomic_store(&g_pending_config_reload, true);
}
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);
atomic_store(&g_pending_config_reload, 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;
}
g_kstuff.probe_available = true;
if (!refresh_kstuff_support_state()) {
log_debug(" [KSTUFF] runtime control probe ready; kstuff not present at "
"startup");
return;
}
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);
atomic_store(&g_pending_config_reload, false);
memset(&g_kstuff, 0, sizeof(g_kstuff));
}
static uint32_t get_effective_pause_delay_seconds(const kstuff_game_entry_t *entry) {
if (!entry || entry->launch_time_us == 0 || entry->pause_deadline_us == 0 ||
entry->pause_deadline_us <= entry->launch_time_us) {
return 0;
}
uint64_t delay_us = entry->pause_deadline_us - entry->launch_time_us;
uint64_t delay_seconds = delay_us / 1000000ull;
if (delay_seconds > UINT32_MAX)
delay_seconds = UINT32_MAX;
return (uint32_t)delay_seconds;
}
+405
View File
@@ -0,0 +1,405 @@
#include "sm_platform.h"
#include <pthread.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;
static pthread_mutex_t g_notifications_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static FILE *g_log_file = NULL;
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) {
pthread_mutex_lock(&g_notifications_mutex);
if (!g_notifications_initialized)
g_notifications_initialized = ensure_notification_icon_present();
pthread_mutex_unlock(&g_notifications_mutex);
}
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
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(&timestamp, &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();
pthread_mutex_lock(&g_notifications_mutex);
bool notifications_ready = g_notifications_initialized;
pthread_mutex_unlock(&g_notifications_mutex);
if (!notifications_ready)
return false;
escaped_message[0] = '\0';
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 FILE *ensure_log_file_open_locked(void) {
if (g_log_file)
return g_log_file;
(void)mkdir(LOG_DIR, 0777);
g_log_file = fopen(LOG_FILE, "a");
if (!g_log_file)
return NULL;
return g_log_file;
}
static void log_to_file_locked(const char *fmt, va_list args) {
FILE *fp = ensure_log_file_open_locked();
if (!fp)
return;
va_list args_file;
time_t rawtime;
struct tm tm_local;
char buffer[80];
time(&rawtime);
(void)localtime_r(&rawtime, &tm_local);
(void)strftime(buffer, sizeof(buffer), "%H:%M:%S", &tm_local);
va_copy(args_file, args);
fprintf(fp, "[%s] ", buffer);
vfprintf(fp, fmt, args_file);
fputc('\n', fp);
fflush(fp);
va_end(args_file);
}
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);
pthread_mutex_lock(&g_log_mutex);
vfprintf(stdout, fmt, args);
fputc('\n', stdout);
fflush(stdout);
log_to_file_locked(fmt, args_copy);
pthread_mutex_unlock(&g_log_mutex);
va_end(args_copy);
va_end(args);
}
void sm_log_shutdown(void) {
pthread_mutex_lock(&g_log_mutex);
if (g_log_file) {
fclose(g_log_file);
g_log_file = NULL;
}
pthread_mutex_unlock(&g_log_mutex);
}
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') {
const char *error_path =
(g_last_error.path[0] != '\0') ? g_last_error.path : path;
notify_system("%s\n%s", g_last_error.message, error_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);
}
+420
View File
@@ -0,0 +1,420 @@
#include "sm_platform.h"
#include "sm_manual.h"
#include "sm_appdb.h"
#include "sm_install_queue.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_paths.h"
#include "sm_title_state.h"
#define MANUAL_STATUS_CAPACITY 1024
typedef enum {
MANUAL_STATUS_UNKNOWN = 0,
MANUAL_STATUS_INSTALLED,
MANUAL_STATUS_DELETED,
} manual_status_t;
typedef struct {
char path[MAX_PATH];
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
manual_status_t status;
bool remove_from_list;
} manual_status_entry_t;
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
static void trim_manual_line(char *line) {
if (!line)
return;
char *start = line;
while (*start != '\0' && isspace((unsigned char)*start))
start++;
if (start != line)
memmove(line, start, strlen(start) + 1u);
size_t len = strlen(line);
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
line[len - 1u] = '\0';
len--;
}
while (len > 1 && line[len - 1u] == '/') {
line[len - 1u] = '\0';
len--;
}
}
static void drain_manual_file_line(FILE *f) {
int ch;
while ((ch = fgetc(f)) != EOF && ch != '\n') {
}
}
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
int *saved_errno) {
int ch;
while ((ch = fgetc(in)) != EOF) {
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
*saved_errno = errno;
if (ch == '\n')
break;
}
}
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
void *ctx) {
if (!visit)
return false;
FILE *f = fopen(MANUAL_LIST_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
return false;
}
char line[MAX_PATH + 64];
bool completed = true;
while (fgets(line, sizeof(line), f) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
trim_manual_line(line);
if (line[0] == '\0' || line[0] == '#') {
if (truncated)
drain_manual_file_line(f);
continue;
}
if (truncated || strlen(line) >= MAX_PATH) {
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
if (truncated)
drain_manual_file_line(f);
continue;
}
if (!visit(line, ctx)) {
completed = false;
break;
}
}
if (ferror(f)) {
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
completed = false;
}
fclose(f);
return completed;
}
static const char *manual_status_name(manual_status_t status) {
switch (status) {
case MANUAL_STATUS_INSTALLED:
return "installed";
case MANUAL_STATUS_DELETED:
return "deleted";
default:
return "unknown";
}
}
static manual_status_t parse_manual_status_name(const char *name) {
if (!name)
return MANUAL_STATUS_UNKNOWN;
if (strcasecmp(name, "installed") == 0)
return MANUAL_STATUS_INSTALLED;
if (strcasecmp(name, "deleted") == 0)
return MANUAL_STATUS_DELETED;
return MANUAL_STATUS_UNKNOWN;
}
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
char *status = strtok(line, "\t");
char *title_id = strtok(NULL, "\t");
char *path = strtok(NULL, "\t");
char *title_name = strtok(NULL, "\n");
if (!status || !title_id || !path)
return false;
trim_manual_line(status);
trim_manual_line(title_id);
trim_manual_line(path);
if (title_name)
trim_manual_line(title_name);
manual_status_t parsed_status = parse_manual_status_name(status);
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
path[0] == '\0') {
return false;
}
memset(entry, 0, sizeof(*entry));
entry->status = parsed_status;
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
(void)strlcpy(entry->path, path, sizeof(entry->path));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
return true;
}
static int load_manual_status(void) {
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
if (!f) {
if (errno != ENOENT)
log_debug(" [MANUAL] status open failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
return 0;
}
int count = 0;
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
while (fgets(line, sizeof(line), f) != NULL) {
if (count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status limit reached (%d)",
MANUAL_STATUS_CAPACITY);
break;
}
manual_status_entry_t entry;
if (!parse_manual_status_line(line, &entry))
continue;
g_manual_status_entries[count++] = entry;
}
if (ferror(f))
log_debug(" [MANUAL] status read failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
fclose(f);
return count;
}
static bool write_manual_status(int entry_count) {
mkdir(LOG_DIR, 0777);
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
FILE *f = fopen(tmp_path, "w");
if (!f) {
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
strerror(errno));
return false;
}
int saved_errno = 0;
for (int i = 0; i < entry_count; i++) {
const manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
entry->title_id[0] == '\0') {
continue;
}
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
entry->title_id, entry->path, entry->title_name) < 0) {
saved_errno = errno;
break;
}
}
if (fflush(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(f) != 0 && saved_errno == 0)
saved_errno = errno;
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] status write failed for %s: %s", tmp_path,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
log_debug(" [MANUAL] status rename failed for %s: %s",
MANUAL_STATUS_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
static int find_manual_status_entry(int entry_count, const char *path,
const char *title_id) {
for (int i = 0; i < entry_count; i++) {
if (path && path[0] != '\0' &&
strcmp(g_manual_status_entries[i].path, path) == 0) {
return i;
}
if (title_id && title_id[0] != '\0' &&
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
return i;
}
}
return -1;
}
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
FILE *in = fopen(MANUAL_LIST_FILE, "r");
if (!in) {
if (errno == ENOENT)
return true;
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
return false;
}
char tmp_path[MAX_PATH];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
FILE *out = fopen(tmp_path, "w");
if (!out) {
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
strerror(errno));
fclose(in);
return false;
}
bool changed = false;
int saved_errno = 0;
char line[MAX_PATH + 64];
while (fgets(line, sizeof(line), in) != NULL) {
bool truncated =
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
char normalized[MAX_PATH + 64];
(void)strlcpy(normalized, line, sizeof(normalized));
trim_manual_line(normalized);
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
bool remove_line = false;
for (int i = 0; i < entry_count; i++) {
if (g_manual_status_entries[i].remove_from_list &&
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
remove_line = true;
break;
}
}
if (remove_line) {
changed = true;
if (truncated)
drain_manual_file_line(in);
continue;
}
}
if (fputs(line, out) == EOF) {
saved_errno = errno;
break;
}
if (truncated)
copy_manual_file_line_remainder(in, out, &saved_errno);
}
if (ferror(in) && saved_errno == 0)
saved_errno = errno;
if (fflush(out) != 0 && saved_errno == 0)
saved_errno = errno;
if (fclose(out) != 0 && saved_errno == 0)
saved_errno = errno;
fclose(in);
if (saved_errno != 0) {
errno = saved_errno;
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
strerror(errno));
(void)unlink(tmp_path);
return false;
}
if (!changed) {
(void)unlink(tmp_path);
return true;
}
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
MANUAL_LIST_FILE, strerror(errno));
(void)unlink(tmp_path);
return false;
}
return true;
}
bool sm_manual_reconcile_deleted_titles(
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
if (!app_db_titles_ready || !app_db_titles)
return true;
int entry_count = load_manual_status();
if (entry_count <= 0)
return true;
bool changed = false;
for (int i = 0; i < entry_count; i++) {
manual_status_entry_t *entry = &g_manual_status_entries[i];
if (entry->status != MANUAL_STATUS_INSTALLED ||
entry->title_id[0] == '\0') {
continue;
}
if (app_db_title_list_contains(app_db_titles, entry->title_id))
continue;
entry->status = MANUAL_STATUS_DELETED;
entry->remove_from_list = true;
sm_install_forget_pending_title(entry->title_id);
clear_register_attempts(entry->title_id);
clear_failed_mount_attempts(entry->title_id);
changed = true;
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
"%s (%s)",
entry->title_name, entry->title_id);
}
if (!changed)
return true;
if (!rewrite_manual_list_without_deleted_entries(entry_count))
return false;
return write_manual_status(entry_count);
}
void sm_manual_note_installed(const char *manual_source_path,
const char *title_id,
const char *title_name) {
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
title_id[0] == '\0') {
return;
}
int entry_count = load_manual_status();
int index =
find_manual_status_entry(entry_count, manual_source_path, title_id);
if (index < 0) {
if (entry_count >= MANUAL_STATUS_CAPACITY) {
log_debug(" [MANUAL] status full, cannot record %s (%s)",
manual_source_path, title_id);
return;
}
index = entry_count++;
memset(&g_manual_status_entries[index], 0,
sizeof(g_manual_status_entries[index]));
}
manual_status_entry_t *entry = &g_manual_status_entries[index];
manual_status_t previous_status = entry->status;
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
strcmp(entry->path, manual_source_path) == 0 &&
strcmp(entry->title_id, title_id) == 0 &&
(!title_name || title_name[0] == '\0' ||
strcmp(entry->title_name, title_name) == 0);
if (unchanged)
return;
entry->status = MANUAL_STATUS_INSTALLED;
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
if (title_name && title_name[0] != '\0')
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
if (!write_manual_status(entry_count))
return;
if (previous_status != MANUAL_STATUS_INSTALLED) {
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
entry->title_name, entry->title_id, entry->path);
}
}
+664
View File
@@ -0,0 +1,664 @@
#include "sm_platform.h"
#include <inttypes.h>
#include <stdlib.h>
#include "sm_config_mount.h"
#include "sm_limits.h"
#include "sm_log.h"
#include "sm_mdbg.h"
#include "sm_time.h"
#include "sm_types.h"
int sceKernelDebugGetPrivateLogText(void *buffer, size_t buffer_size,
char **text, uint64_t *text_size);
int sceKernelDebugGetSdkLogText(void *buffer, size_t buffer_size,
char **text, uint64_t *text_size);
uint64_t sceKernelDebugGetLogBufferSize(void) __asm__("C49jelxiaVE");
int mdbg_call(void *cmd, void *req, void *res);
#define MDBG_CMD_TYPE_SERVICE 1ull
#define MDBG_CMD_PROCESS_STATE 30ull
#define MDBG_SUBCMD_FLAGS 2ull
#define MDBG_FLAG_EXCEPTION_STOP 0x00080000ull
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
#define MDBG_USE_PRIVATE_LOG_TEXT 0
#endif
#ifndef MDBG_SKIP_PRIVILEGE_ELEVATION
#define MDBG_SKIP_PRIVILEGE_ELEVATION 0
#endif
#if MDBG_USE_PRIVATE_LOG_TEXT
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetPrivateLogText
#else
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetSdkLogText
#endif
typedef struct {
uint64_t type;
uint64_t cmd;
} mdbg_cmd_t;
typedef struct {
int64_t pid;
int64_t subcmd;
uint64_t arg;
uint64_t reserved[5];
} mdbg_req_t;
typedef struct {
int64_t status;
uint64_t value;
uint64_t reserved[2];
} mdbg_res_t;
typedef struct {
bool active;
bool log_monitoring_active;
bool pause_seen;
pid_t pid;
uint32_t pause_delay_seconds;
uint64_t monitor_deadline_us;
uint64_t pause_time_us;
uint64_t next_poll_us;
char title_id[MAX_TITLE_ID];
char rtld_error_prefix[MDBG_RTLD_ERROR_PREFIX_SIZE];
} mdbg_game_state_t;
typedef struct {
bool privilege_probe_done;
bool privilege_ready;
size_t log_buffer_size;
size_t log_snapshot_length;
size_t log_line_length;
char *log_snapshot;
char *log_storage;
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
mdbg_game_state_t game;
} sm_mdbg_state_t;
static sm_mdbg_state_t g_mdbg;
static bool sm_mdbg_enabled(void);
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
static int elevate_to_coredump(void);
#endif
static bool ensure_mdbg_privileges(void);
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
int64_t *status_out, uint64_t *value_out);
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
static bool is_mdbg_process_gone_error(int ret);
static void reset_log_line_buffer(void);
static void reset_log_snapshot(void);
static void free_log_buffers(void);
static bool ensure_log_buffers(void);
static int fetch_log_text(const char **text_out, size_t *text_len_out);
static size_t find_log_overlap(const char *current, size_t current_len);
static void update_log_snapshot(const char *text, size_t text_len);
static void clear_tracked_game(void);
static bool matches_tracked_rtld_error(const char *text);
static void summarize_failure_reason(const char *reason, char *summary_out,
size_t summary_out_size,
bool is_rtld_error);
static void handle_pre_pause_failure(const char *reason);
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
static void process_log_line(const char *line, uint64_t now_us);
static void append_log_char(char ch, uint64_t now_us);
static void poll_log_monitor(uint64_t now_us);
static void start_log_monitoring(void);
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
static bool sm_mdbg_enabled(void) {
return runtime_config()->kstuff_crash_detection_enabled &&
runtime_config()->kstuff_game_auto_toggle;
}
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
#define SCE_AUTHID_COREDUMP 0x4800000000000006ull
static int elevate_to_coredump(void) {
static const uint8_t k_priv_caps[16] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
pid_t pid = getpid();
if (kernel_set_ucred_authid(pid, SCE_AUTHID_COREDUMP) < 0)
return -1;
if (kernel_set_ucred_caps(pid, k_priv_caps) < 0)
return -1;
return 0;
}
#endif
static bool ensure_mdbg_privileges(void) {
if (g_mdbg.privilege_probe_done)
return g_mdbg.privilege_ready;
g_mdbg.privilege_probe_done = true;
#if MDBG_SKIP_PRIVILEGE_ELEVATION
g_mdbg.privilege_ready = true;
log_debug(" [MDBG] coredump privilege probe skipped");
#else
g_mdbg.privilege_ready = elevate_to_coredump() == 0;
if (g_mdbg.privilege_ready) {
log_debug(" [MDBG] coredump privileges enabled");
} else {
log_debug(" [MDBG] failed to enable coredump privileges; mdbg polling "
"disabled, log monitoring disabled");
}
#endif
return g_mdbg.privilege_ready;
}
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
int64_t *status_out, uint64_t *value_out) {
mdbg_cmd_t cmd = {MDBG_CMD_TYPE_SERVICE, MDBG_CMD_PROCESS_STATE};
mdbg_req_t req = {
.pid = pid,
.subcmd = (int64_t)subcmd,
.arg = arg,
};
mdbg_res_t res = {0};
int ret = mdbg_call(&cmd, &req, &res);
if (ret != 0)
return ret;
if (status_out)
*status_out = res.status;
if (value_out)
*value_out = res.value;
return 0;
}
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
int64_t status;
uint64_t value;
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
if (ret < 0)
return ret;
if (status != 0)
return (int)status;
if (flags_out)
*flags_out = value;
return 0;
}
static bool is_mdbg_process_gone_error(int ret) {
return ret == -ESRCH || ret == -ENOENT || ret == ESRCH || ret == ENOENT;
}
static void reset_log_line_buffer(void) {
g_mdbg.log_line_length = 0;
g_mdbg.log_line[0] = '\0';
}
static void reset_log_snapshot(void) {
g_mdbg.log_snapshot_length = 0;
reset_log_line_buffer();
}
static void free_log_buffers(void) {
free(g_mdbg.log_snapshot);
free(g_mdbg.log_storage);
g_mdbg.log_snapshot = NULL;
g_mdbg.log_storage = NULL;
g_mdbg.log_buffer_size = 0;
reset_log_snapshot();
}
static bool ensure_log_buffers(void) {
if (g_mdbg.log_storage && g_mdbg.log_snapshot && g_mdbg.log_buffer_size != 0)
return true;
free_log_buffers();
const char *title_id =
g_mdbg.game.title_id[0] != '\0' ? g_mdbg.game.title_id : "?";
uint64_t raw_size = sceKernelDebugGetLogBufferSize();
if (raw_size == 0) {
log_debug(" [MDBG] invalid log buffer size for %s: 0x%016" PRIx64,
title_id,
raw_size);
return false;
}
size_t buffer_size = (size_t)raw_size;
char *storage = malloc(buffer_size);
char *snapshot = malloc(buffer_size);
if (!storage || !snapshot) {
free(storage);
free(snapshot);
log_debug(" [MDBG] failed to allocate log buffers for %s: size=0x%zx",
title_id,
buffer_size);
return false;
}
g_mdbg.log_storage = storage;
g_mdbg.log_snapshot = snapshot;
g_mdbg.log_buffer_size = buffer_size;
reset_log_snapshot();
log_debug(" [MDBG] log monitor ready: %s buffer_size=0x%zx", title_id,
buffer_size);
return true;
}
static int fetch_log_text(const char **text_out, size_t *text_len_out) {
char *raw_text = NULL;
uint64_t raw_len = 0;
*text_out = NULL;
*text_len_out = 0;
int ret =
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
&raw_len);
if (ret < 0)
return ret;
if (!raw_text || raw_len == 0) {
return 0;
}
if (raw_len > g_mdbg.log_buffer_size)
return -EOVERFLOW;
*text_out = raw_text;
*text_len_out = (size_t)raw_len;
return 0;
}
static size_t find_log_overlap(const char *current, size_t current_len) {
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
current_len == 0) {
return 0;
}
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
? g_mdbg.log_snapshot_length
: current_len;
const char *search = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length -
max_overlap;
const char *end = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length;
while (search < end) {
const char *candidate =
memchr(search, current[0], (size_t)(end - search));
if (!candidate)
break;
size_t overlap = (size_t)(end - candidate);
if (!memcmp(candidate, current, overlap))
return overlap;
search = candidate + 1;
}
return 0;
}
static void update_log_snapshot(const char *text, size_t text_len) {
if (!g_mdbg.log_snapshot || g_mdbg.log_buffer_size == 0)
return;
if (!text || text_len == 0) {
g_mdbg.log_snapshot_length = 0;
return;
}
if (text_len > g_mdbg.log_buffer_size)
text_len = g_mdbg.log_buffer_size;
memcpy(g_mdbg.log_snapshot, text, text_len);
g_mdbg.log_snapshot_length = text_len;
}
static void clear_tracked_game(void) {
memset(&g_mdbg.game, 0, sizeof(g_mdbg.game));
free_log_buffers();
}
static bool matches_tracked_rtld_error(const char *text) {
return text &&
g_mdbg.game.rtld_error_prefix[0] != '\0' &&
strstr(text, g_mdbg.game.rtld_error_prefix) != NULL;
}
static void summarize_failure_reason(const char *reason, char *summary_out,
size_t summary_out_size,
bool is_rtld_error) {
if (!summary_out || summary_out_size == 0)
return;
summary_out[0] = '\0';
if (!reason || reason[0] == '\0')
return;
if (is_rtld_error) {
const char *open_paren = strrchr(reason, '(');
const char *close_paren =
open_paren ? strchr(open_paren + 1, ')') : NULL;
if (open_paren && close_paren && close_paren > open_paren + 1) {
int written = snprintf(summary_out, summary_out_size,
"can't load module %.*s after KStuff pause",
(int)(close_paren - open_paren - 1),
open_paren + 1);
if (written > 0 && (size_t)written < summary_out_size)
return;
}
(void)strlcpy(summary_out, "can't load module after KStuff pause",
summary_out_size);
return;
}
(void)strlcpy(summary_out, reason, summary_out_size);
}
static void handle_pre_pause_failure(const char *reason) {
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
"blame.",
g_mdbg.game.title_id);
clear_tracked_game();
}
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
now_us < g_mdbg.game.pause_time_us) {
handle_pre_pause_failure(reason);
return;
}
uint64_t post_pause_us = now_us - g_mdbg.game.pause_time_us;
if (post_pause_us > MDBG_AUTOTUNE_WINDOW_US) {
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
reason ? ": " : "", reason ? reason : "");
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
g_mdbg.game.title_id);
clear_tracked_game();
return;
}
char reason_summary[128];
bool is_rtld_error = matches_tracked_rtld_error(reason);
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
is_rtld_error);
uint32_t tuned_delay_seconds = 0;
if (upsert_kstuff_autotune_pause_delay(g_mdbg.game.title_id,
g_mdbg.game.pause_delay_seconds,
&tuned_delay_seconds)) {
log_debug(" [MDBG] autotune pause delay updated: %s=%us",
g_mdbg.game.title_id, tuned_delay_seconds);
if (reason_summary[0] != '\0')
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
if (is_rtld_error) {
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
g_mdbg.game.title_id,
reason_summary[0] != '\0' ? reason_summary
: "Can't load module after "
"KStuff pause",
tuned_delay_seconds);
} else {
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
"increased to %us. Launch the game again.",
g_mdbg.game.title_id, tuned_delay_seconds);
}
clear_tracked_game();
return;
}
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
"autotune delay.",
g_mdbg.game.title_id);
clear_tracked_game();
}
static void process_log_line(const char *line, uint64_t now_us) {
if (!matches_tracked_rtld_error(line))
return;
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
handle_post_pause_failure(line, now_us);
}
static void append_log_char(char ch, uint64_t now_us) {
if (!g_mdbg.game.active)
return;
if (ch == '\r' || ch == '\n') {
if (g_mdbg.log_line_length == 0)
return;
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
process_log_line(g_mdbg.log_line, now_us);
reset_log_line_buffer();
return;
}
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
process_log_line(g_mdbg.log_line, now_us);
reset_log_line_buffer();
}
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
}
static void poll_log_monitor(uint64_t now_us) {
if (!g_mdbg.game.active || !g_mdbg.game.log_monitoring_active) {
return;
}
if (!ensure_log_buffers()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
const char *text = NULL;
size_t text_len = 0;
int ret = fetch_log_text(&text, &text_len);
if (ret < 0) {
log_debug(" [MDBG] log snapshot failed for %s: 0x%08x",
g_mdbg.game.title_id, ret);
g_mdbg.game.log_monitoring_active = false;
reset_log_snapshot();
return;
}
size_t skip = 0;
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
skip = g_mdbg.log_snapshot_length;
} else {
skip = find_log_overlap(text, text_len);
if (skip == 0 && g_mdbg.log_snapshot_length != 0) {
log_debug(" [MDBG] log stream reset or truncated for %s",
g_mdbg.game.title_id);
reset_log_line_buffer();
}
}
for (size_t i = skip; i < text_len; ++i) {
append_log_char(text[i], now_us);
if (!g_mdbg.game.active)
return;
}
update_log_snapshot(text, text_len);
}
static void start_log_monitoring(void) {
if (!g_mdbg.game.active)
return;
if (!ensure_mdbg_privileges()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
if (!ensure_log_buffers()) {
g_mdbg.game.log_monitoring_active = false;
return;
}
g_mdbg.game.log_monitoring_active = true;
const char *text = NULL;
size_t text_len = 0;
int ret = fetch_log_text(&text, &text_len);
if (ret < 0) {
log_debug(" [MDBG] initial log snapshot failed for %s: 0x%08x",
g_mdbg.game.title_id, ret);
g_mdbg.game.log_monitoring_active = false;
reset_log_snapshot();
return;
}
update_log_snapshot(text, text_len);
reset_log_line_buffer();
}
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
if (!g_mdbg.game.active)
return;
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
handle_post_pause_failure("crash-candidate", now_us);
}
void sm_mdbg_init(void) {
memset(&g_mdbg, 0, sizeof(g_mdbg));
}
void sm_mdbg_shutdown(void) {
free_log_buffers();
memset(&g_mdbg, 0, sizeof(g_mdbg));
}
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
if (pid <= 0 || !title_id || title_id[0] == '\0')
return;
if (!sm_mdbg_enabled()) {
clear_tracked_game();
return;
}
if (g_mdbg.game.active && g_mdbg.game.pid != pid) {
log_debug(" [MDBG] replacing tracked game pid=%ld (%s) with pid=%ld (%s)",
(long)g_mdbg.game.pid, g_mdbg.game.title_id, (long)pid, title_id);
}
clear_tracked_game();
uint64_t now_us = monotonic_time_us();
g_mdbg.game.active = true;
g_mdbg.game.pid = pid;
g_mdbg.game.next_poll_us = now_us;
(void)strlcpy(g_mdbg.game.title_id, title_id, sizeof(g_mdbg.game.title_id));
int rtld_prefix_len =
snprintf(g_mdbg.game.rtld_error_prefix,
sizeof(g_mdbg.game.rtld_error_prefix),
"[rtld] <%ld> ERROR", (long)pid);
if (rtld_prefix_len <= 0 ||
(size_t)rtld_prefix_len >= sizeof(g_mdbg.game.rtld_error_prefix)) {
g_mdbg.game.rtld_error_prefix[0] = '\0';
}
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
g_mdbg.game.title_id, (long)pid, app_id);
}
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
uint32_t pause_delay_seconds) {
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
return;
g_mdbg.game.pause_seen = true;
g_mdbg.game.pause_time_us = pause_time_us;
g_mdbg.game.monitor_deadline_us =
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
g_mdbg.game.next_poll_us = pause_time_us;
start_log_monitoring();
}
void sm_mdbg_game_on_exit(pid_t pid) {
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
return;
clear_tracked_game();
}
void sm_mdbg_game_shutdown(void) {
clear_tracked_game();
}
uint64_t sm_mdbg_next_wake_us(uint64_t now_us) {
(void)now_us;
if (!sm_mdbg_enabled())
return 0;
if (!g_mdbg.game.active)
return 0;
uint64_t next_wake_us = g_mdbg.game.next_poll_us;
if (g_mdbg.game.monitor_deadline_us != 0 &&
(next_wake_us == 0 || g_mdbg.game.monitor_deadline_us < next_wake_us)) {
next_wake_us = g_mdbg.game.monitor_deadline_us;
}
return next_wake_us;
}
void sm_mdbg_poll(void) {
if (!sm_mdbg_enabled())
return;
if (!g_mdbg.game.active)
return;
uint64_t now_us = monotonic_time_us();
if (now_us < g_mdbg.game.next_poll_us) {
return;
}
if (g_mdbg.game.pause_seen && g_mdbg.game.monitor_deadline_us != 0 &&
now_us >= g_mdbg.game.monitor_deadline_us) {
log_debug(" [MDBG] crash monitoring window expired for %s pid=%ld",
g_mdbg.game.title_id, (long)g_mdbg.game.pid);
clear_tracked_game();
return;
}
if (!ensure_mdbg_privileges()) {
g_mdbg.game.next_poll_us = 0;
return;
}
poll_log_monitor(now_us);
if (!g_mdbg.game.active)
return;
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
uint64_t flags = 0;
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
if (is_mdbg_process_gone_error(ret)) {
clear_tracked_game();
return;
}
if (ret != 0)
return;
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
return;
handle_crash_candidate(flags, now_us);
}
+248
View File
@@ -0,0 +1,248 @@
#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_path_utils.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 ---
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
if (path_exists(devname) == 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 (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) {
*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) {
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;
}
+306
View File
@@ -0,0 +1,306 @@
#include "sm_platform.h"
#include "sm_path_state.h"
#include "sm_filesystem.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 backport_mount_blocked;
bool manual_missing_source_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) {
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) {
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 (!path_exists(g_path_state[k].path) &&
!g_path_state[k].manual_missing_source_logged) {
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].backport_mount_blocked &&
!g_path_state[k].manual_missing_source_logged &&
!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) {
*valid_out = 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;
}
*valid_out = entry->game_info_valid;
if (!entry->game_info_valid)
return true;
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
(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) {
struct PathStateEntry *entry = get_or_create_path_state(path);
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 (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
if (changed)
rebuild_path_state_hash();
}
void prune_path_state_for_root(const char *root) {
if (!root || root[0] == '\0') {
prune_path_state();
return;
}
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 (!path_matches_root_or_child(g_path_state[k].path, root))
continue;
if (path_exists(g_path_state[k].path))
continue;
if (g_path_state[k].manual_missing_source_logged)
continue;
memset(&g_path_state[k], 0, sizeof(g_path_state[k]));
changed = true;
}
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->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->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;
}
bool is_backport_mount_blocked(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = find_path_state(path);
return entry ? entry->backport_mount_blocked : false;
}
void note_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry)
return;
if (entry->backport_mount_blocked)
return;
entry->backport_mount_blocked = true;
}
void clear_backport_mount_failure(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->backport_mount_blocked = false;
}
bool note_manual_missing_source_once(const char *path) {
if (!path || path[0] == '\0')
return false;
struct PathStateEntry *entry = get_or_create_path_state(path);
if (!entry || entry->manual_missing_source_logged)
return false;
entry->manual_missing_source_logged = true;
return true;
}
void clear_manual_missing_source(const char *path) {
if (!path || path[0] == '\0')
return;
struct PathStateEntry *entry = find_path_state(path);
if (!entry)
return;
entry->manual_missing_source_logged = false;
}
+56
View File
@@ -0,0 +1,56 @@
#include "sm_platform.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
const char *get_filename_component(const char *path) {
const char *base = strrchr(path, '/');
if (!base)
base = strrchr(path, '\\');
return base ? base + 1 : path;
}
bool path_exists(const char *path) {
struct stat st;
return (path && path[0] != '\0' && stat(path, &st) == 0);
}
bool is_under_image_mount_base(const char *path) {
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
path[image_prefix_len] == '/');
}
bool path_matches_root_or_child(const char *path, const char *root) {
if (!path || !root || root[0] == '\0')
return false;
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
return false;
return path[root_len] == '\0' || path[root_len] == '/';
}
bool is_usb_storage_path(const char *path) {
static const char *usb_roots[] = {
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
"/mnt/ext0",
};
if (!path || path[0] == '\0')
return false;
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
if (path_matches_root_or_child(path, usb_roots[i]))
return true;
}
return false;
}
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
if (is_under_image_mount_base(scan_path))
return false;
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
DEFAULT_BACKPORTS_DIR_NAME);
return written >= 0 && (size_t)written < MAX_PATH;
}
+881
View File
@@ -0,0 +1,881 @@
#include "sm_platform.h"
#include "sm_runtime.h"
#include "sm_scan.h"
#include "sm_scan_tree.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"
#include "sm_install_queue.h"
#include "sm_manual.h"
typedef struct {
char discovered_param_roots[MAX_PENDING][MAX_PATH];
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
bool checked_appmeta_present[MAX_PENDING];
int checked_appmeta_count;
int blocked_ppsa_uninstall_count;
} scan_workspace_t;
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
// state on the stack each cycle.
static scan_workspace_t g_scan_workspace;
static void reset_scan_workspace(void) {
g_scan_workspace.checked_appmeta_count = 0;
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
}
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
if (!title_id || title_id[0] == '\0')
return false;
for (int i = 0; i < g_scan_workspace.blocked_ppsa_uninstall_count; i++) {
if (strcmp(g_scan_workspace.blocked_ppsa_uninstall_titles[i], title_id) == 0)
return true;
}
return false;
}
static void remember_blocked_ppsa_uninstall(const char *title_id) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return;
if (g_scan_workspace.blocked_ppsa_uninstall_count >= MAX_PENDING)
return;
int slot = g_scan_workspace.blocked_ppsa_uninstall_count++;
(void)strlcpy(g_scan_workspace.blocked_ppsa_uninstall_titles[slot], title_id,
sizeof(g_scan_workspace.blocked_ppsa_uninstall_titles[slot]));
}
static bool is_blocked_ppsa_title(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready, const char *title_id) {
return blocked_ppsa_titles_ready &&
app_db_title_list_contains(blocked_ppsa_titles, title_id);
}
static void request_blocked_ppsa_uninstall(const char *title_id,
const char *source_path) {
if (!title_id || title_id[0] == '\0' ||
blocked_ppsa_uninstall_requested(title_id))
return;
int res = sceAppInstUtilAppUnInstall(title_id);
if (res == 0) {
remember_blocked_ppsa_uninstall(title_id);
sm_install_forget_pending_title(title_id);
invalidate_app_db_title_cache();
if (source_path && source_path[0] != '\0') {
log_debug(" [REG] requested blocked PPSA uninstall: %s source=%s",
title_id, source_path);
} else {
log_debug(" [REG] requested blocked PPSA uninstall: %s", title_id);
}
return;
}
log_debug(" [REG] blocked PPSA uninstall failed: %s code=0x%08X", title_id,
(uint32_t)res);
}
static bool get_appmeta_present_for_scan_cycle(const char *title_id) {
for (int i = 0; i < g_scan_workspace.checked_appmeta_count; i++) {
if (strcmp(g_scan_workspace.checked_appmeta_titles[i], title_id) == 0)
return g_scan_workspace.checked_appmeta_present[i];
}
bool present = has_appmeta_data(title_id);
if (g_scan_workspace.checked_appmeta_count < MAX_PENDING) {
int slot = g_scan_workspace.checked_appmeta_count++;
(void)strlcpy(g_scan_workspace.checked_appmeta_titles[slot], title_id,
sizeof(g_scan_workspace.checked_appmeta_titles[slot]));
g_scan_workspace.checked_appmeta_present[slot] = present;
}
return present;
}
static bool is_under_discovered_param_root(
const char *path, char discovered_param_roots[][MAX_PATH],
int discovered_count) {
for (int i = 0; i < discovered_count; i++) {
const char *root = discovered_param_roots[i];
size_t root_len = strlen(root);
if (strncmp(path, root, root_len) != 0)
continue;
if (path[root_len] == '\0' || path[root_len] == '/')
return true;
}
return false;
}
// --- Candidate Discovery ---
typedef struct {
char title_id[MAX_TITLE_ID];
char title_name[MAX_TITLE_NAME];
} directory_candidate_info_t;
typedef enum {
DIRECTORY_CANDIDATE_DESCEND = 0,
DIRECTORY_CANDIDATE_SKIP_DESCEND,
DIRECTORY_CANDIDATE_READY,
} directory_candidate_probe_t;
typedef enum {
EXISTING_DIRECTORY_CONTINUE = 0,
EXISTING_DIRECTORY_HANDLED,
EXISTING_DIRECTORY_PREFER_CURRENT,
EXISTING_DIRECTORY_PREFER_CACHED,
} existing_directory_result_t;
typedef struct {
const struct AppDbTitleList *titles;
const struct AppDbTitleList *blocked_ppsa_titles;
bool titles_ready;
bool blocked_ppsa_titles_ready;
} scan_app_db_context_t;
static directory_candidate_probe_t probe_directory_candidate(
const char *full_path, char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
directory_candidate_info_t *info_out) {
struct stat param_st;
if (!allow_known_param_root &&
is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
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 DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (!directory_has_param_json(full_path, &param_st)) {
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 DIRECTORY_CANDIDATE_DESCEND;
}
if (!get_game_info(full_path, &param_st, info_out->title_id,
info_out->title_name)) {
record_missing_param_failure(full_path);
log_debug(" [SKIP] game info unavailable: %s", full_path);
return DIRECTORY_CANDIDATE_SKIP_DESCEND;
}
if (*discovered_param_root_count < MAX_PENDING &&
!is_under_discovered_param_root(full_path, discovered_param_roots,
*discovered_param_root_count)) {
(void)strlcpy(discovered_param_roots[*discovered_param_root_count], full_path,
MAX_PATH);
(*discovered_param_root_count)++;
}
clear_missing_param_entry(full_path);
return DIRECTORY_CANDIDATE_READY;
}
static int find_scan_candidate_index_by_title_id(scan_candidate_t *candidates,
int candidate_count,
const char *title_id) {
for (int i = 0; i < candidate_count; i++) {
if (strcmp(candidates[i].title_id, title_id) == 0)
return i;
}
return -1;
}
static void mark_scan_candidate_manual(scan_candidate_t *candidate,
const char *manual_source_path) {
if (!candidate || !manual_source_path || manual_source_path[0] == '\0')
return;
candidate->manual = true;
(void)strlcpy(candidate->manual_source_path, manual_source_path,
sizeof(candidate->manual_source_path));
}
static void remove_scan_candidate_at(scan_candidate_t *candidates,
int *candidate_count, int index) {
int trailing_count = *candidate_count - index - 1;
if (trailing_count > 0) {
memmove(&candidates[index], &candidates[index + 1],
(size_t)trailing_count * sizeof(candidates[0]));
}
(*candidate_count)--;
}
static void notify_duplicate_scan_candidate(const char *title_id,
const char *ignored_path,
const char *existing_path) {
notify_duplicate_title_once(title_id, ignored_path, existing_path);
}
static existing_directory_result_t handle_existing_directory_candidate(
const char *full_path, const scan_app_db_context_t *app_db,
const directory_candidate_info_t *info,
bool *installed_out, bool *in_app_db_out,
char preferred_existing_path_out[MAX_PATH]) {
char tracked_path[MAX_PATH];
preferred_existing_path_out[0] = '\0';
bool has_tracked_path =
read_mount_link(info->title_id, tracked_path, sizeof(tracked_path));
bool link_matches_source =
has_tracked_path && strcmp(tracked_path, full_path) == 0;
if (is_blocked_ppsa_title(app_db->blocked_ppsa_titles,
app_db->blocked_ppsa_titles_ready,
info->title_id)) {
request_blocked_ppsa_uninstall(info->title_id, full_path);
return EXISTING_DIRECTORY_HANDLED;
}
if (!app_db->titles_ready) {
if (link_matches_source && is_data_mounted(info->title_id)) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
return EXISTING_DIRECTORY_HANDLED;
}
bool in_app_db = app_db_title_list_contains(app_db->titles, info->title_id);
bool installed = in_app_db && is_installed(info->title_id);
bool appmeta_present =
installed ? get_appmeta_present_for_scan_cycle(info->title_id) : false;
link_matches_source = installed && link_matches_source;
bool source_valid = false;
if (link_matches_source) {
source_valid = is_data_mounted(info->title_id);
if (!source_valid && mount_title_nullfs(info->title_id, full_path))
source_valid = is_data_mounted(info->title_id);
}
if (source_valid && appmeta_present) {
cache_game_entry(full_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CURRENT;
}
if (installed && appmeta_present && has_tracked_path &&
strcmp(tracked_path, full_path) != 0 &&
is_data_mounted(info->title_id)) {
(void)strlcpy(preferred_existing_path_out, tracked_path, MAX_PATH);
cache_game_entry(tracked_path, info->title_id, info->title_name);
return EXISTING_DIRECTORY_PREFER_CACHED;
}
if (!in_app_db && is_title_install_pending(info->title_id))
return EXISTING_DIRECTORY_HANDLED;
if (!in_app_db && was_register_attempted(info->title_id)) {
return EXISTING_DIRECTORY_HANDLED;
}
*installed_out = installed;
*in_app_db_out = in_app_db;
return EXISTING_DIRECTORY_CONTINUE;
}
static bool enqueue_directory_candidate(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const directory_candidate_info_t *info, bool installed,
bool in_app_db, const char *manual_source_path,
bool *unstable_found_out) {
char metadata_path[MAX_PATH];
uint8_t failed_attempts = get_failed_mount_attempts(info->title_id);
if (failed_attempts >= MAX_FAILED_MOUNT_ATTEMPTS) {
log_debug(" [SKIP] mount/register retry limit reached (%u/%u): %s (%s)",
(unsigned)failed_attempts, (unsigned)MAX_FAILED_MOUNT_ATTEMPTS,
info->title_name, info->title_id);
return true;
}
int written =
snprintf(metadata_path, sizeof(metadata_path), "%s/sce_sys", full_path);
if (written < 0 || (size_t)written >= sizeof(metadata_path)) {
log_debug(" [SKIP] metadata path too long: %s (%s)", info->title_name,
full_path);
return true;
}
if (!wait_for_stability_fast(metadata_path, info->title_name)) {
if (unstable_found_out)
*unstable_found_out = true;
log_debug(" [SKIP] source not stable yet: %s (%s)", info->title_name,
full_path);
return true;
}
if (*candidate_count >= max_candidates) {
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
info->title_name, info->title_id);
return true;
}
(void)strlcpy(candidates[*candidate_count].path, full_path,
sizeof(candidates[*candidate_count].path));
(void)strlcpy(candidates[*candidate_count].title_id, info->title_id,
sizeof(candidates[*candidate_count].title_id));
(void)strlcpy(candidates[*candidate_count].title_name, info->title_name,
sizeof(candidates[*candidate_count].title_name));
candidates[*candidate_count].manual_source_path[0] = '\0';
candidates[*candidate_count].installed = installed;
candidates[*candidate_count].in_app_db = in_app_db;
candidates[*candidate_count].manual = false;
mark_scan_candidate_manual(&candidates[*candidate_count], manual_source_path);
(*candidate_count)++;
return true;
}
static bool try_collect_candidate_for_directory(
const char *full_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool allow_known_param_root,
const char *manual_source_path, bool *unstable_found_out) {
directory_candidate_info_t info;
directory_candidate_probe_t probe_result =
probe_directory_candidate(full_path, discovered_param_roots,
discovered_param_root_count,
allow_known_param_root, &info);
if (probe_result == DIRECTORY_CANDIDATE_SKIP_DESCEND)
return true;
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
return false;
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
candidates, *candidate_count, info.title_id);
const char *duplicate_candidate_path =
duplicate_candidate_index >= 0 ? candidates[duplicate_candidate_index].path
: NULL;
bool duplicate_candidate_same_path =
duplicate_candidate_path && strcmp(duplicate_candidate_path, full_path) == 0;
bool installed = false;
bool in_app_db = false;
char preferred_existing_path[MAX_PATH];
existing_directory_result_t existing_result =
handle_existing_directory_candidate(full_path, app_db, &info,
&installed, &in_app_db,
preferred_existing_path);
if (existing_result == EXISTING_DIRECTORY_PREFER_CURRENT) {
if (manual_source_path)
sm_manual_note_installed(manual_source_path, info.title_id,
info.title_name);
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, duplicate_candidate_path,
full_path);
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
}
return true;
}
if (existing_result == EXISTING_DIRECTORY_PREFER_CACHED) {
notify_duplicate_scan_candidate(info.title_id, full_path,
preferred_existing_path);
if (duplicate_candidate_index >= 0)
remove_scan_candidate_at(candidates, candidate_count,
duplicate_candidate_index);
return true;
}
if (existing_result == EXISTING_DIRECTORY_HANDLED) {
if (duplicate_candidate_index >= 0 && !duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
if (duplicate_candidate_index >= 0)
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
if (duplicate_candidate_index >= 0) {
if (!duplicate_candidate_same_path)
notify_duplicate_scan_candidate(info.title_id, full_path,
duplicate_candidate_path);
mark_scan_candidate_manual(&candidates[duplicate_candidate_index],
manual_source_path);
return true;
}
return enqueue_directory_candidate(full_path, candidates, max_candidates,
candidate_count, &info, installed,
in_app_db, manual_source_path,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
const char *manual_source_path;
bool *unstable_found_out;
} collect_candidates_walk_ctx_t;
static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
if (depth_from_root == 0u)
return SM_SCAN_TREE_DIR_DESCEND;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
if (try_collect_candidate_for_directory(
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, false, ctx->manual_source_path,
ctx->unstable_found_out)) {
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
}
return SM_SCAN_TREE_DIR_DESCEND;
}
static bool collect_candidate_image_visit(const char *image_path,
const char *image_name,
unsigned int depth_from_root,
void *ctx_ptr) {
(void)depth_from_root;
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out);
return true;
}
static void collect_scan_candidates_from_manual_root(
const char *scan_path, const char *manual_source_path,
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
bool try_root_candidate = true;
if (is_under_image_mount_base(scan_path)) {
struct stat param_st;
try_root_candidate = directory_has_param_json(scan_path, &param_st);
}
if (try_root_candidate) {
if (try_collect_candidate_for_directory(
scan_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count, true,
manual_source_path, unstable_found_out)) {
return;
}
}
unsigned int scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = manual_source_path,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = NULL,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_path(
const char *manual_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
struct stat st;
if (stat(manual_path, &st) != 0) {
if (note_manual_missing_source_once(manual_path))
log_debug(" [MANUAL] source unavailable: %s", manual_path);
return;
}
clear_manual_missing_source(manual_path);
const char *name = get_filename_component(manual_path);
if (S_ISREG(st.st_mode) && is_supported_image_file_name(name)) {
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
return;
char mount_point[MAX_PATH];
get_image_mount_point_for_source(manual_path, mount_point);
collect_scan_candidates_from_manual_root(
mount_point, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
return;
}
struct stat param_st;
if (!S_ISDIR(st.st_mode) || !directory_has_param_json(manual_path, &param_st)) {
log_debug(" [MANUAL] expected game directory or image file: %s",
manual_path);
return;
}
collect_scan_candidates_from_manual_root(
manual_path, manual_path, candidates, max_candidates, candidate_count,
app_db, discovered_param_roots, discovered_param_root_count,
unstable_found_out);
}
typedef struct {
scan_candidate_t *candidates;
int max_candidates;
int *candidate_count;
const scan_app_db_context_t *app_db;
char (*discovered_param_roots)[MAX_PATH];
int *discovered_param_root_count;
bool *unstable_found_out;
int path_count;
} manual_scan_ctx_t;
static bool collect_manual_path_visit(const char *manual_path, void *ctx_ptr) {
manual_scan_ctx_t *ctx = (manual_scan_ctx_t *)ctx_ptr;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
if (*ctx->candidate_count >= ctx->max_candidates) {
log_debug(" [MANUAL] candidate queue full (%d), remaining sources deferred",
ctx->max_candidates);
return false;
}
ctx->path_count++;
collect_scan_candidates_from_manual_path(
manual_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
ctx->app_db, ctx->discovered_param_roots,
ctx->discovered_param_root_count, ctx->unstable_found_out);
return true;
}
static bool build_backport_path_for_scan_root(const char *title_id,
const char *scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0' || !scan_path || scan_path[0] == '\0')
return false;
char backport_root[MAX_PATH];
if (!build_backports_root_path(scan_path, backport_root))
return false;
int written = snprintf(backport_path, MAX_PATH, "%s/%s", backport_root,
title_id);
return written > 0 && (size_t)written < MAX_PATH;
}
static bool backport_path_is_dir(const char *backport_path) {
struct stat st;
return backport_path && stat(backport_path, &st) == 0 &&
S_ISDIR(st.st_mode);
}
static bool resolve_backport_path(const char *title_id,
const char *owning_scan_path,
char backport_path[MAX_PATH]) {
if (!title_id || title_id[0] == '\0')
return false;
backport_path[0] = '\0';
if (build_backport_path_for_scan_root(title_id, owning_scan_path,
backport_path) &&
backport_path_is_dir(backport_path)) {
return true;
}
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
continue;
char candidate_path[MAX_PATH];
if (!build_backport_path_for_scan_root(title_id, scan_path, candidate_path))
continue;
if (!backport_path_is_dir(candidate_path))
continue;
(void)strlcpy(backport_path, candidate_path, MAX_PATH);
return true;
}
backport_path[0] = '\0';
return false;
}
typedef struct {
bool *unstable_found_out;
} backport_overlay_ctx_t;
static bool mount_backport_overlay_for_cached_game(const char *source_path,
const char *title_id,
const char *title_name,
const char *owning_scan_root,
void *ctx_ptr) {
(void)title_name;
if (should_stop_requested() || runtime_sleep_mode_active())
return false;
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
char backport_path[MAX_PATH];
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
return true;
}
if (is_backport_mount_blocked(backport_path))
return true;
bool overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
if (!wait_for_stability_fast(backport_path, "BKP")) {
if (ctx->unstable_found_out)
*ctx->unstable_found_out = true;
return true;
}
overlay_active = false;
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
&overlay_active) ||
overlay_active) {
return true;
}
char system_ex_path[MAX_PATH];
snprintf(system_ex_path, sizeof(system_ex_path), "/system_ex/app/%s",
title_id);
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
note_backport_mount_failure(backport_path);
return true;
}
clear_backport_mount_failure(backport_path);
return true;
}
void mount_backport_overlays(bool *unstable_found_out) {
backport_overlay_ctx_t ctx = {
.unstable_found_out = unstable_found_out,
};
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
}
// --- 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();
}
void cleanup_lost_sources_for_scan_root(const char *scan_root) {
prune_game_cache_for_root(scan_root);
cleanup_mount_links(scan_root, true);
cleanup_stale_image_mounts_for_root(scan_root);
prune_path_state_for_root(scan_root);
}
void unmount_usb_sources_for_suspend(void) {
log_debug("[SLEEP] USB-backed mount cleanup begin");
cleanup_usb_mount_links_for_suspend();
if (!unmount_usb_image_mounts_for_suspend()) {
log_debug("[SLEEP] some USB-backed image mounts remain busy");
}
cleanup_mount_dirs();
log_debug("[SLEEP] USB-backed mount cleanup done");
}
static void collect_scan_candidates_from_root(
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
int *candidate_count, const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH],
int *discovered_param_root_count, bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
unsigned int scan_depth = runtime_config()->scan_depth;
if (scan_depth < MIN_SCAN_DEPTH)
scan_depth = MIN_SCAN_DEPTH;
collect_candidates_walk_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.manual_source_path = NULL,
.unstable_found_out = unstable_found_out,
};
sm_scan_tree_callbacks_t callbacks = {
.on_directory = collect_candidate_directory_visit,
.on_image_file = collect_candidate_image_visit,
};
(void)sm_scan_tree_walk(scan_path, scan_path, 0u, scan_depth, &callbacks, &ctx);
}
static void collect_scan_candidates_from_manual_list(
scan_candidate_t *candidates, int max_candidates, int *candidate_count,
const scan_app_db_context_t *app_db,
char discovered_param_roots[][MAX_PATH], int *discovered_param_root_count,
bool *unstable_found_out) {
if (should_stop_requested() || runtime_sleep_mode_active())
return;
(void)sm_manual_reconcile_deleted_titles(app_db->titles,
app_db->titles_ready);
manual_scan_ctx_t ctx = {
.candidates = candidates,
.max_candidates = max_candidates,
.candidate_count = candidate_count,
.app_db = app_db,
.discovered_param_roots = discovered_param_roots,
.discovered_param_root_count = discovered_param_root_count,
.unstable_found_out = unstable_found_out,
.path_count = 0,
};
if (!sm_manual_for_each_path(collect_manual_path_visit, &ctx) ||
ctx.path_count <= 0) {
return;
}
log_debug(" [MANUAL] scanned %d manual source(s)", ctx.path_count);
}
static void uninstall_blocked_ppsa_titles(
const struct AppDbTitleList *blocked_ppsa_titles,
bool blocked_ppsa_titles_ready) {
if (!blocked_ppsa_titles_ready || !blocked_ppsa_titles)
return;
int requested_before = g_scan_workspace.blocked_ppsa_uninstall_count;
for (int i = 0; i < blocked_ppsa_titles->count &&
!should_stop_requested() && !runtime_sleep_mode_active();
i++) {
request_blocked_ppsa_uninstall(blocked_ppsa_titles->ids[i], NULL);
}
int requested = g_scan_workspace.blocked_ppsa_uninstall_count - requested_before;
if (requested > 0)
log_debug(" [REG] requested %d blocked PPSA uninstall(s)", requested);
}
int collect_scan_candidates_for_scan_root(const char *scan_root,
scan_candidate_t *candidates,
int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
collect_scan_candidates_from_root(scan_root, candidates, max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
int *total_found_out,
bool *unstable_found_out) {
reset_scan_workspace();
int candidate_count = 0;
struct AppDbTitleList app_db_titles = {0};
struct AppDbTitleList blocked_ppsa_titles = {0};
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
bool blocked_ppsa_titles_ready =
get_app_db_blocked_uninstall_ppsa_list(&blocked_ppsa_titles);
int discovered_param_root_count = 0;
if (!app_db_titles_ready)
log_debug(" [DB] app.db title list unavailable for this scan cycle");
if (!blocked_ppsa_titles_ready)
log_debug(" [DB] blocked PPSA uninstall list unavailable for this scan cycle");
scan_app_db_context_t app_db = {
.titles = &app_db_titles,
.blocked_ppsa_titles = &blocked_ppsa_titles,
.titles_ready = app_db_titles_ready,
.blocked_ppsa_titles_ready = blocked_ppsa_titles_ready,
};
for (int i = 0; i < get_scan_path_count(); i++) {
if (should_stop_requested() || runtime_sleep_mode_active())
break;
collect_scan_candidates_from_root(get_scan_path(i), candidates,
max_candidates,
&candidate_count, &app_db,
g_scan_workspace.discovered_param_roots,
&discovered_param_root_count,
unstable_found_out);
}
collect_scan_candidates_from_manual_list(
candidates, max_candidates, &candidate_count, &app_db,
g_scan_workspace.discovered_param_roots, &discovered_param_root_count,
unstable_found_out);
uninstall_blocked_ppsa_titles(&blocked_ppsa_titles,
blocked_ppsa_titles_ready);
if (total_found_out)
*total_found_out = discovered_param_root_count;
free_app_db_title_list(&blocked_ppsa_titles);
free_app_db_title_list(&app_db_titles);
return candidate_count;
}
+121
View File
@@ -0,0 +1,121 @@
#include "sm_platform.h"
#include "sm_scan_tree.h"
#include "sm_config_mount.h"
#include "sm_filesystem.h"
#include "sm_image.h"
#include "sm_path_utils.h"
#include "sm_paths.h"
#include "sm_runtime.h"
static void classify_scan_tree_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);
}
}
*is_dir_out = is_dir;
*is_regular_out = is_regular;
}
static bool is_distinct_configured_scan_root(const char *current_scan_root,
const char *path) {
for (int i = 0; i < get_scan_path_count(); i++) {
const char *scan_path = get_scan_path(i);
if (strcmp(scan_path, path) != 0)
continue;
if (strcmp(current_scan_root, path) == 0)
return false;
return true;
}
return false;
}
bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
unsigned int depth_from_root,
unsigned int remaining_depth,
const sm_scan_tree_callbacks_t *callbacks, void *ctx) {
if (should_stop_requested() || runtime_sleep_mode_active())
return true;
if (depth_from_root > 0u &&
is_distinct_configured_scan_root(scan_root, dir_path)) {
return true;
}
sm_scan_tree_dir_visit_t dir_visit = SM_SCAN_TREE_DIR_DESCEND;
if (callbacks->on_directory) {
dir_visit = callbacks->on_directory(dir_path, depth_from_root, ctx);
if (dir_visit == SM_SCAN_TREE_DIR_ABORT)
return false;
if (dir_visit == SM_SCAN_TREE_DIR_SKIP_DESCEND)
return true;
}
if (remaining_depth == 0u)
return true;
DIR *d = opendir(dir_path);
if (!d)
return true;
bool skip_backports_root =
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
bool allow_image_file_visits =
callbacks->on_image_file &&
!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE);
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (should_stop_requested() || runtime_sleep_mode_active()) {
closedir(d);
return true;
}
if (entry->d_name[0] == '.')
continue;
if (skip_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", dir_path, entry->d_name);
bool is_dir = false;
bool is_regular = false;
classify_scan_tree_entry(full_path, entry->d_type, &is_dir, &is_regular);
if (allow_image_file_visits && is_regular &&
is_supported_image_file_name(entry->d_name)) {
if (!callbacks->on_image_file(full_path, entry->d_name, depth_from_root + 1u,
ctx)) {
closedir(d);
return false;
}
}
if (!is_dir)
continue;
if (!sm_scan_tree_walk(scan_root, full_path, depth_from_root + 1u,
remaining_depth - 1u, callbacks, ctx)) {
closedir(d);
return false;
}
}
closedir(d);
return true;
}
+1674
View File
File diff suppressed because it is too large. Load diff
+789
View File
@@ -0,0 +1,789 @@
#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_scan.h"
#include "sm_shellcore_flags.h"
#define SHELLCORE_FLAG_WAITMODE_OR 2u
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
typedef intptr_t sm_kernel_event_flag_t;
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 required;
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 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 shellcore_flag_monitor_t g_shellcore_flags[] = {
{.name = "SceShellCoreUtilAppFocus", .handle = -1, .required = true},
{.name = "SceSystemStateMgrInfo", .handle = -1, .required = true},
{.name = "SceLncUtilSystemStatus", .handle = -1, .required = false},
{.name = "SceSystemStateMgrStatus", .handle = -1},
/* {.name = "SceSysCoreSuspend", .handle = -1},
{.name = "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;
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
{0x0000000000000004ULL, "BD_DRIVE_READY"},
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
{0x0000000000000020ULL, "TICK_0x20"},
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
{0x0000000000000200ULL, "REBOOT_REQUEST"},
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
{SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS,
"SHELLUI_SHUTDOWN_IN_PROGRESS"},
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
};
#if 0
static const shellcore_flag_bit_desc_t g_power_control_bits[] = {
{0x0000000000020000ULL, "SCREEN_SAVER_ON"},
{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_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 (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, "SceSystemStateMgrStatus") == 0) {
if (count_out) {
*count_out = sizeof(g_system_state_mgr_status_bits) /
sizeof(g_system_state_mgr_status_bits[0]);
}
return g_system_state_mgr_status_bits;
}
#if 0
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
if (count_out)
*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, "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 enter_sleep_mode_and_cleanup(const char *reason) {
if (request_runtime_sleep_mode(true, reason)) {
runtime_mount_state_lock();
unmount_usb_sources_for_suspend();
runtime_mount_state_unlock();
}
}
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
if (!flag)
return;
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 bool open_shellcore_flags(size_t *opened_count_out) {
size_t opened_count = 0;
size_t required_count = 0;
size_t required_opened = 0;
if (opened_count_out)
*opened_count_out = 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;
if (flag->required)
required_count++;
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;
if (flag->required)
required_opened++;
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++;
}
if (opened_count_out)
*opened_count_out = opened_count;
return required_opened == required_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_main_on_standby = false;
bool entered_suspend_on_going = false;
bool entered_resume_working = false;
bool entered_shellui_shutdown_in_progress = false;
bool is_system_state_mgr_info = false;
unsigned current_state = 0;
unsigned previous_state = 0;
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_main_on_standby =
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
entered_suspend_on_going =
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
(!flag->has_last_pattern ||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
entered_resume_working =
flag->has_last_pattern &&
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
entered_shellui_shutdown_in_progress =
(result_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
(!flag->has_last_pattern ||
(flag->last_pattern &
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
}
flag->last_pattern = result_pattern;
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_main_on_standby || entered_suspend_on_going) {
const char *sleep_reason =
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
enter_sleep_mode_and_cleanup(sleep_reason);
}
if (entered_shellui_shutdown_in_progress) {
enter_sleep_mode_and_cleanup(
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
}
if (entered_resume_working) {
request_scan_now("SceSystemStateMgrInfo=WORKING");
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
}
}
static void *shellcore_flag_thread_main(void *arg) {
(void)arg;
size_t opened_count = 0;
if (!open_shellcore_flags(&opened_count)) {
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();
}
+49
View File
@@ -0,0 +1,49 @@
#include "sm_platform.h"
#include "sm_stability.h"
#include "sm_config_mount.h"
#include "sm_log.h"
#include "sm_types.h"
static time_t get_path_last_change_time(const struct stat *st) {
if (!st)
return 0;
return (st->st_ctime > st->st_mtime) ? st->st_ctime : st->st_mtime;
}
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, get_path_last_change_time(&st));
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;
}
+12
View File
@@ -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;
}
+164
View File
@@ -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) {
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) {
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->register_attempts < UINT8_MAX)
entry->register_attempts++;
}
void clear_register_attempts(const char *title_id) {
struct TitleStateEntry *entry = find_title_state(title_id);
if (entry)
entry->register_attempts = 0;
}
void notify_duplicate_title_once(const char *title_id, const char *path_a,
const char *path_b) {
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
if (entry->duplicate_notified_once)
return;
entry->duplicate_notified_once = true;
notify_system("Duplicate %s ignored:\n%s\nexisting: %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->mount_reg_attempts < UINT8_MAX)
entry->mount_reg_attempts++;
return entry->mount_reg_attempts;
}