mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-19 21:29:58 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3530d8d88e | ||
|
|
4dafdde002 | ||
|
|
8566c0294c | ||
|
|
56dd338abd | ||
|
|
090723879c | ||
|
|
16873e01f2 | ||
|
|
f20b97db65 | ||
|
|
9d6ab61369 | ||
|
|
25182da557 | ||
|
|
72cf4460a9 | ||
|
|
cc0f4e0684 | ||
|
|
8cdcd112f5 | ||
|
|
c41c3eeba8 | ||
|
|
3d6a300742 | ||
|
|
51d01011ff | ||
|
|
9e803cff7f | ||
|
|
dee175e2bd | ||
|
|
860e9de3b1 | ||
|
|
e0b41b3ca6 | ||
|
|
5c4470cdcd | ||
|
|
5b6be76272 | ||
|
|
c66bff6d2a | ||
|
|
2df61d7e2d | ||
|
|
e960fc8079 | ||
|
|
d1c6978e78 | ||
|
|
4eadb06f57 | ||
|
|
78f536b41a | ||
|
|
99e000eed9 | ||
|
|
8622d8757f | ||
|
|
bcffa18997 | ||
|
|
e00dda0075 | ||
|
|
315265f5db | ||
|
|
30b04358f4 | ||
|
|
a6ad4922a9 | ||
|
|
ebd649e0ea | ||
|
|
4072ced15d | ||
|
|
fee1029b0a | ||
|
|
cbd0fce4f9 | ||
|
|
9fa6ad94de | ||
|
|
d4c4cda739 | ||
|
|
954b3a7d17 | ||
|
|
3652f506c0 | ||
|
|
6c188a03bb | ||
|
|
2680ed28d7 | ||
|
|
f401660113 | ||
|
|
11a135cf1c | ||
|
|
146942c138 | ||
|
|
a8f1a216bd | ||
|
|
c37088543d | ||
|
|
059f8b40f1 | ||
|
|
6f61e8d29e | ||
|
|
a5471da456 | ||
|
|
0abfecd3e7 | ||
|
|
9f1786905c | ||
|
|
fa714f868f | ||
|
|
17bb561e8d | ||
|
|
0efeec16fa | ||
|
|
a584121d76 | ||
|
|
45da514549 | ||
|
|
e272c9736c | ||
|
|
902a6917df | ||
|
|
6df8a472e3 | ||
|
|
4e9afa7885 | ||
|
|
40967215b4 | ||
|
|
a23822a26f | ||
|
|
97ffedf0df | ||
|
|
bdd3083d65 | ||
|
|
bcb906e5b6 | ||
|
|
47a033b50b | ||
|
|
47bded61da | ||
|
|
5d0ca6d5d1 | ||
|
|
ebfe0376c6 | ||
|
|
a6a5437a42 | ||
|
|
8c1ebbdf28 | ||
|
|
853ff28b85 | ||
|
|
e9098631c1 | ||
|
|
bf24df419b | ||
|
|
ff70cb1bc8 | ||
|
|
9254525a6c | ||
|
|
26ba93c9aa | ||
|
|
43d61dce37 | ||
|
|
7759b83293 | ||
|
|
0eca3fa138 | ||
|
|
e30a296a53 | ||
|
|
b374c3163e | ||
|
|
a71a03d0db | ||
|
|
56e8ba2185 | ||
|
|
18a97ecca9 | ||
|
|
270264cc75 | ||
|
|
ef814ad56b | ||
|
|
cec94656c6 | ||
|
|
4ff9a3dffe | ||
|
|
fe7dad099c | ||
|
|
c02f237ddf | ||
|
|
d318b5e607 | ||
|
|
6f97cde602 | ||
|
|
edcac6bdd7 | ||
|
|
27feba2085 | ||
|
|
82603fce7d | ||
|
|
01b64346c8 | ||
|
|
7e48103149 | ||
|
|
124e757199 | ||
|
|
0fcc970a20 | ||
|
|
7684e8431d | ||
|
|
3166434db1 | ||
|
|
c785daaddf | ||
|
|
2db6b49439 | ||
|
|
aff2fc0e6c | ||
|
|
ae36dd5871 | ||
|
|
cc3e07d201 | ||
|
|
2c03ede25c |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
custom:
|
||||
- https://github.com/drakmor/shadowMountPlus#support-development
|
||||
@@ -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
|
||||
@@ -0,0 +1,130 @@
|
||||
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
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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 }}
|
||||
@@ -4,16 +4,19 @@ 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 -Wextra -Wstrict-prototypes -Wmissing-prototypes -Werror=strict-prototypes -Werror=missing-prototypes -D_BSD_SOURCE -std=gnu11 -Iinclude -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 -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
|
||||
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
|
||||
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
@@ -22,16 +25,23 @@ HEADERS := $(wildcard include/*.h)
|
||||
all: shadowmountplus.elf
|
||||
|
||||
# Build Daemon
|
||||
shadowmountplus.elf: $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-debug $@
|
||||
rm -f src/notify_icon_asset.c
|
||||
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 src/notify_icon_asset.c
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
@@ -1,16 +1,26 @@
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Version:** `1.6beta3`
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
**Discord:** https://discord.gg/x2Ppvzwjhm
|
||||
|
||||
|
||||
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
|
||||
|
||||
|
||||
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
|
||||
|
||||
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **Kstuff v1.6.6+** or **Kstuff-lite v1.0+**.
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
|
||||
|
||||
|
||||
## 💜 Support Development
|
||||
|
||||
If you want to support this project, you can donate
|
||||
- USDT (TRC-20): **`TKaUGEwMm9KBXzEoiaaKYBX2yCHAKASW3p`**
|
||||
- USDT (ERC-20): **`0x313dD245dBA957A5560618eA882d08e66aaFb430`**
|
||||
- USDC (Solana): **`5kv7j2RbUGaSP1kU1cZWj9jHH7d6rfvxmK6YXTYbH4um`**
|
||||
|
||||
|
||||
|
||||
## Current image support
|
||||
@@ -19,42 +29,58 @@
|
||||
|
||||
| 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 |
|
||||
| `.ffpfsc` | `pfs` container | `LVD` | Experimental container for nested images |
|
||||
|
||||
Notes:
|
||||
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
|
||||
- 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`)
|
||||
- `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_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `20`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `10`)
|
||||
- `kstuff_crash_detection=1|0` (`1` enables crash monitoring and pause-delay autotune updates; default: `1`)
|
||||
- `kstuff_pause_delay_image_seconds=<0..3600>` (delay before pausing kstuff for image-backed launches; default: `25`)
|
||||
- `kstuff_pause_delay_direct_seconds=<0..3600>` (delay before pausing kstuff for direct/non-image launches; default: `15`)
|
||||
- `kstuff_no_pause=<TITLE_ID>` (repeatable; keeps kstuff enabled for matching titles)
|
||||
- `kstuff_delay=<TITLE_ID>:<0..3600>` (repeatable; per-title pause delay override, last matching rule wins)
|
||||
- `/data/shadowmount/autotune.ini` may also provide per-title pause-delay overrides with highest priority:
|
||||
- `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`)
|
||||
@@ -73,31 +99,56 @@ 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/pfsc` and `/mnt/shadowmnt` are always added automatically, even with custom paths.
|
||||
- With `scan_depth=1` (default), only first-level subfolders are checked.
|
||||
- With `scan_depth=2`, one additional nested level is checked.
|
||||
- If `recursive_scan=1` is set, ShadowMount+ forces `scan_depth=2`.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `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:
|
||||
- For each `scanpath`, use:
|
||||
- `<scanpath>/backports/<TITLE_ID>/`
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from the same scan path.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ also mounts it into that game's sandbox `common/lib`.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib2` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`; otherwise it falls back to `app0/fakelib`.
|
||||
- If `global_fakelib=1` and `global_fakelib_path` exists as a directory, ShadowMount+ also mounts that folder into the same sandbox `common/lib`.
|
||||
- When both global and per-game fakelib exist, the default gives the game's own `fakelib` priority by mounting `/data/shadowmount/fakelib` first and then the game-specific fakelib.
|
||||
- `global_fakelib_priority=global` reverses that priority.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -108,9 +159,11 @@ Validation:
|
||||
|
||||
Image mountpoints are created under:
|
||||
|
||||
`/data/imgmnt/<fs>/<image_name>`
|
||||
`/mnt/shadowmnt/<image_name>_<hash>`
|
||||
|
||||
Where `<fs>` is one of: `ufsmnt`, `exfatmnt`, `pfsmnt`.
|
||||
PFSC container mountpoints are created under:
|
||||
|
||||
`/mnt/shadowmnt/pfsc/<image_name>_<hash>`
|
||||
|
||||
Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Game files must be placed at the image root.
|
||||
@@ -118,6 +171,92 @@ Image layout requirement (`.ffpkg`, `.exfat`, `.ffpfs`):
|
||||
- Valid example: `/sce_sys/param.json` exists directly from image root.
|
||||
- Invalid example: `/GAME_FOLDER/sce_sys/param.json` (extra nesting level).
|
||||
|
||||
PFSC container layout requirement (`.ffpfsc`):
|
||||
- Do not place game files directly in the container root.
|
||||
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
|
||||
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification and GDDR5 cache setup are kept in code but currently disabled.
|
||||
|
||||
## Compressed PFS containers (`.ffpfsc`)
|
||||
|
||||
Compressed PFS mode is intended only for nested images. During packing, data is
|
||||
zero-padded to a `64 KB` sector boundary, so the compressed PFS should be used
|
||||
as an outer container for another image rather than as a direct game-file
|
||||
layout.
|
||||
|
||||
Recommended layouts:
|
||||
- exFAT image inside compressed PFS.
|
||||
- Uncompressed PFS image inside compressed PFS.
|
||||
|
||||
MkPFS uses zLib compression. Decompression is hardware-assisted, but throughput
|
||||
is limited to roughly `150-250 MB/s`. This is about one third of the speed of an
|
||||
external USB drive, FFPKG/exFAT images, or about one tenth of the internal drive
|
||||
speed. Keep this in mind when choosing which games to pack. Games that read large
|
||||
amounts of data or stream textures continuously may stutter.
|
||||
|
||||
Use the official [PSBrew/MkPFS](https://github.com/PSBrew/MkPFS) tool to pack
|
||||
PFS images.
|
||||
|
||||
### Packing an uncompressed PFS image into compressed PFS
|
||||
|
||||
First create an uncompressed nested PFS image:
|
||||
|
||||
```bash
|
||||
mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --version PS5 --inode-bits 32 \
|
||||
'./PPSA07923/PPSA07923-app' \
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image (**pfs_image.dat**) into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./pfs_image.dat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary nested image can be removed:
|
||||
|
||||
```bash
|
||||
rm './pfs_image.dat'
|
||||
```
|
||||
|
||||
### Packing an exFAT image into compressed PFS
|
||||
|
||||
First create a normal exFAT image using one of the methods from
|
||||
`Creating an exFAT image`. The nested exFAT image name must keep the `.exfat`
|
||||
extension, for example `PPSA12345.exfat`.
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
chmod +x mkexfat.sh
|
||||
./mkexfat.sh ./PPSA12345-app ./PPSA12345.exfat
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```cmd
|
||||
make_image.bat "C:\images\PPSA12345.exfat" "C:\payload\PPSA12345-app"
|
||||
```
|
||||
|
||||
The exFAT image must contain the game files at the image root, without an extra
|
||||
top-level folder.
|
||||
|
||||
Then pack the exFAT image into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
'./PPSA12345.exfat' \
|
||||
'./PPSA12345.ffpfsc'
|
||||
```
|
||||
|
||||
After successful packing, the temporary exFAT image can be removed:
|
||||
|
||||
```bash
|
||||
rm './PPSA12345.exfat'
|
||||
```
|
||||
|
||||
## Scan paths
|
||||
|
||||
Default scan locations:
|
||||
@@ -132,18 +271,57 @@ 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/pfsc` (mounted PFSC container scan)
|
||||
- `/mnt/shadowmnt` (mounted image content scan)
|
||||
|
||||
You can override scan roots with `scanpath=...` entries in `/data/shadowmount/config.ini`.
|
||||
|
||||
## Manual install list
|
||||
|
||||
For games that should not live under the normal scan paths, ShadowMountPlus also
|
||||
checks:
|
||||
|
||||
`/data/shadowmount/manual.lst`
|
||||
|
||||
Add one source per line:
|
||||
- Path to a game folder, where `sce_sys/param.json` exists inside that folder.
|
||||
- Path to a supported image file: `.ffpkg`, `.exfat`, `.ffpfs`, or `.ffpfsc`.
|
||||
- Empty lines and lines starting with `#` are ignored.
|
||||
|
||||
Example:
|
||||
```text
|
||||
/mnt/usb0/MyGames/PPSA12345
|
||||
/mnt/usb0/images/PPSA54321.ffpkg
|
||||
# /mnt/usb0/disabled/PPSA00000
|
||||
```
|
||||
|
||||
`manual.lst` is watched for changes. When you add a new line, ShadowMountPlus
|
||||
rescans shortly after the write, mounts images when needed, and installs the
|
||||
game through the same pipeline as normal scan path discoveries.
|
||||
|
||||
Manual install state is tracked in:
|
||||
|
||||
`/data/shadowmount/manual.status`
|
||||
|
||||
This status file is managed by ShadowMountPlus. If a manually installed game is
|
||||
later removed by the user and disappears from `app.db`, ShadowMountPlus marks it
|
||||
as deleted in `manual.status` and removes the matching source line from
|
||||
`manual.lst` so it is not installed again automatically. If you later add the
|
||||
same source path back to `manual.lst`, it will be processed again and the status
|
||||
will be updated after installation.
|
||||
|
||||
You can use **Dump Installer** to install or prepare game dumps for this manual
|
||||
workflow, then add the resulting game folder path or image file path to
|
||||
`manual.lst`.
|
||||
|
||||
Recommended folder structure:
|
||||
- Default mode (`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>/`
|
||||
@@ -151,6 +329,8 @@ Recommended folder structure:
|
||||
|
||||
## 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`
|
||||
@@ -162,6 +342,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`
|
||||
@@ -172,6 +353,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:
|
||||
@@ -193,14 +375,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
|
||||
@@ -231,16 +419,18 @@ 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/pfsc` and `/mnt/shadowmnt` are 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 image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS).
|
||||
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder);
|
||||
- PFSC container (`.ffpfsc`): nested supported image files are scanned; direct game files inside the container are ignored.
|
||||
- If you see `missing/invalid param.json` for an image, check via FTP that files are present under `/mnt/shadowmnt/<image_name>_<hash>/` and include `sce_sys/param.json`.
|
||||
- If you see image mount failure, check image integrity and filesystem type (`.ffpkg`=UFS, `.exfat`=exFAT, `.ffpfs`=PFS, `.ffpfsc`=PFS container).
|
||||
- If you see duplicate titleId notification, keep only one source per `<TITLE_ID>`.
|
||||
|
||||
If a game is mounted but does not start:
|
||||
@@ -262,5 +452,6 @@ If a game is mounted but does not start:
|
||||
* Gezine
|
||||
* earthonion
|
||||
* LightningMods
|
||||
* RenanGBarreto for his excellent https://github.com/PSBrew/MkPFS
|
||||
* john-tornblom for SDK
|
||||
* PS5 R&D Community
|
||||
+103
-42
@@ -1,85 +1,146 @@
|
||||
# ShadowMount runtime config example
|
||||
# Place as: /data/shadowmount/config.ini
|
||||
#
|
||||
# All keys are optional.
|
||||
# If a key is missing, built-in defaults are used.
|
||||
# This file is a template.
|
||||
# Nothing below is active until you remove the leading '#'.
|
||||
#
|
||||
# How to use this file:
|
||||
# 1. Uncomment only the parameters you really want to override.
|
||||
# 2. Lines that start with '# key=value' are real parameters you can uncomment directly.
|
||||
# 3. Lines that start with '##' are examples/templates only. Edit them before use.
|
||||
|
||||
# Debug logging:
|
||||
# 1/true/yes/on -> enable log_debug output to console and /data/shadowmount/debug.log
|
||||
# 0/false/no/off -> disable log_debug output
|
||||
debug=1
|
||||
# Default: 1
|
||||
# debug=1
|
||||
|
||||
# Quiet mode:
|
||||
# 1/true/yes/on -> only show the startup version popup and runtime errors
|
||||
# 0/false/no/off -> also show normal informational popups (default)
|
||||
quiet_mode=0
|
||||
# 0/false/no/off -> also show normal informational popups
|
||||
# Default: 0
|
||||
# quiet_mode=0
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
mount_read_only=1
|
||||
# Default: 1
|
||||
# mount_read_only=1
|
||||
|
||||
# Mounting even damaged file systems
|
||||
# Mounting even damaged file systems:
|
||||
# 1/true/yes/on -> include force option
|
||||
# 0/false/no/off -> default mount options (default)
|
||||
force_mount=0
|
||||
# 0/false/no/off -> default mount options
|
||||
# Default: 0
|
||||
# force_mount=0
|
||||
|
||||
# Per-image mount mode override by file name (can be repeated many times):
|
||||
# image_ro=<image_filename>
|
||||
# image_rw=<image_filename>
|
||||
# image_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
|
||||
|
||||
# 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
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount /mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib into common/lib
|
||||
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
backport_fakelib=1
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
# Global fakelib overlay:
|
||||
# 1/true/yes/on -> also mount global fakelib for every tracked game when present
|
||||
# 0/false/no/off -> do not use the global fakelib folder
|
||||
# Default: 1
|
||||
# global_fakelib=1
|
||||
#
|
||||
# Default global fakelib folder:
|
||||
# global_fakelib_path=/data/shadowmount/fakelib
|
||||
#
|
||||
# Priority when both global and per-game fakelib exist:
|
||||
# game -> mount global first, then game-specific fakelib (game wins)
|
||||
# global -> mount game-specific fakelib first, then global fakelib (global wins)
|
||||
# Default: game
|
||||
# global_fakelib_priority=game
|
||||
#
|
||||
## Exclude specific title IDs from the global fakelib overlay (repeatable):
|
||||
## global_fakelib_exclude=PPSA12345
|
||||
|
||||
# Auto-manage kstuff around game launches:
|
||||
# 1/true/yes/on -> pause kstuff after game launch, resume immediately on stop
|
||||
# 0/false/no/off -> leave kstuff state untouched
|
||||
kstuff_game_auto_toggle=1
|
||||
# Default: 1
|
||||
# kstuff_game_auto_toggle=1
|
||||
|
||||
# Detect 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.
|
||||
kstuff_pause_delay_image_seconds=20
|
||||
# Default: 25
|
||||
# kstuff_pause_delay_image_seconds=25
|
||||
|
||||
# Delay before pausing kstuff for direct/non-image launches.
|
||||
kstuff_pause_delay_direct_seconds=10
|
||||
|
||||
# Keep kstuff enabled for specific title IDs (can be repeated).
|
||||
# kstuff_no_pause=PPSA12345
|
||||
|
||||
# Override pause delay for specific title IDs (can be repeated).
|
||||
# Format: TITLEID:SECONDS
|
||||
# kstuff_delay=PPSA12345:30
|
||||
# Default: 15
|
||||
# kstuff_pause_delay_direct_seconds=15
|
||||
|
||||
# Backend-specific sector size overrides:
|
||||
lvd_exfat_sector_size=512
|
||||
lvd_ufs_sector_size=4096
|
||||
lvd_pfs_sector_size=32768
|
||||
md_exfat_sector_size=512
|
||||
md_ufs_sector_size=512
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=32768, md_exfat=512, md_ufs=512
|
||||
# lvd_exfat_sector_size=512
|
||||
# lvd_ufs_sector_size=4096
|
||||
# lvd_pfs_sector_size=32768
|
||||
# md_exfat_sector_size=512
|
||||
# md_ufs_sector_size=512
|
||||
|
||||
# Optional custom scan roots (can be repeated):
|
||||
# If at least one scanpath=... is present, only these paths are used.
|
||||
# "/data/imgmnt/{ufsmnt,exfatmnt,pfsmnt}" are always added automatically.
|
||||
# "/mnt/shadowmnt/pfsc" and "/mnt/shadowmnt" are always added automatically.
|
||||
# Default: use built-in scan path list
|
||||
# scanpath=/data/homebrew
|
||||
# scanpath=/data/etaHEN/games
|
||||
|
||||
## Per-image mount mode override by file name (repeatable):
|
||||
## image_ro=<image_filename>
|
||||
## image_rw=<image_filename>
|
||||
## image_sector=<image_filename>:<sector_size>
|
||||
##
|
||||
## Examples:
|
||||
## image_ro=PPSA12345.ffpfs
|
||||
## image_rw=My Game Dump.exfat
|
||||
## image_sector=My Game Dump.exfat:65536
|
||||
|
||||
## Keep kstuff enabled for specific title IDs (repeatable):
|
||||
## kstuff_no_pause=PPSA12345
|
||||
|
||||
## Override pause delay for specific title IDs (repeatable):
|
||||
## Format: TITLEID:SECONDS
|
||||
## kstuff_delay=PPSA12345:30
|
||||
|
||||
## More custom scan root examples:
|
||||
## scanpath=/data/homebrew
|
||||
## scanpath=/data/etaHEN/games
|
||||
+10
-2
@@ -7,14 +7,22 @@ struct AppDbTitleList;
|
||||
|
||||
// Close the cached app.db handle and release related resources.
|
||||
void shutdown_app_db(void);
|
||||
// Normalize existing snd0info rows during process startup.
|
||||
bool app_db_run_startup_maintenance(void);
|
||||
// Update snd0 metadata for a registered title.
|
||||
int update_snd0info(const char *title_id);
|
||||
// Normalize an existing snd0Info path for one title.
|
||||
int normalize_snd0info_for_title(const char *title_id);
|
||||
// Check whether a title ID exists in a cached app.db title list.
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id);
|
||||
// Release a title-list snapshot returned by get_app_db_title_list_cached().
|
||||
void free_app_db_title_list(struct AppDbTitleList *list);
|
||||
// Drop the cached app.db title list so it is reloaded on next access.
|
||||
void invalidate_app_db_title_cache(void);
|
||||
// Return the cached app.db title list, loading it if needed.
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out);
|
||||
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
|
||||
// Copy the cached PPSA titles that app.db marks as not uninstallable.
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out);
|
||||
|
||||
#endif
|
||||
@@ -10,18 +10,38 @@ typedef struct runtime_config runtime_config_t;
|
||||
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);
|
||||
// Return scan depth for a root, including managed container-root expansion.
|
||||
uint32_t get_scan_depth_for_root(const char *scan_path);
|
||||
// Resolve a per-image read-only override from the file name.
|
||||
bool get_image_mode_override(const char *filename, bool *mount_read_only_out);
|
||||
// Resolve a per-image sector-size override from autotune.ini or config.ini.
|
||||
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
|
||||
+12
-5
@@ -7,11 +7,13 @@
|
||||
|
||||
// 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 an installed title.
|
||||
// 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> entry and optionally build its path.
|
||||
// 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.
|
||||
@@ -22,21 +24,26 @@ void cleanup_duplicate_title_mounts(void);
|
||||
int remount_system_ex(void);
|
||||
// Mount a title source into /system_ex/app/<title_id> via nullfs.
|
||||
bool mount_title_nullfs(const char *title_id, const char *src_path);
|
||||
// Roll back a just-mounted title nullfs layer when publication is aborted.
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path);
|
||||
// Reconcile the title mount stack against the expected source/backport state.
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out);
|
||||
// Mount a prepared backport overlay on top of an already mounted title.
|
||||
void mount_backport_overlay(const char *mount_point,
|
||||
// Returns false only when nmount() itself fails.
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id);
|
||||
// Unmount all managed /system_ex/app/<title_id> mount stacks on shutdown.
|
||||
void shutdown_title_mounts(void);
|
||||
// Return true when path is equal to root or is under it.
|
||||
bool path_matches_root_or_child(const char *path, const char *root);
|
||||
// Remove stale mount links and optionally restore image-backed mounts.
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind);
|
||||
// Unmount and remove title links that point under a source root.
|
||||
void cleanup_mount_links_for_source_unmount(const char *source_root);
|
||||
// Unmount and remove title links backed by USB sources or USB-backed images.
|
||||
void cleanup_usb_mount_links_for_suspend(void);
|
||||
// Recursively copy a directory tree.
|
||||
int copy_dir(const char *src, const char *dst);
|
||||
// Copy a single file.
|
||||
|
||||
@@ -3,14 +3,23 @@
|
||||
|
||||
#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);
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Start the shared game lifecycle watcher used by backport fakelib and kstuff.
|
||||
// Start the shared game lifecycle watcher used by runtime game features.
|
||||
bool start_game_lifecycle_watcher(void);
|
||||
// Wake the shared watcher so pending lifecycle work is processed immediately.
|
||||
void wake_game_lifecycle_watcher(void);
|
||||
// Stop the shared watcher and let lifecycle modules clean up tracked state.
|
||||
void stop_game_lifecycle_watcher(void);
|
||||
// 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
-1
@@ -13,11 +13,23 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
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.
|
||||
void maybe_mount_image_file(const char *full_path, const char *name);
|
||||
bool maybe_mount_image_file(const char *full_path, const char *name,
|
||||
bool *unstable_out);
|
||||
// Build the deterministic runtime mount point for an image source path.
|
||||
void get_image_mount_point_for_source(const char *file_path,
|
||||
char mount_point[MAX_PATH]);
|
||||
// Return true when the filename has a supported image extension.
|
||||
bool is_supported_image_file_name(const char *name);
|
||||
// Return true when the source path is a supported image, including nested names.
|
||||
bool is_supported_image_file_path(const char *full_path, const char *name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SM_INSTALL_QUEUE_H
|
||||
#define SM_INSTALL_QUEUE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Return whether a title is already waiting for async batch installation.
|
||||
bool is_title_install_pending(const char *title_id);
|
||||
// Drop queued/submitted async install state for a title after confirmed removal.
|
||||
void sm_install_forget_pending_title(const char *title_id);
|
||||
// Return the next pending-install wake deadline in monotonic microseconds.
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us);
|
||||
// Poll async batch installs for success/timeout completion only.
|
||||
void sm_install_poll_pending(void);
|
||||
// Poll async batch installs for success/timeout completion and auto-submit
|
||||
// queued installs when possible.
|
||||
void sm_install_service_pending(void);
|
||||
// Queue a staged title for the next batch install request.
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0);
|
||||
// Submit all queued titles when no batch install is currently pending.
|
||||
bool sm_install_submit_queued(void);
|
||||
// Record retry state for every queued title after a batch submit failure.
|
||||
void sm_install_note_submit_failure(void);
|
||||
|
||||
#endif
|
||||
@@ -31,5 +31,11 @@ void sm_kstuff_game_on_exit(pid_t pid);
|
||||
void sm_kstuff_game_poll(void);
|
||||
// Clear tracked game state and restore kstuff if the watcher paused it.
|
||||
void sm_kstuff_game_shutdown(void);
|
||||
// Pause kstuff for runtime sleep without restoring game-driven pauses.
|
||||
void sm_kstuff_sleep_enter(void);
|
||||
// Restore kstuff after runtime sleep when sleep entry disabled it.
|
||||
void sm_kstuff_sleep_leave(void);
|
||||
// Re-apply runtime config changes to the currently tracked game, if any.
|
||||
void sm_kstuff_on_config_reload(void);
|
||||
|
||||
#endif
|
||||
+9
-4
@@ -1,15 +1,17 @@
|
||||
#ifndef SM_LIMITS_H
|
||||
#define SM_LIMITS_H
|
||||
|
||||
#define DEFAULT_SCAN_INTERVAL_US 10000000u
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 20u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 10u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
|
||||
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_IMAGE_MOUNTS 64
|
||||
#define MAX_IMAGE_MOUNTS 256
|
||||
#define MAX_IMAGE_MODE_RULES 128
|
||||
#define MAX_KSTUFF_TITLE_RULES 128
|
||||
#define MAX_FAKELIB_EXCLUDE_RULES 128
|
||||
#define PATH_STATE_CAPACITY MAX_PENDING
|
||||
#define TITLE_STATE_CAPACITY MAX_PENDING
|
||||
#define STATE_HASH_SIZE 1024u
|
||||
@@ -27,6 +29,8 @@
|
||||
#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
|
||||
@@ -41,5 +45,6 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
#endif
|
||||
@@ -9,6 +9,8 @@ typedef struct sm_error sm_error_t;
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef SM_MANUAL_H
|
||||
#define SM_MANUAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Visit each manual source path from manual.lst.
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx);
|
||||
// Mark previously installed manual titles as deleted when they disappear from
|
||||
// app.db, and remove their source lines from manual.lst.
|
||||
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready);
|
||||
// Record that a manual source is installed and associated with a title.
|
||||
void sm_manual_note_installed(const char *manual_source_path,
|
||||
const char *title_id,
|
||||
const char *title_name);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_MDBG_H
|
||||
#define SM_MDBG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Initialize mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_init(void);
|
||||
// Shut down mdbg-backed crash monitoring state.
|
||||
void sm_mdbg_shutdown(void);
|
||||
// Start monitoring the tracked game process for crash-candidate state.
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id);
|
||||
// Record that kstuff auto-pause was actually applied to the tracked game.
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds);
|
||||
// Stop monitoring the tracked game process on exit.
|
||||
void sm_mdbg_game_on_exit(pid_t pid);
|
||||
// Clear tracked game monitoring state unconditionally.
|
||||
void sm_mdbg_game_shutdown(void);
|
||||
// Return the next wake deadline in monotonic microseconds, or 0 when idle.
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
|
||||
// Poll the tracked process for mdbg crash-candidate state.
|
||||
void sm_mdbg_poll(void);
|
||||
|
||||
#endif
|
||||
+30
-17
@@ -9,36 +9,48 @@
|
||||
#define UFS_ATTACH_USE_MDCTL 0
|
||||
|
||||
// --- LVD Definitions ---
|
||||
// - ioctl: ATTACH=0xC0286D00, DETACH=0xC0286D01, ATTACH2 path=0xC0286D09.
|
||||
// - single-image path uses raw option flags 0x8/0x9 -> normalized 0x14/0x1C.
|
||||
// - DownloadData/LWFS path (imgtype=7) uses normalized options 0x16/0x1E.
|
||||
// - image_type values accepted by validator: 0..0xC (13 values total).
|
||||
// this code uses image_type=7 for UFS (DownloadData-like), 0 for generic path.
|
||||
// - layer source_type observed: 1=file, 2=char/block-like source (/dev/sbram0).
|
||||
// - layer entry flag bit0 is "no bitmap file specified".
|
||||
// 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 0xC0286D00
|
||||
#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 1
|
||||
#define LVD_ATTACH_OPTION_FLAGS_DEFAULT 0x9
|
||||
#define LVD_ATTACH_OPTION_FLAGS_RW 0x8
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RO 0x1E
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RW 0x16
|
||||
#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 32768u
|
||||
#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 0
|
||||
#define LVD_ATTACH_IMAGE_TYPE_SINGLE 0
|
||||
#define LVD_ATTACH_IMAGE_TYPE_UFS_DOWNLOAD_DATA 7
|
||||
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 0 // also works 5
|
||||
#define LVD_ATTACH_IMAGE_TYPE_PFS_SAVE_DATA 5
|
||||
#define PFS_NESTED_OUTER_IMG_TYPE 0x02u
|
||||
#define PFS_NESTED_INNER_IMG_TYPE 0x82u
|
||||
#define LVD_ATTACH_LAYER_COUNT 1
|
||||
#define LVD_ATTACH_LAYER_ARRAY_SIZE 3
|
||||
#define LVD_ENTRY_TYPE_FILE 1
|
||||
#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
|
||||
@@ -59,10 +71,11 @@
|
||||
#define DEVPFS_MKEYMODE_GD "GD"
|
||||
#define DEVPFS_MKEYMODE_AC "AC"
|
||||
#define PFS_MOUNT_BUDGET_ID DEVPFS_BUDGET_GAME
|
||||
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_SD
|
||||
#define PFS_MOUNT_MKEYMODE DEVPFS_MKEYMODE_AC
|
||||
#define PFS_MOUNT_SIGVERIFY 0
|
||||
#define PFS_MOUNT_PLAYGO 0
|
||||
#define PFS_MOUNT_DISC 0
|
||||
#define PFS_GDDR5_CACHE_IOCTL 0x80209168
|
||||
// 4x64-bit PFS key encoded as 64 hex chars.
|
||||
#define PFS_ZERO_EKPFS_KEY_HEX \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
@@ -10,6 +10,8 @@ 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);
|
||||
|
||||
@@ -14,6 +14,8 @@ void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
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.
|
||||
@@ -26,5 +28,15 @@ bool is_image_mount_limited(const char *path);
|
||||
uint8_t bump_image_mount_attempts(const char *path);
|
||||
// Clear image mount retry state for a path.
|
||||
void clear_image_mount_attempts(const char *path);
|
||||
// Return whether backport overlay remounts are suppressed for a path.
|
||||
bool is_backport_mount_blocked(const char *path);
|
||||
// Record the first failed backport overlay mount for a path.
|
||||
void note_backport_mount_failure(const char *path);
|
||||
// Clear failed backport overlay mount state for a path.
|
||||
void clear_backport_mount_failure(const char *path);
|
||||
// Return true once for a missing manual source until the path appears again.
|
||||
bool note_manual_missing_source_once(const char *path);
|
||||
// Allow a manual source to be reported missing again after it becomes visible.
|
||||
void clear_manual_missing_source(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -8,8 +8,16 @@
|
||||
|
||||
// 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 a path is the PFSC container mount root or lives under it.
|
||||
bool is_pfsc_image_mount_base_or_child(const char *path);
|
||||
// Return true when path is equal to root or is under it.
|
||||
bool path_matches_root_or_child(const char *path, const char *root);
|
||||
// Return true when a path lives on USB-backed storage.
|
||||
bool is_usb_storage_path(const char *path);
|
||||
// Build "<scan_path>/backports" for a managed scan root.
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]);
|
||||
|
||||
|
||||
+9
-4
@@ -1,16 +1,21 @@
|
||||
#ifndef SM_PATHS_H
|
||||
#define SM_PATHS_H
|
||||
|
||||
#define IMAGE_MOUNT_BASE "/data/imgmnt"
|
||||
#define IMAGE_MOUNT_SUBDIR_UFS "ufsmnt"
|
||||
#define IMAGE_MOUNT_SUBDIR_EXFAT "exfatmnt"
|
||||
#define IMAGE_MOUNT_SUBDIR_PFS "pfsmnt"
|
||||
#define IMAGE_MOUNT_BASE "/mnt/shadowmnt"
|
||||
#define PFSC_IMAGE_MOUNT_BASE "/mnt/shadowmnt/pfsc"
|
||||
|
||||
#define DEFAULT_BACKPORTS_DIR_NAME "backports"
|
||||
#define DEFAULT_GLOBAL_FAKELIB_PATH "/data/shadowmount/fakelib"
|
||||
#define LOG_DIR "/data/shadowmount"
|
||||
#define LOG_FILE "/data/shadowmount/debug.log"
|
||||
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
|
||||
#define CONFIG_FILE "/data/shadowmount/config.ini"
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
#define APP_BASE "/user/app"
|
||||
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
#define KILL_FILE "/data/shadowmount/STOP"
|
||||
#define TOAST_FILE "/data/shadowmount/notify.txt"
|
||||
#define NOTIFY_ICON_DIR "/user/data/shadowmount"
|
||||
|
||||
@@ -35,10 +35,16 @@ typedef struct {
|
||||
char unknown2[0x3c];
|
||||
} app_info_t;
|
||||
|
||||
static inline uint32_t sm_firmware_major_version(void) {
|
||||
uint32_t fw = kernel_get_fw_version();
|
||||
uint32_t major_bcd = (fw >> 24) & 0xFFu;
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallTitleDir(const char *title_id,
|
||||
const char *install_path, void *reserved);
|
||||
int sceAppInstUtilAppInstallAll(void);
|
||||
int sceAppInstUtilAppUnInstall(const char *title_id);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
int sceUserServiceInitialize(void *);
|
||||
|
||||
+11
-2
@@ -2,6 +2,7 @@
|
||||
#define SM_RUNTIME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
// Install process signal handlers used for graceful shutdown.
|
||||
@@ -12,10 +13,18 @@ pid_t find_pid_by_name(const char *name, bool exclude_self);
|
||||
bool should_stop_requested(void);
|
||||
// Request graceful shutdown with a descriptive source string.
|
||||
void request_shutdown_stop(const char *reason);
|
||||
// Return true when the payload is paused for system suspend/resume.
|
||||
bool runtime_sleep_mode_active(void);
|
||||
// Enter or leave sleep mode with a descriptive source string.
|
||||
// Returns true when the runtime sleep state changed.
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason);
|
||||
// Serialize mount/link/cache publication against suspend cleanup.
|
||||
void runtime_mount_state_lock(void);
|
||||
void runtime_mount_state_unlock(void);
|
||||
// Request an immediate scan cycle with a descriptive source string.
|
||||
void request_scan_now(const char *reason);
|
||||
// Consume a pending immediate scan request, returning true if one was pending.
|
||||
bool consume_scan_now_request(const char **reason_out);
|
||||
// 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);
|
||||
|
||||
|
||||
+16
-3
@@ -1,14 +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);
|
||||
// Mount stable per-root backport overlays for already mounted titles.
|
||||
void mount_backport_overlays(void);
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Check whether a path is old enough to be treated as stable.
|
||||
// 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.
|
||||
|
||||
@@ -10,9 +10,13 @@ bool was_register_attempted(const char *title_id);
|
||||
uint8_t get_register_attempts(const char *title_id);
|
||||
// Record a registration attempt for a title.
|
||||
void mark_register_attempted(const char *title_id);
|
||||
// Clear registration attempt state after success or confirmed user removal.
|
||||
void clear_register_attempts(const char *title_id);
|
||||
// Notify once about duplicate title IDs from different paths.
|
||||
void notify_duplicate_title_once(const char *title_id, const char *path_a,
|
||||
const char *path_b);
|
||||
// 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.
|
||||
|
||||
+29
-19
@@ -29,14 +29,12 @@ typedef struct {
|
||||
} devpfs_mount_profile_t;
|
||||
|
||||
typedef struct {
|
||||
// Source object class (observed: 1=file, 2=device-like source).
|
||||
// Source object class (observed: 1=file, 2=device-like/special source).
|
||||
uint16_t source_type;
|
||||
// Layer behavior flags (observed bit0 = no bitmap file path).
|
||||
uint8_t entry_flags;
|
||||
// Layer descriptor flags (observed bit0 = no bitmap file path).
|
||||
uint16_t flags;
|
||||
// Must be zero.
|
||||
uint8_t reserved0;
|
||||
// Must be zero.
|
||||
uint32_t reserved1;
|
||||
uint32_t reserved0;
|
||||
// Backing file or device path.
|
||||
const char *path;
|
||||
// Data start offset in backing object (bytes).
|
||||
@@ -49,28 +47,28 @@ typedef struct {
|
||||
uint64_t bitmap_offset;
|
||||
// Bitmap size (bytes), 0 when bitmap is unused.
|
||||
uint64_t bitmap_size;
|
||||
} lvd_kernel_layer_t;
|
||||
} lvd_ioctl_layer_v0_t;
|
||||
|
||||
typedef struct {
|
||||
// Protocol version for /dev/lvdctl ioctl payload (valid <=1).
|
||||
// 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;
|
||||
// Sector-like size fields used by LVD attach request validation.
|
||||
// In refs these are populated from statfs and clamped to <= 4096.
|
||||
uint32_t sector_size_0;
|
||||
uint32_t sector_size_1;
|
||||
// Encoded option length derived from option flags (0x14 for 0x8, 0x1C for 0x9).
|
||||
uint16_t option_len;
|
||||
// LVD image type id (validator accepts 0..0xC; this code uses 0).
|
||||
// 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 layer array in user payload.
|
||||
lvd_kernel_layer_t *layers_ptr;
|
||||
} lvd_ioctl_attach_t;
|
||||
// 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.
|
||||
@@ -91,8 +89,10 @@ typedef struct scan_candidate {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char manual_source_path[MAX_PATH];
|
||||
bool installed;
|
||||
bool in_app_db;
|
||||
bool manual;
|
||||
} scan_candidate_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -108,9 +108,18 @@ typedef struct runtime_config {
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool recursive_scan;
|
||||
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;
|
||||
@@ -129,6 +138,7 @@ typedef enum {
|
||||
IMAGE_FS_UFS,
|
||||
IMAGE_FS_EXFAT,
|
||||
IMAGE_FS_PFS,
|
||||
IMAGE_FS_PFSC_CONTAINER,
|
||||
} image_fs_type_t;
|
||||
|
||||
typedef struct sm_error {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
asm(".global \"C49jelxiaVE\"\n"
|
||||
".type \"C49jelxiaVE\" @function\n"
|
||||
"\"C49jelxiaVE\":\n");
|
||||
+215
-84
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
@@ -13,10 +14,14 @@
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#ifndef SHADOWMOUNT_VERSION
|
||||
@@ -28,18 +33,38 @@
|
||||
#define BACKPORK_PROCESS_NAME_ALT "ps5-backpork.elf"
|
||||
#define RESTART_WAIT_POLL_US 200000u
|
||||
#define RESTART_WAIT_MAX_US 60000000u
|
||||
#define STOP_FILE_POLL_INTERVAL_US 3000000ull
|
||||
#define KINFO_PID_OFFSET 72
|
||||
#define KINFO_TDNAME_OFFSET 447
|
||||
|
||||
//#define AUTHID_BASE 0x4801000000000013L
|
||||
#define AUTHID_BASE 0x4800000000000006ull
|
||||
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested = 0;
|
||||
static atomic_bool g_shutdown_on_going_stop_requested = false;
|
||||
static atomic_bool g_scan_now_requested = false;
|
||||
static atomic_bool g_runtime_sleep_mode_active = false;
|
||||
static _Atomic(uintptr_t) g_shutdown_stop_reason_bits = 0;
|
||||
static _Atomic(uintptr_t) g_scan_now_reason_bits = 0;
|
||||
static atomic_uint_fast64_t g_next_stop_file_poll_us = 0;
|
||||
static pthread_mutex_t g_runtime_mount_state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t reason_mutex;
|
||||
char reason[128];
|
||||
} immediate_scan_request_t;
|
||||
|
||||
static immediate_scan_request_t g_scan_now = {
|
||||
.reason_mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.reason = {0},
|
||||
};
|
||||
|
||||
extern unsigned char config_ini_example[];
|
||||
extern unsigned int config_ini_example_len;
|
||||
|
||||
static void on_signal(int sig) {
|
||||
(void)sig;
|
||||
g_stop_requested = 1;
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
void install_signal_handlers(void) {
|
||||
@@ -57,8 +82,20 @@ void install_signal_handlers(void) {
|
||||
bool should_stop_requested(void) {
|
||||
if (g_stop_requested)
|
||||
return true;
|
||||
if (access(KILL_FILE, F_OK) == 0) {
|
||||
remove(KILL_FILE);
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us != 0) {
|
||||
uint64_t next_poll_us =
|
||||
atomic_load_explicit(&g_next_stop_file_poll_us, memory_order_acquire);
|
||||
if (next_poll_us != 0 && now_us < next_poll_us)
|
||||
return false;
|
||||
atomic_store_explicit(&g_next_stop_file_poll_us,
|
||||
now_us + STOP_FILE_POLL_INTERVAL_US,
|
||||
memory_order_release);
|
||||
}
|
||||
|
||||
if (remove(KILL_FILE) == 0) {
|
||||
g_stop_requested = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -67,39 +104,97 @@ bool should_stop_requested(void) {
|
||||
void request_shutdown_stop(const char *reason) {
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown shutdown source";
|
||||
static char g_shutdown_stop_reason[128];
|
||||
bool already_requested =
|
||||
atomic_exchange_explicit(&g_shutdown_on_going_stop_requested, true,
|
||||
memory_order_acq_rel);
|
||||
if (!already_requested) {
|
||||
(void)strlcpy(g_shutdown_stop_reason, resolved_reason,
|
||||
sizeof(g_shutdown_stop_reason));
|
||||
atomic_store_explicit(&g_shutdown_stop_reason_bits,
|
||||
(uintptr_t)resolved_reason, memory_order_release);
|
||||
log_debug("[SHUTDOWN] requested by %s", resolved_reason);
|
||||
(uintptr_t)g_shutdown_stop_reason,
|
||||
memory_order_release);
|
||||
log_debug("[SHUTDOWN] requested by %s", g_shutdown_stop_reason);
|
||||
}
|
||||
g_stop_requested = 1;
|
||||
sm_scanner_wake();
|
||||
wake_game_lifecycle_watcher();
|
||||
}
|
||||
|
||||
bool runtime_sleep_mode_active(void) {
|
||||
return atomic_load_explicit(&g_runtime_sleep_mode_active,
|
||||
memory_order_acquire);
|
||||
}
|
||||
|
||||
static void clear_scan_now_request(void) {
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
g_scan_now.reason[0] = '\0';
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
}
|
||||
|
||||
bool request_runtime_sleep_mode(bool active, const char *reason) {
|
||||
bool previous = atomic_exchange_explicit(&g_runtime_sleep_mode_active, active,
|
||||
memory_order_acq_rel);
|
||||
if (previous == active)
|
||||
return false;
|
||||
|
||||
if (active)
|
||||
clear_scan_now_request();
|
||||
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown sleep source";
|
||||
log_debug("[SLEEP] %s by %s", active ? "entered" : "left",
|
||||
resolved_reason);
|
||||
sm_scanner_wake();
|
||||
wake_game_lifecycle_watcher();
|
||||
return true;
|
||||
}
|
||||
|
||||
void runtime_mount_state_lock(void) {
|
||||
pthread_mutex_lock(&g_runtime_mount_state_mutex);
|
||||
}
|
||||
|
||||
void runtime_mount_state_unlock(void) {
|
||||
pthread_mutex_unlock(&g_runtime_mount_state_mutex);
|
||||
}
|
||||
|
||||
void request_scan_now(const char *reason) {
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown scan source";
|
||||
bool already_requested =
|
||||
atomic_exchange_explicit(&g_scan_now_requested, true, memory_order_acq_rel);
|
||||
if (!already_requested) {
|
||||
atomic_store_explicit(&g_scan_now_reason_bits, (uintptr_t)resolved_reason,
|
||||
memory_order_release);
|
||||
log_debug("[SCAN] immediate scan requested by %s", resolved_reason);
|
||||
bool resume_scan =
|
||||
strcmp(resolved_reason, "SceSystemStateMgrInfo=WORKING") == 0;
|
||||
if (runtime_sleep_mode_active() && !resume_scan)
|
||||
return;
|
||||
|
||||
char log_reason[sizeof(g_scan_now.reason)];
|
||||
bool should_log = !resume_scan;
|
||||
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
if (g_scan_now.reason[0] == '\0') {
|
||||
(void)strlcpy(g_scan_now.reason, resolved_reason, sizeof(g_scan_now.reason));
|
||||
(void)strlcpy(log_reason, g_scan_now.reason, sizeof(log_reason));
|
||||
} else {
|
||||
should_log = false;
|
||||
}
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
|
||||
if (should_log)
|
||||
log_debug("[SCAN] immediate scan requested by %s", log_reason);
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
bool consume_scan_now_request(const char **reason_out) {
|
||||
if (!atomic_load_explicit(&g_scan_now_requested, memory_order_acquire))
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size) {
|
||||
if (reason_out && reason_out_size > 0)
|
||||
reason_out[0] = '\0';
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
if (g_scan_now.reason[0] == '\0') {
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
return false;
|
||||
|
||||
if (reason_out)
|
||||
*reason_out = (const char *)atomic_load_explicit(&g_scan_now_reason_bits,
|
||||
memory_order_acquire);
|
||||
atomic_store_explicit(&g_scan_now_reason_bits, (uintptr_t)0,
|
||||
memory_order_release);
|
||||
atomic_store_explicit(&g_scan_now_requested, false, memory_order_release);
|
||||
}
|
||||
if (reason_out && reason_out_size > 0)
|
||||
(void)strlcpy(reason_out, g_scan_now.reason, reason_out_size);
|
||||
g_scan_now.reason[0] = '\0';
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,8 +204,6 @@ bool sleep_with_stop_check(unsigned int total_us) {
|
||||
while (slept < total_us) {
|
||||
if (should_stop_requested())
|
||||
return true;
|
||||
if (atomic_load_explicit(&g_scan_now_requested, memory_order_acquire))
|
||||
return false;
|
||||
unsigned int remain = total_us - slept;
|
||||
unsigned int step = remain < chunk_us ? remain : chunk_us;
|
||||
sceKernelUsleep(step);
|
||||
@@ -137,9 +230,6 @@ static void get_firmware_version_string(char out[32]) {
|
||||
}
|
||||
|
||||
pid_t find_pid_by_name(const char *name, bool exclude_self) {
|
||||
if (!name || name[0] == '\0')
|
||||
return -1;
|
||||
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
|
||||
size_t buf_size = 0;
|
||||
if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) != 0)
|
||||
@@ -209,9 +299,6 @@ static bool wait_for_existing_instance_exit(pid_t target_pid) {
|
||||
static void log_non_empty_scan_paths(void) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
if (!scan_path)
|
||||
continue;
|
||||
|
||||
DIR *d = opendir(scan_path);
|
||||
if (!d)
|
||||
continue;
|
||||
@@ -234,6 +321,76 @@ static void log_non_empty_scan_paths(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void ensure_kstuff_noautomount_file(void) {
|
||||
if (path_exists(KSTUFF_NOAUTOMOUNT_FILE))
|
||||
return;
|
||||
|
||||
int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_RDONLY | O_CREAT, 0666);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
printf("[KSTUFF] Created startup sentinel: %s\n",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[KSTUFF] Failed to create %s: %s\n", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
static bool write_buffer_to_fd(int fd, const unsigned char *buf, size_t size) {
|
||||
size_t offset = 0;
|
||||
while (offset < size) {
|
||||
ssize_t written = write(fd, buf + offset, size - offset);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (written == 0) {
|
||||
errno = EIO;
|
||||
return false;
|
||||
}
|
||||
offset += (size_t)written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ensure_runtime_config_file(void) {
|
||||
int fd = open(CONFIG_FILE, O_WRONLY | O_CREAT | O_EXCL, 0666);
|
||||
if (fd < 0) {
|
||||
if (errno == EEXIST)
|
||||
return;
|
||||
printf("[CFG] Failed to create %s: %s\n", CONFIG_FILE, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t template_size = (size_t)config_ini_example_len;
|
||||
int saved_errno = 0;
|
||||
if (!write_buffer_to_fd(fd, config_ini_example, template_size))
|
||||
saved_errno = errno;
|
||||
if (close(fd) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
printf("[CFG] Failed to write %s: %s\n", CONFIG_FILE, strerror(errno));
|
||||
(void)unlink(CONFIG_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[CFG] Created default config from template: %s\n", CONFIG_FILE);
|
||||
}
|
||||
|
||||
static void cleanup_kstuff_noautomount_files(void) {
|
||||
if (unlink(KSTUFF_NOAUTOMOUNT_FILE) == 0) {
|
||||
log_debug("[KSTUFF] removed shutdown sentinel: %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
} else if (errno != ENOENT) {
|
||||
log_debug("[KSTUFF] failed to remove %s: %s", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_conflicting_backpork(void) {
|
||||
if (!runtime_config()->backport_fakelib_enabled)
|
||||
return;
|
||||
@@ -262,18 +419,16 @@ static void stop_conflicting_backpork(void) {
|
||||
|
||||
int main(void) {
|
||||
bool restarted_previous_instance = false;
|
||||
bool kstuff_runtime_enabled = false;
|
||||
pid_t existing_pid = 0;
|
||||
const char *scan_reason = NULL;
|
||||
scan_candidate_t candidates[MAX_PENDING];
|
||||
memset(candidates, 0, sizeof(candidates));
|
||||
|
||||
sceUserServiceInitialize(0);
|
||||
sceAppInstUtilInitialize();
|
||||
kernel_set_ucred_authid(-1, 0x4801000000000013L);
|
||||
kernel_set_ucred_authid(-1, AUTHID_BASE);
|
||||
install_signal_handlers();
|
||||
|
||||
mkdir(LOG_DIR, 0777);
|
||||
ensure_runtime_config_file();
|
||||
ensure_kstuff_noautomount_file();
|
||||
existing_pid = find_pid_by_name(PAYLOAD_NAME, true);
|
||||
if (existing_pid < 0) {
|
||||
printf("[RESTART] Failed to enumerate running processes.\n");
|
||||
@@ -298,6 +453,8 @@ int main(void) {
|
||||
|
||||
(void)unlink(LOG_FILE_PREV);
|
||||
(void)rename(LOG_FILE, LOG_FILE_PREV);
|
||||
if (!sm_scanner_init())
|
||||
log_debug(" [SCAN] scanner service init incomplete; steady-state scanner will stop if initialization cannot be completed");
|
||||
|
||||
char firmware_version[32];
|
||||
get_firmware_version_string(firmware_version);
|
||||
@@ -314,10 +471,9 @@ int main(void) {
|
||||
stop_conflicting_backpork();
|
||||
if (!sm_shellcore_flags_start())
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
kstuff_runtime_enabled = runtime_config()->kstuff_game_auto_toggle;
|
||||
if (kstuff_runtime_enabled)
|
||||
sm_kstuff_init();
|
||||
if (!start_game_lifecycle_watcher())
|
||||
sm_mdbg_init();
|
||||
sm_kstuff_init();
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
|
||||
if (mkdir("/system_ex/app", 0777) != 0 && errno != EEXIST) {
|
||||
@@ -330,8 +486,12 @@ int main(void) {
|
||||
notify_system("ShadowMount+ v%s exFAT/UFS/PFS", SHADOWMOUNT_VERSION);
|
||||
log_non_empty_scan_paths();
|
||||
|
||||
if (runtime_config()->recursive_scan)
|
||||
notify_system_info("ShadowMount+: Recursive scan enabled.");
|
||||
if (runtime_config()->legacy_recursive_scan_forced) {
|
||||
notify_system_info("ShadowMount+: recursive_scan=1 deprecated, using scan_depth=2.");
|
||||
} else if (runtime_config()->scan_depth > 1u) {
|
||||
notify_system_info("ShadowMount+: scan depth %u enabled.",
|
||||
runtime_config()->scan_depth);
|
||||
}
|
||||
|
||||
cleanup_mount_dirs();
|
||||
if (!wait_for_lvd_release()) {
|
||||
@@ -339,57 +499,27 @@ int main(void) {
|
||||
goto shutdown;
|
||||
}
|
||||
|
||||
log_debug("[STARTUP] cleanup_staged_mount_links begin");
|
||||
cleanup_staged_mount_links();
|
||||
log_debug("[STARTUP] cleanup_duplicate_title_mounts begin");
|
||||
cleanup_duplicate_title_mounts();
|
||||
cleanup_lost_sources_before_scan();
|
||||
int total_found_games = 0;
|
||||
int candidate_count =
|
||||
collect_scan_candidates(candidates, MAX_PENDING, &total_found_games);
|
||||
if (candidate_count > 0) {
|
||||
int new_games = 0;
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (!candidates[i].installed)
|
||||
new_games++;
|
||||
}
|
||||
if (new_games > 0)
|
||||
notify_system_info("Found %d new games. Executing...", new_games);
|
||||
process_scan_candidates(candidates, candidate_count);
|
||||
}
|
||||
mount_backport_overlays();
|
||||
notify_system_rich(true, "Library Synchronized.\nFound %d games.",
|
||||
total_found_games);
|
||||
|
||||
while (true) {
|
||||
if (should_stop_requested()) {
|
||||
log_debug("[SHUTDOWN] stop requested");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!consume_scan_now_request(&scan_reason)) {
|
||||
if (sleep_with_stop_check(runtime_config()->scan_interval_us)) {
|
||||
log_debug("[SHUTDOWN] stop requested during sleep");
|
||||
break;
|
||||
}
|
||||
(void)consume_scan_now_request(&scan_reason);
|
||||
}
|
||||
|
||||
if (scan_reason) {
|
||||
log_debug("[SCAN] running immediate scan (%s)", scan_reason);
|
||||
scan_reason = NULL;
|
||||
}
|
||||
|
||||
cleanup_lost_sources_before_scan();
|
||||
candidate_count = collect_scan_candidates(candidates, MAX_PENDING, NULL);
|
||||
if (candidate_count > 0)
|
||||
process_scan_candidates(candidates, candidate_count);
|
||||
mount_backport_overlays();
|
||||
if (!app_db_run_startup_maintenance())
|
||||
log_debug(" [DB] startup snd0info maintenance unavailable");
|
||||
log_debug("[STARTUP] scanner startup sync begin");
|
||||
if (!sm_scanner_run_startup_sync()) {
|
||||
log_debug("[STARTUP] scanner startup sync aborted");
|
||||
goto shutdown;
|
||||
}
|
||||
log_debug("[STARTUP] scanner startup sync done");
|
||||
sm_scanner_run_loop();
|
||||
|
||||
shutdown:
|
||||
sm_shellcore_flags_stop();
|
||||
stop_game_lifecycle_watcher();
|
||||
if (kstuff_runtime_enabled)
|
||||
sm_kstuff_shutdown();
|
||||
sm_scanner_shutdown();
|
||||
sm_kstuff_shutdown();
|
||||
sm_mdbg_shutdown();
|
||||
cleanup_kstuff_noautomount_files();
|
||||
shutdown_title_mounts();
|
||||
if (!shutdown_image_mounts()) {
|
||||
log_debug("[SHUTDOWN] some image mounts or devices were not fully released");
|
||||
@@ -405,6 +535,7 @@ shutdown:
|
||||
shutdown_reason ? shutdown_reason : "unknown shutdown source");
|
||||
}
|
||||
|
||||
sm_log_shutdown();
|
||||
sceUserServiceTerminate();
|
||||
return 0;
|
||||
}
|
||||
+326
-56
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
@@ -11,24 +12,34 @@
|
||||
|
||||
static sqlite3 *g_app_db;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0;
|
||||
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
|
||||
static struct AppDbTitleList g_app_db_title_cache;
|
||||
static struct AppDbTitleList g_app_db_blocked_uninstall_ppsa_cache;
|
||||
static bool g_app_db_title_cache_ready = false;
|
||||
static bool g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
static time_t g_app_db_title_cache_mtime = 0;
|
||||
static time_t g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
static pthread_mutex_t g_app_db_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
|
||||
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
|
||||
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_normalize_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
|
||||
g_app_db_stmt_normalize_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
if (!list)
|
||||
return;
|
||||
void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free(list->ids);
|
||||
list->ids = NULL;
|
||||
list->count = 0;
|
||||
@@ -36,10 +47,15 @@ static void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
}
|
||||
|
||||
void shutdown_app_db(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
@@ -91,10 +107,122 @@ static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
|
||||
return -1;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
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 "
|
||||
@@ -104,7 +232,7 @@ int update_snd0info(const char *title_id) {
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
@@ -112,34 +240,86 @@ int update_snd0info(const char *title_id) {
|
||||
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
if (g_app_db) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
return changes;
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
if (g_app_db) {
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
int normalize_snd0info_for_title(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
if (!g_app_db_stmt_normalize_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_normalize_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
@@ -168,50 +348,32 @@ static int compare_title_id_str(const void *a, const void *b) {
|
||||
|
||||
bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\0')
|
||||
if (list->count <= 0)
|
||||
return false;
|
||||
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
|
||||
compare_title_id_str) != NULL;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
if (!list)
|
||||
return false;
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK) {
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list,
|
||||
sqlite3_stmt *stmt,
|
||||
const char *label) {
|
||||
int busy_attempts = 0;
|
||||
while (!should_stop_requested()) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (title_id && title_id[0] != '\0') {
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] title list allocation failed");
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
@@ -221,8 +383,8 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
@@ -235,15 +397,85 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId LIKE 'PPSA%' "
|
||||
"AND uninstallable = 0 "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc =
|
||||
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"blocked PPSA uninstallable query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
|
||||
list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
|
||||
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
const struct AppDbTitleList *src) {
|
||||
if (!dst || !src)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(dst);
|
||||
if (src->count <= 0)
|
||||
return true;
|
||||
|
||||
char(*ids)[MAX_TITLE_ID] = malloc((size_t)src->count * sizeof(*ids));
|
||||
if (!ids)
|
||||
return false;
|
||||
|
||||
memcpy(ids, src->ids, (size_t)src->count * sizeof(*ids));
|
||||
dst->ids = ids;
|
||||
dst->count = src->count;
|
||||
dst->capacity = src->count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
*list_out = NULL;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
@@ -258,11 +490,49 @@ bool get_app_db_title_list_cached(const struct AppDbTitleList **list_out) {
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready)
|
||||
if (!g_app_db_title_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*list_out = &g_app_db_title_cache;
|
||||
return true;
|
||||
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready ||
|
||||
(app_db_stat_rc == 0 &&
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 &&
|
||||
load_app_db_blocked_uninstall_ppsa_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_blocked_uninstall_ppsa_cache = fresh;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = st.st_mtime;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_blocked_uninstall_ppsa_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool loaded =
|
||||
copy_app_db_title_list(list_out, &g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return loaded;
|
||||
}
|
||||
+1069
-205
File diff suppressed because it is too large.
Load diff
+171
-36
@@ -5,13 +5,23 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
typedef struct {
|
||||
char source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
const char *label;
|
||||
} fakelib_layer_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
pid_t pid;
|
||||
char mount_path[MAX_PATH];
|
||||
} fakelib_mount_t;
|
||||
fakelib_layer_t layers[2];
|
||||
size_t layer_count;
|
||||
} fakelib_session_t;
|
||||
|
||||
static fakelib_mount_t g_fakelib_mount;
|
||||
static fakelib_session_t g_fakelib_mount;
|
||||
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
bool sm_fakelib_game_feature_enabled(void) {
|
||||
return runtime_config()->backport_fakelib_enabled;
|
||||
@@ -19,38 +29,61 @@ bool sm_fakelib_game_feature_enabled(void) {
|
||||
|
||||
static bool mount_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path) {
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(source_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path)};
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_path),
|
||||
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
|
||||
IOVEC_ENTRY("notime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)};
|
||||
|
||||
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), 0) == 0) {
|
||||
notify_system_info("Game backported: %s", title_id);
|
||||
log_debug(" [FAKELIB] libraries mounted for %s: %s -> %s", title_id,
|
||||
source_path, mount_path);
|
||||
log_debug(" [FAKELIB] %s libraries mounted for %s: %s -> %s", label,
|
||||
title_id, source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] mount failed for %s (%s -> %s): %s", title_id,
|
||||
source_path, mount_path, strerror(errno));
|
||||
log_debug(" [FAKELIB] %s mount failed for %s (%s -> %s): %s", label,
|
||||
title_id, source_path, mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_fakelib_overlay(const char *mount_path) {
|
||||
static bool unmount_fakelib_overlay(const fakelib_layer_t *layer) {
|
||||
const char *mount_path = layer->mount_path;
|
||||
if (unmount(mount_path, MNT_FORCE) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
|
||||
layer->source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] unmount failed for %s: %s", mount_path,
|
||||
strerror(errno));
|
||||
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
|
||||
mount_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
char source_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
static bool track_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
|
||||
return false;
|
||||
|
||||
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
|
||||
layer->label = label;
|
||||
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
|
||||
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_context(const char *title_id,
|
||||
char game_source_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
game_source_path[0] = '\0';
|
||||
mount_path[0] = '\0';
|
||||
|
||||
char sandbox_id[MAX_TITLE_ID];
|
||||
DIR *d = opendir("/mnt/sandbox");
|
||||
if (!d)
|
||||
@@ -71,8 +104,14 @@ static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
const char *suffix = entry->d_name + title_len + 1u;
|
||||
if (!isdigit((unsigned char)suffix[0]))
|
||||
continue;
|
||||
if (strlen(entry->d_name) >= sizeof(sandbox_id))
|
||||
continue;
|
||||
|
||||
int idx = atoi(suffix);
|
||||
errno = 0;
|
||||
long idx_long = strtol(suffix, NULL, 10);
|
||||
if (errno == ERANGE || idx_long > INT_MAX)
|
||||
continue;
|
||||
int idx = (int)idx_long;
|
||||
if (idx < best_index)
|
||||
continue;
|
||||
|
||||
@@ -85,10 +124,18 @@ static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
if (!found)
|
||||
return false;
|
||||
|
||||
snprintf(source_path, MAX_PATH, "/mnt/sandbox/%s/app0/fakelib", sandbox_id);
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib2",
|
||||
sandbox_id);
|
||||
struct stat st;
|
||||
if (stat(source_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
return false;
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
} else {
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
sandbox_id);
|
||||
if (stat(source_path, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
(void)strlcpy(game_source_path, source_path, MAX_PATH);
|
||||
}
|
||||
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
@@ -116,12 +163,46 @@ static bool resolve_sandbox_fakelib_context(const char *title_id,
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (!g_fakelib_mount.active)
|
||||
return true;
|
||||
static bool resolve_global_fakelib_source(const char *title_id,
|
||||
char source_path[MAX_PATH]) {
|
||||
source_path[0] = '\0';
|
||||
|
||||
if (!unmount_fakelib_overlay(g_fakelib_mount.mount_path)) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->global_fakelib_enabled)
|
||||
return false;
|
||||
if (is_global_fakelib_excluded_for_title(title_id))
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cfg->global_fakelib_path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
|
||||
title_id, cfg->global_fakelib_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode)) {
|
||||
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
|
||||
title_id, cfg->global_fakelib_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cleanup_fakelib_mount(void) {
|
||||
if (g_fakelib_mount.layer_count == 0) {
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
return true;
|
||||
}
|
||||
|
||||
while (g_fakelib_mount.layer_count > 0) {
|
||||
fakelib_layer_t *layer =
|
||||
&g_fakelib_mount.layers[g_fakelib_mount.layer_count - 1];
|
||||
if (!unmount_fakelib_overlay(layer))
|
||||
return false;
|
||||
memset(layer, 0, sizeof(*layer));
|
||||
g_fakelib_mount.layer_count--;
|
||||
}
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
@@ -129,48 +210,102 @@ static bool cleanup_fakelib_mount(void) {
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id) {
|
||||
if (!sm_fakelib_game_feature_enabled())
|
||||
return;
|
||||
|
||||
if (g_fakelib_mount.active && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.active && g_fakelib_mount.pid != pid) {
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_fakelib_mount.layer_count > 0 && g_fakelib_mount.pid != pid) {
|
||||
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
|
||||
(long)g_fakelib_mount.pid, (long)pid, title_id);
|
||||
if (!cleanup_fakelib_mount()) {
|
||||
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
|
||||
(long)g_fakelib_mount.pid, title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
char game_source_path[MAX_PATH];
|
||||
char global_source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
if (!resolve_sandbox_fakelib_context(title_id, source_path, mount_path))
|
||||
if (!resolve_sandbox_context(title_id, game_source_path, mount_path)) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path))
|
||||
bool has_global =
|
||||
resolve_global_fakelib_source(title_id, global_source_path);
|
||||
bool has_game = (game_source_path[0] != '\0');
|
||||
if (!has_global && !has_game) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
g_fakelib_mount.active = true;
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
g_fakelib_mount.pid = pid;
|
||||
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
|
||||
sizeof(g_fakelib_mount.mount_path));
|
||||
|
||||
bool global_first = runtime_config()->global_fakelib_mount_first;
|
||||
bool same_source =
|
||||
has_global && has_game &&
|
||||
strcmp(global_source_path, game_source_path) == 0;
|
||||
if (global_first && has_global && !same_source) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_game) {
|
||||
if (!track_fakelib_overlay(title_id, game_source_path, mount_path,
|
||||
"game")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!global_first && has_global && !same_source) {
|
||||
if (!track_fakelib_overlay(title_id, global_source_path, mount_path,
|
||||
"global")) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_game)
|
||||
notify_system_info("Game backported: %s", title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exit(pid_t pid) {
|
||||
if (!g_fakelib_mount.active || g_fakelib_mount.pid != pid)
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (g_fakelib_mount.layer_count == 0 || g_fakelib_mount.pid != pid) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
|
||||
g_fakelib_mount.mount_path);
|
||||
cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_shutdown(void) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
+263
-63
@@ -12,15 +12,23 @@
|
||||
// --- FILESYSTEM ---
|
||||
bool is_installed(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "/user/app/%s", title_id);
|
||||
snprintf(path, sizeof(path), "%s/%s", APP_BASE, title_id);
|
||||
struct stat st;
|
||||
return (stat(path, &st) == 0);
|
||||
}
|
||||
|
||||
bool has_appmeta_data(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "%s/%s/param.json", APPMETA_BASE, title_id);
|
||||
struct stat st;
|
||||
return (stat(path, &st) == 0 && S_ISREG(st.st_mode));
|
||||
}
|
||||
|
||||
bool is_data_mounted(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "/system_ex/app/%s/sce_sys/param.json",
|
||||
title_id);
|
||||
return (access(path, F_OK) == 0);
|
||||
return path_exists(path);
|
||||
}
|
||||
|
||||
static bool read_mount_link_file(const char *lnk_path, char *out,
|
||||
@@ -38,16 +46,27 @@ static bool read_mount_link_file(const char *lnk_path, char *out,
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
size_t len = strcspn(out, "\r\n");
|
||||
bool truncated = false;
|
||||
if (out[len] == '\0' && len + 1u >= out_size) {
|
||||
int next = fgetc(f);
|
||||
truncated = (next != EOF && next != '\n' && next != '\r');
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
if (truncated) {
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
out[len] = '\0';
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
static void build_title_link_path(const char *title_id, const char *filename,
|
||||
char out[MAX_PATH]) {
|
||||
snprintf(out, MAX_PATH, "/user/app/%s/%s", title_id, filename);
|
||||
snprintf(out, MAX_PATH, "%s/%s/%s", APP_BASE, title_id, filename);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -71,10 +90,10 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id,
|
||||
static void for_each_title_app_dir(const char *open_error_context,
|
||||
bool stop_on_signal,
|
||||
title_app_dir_iter_fn fn, void *ctx) {
|
||||
DIR *d = opendir("/user/app");
|
||||
DIR *d = opendir(APP_BASE);
|
||||
if (!d) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [LINK] open /user/app failed%s: %s",
|
||||
log_debug(" [LINK] open %s failed%s: %s", APP_BASE,
|
||||
open_error_context ? open_error_context : "", strerror(errno));
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +141,7 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
path_size = app_dir_size;
|
||||
}
|
||||
|
||||
int written = snprintf(path, path_size, "/user/app/%s", entry->d_name);
|
||||
int written = snprintf(path, path_size, "%s/%s", APP_BASE, entry->d_name);
|
||||
if (written < 0 || (size_t)written >= path_size) {
|
||||
if (app_dir && app_dir_size > 0)
|
||||
app_dir[0] = '\0';
|
||||
@@ -146,18 +165,21 @@ static bool source_path_needs_cleanup(const char *source_path,
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = resolve_image_source_from_mount_cache(
|
||||
source_path, image_source_path, sizeof(image_source_path));
|
||||
if (has_image_source && access(image_source_path, F_OK) != 0)
|
||||
if (has_image_source && !path_exists(image_source_path))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (access(source_path, F_OK) != 0)
|
||||
if (!path_exists(source_path))
|
||||
return true;
|
||||
if (path_matches_root_or_child(source_path, "/system_ex/app"))
|
||||
return true;
|
||||
|
||||
char eboot_path[MAX_PATH];
|
||||
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
|
||||
if (access(eboot_path, F_OK) == 0)
|
||||
int written =
|
||||
snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin", source_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(eboot_path))
|
||||
return true;
|
||||
if (path_exists(eboot_path))
|
||||
return false;
|
||||
|
||||
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
|
||||
@@ -165,7 +187,23 @@ static bool source_path_needs_cleanup(const char *source_path,
|
||||
*tried_image_recovery = true;
|
||||
}
|
||||
|
||||
return access(eboot_path, F_OK) != 0;
|
||||
return !path_exists(eboot_path);
|
||||
}
|
||||
|
||||
static bool resolve_mount_image_source_path(const title_link_paths_t *paths,
|
||||
const char *source_path,
|
||||
char image_source_path[MAX_PATH]) {
|
||||
image_source_path[0] = '\0';
|
||||
if (!is_under_image_mount_base(source_path))
|
||||
return false;
|
||||
|
||||
if (read_mount_link_file(paths->mount_image_link, image_source_path, MAX_PATH)) {
|
||||
(void)cache_image_source_mapping(image_source_path, source_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return resolve_image_source_from_mount_cache(source_path, image_source_path,
|
||||
MAX_PATH);
|
||||
}
|
||||
|
||||
static bool cleanup_staged_mount_links_entry(const char *title_id,
|
||||
@@ -389,6 +427,7 @@ static bool inspect_title_stack(const char *title_id, const char *source_path,
|
||||
state_out->system_ex_path, mntbuf[i].f_fstypename,
|
||||
mntbuf[i].f_mntfromname, (unsigned long)mntbuf[i].f_flags);
|
||||
}
|
||||
errno = inspect_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -444,6 +483,24 @@ static bool unmount_top_controlled_layer(const char *path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool title_stack_top_is_managed(const title_mount_state_t *state) {
|
||||
return state->top_is_our_nullfs || state->top_kind == TITLE_STACK_TOP_BACKPORT;
|
||||
}
|
||||
|
||||
bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state))
|
||||
return false;
|
||||
if (!state.top_is_our_nullfs)
|
||||
return false;
|
||||
|
||||
if (!unmount_top_controlled_layer(state.system_ex_path))
|
||||
return false;
|
||||
log_debug(" [LINK] rolled back unpublished nullfs mount: %s -> %s",
|
||||
src_path, state.system_ex_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
@@ -484,31 +541,36 @@ bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
return backport_present;
|
||||
}
|
||||
|
||||
void mount_backport_overlay(const char *mount_point,
|
||||
bool mount_backport_overlay(const char *mount_point,
|
||||
const char *backport_path,
|
||||
const char *title_id) {
|
||||
struct stat backport_st;
|
||||
if (stat(backport_path, &backport_st) != 0 || !S_ISDIR(backport_st.st_mode))
|
||||
return;
|
||||
return true;
|
||||
|
||||
struct statfs mounted_sfs;
|
||||
if (statfs(mount_point, &mounted_sfs) != 0 ||
|
||||
strcmp(mounted_sfs.f_mntonname, mount_point) != 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (strcmp(mounted_sfs.f_fstypename, "unionfs") == 0)
|
||||
return;
|
||||
return true;
|
||||
|
||||
bool mount_read_only = ((mounted_sfs.f_flags & MNT_RDONLY) != 0);
|
||||
struct iovec overlay_iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("unionfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(backport_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point)};
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(mount_point),
|
||||
IOVEC_ENTRY("copymode"), IOVEC_ENTRY("transparent"),
|
||||
IOVEC_ENTRY("noatime"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("fnodup"), IOVEC_ENTRY(NULL)
|
||||
};
|
||||
|
||||
int overlay_flags = mount_read_only ? MNT_RDONLY : 0;
|
||||
if (nmount(overlay_iov, IOVEC_SIZE(overlay_iov), overlay_flags) == 0) {
|
||||
log_debug(" [IMG] backport overlay mounted (%s): %s -> %s",
|
||||
mount_read_only ? "ro" : "rw", backport_path, mount_point);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
int overlay_err = errno;
|
||||
@@ -516,6 +578,7 @@ void mount_backport_overlay(const char *mount_point,
|
||||
mount_point, strerror(overlay_err));
|
||||
notify_system("Backport overlay failed: %s\n%s\n0x%08X", title_id,
|
||||
backport_path, (uint32_t)overlay_err);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
@@ -531,6 +594,58 @@ static bool unmount_controlled_mount_stack(const char *path) {
|
||||
return !get_top_mount(path, &mount_st);
|
||||
}
|
||||
|
||||
static void unmount_mount_point_for_recovery(const char *path) {
|
||||
if (!unmount_controlled_mount_stack(path)) {
|
||||
log_debug(" [LINK] failed to reset mount stack for recovery: %s", path);
|
||||
}
|
||||
|
||||
if (unmount(path, 0) == 0) {
|
||||
log_debug(" [LINK] extra unmount for recovery: %s", path);
|
||||
return;
|
||||
}
|
||||
if (errno == ENOENT || errno == EINVAL)
|
||||
return;
|
||||
|
||||
if (unmount(path, MNT_FORCE) == 0) {
|
||||
log_debug(" [LINK] extra forced unmount for recovery: %s", path);
|
||||
return;
|
||||
}
|
||||
if (errno == ENOENT || errno == EINVAL)
|
||||
return;
|
||||
|
||||
log_debug(" [LINK] extra unmount for recovery did not complete for %s: %s",
|
||||
path, strerror(errno));
|
||||
}
|
||||
|
||||
static bool recover_mount_directory_after_stat_failure(
|
||||
const char *path, struct stat *st_out) {
|
||||
log_debug(" [LINK] resetting mount stack after stat failure: %s", path);
|
||||
unmount_mount_point_for_recovery(path);
|
||||
|
||||
if (stat(path, st_out) == 0)
|
||||
return true;
|
||||
|
||||
int stat_errno = errno;
|
||||
if (stat_errno != ENOENT) {
|
||||
log_debug(" [LINK] mount directory stat failed after reset for %s: %s",
|
||||
path, strerror(stat_errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mkdir(path, 0755) != 0 && errno != EEXIST) {
|
||||
log_debug(" [LINK] failed to recreate mount directory %s: %s", path,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stat(path, st_out) == 0)
|
||||
return true;
|
||||
|
||||
log_debug(" [LINK] mount directory stat failed after recreate for %s: %s",
|
||||
path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
@@ -547,6 +662,12 @@ static bool cleanup_duplicate_title_mounts_entry(const char *title_id,
|
||||
!state.has_our_nullfs) {
|
||||
return true;
|
||||
}
|
||||
if (!title_stack_top_is_managed(&state)) {
|
||||
log_debug(" [LINK] duplicate managed mount layers kept for %s: top layer "
|
||||
"is not managed by ShadowMount",
|
||||
state.system_ex_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(
|
||||
" [LINK] duplicate managed mount layers for %s: nullfs=%d backport=%d. "
|
||||
@@ -579,6 +700,10 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
if ((unsigned long)file_size > MAX_PARAM_JSON_SIZE) {
|
||||
fclose(fs);
|
||||
return -1;
|
||||
}
|
||||
rewind(fs);
|
||||
|
||||
size_t len = (size_t)file_size;
|
||||
@@ -618,6 +743,8 @@ static int copy_param_json_rewrite(const char *src, const char *dst) {
|
||||
|
||||
if (ret == 0 && hit)
|
||||
log_debug(" [COPY] param.json patched: %s", dst);
|
||||
if (ret != 0)
|
||||
(void)unlink(dst);
|
||||
|
||||
free(buf);
|
||||
return ret;
|
||||
@@ -656,6 +783,8 @@ int copy_file(const char *src, const char *dst) {
|
||||
ret = -1;
|
||||
if (fclose(fs) != 0)
|
||||
ret = -1;
|
||||
if (ret != 0)
|
||||
(void)unlink(dst);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -673,8 +802,13 @@ int copy_dir(const char *src, const char *dst) {
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
|
||||
continue;
|
||||
snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
|
||||
snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
|
||||
int src_written = snprintf(ss, sizeof(ss), "%s/%s", src, e->d_name);
|
||||
int dst_written = snprintf(dd, sizeof(dd), "%s/%s", dst, e->d_name);
|
||||
if (src_written < 0 || (size_t)src_written >= sizeof(ss) ||
|
||||
dst_written < 0 || (size_t)dst_written >= sizeof(dd)) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if (lstat(ss, &lst) != 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
@@ -726,7 +860,12 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
char src_eboot[MAX_PATH];
|
||||
char dst_eboot[MAX_PATH];
|
||||
snprintf(dst, sizeof(dst), "/system_ex/app/%s", title_id);
|
||||
snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
|
||||
int written = snprintf(src_eboot, sizeof(src_eboot), "%s/eboot.bin", src_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(src_eboot)) {
|
||||
log_debug(" [LINK] source eboot path too long for %s: %s", title_id,
|
||||
src_path);
|
||||
return false;
|
||||
}
|
||||
snprintf(dst_eboot, sizeof(dst_eboot), "%s/eboot.bin", dst);
|
||||
|
||||
struct stat src_st;
|
||||
@@ -740,7 +879,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
title_id, src_path, (unsigned)(src_st.st_mode & 077777));
|
||||
return false;
|
||||
}
|
||||
if (access(src_eboot, F_OK) != 0) {
|
||||
if (!path_exists(src_eboot)) {
|
||||
bool tried_image_recovery = false;
|
||||
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
|
||||
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
|
||||
@@ -749,26 +888,6 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
}
|
||||
}
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
|
||||
log_debug(" [LINK] inspect failed for %s (%s): %s", title_id, dst,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (state.mounted) {
|
||||
if (state.has_our_nullfs &&
|
||||
(state.top_is_our_nullfs || state.has_our_backport) &&
|
||||
access(dst_eboot, F_OK) == 0) {
|
||||
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
|
||||
return true;
|
||||
}
|
||||
if (!unmount_controlled_mount_stack(dst)) {
|
||||
log_debug(" [LINK] failed to reset mount stack for %s (%s)", title_id,
|
||||
dst);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int mkdir_res = mkdir(dst, 0755);
|
||||
if (mkdir_res != 0) {
|
||||
if (errno != EEXIST) {
|
||||
@@ -778,9 +897,11 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
}
|
||||
struct stat dst_st;
|
||||
if (stat(dst, &dst_st) != 0) {
|
||||
int stat_errno = errno;
|
||||
log_debug(" [LINK] mount directory exists but stat failed for %s: %s",
|
||||
dst, strerror(errno));
|
||||
return false;
|
||||
dst, strerror(stat_errno));
|
||||
if (!recover_mount_directory_after_stat_failure(dst, &dst_st))
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(dst_st.st_mode)) {
|
||||
log_debug(" [LINK] mount target is not a directory: %s mode=0%o", dst,
|
||||
@@ -789,19 +910,47 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
}
|
||||
}
|
||||
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, src_path, NULL, &state)) {
|
||||
log_debug(" [LINK] inspect failed for %s (%s): %s", title_id, dst,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (state.mounted) {
|
||||
if (state.has_our_nullfs && title_stack_top_is_managed(&state) &&
|
||||
path_exists(dst_eboot)) {
|
||||
log_debug(" [LINK] mount stack already active: %s -> %s", src_path, dst);
|
||||
return true;
|
||||
}
|
||||
if (!unmount_controlled_mount_stack(dst)) {
|
||||
log_debug(" [LINK] failed to reset mount stack for %s (%s)", title_id,
|
||||
dst);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct iovec iov[] = {
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(src_path),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst),
|
||||
};
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (nmount(iov, IOVEC_SIZE(iov), 0) != 0) {
|
||||
log_debug(" [LINK] Failed to auto-mount nullfs title=%s src=%s dst=%s: %s",
|
||||
title_id, src_path, dst, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (access(dst_eboot, F_OK) != 0) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL)
|
||||
(void)unmount(dst, MNT_FORCE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path_exists(dst_eboot)) {
|
||||
log_debug(" [LINK] mounted nullfs but eboot.bin is missing at target: %s",
|
||||
dst_eboot);
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
@@ -816,19 +965,12 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool path_matches_root_or_child(const char *path, const char *root) {
|
||||
if (!path || !root || root[0] == '\0')
|
||||
return false;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
return (path[root_len] == '\0' || path[root_len] == '/');
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *removed_source_root;
|
||||
bool unmount_system_ex_bind;
|
||||
bool tried_image_recovery;
|
||||
bool force_remove_matching_source;
|
||||
bool match_usb_sources;
|
||||
} cleanup_mount_links_ctx_t;
|
||||
|
||||
static bool cleanup_mount_links_entry(const char *title_id,
|
||||
@@ -841,20 +983,43 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
return true;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
source_path[0] = '\0';
|
||||
image_source_path[0] = '\0';
|
||||
bool has_image_source = false;
|
||||
bool should_remove = false;
|
||||
bool matches_removed_source = false;
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
should_remove = true;
|
||||
} else {
|
||||
has_image_source =
|
||||
resolve_mount_image_source_path(paths, source_path, image_source_path);
|
||||
}
|
||||
|
||||
if (!should_remove) {
|
||||
if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
|
||||
if (ctx->match_usb_sources) {
|
||||
matches_removed_source =
|
||||
path_matches_root_or_child(source_path, ctx->removed_source_root);
|
||||
should_remove = matches_removed_source;
|
||||
is_usb_storage_path(source_path) ||
|
||||
(has_image_source && is_usb_storage_path(image_source_path));
|
||||
if (!matches_removed_source)
|
||||
return true;
|
||||
should_remove = true;
|
||||
} else if (ctx->removed_source_root && ctx->removed_source_root[0] != '\0') {
|
||||
matches_removed_source =
|
||||
path_matches_root_or_child(source_path, ctx->removed_source_root) ||
|
||||
(has_image_source &&
|
||||
path_matches_root_or_child(image_source_path,
|
||||
ctx->removed_source_root));
|
||||
if (!matches_removed_source)
|
||||
return true;
|
||||
should_remove = ctx->force_remove_matching_source ||
|
||||
(has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
} else {
|
||||
should_remove =
|
||||
source_path_needs_cleanup(source_path, &ctx->tried_image_recovery);
|
||||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,13 +1030,14 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
bool mount_link_staged = false;
|
||||
if (ctx->unmount_system_ex_bind) {
|
||||
title_mount_state_t state;
|
||||
memset(&state, 0, sizeof(state));
|
||||
bool inspected = inspect_title_stack(title_id, source_path,
|
||||
ctx->removed_source_root, &state);
|
||||
bool top_is_ours = state.top_is_our_nullfs || state.has_our_backport;
|
||||
bool top_is_ours = inspected && title_stack_top_is_managed(&state);
|
||||
bool stack_matches_link = false;
|
||||
if (source_path[0] != '\0') {
|
||||
if (inspected && source_path[0] != '\0') {
|
||||
stack_matches_link = state.has_our_nullfs;
|
||||
} else if (matches_removed_source && ctx->removed_source_root &&
|
||||
} else if (inspected && matches_removed_source && ctx->removed_source_root &&
|
||||
ctx->removed_source_root[0] != '\0') {
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
@@ -891,6 +1057,11 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] failed to unmount mount stack for %s",
|
||||
state.system_ex_path);
|
||||
} else if (ctx->match_usb_sources) {
|
||||
log_debug(" [LINK] unmounted USB-backed mount stack: %s source=%s%s%s",
|
||||
state.system_ex_path, source_path,
|
||||
has_image_source ? " image=" : "",
|
||||
has_image_source ? image_source_path : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -928,11 +1099,40 @@ void cleanup_mount_links(const char *removed_source_root,
|
||||
.removed_source_root = removed_source_root,
|
||||
.unmount_system_ex_bind = unmount_system_ex_bind,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = false,
|
||||
};
|
||||
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
|
||||
&ctx);
|
||||
}
|
||||
|
||||
void cleanup_mount_links_for_source_unmount(const char *source_root) {
|
||||
if (!source_root || source_root[0] == '\0')
|
||||
return;
|
||||
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = source_root,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = true,
|
||||
.match_usb_sources = false,
|
||||
};
|
||||
for_each_title_app_dir(" during source unmount cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
}
|
||||
|
||||
void cleanup_usb_mount_links_for_suspend(void) {
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.removed_source_root = NULL,
|
||||
.unmount_system_ex_bind = true,
|
||||
.tried_image_recovery = false,
|
||||
.force_remove_matching_source = false,
|
||||
.match_usb_sources = true,
|
||||
};
|
||||
for_each_title_app_dir(" during USB suspend cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
}
|
||||
|
||||
static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
@@ -946,7 +1146,7 @@ static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state) ||
|
||||
!state.has_our_nullfs ||
|
||||
(!state.top_is_our_nullfs && !state.has_our_backport)) {
|
||||
!title_stack_top_is_managed(&state)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+133
-17
@@ -1,27 +1,110 @@
|
||||
#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) {
|
||||
(void)strlcpy(g_game_cache[k].path, path, sizeof(g_game_cache[k].path));
|
||||
(void)strlcpy(g_game_cache[k].title_id, title_id,
|
||||
sizeof(g_game_cache[k].title_id));
|
||||
(void)strlcpy(g_game_cache[k].title_name, title_name,
|
||||
sizeof(g_game_cache[k].title_name));
|
||||
g_game_cache[k].valid = true;
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -31,16 +114,52 @@ void prune_game_cache(void) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (access(g_game_cache[k].path, F_OK) == 0)
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
}
|
||||
|
||||
if (g_game_cache[k].title_id[0] != '\0')
|
||||
log_debug(" [CACHE] source removed: %s (%s)", g_game_cache[k].title_id,
|
||||
g_game_cache[k].path);
|
||||
else
|
||||
log_debug(" [CACHE] source removed: %s", g_game_cache[k].path);
|
||||
void prune_game_cache_for_root(const char *root) {
|
||||
if (!root || root[0] == '\0') {
|
||||
prune_game_cache();
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&g_game_cache[k], 0, sizeof(g_game_cache[k]));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,14 +188,11 @@ bool find_cached_game(const char *path, const char *title_id,
|
||||
}
|
||||
|
||||
void clear_cached_game(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0)
|
||||
continue;
|
||||
memset(&g_game_cache[k], 0, sizeof(g_game_cache[k]));
|
||||
clear_game_cache_slot(k, "removed from duplicate tracking");
|
||||
}
|
||||
}
|
||||
+153
-19
@@ -2,14 +2,19 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
@@ -33,11 +38,49 @@ 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;
|
||||
return strncmp(title_id, "PPSA", 4) == 0 ||
|
||||
strncmp(title_id, "CUSA", 4) == 0 ||
|
||||
strncmp(title_id, "FAKE", 4) == 0;
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
@@ -133,15 +176,13 @@ static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
|
||||
log_debug(" [GAME] started: %s pid=%ld app_id=0x%08X", title_id,
|
||||
(long)pid, app_id);
|
||||
publish_active_game(pid, title_id);
|
||||
sm_kstuff_game_on_exec(pid, title_id, app_id, exec_time_us);
|
||||
sm_fakelib_game_on_exec(pid, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void clear_pending_game_launch(pending_game_launch_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
}
|
||||
|
||||
@@ -160,6 +201,31 @@ static void close_game_lifecycle_wake_pipe(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void drain_game_lifecycle_wake_pipe(void) {
|
||||
char drain_buf[32];
|
||||
while (read(g_game_lifecycle_wake_pipe[0], drain_buf, sizeof(drain_buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
|
||||
while (true) {
|
||||
int nev = kevent(kq, NULL, 0, events, sizeof(events) / sizeof(events[0]),
|
||||
&timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] stale event drain failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
@@ -214,8 +280,6 @@ static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (!entry)
|
||||
entry = reserve_pending_game_launch(now_us);
|
||||
if (!entry)
|
||||
return NULL;
|
||||
|
||||
if (entry->active && entry->pid != pid) {
|
||||
log_debug(" [GAME] replacing pending exec pid=%ld with pid=%ld",
|
||||
@@ -235,11 +299,7 @@ static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
|
||||
uint64_t now_us,
|
||||
const char *title_id) {
|
||||
clear_all_pending_game_launches();
|
||||
if (!queue_pending_game_launch(pid, exec_time_us, now_us)) {
|
||||
log_debug(" [GAME] failed to defer launch retry for %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return;
|
||||
}
|
||||
queue_pending_game_launch(pid, exec_time_us, now_us);
|
||||
|
||||
log_debug(" [GAME] deferring launch retry for %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
@@ -273,6 +333,7 @@ static const struct timespec *compute_game_wait_timeout(
|
||||
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, next_pending_game_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_kstuff_game_next_wake_us(now_us));
|
||||
next_wake_us = min_nonzero_u64(next_wake_us, sm_mdbg_next_wake_us(now_us));
|
||||
if (next_wake_us == 0)
|
||||
return NULL;
|
||||
|
||||
@@ -323,6 +384,7 @@ static void poll_game_modules(int kq) {
|
||||
}
|
||||
|
||||
sm_kstuff_game_poll();
|
||||
sm_mdbg_poll();
|
||||
}
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid) {
|
||||
@@ -357,17 +419,42 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
if (!is_process_alive(pid))
|
||||
return;
|
||||
|
||||
pending_game_launch_t *entry = queue_pending_game_launch(pid, now_us, now_us);
|
||||
if (!entry)
|
||||
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) {
|
||||
@@ -410,7 +497,42 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
set_game_lifecycle_start_result(true);
|
||||
log_debug(" [GAME] lifecycle watcher started");
|
||||
|
||||
bool sleep_cleanup_done = false;
|
||||
pid_t suspended_game_pid = 0;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
publish_active_game(0, NULL);
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_game_lifecycle_wake_pipe[0], &readfds);
|
||||
int rc = select(g_game_lifecycle_wake_pipe[0] + 1, &readfds, NULL, NULL,
|
||||
NULL);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [GAME] sleep wait failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
|
||||
struct kevent event;
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr = compute_game_wait_timeout(&timeout);
|
||||
@@ -426,9 +548,7 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
char drain_buf[32];
|
||||
(void)read(g_game_lifecycle_wake_pipe[0], drain_buf,
|
||||
sizeof(drain_buf));
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if ((event.fflags & NOTE_TRACKERR) != 0) {
|
||||
log_debug(" [GAME] NOTE_TRACKERR for pid=%ld", (long)event.ident);
|
||||
} else {
|
||||
@@ -439,20 +559,21 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
poll_game_modules(kq);
|
||||
}
|
||||
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_game_shutdown();
|
||||
publish_active_game(0, NULL);
|
||||
close(kq);
|
||||
log_debug(" [GAME] lifecycle watcher stopped");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool start_game_lifecycle_watcher(void) {
|
||||
if (!sm_fakelib_game_feature_enabled() && !sm_kstuff_game_feature_enabled())
|
||||
return true;
|
||||
if (g_game_lifecycle_thread_started)
|
||||
return true;
|
||||
if (pipe(g_game_lifecycle_wake_pipe) != 0) {
|
||||
@@ -519,5 +640,18 @@ void stop_game_lifecycle_watcher(void) {
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
publish_active_game(0, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
bool sm_game_lifecycle_has_active_game(void) {
|
||||
return atomic_load(&g_active_game_pid) > 0;
|
||||
}
|
||||
|
||||
bool refresh_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return start_game_lifecycle_watcher();
|
||||
|
||||
wake_game_lifecycle_watcher();
|
||||
return true;
|
||||
}
|
||||
+8
-8
@@ -32,24 +32,27 @@ static int extract_json_string(const char *json, const char *key, char *out,
|
||||
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name) {
|
||||
if (!out_id || !out_name)
|
||||
return false;
|
||||
out_id[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
if (!base_path || !param_st || !S_ISREG(param_st->st_mode))
|
||||
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 > 1024 * 1024) {
|
||||
if (param_st->st_size <= 0 || param_st->st_size > MAX_PARAM_JSON_SIZE) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
|
||||
char path[MAX_PATH];
|
||||
snprintf(path, sizeof(path), "%s/sce_sys/param.json", base_path);
|
||||
int written = snprintf(path, sizeof(path), "%s/sce_sys/param.json",
|
||||
base_path);
|
||||
if (written < 0 || (size_t)written >= sizeof(path)) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
return false;
|
||||
}
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
store_cached_game_info(base_path, param_st, false, "", "");
|
||||
@@ -91,9 +94,6 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
}
|
||||
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out) {
|
||||
if (!dir_path || dir_path[0] == '\0')
|
||||
return false;
|
||||
|
||||
int dir_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
|
||||
if (dir_fd < 0)
|
||||
return false;
|
||||
|
||||
+630
-193
File diff suppressed because it is too large.
Load diff
+37
-15
@@ -1,4 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_limits.h"
|
||||
|
||||
@@ -11,19 +13,16 @@ struct ImageCache {
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
if ((!path || path[0] == '\0') && (!mount_point || mount_point[0] == '\0'))
|
||||
return -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (mount_point && mount_point[0] != '\0' &&
|
||||
strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
return k;
|
||||
}
|
||||
if (path && path[0] != '\0' && strcmp(g_image_cache[k].path, path) == 0)
|
||||
if (path && strcmp(g_image_cache[k].path, path) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
@@ -55,24 +54,32 @@ static int upsert_image_source_mapping(const char *path, const char *mount_point
|
||||
}
|
||||
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point) {
|
||||
return upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
if (!entry_out || index < 0 || index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_cache[index].valid) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -83,38 +90,53 @@ bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS)
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return;
|
||||
}
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0)
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
*backend_out = entry->backend;
|
||||
*unit_out = entry->unit_id;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
if (entry_index < 0 || !path_out || path_out_size == 0)
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
|
||||
path_out[0] = '\0';
|
||||
(void)strlcpy(path_out, entry->path, path_out_size);
|
||||
return path_out[0] != '\0';
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
+312
-36
@@ -1,6 +1,8 @@
|
||||
#include "sm_platform.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_install.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_log.h"
|
||||
@@ -10,6 +12,54 @@
|
||||
#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");
|
||||
@@ -34,17 +84,126 @@ static bool write_link_file(const char *path, const char *value) {
|
||||
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;
|
||||
}
|
||||
|
||||
static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
|
||||
const char *dst_base,
|
||||
const char *relative_path) {
|
||||
char src_path[MAX_PATH];
|
||||
char dst_path[MAX_PATH];
|
||||
|
||||
snprintf(src_path, sizeof(src_path), "%s/%s", src_sce_sys, relative_path);
|
||||
if (access(src_path, F_OK) != 0)
|
||||
return true;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
|
||||
if (access(dst_path, F_OK) == 0)
|
||||
return true;
|
||||
|
||||
if (copy_file(src_path, dst_path) == 0)
|
||||
return true;
|
||||
|
||||
log_debug(" [COPY] Failed to copy trophy metadata: %s -> %s", src_path,
|
||||
dst_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool update_trophy_metadata(const char *title_id,
|
||||
const char *src_sce_sys) {
|
||||
char dst_base[MAX_PATH];
|
||||
char dst_dir[MAX_PATH];
|
||||
bool ok = true;
|
||||
|
||||
snprintf(dst_base, sizeof(dst_base), "/system_data/priv/appmeta/%s",
|
||||
title_id);
|
||||
mkdir("/system_data/priv/appmeta", 0755);
|
||||
mkdir(dst_base, 0755);
|
||||
|
||||
snprintf(dst_dir, sizeof(dst_dir), "%s/trophy2", dst_base);
|
||||
mkdir(dst_dir, 0755);
|
||||
snprintf(dst_dir, sizeof(dst_dir), "%s/uds", dst_base);
|
||||
mkdir(dst_dir, 0755);
|
||||
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
|
||||
"trophy2/npbind.dat"))
|
||||
ok = false;
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base,
|
||||
"uds/npbind.dat"))
|
||||
ok = false;
|
||||
if (!copy_optional_trophy_metadata_file(src_sce_sys, dst_base, "param.json"))
|
||||
ok = false;
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- Install/Remount Action ---
|
||||
static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
bool should_register) {
|
||||
bool should_register,
|
||||
bool use_app_install_all,
|
||||
bool *has_src_snd0_out) {
|
||||
char user_appmeta_dir[MAX_PATH];
|
||||
char user_app_dir[MAX_PATH];
|
||||
char user_sce_sys[MAX_PATH];
|
||||
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_app_dir, sizeof(user_app_dir), "/user/app/%s", title_id);
|
||||
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(
|
||||
@@ -55,35 +214,86 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
}
|
||||
}
|
||||
|
||||
// COPY FILES
|
||||
if (!is_remount) {
|
||||
snprintf(user_sce_sys, sizeof(user_sce_sys), "%s/sce_sys", user_app_dir);
|
||||
mkdir(user_app_dir, 0777);
|
||||
mkdir(user_sce_sys, 0777);
|
||||
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
|
||||
appmeta_missing = !has_appmeta_data(title_id);
|
||||
if (is_remount && appmeta_missing) {
|
||||
log_debug(" [REG] appmeta missing, restaging metadata only: %s", title_id);
|
||||
}
|
||||
|
||||
snprintf(src_sce_sys, sizeof(src_sce_sys), "%s/sce_sys", src_path);
|
||||
restage_staging = (!is_remount || should_register);
|
||||
restage_appmeta = (!is_remount || appmeta_missing);
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
// COPY FILES
|
||||
snprintf(src_snd0, sizeof(src_snd0), "%s/snd0.at9", src_sce_sys);
|
||||
has_src_snd0 = path_exists(src_snd0);
|
||||
if (!restage_staging && !restage_appmeta)
|
||||
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
|
||||
|
||||
if (restage_staging) {
|
||||
mkdir(APP_BASE, 0777);
|
||||
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: %s -> %s", src_sce_sys,
|
||||
log_debug(" [COPY] Failed to copy sce_sys staging: %s -> %s", src_sce_sys,
|
||||
user_sce_sys);
|
||||
return false;
|
||||
}
|
||||
|
||||
char icon_src[MAX_PATH], icon_dst[MAX_PATH];
|
||||
snprintf(icon_src, sizeof(icon_src), "%s/sce_sys/icon0.png", src_path);
|
||||
snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id);
|
||||
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 icon: %s -> %s", icon_src, icon_dst);
|
||||
log_debug(" [COPY] Failed to copy staged icon: %s -> %s", icon_src,
|
||||
icon_dst);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log_debug(" [SPEED] Skipping file copy (Assets already exist)");
|
||||
}
|
||||
|
||||
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 (!update_trophy_metadata(title_id, src_sce_sys))
|
||||
return false;
|
||||
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
log_debug(" [LINK] nullfs mount failed: title=%s src=%s", title_id,
|
||||
src_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// WRITE TRACKER
|
||||
char lnk_path[MAX_PATH];
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path))
|
||||
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);
|
||||
|
||||
@@ -91,8 +301,12 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(img_lnk_path, sizeof(img_lnk_path), "%s/mount_img.lnk",
|
||||
user_app_dir);
|
||||
if (has_image_source) {
|
||||
if (!write_link_file(img_lnk_path, image_source_path))
|
||||
if (!write_link_file(img_lnk_path, image_source_path)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
@@ -103,30 +317,47 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
if (!mount_title_nullfs(title_id, src_path)) {
|
||||
log_debug(" [LINK] mount.lnk created but nullfs mount failed: title=%s "
|
||||
"src=%s",
|
||||
title_id, src_path);
|
||||
return false;
|
||||
}
|
||||
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 (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
|
||||
char src_snd0[MAX_PATH];
|
||||
snprintf(src_snd0, sizeof(src_snd0), "%s/sce_sys/snd0.at9", src_path);
|
||||
bool has_src_snd0 = (access(src_snd0, F_OK) == 0);
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Installed NEW!");
|
||||
notify_game_installed_rich(title_id);
|
||||
if (has_src_snd0) {
|
||||
@@ -136,6 +367,7 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
}
|
||||
} else if ((uint32_t)res == 0x80990002u) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Restored.");
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
@@ -156,24 +388,65 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
// --- Execution (per discovered candidate) ---
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
sm_install_poll_pending();
|
||||
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (should_stop_requested())
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
const scan_candidate_t *c = &candidates[i];
|
||||
bool has_src_snd0 = false;
|
||||
bool use_app_install_all =
|
||||
runtime_config()->app_install_all_enabled && !c->in_app_db;
|
||||
if (c->installed) {
|
||||
log_debug(" [ACTION] Remounting: %s", c->title_name);
|
||||
} else {
|
||||
log_debug(" [ACTION] Installing: %s (%s)", c->title_name, c->title_id);
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
if (!use_app_install_all) {
|
||||
notify_system_info("Installing: %s (%s)...", c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (mount_and_install(c->path, c->title_id, c->title_name, c->installed,
|
||||
!c->in_app_db)) {
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
bool mounted = mount_and_install(c->path, c->title_id, c->title_name,
|
||||
c->installed, !c->in_app_db,
|
||||
use_app_install_all, &has_src_snd0);
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
if (mounted) {
|
||||
if (c->manual && !use_app_install_all) {
|
||||
sm_manual_note_installed(c->manual_source_path, c->title_id,
|
||||
c->title_name);
|
||||
}
|
||||
if (use_app_install_all) {
|
||||
if (!sm_install_queue_candidate(c, has_src_snd0)) {
|
||||
log_debug(" [REG] Failed to queue staged install: %s (%s)",
|
||||
c->title_name, c->title_id);
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, c->title_name,
|
||||
c->title_id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
} else {
|
||||
clear_failed_mount_attempts(c->title_id);
|
||||
cache_game_entry(c->path, c->title_id, c->title_name);
|
||||
}
|
||||
} else {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(c->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
@@ -183,4 +456,7 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
#define INSTALL_WAIT_TIMEOUT_US (300ull * 1000000ull)
|
||||
#define INSTALL_POLL_INTERVAL_US (2ull * 1000000ull)
|
||||
#define INSTALL_SUBMIT_RETRY_US (2ull * 1000000ull)
|
||||
|
||||
typedef enum {
|
||||
INSTALL_TRACK_NONE = 0,
|
||||
INSTALL_TRACK_QUEUED,
|
||||
INSTALL_TRACK_SUBMITTED,
|
||||
} install_track_state_t;
|
||||
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char source_path[MAX_PATH];
|
||||
char manual_source_path[MAX_PATH];
|
||||
uint64_t requested_at_us;
|
||||
bool has_src_snd0;
|
||||
bool manual;
|
||||
install_track_state_t state;
|
||||
} pending_install_entry_t;
|
||||
|
||||
static pending_install_entry_t g_pending_installs[MAX_PENDING];
|
||||
static uint64_t g_pending_install_poll_due_us = 0;
|
||||
static uint64_t g_queued_install_submit_due_us = 0;
|
||||
static int g_tracked_install_count = 0;
|
||||
static int g_submitted_install_count = 0;
|
||||
static bool g_queued_install_batch_announced = false;
|
||||
static bool g_queued_install_submit_failure_notified = false;
|
||||
|
||||
static int count_queued_installs(void);
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry);
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry);
|
||||
|
||||
static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
continue;
|
||||
if (strcmp(g_pending_installs[i].title_id, title_id) == 0)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *reserve_pending_install_entry(
|
||||
const char *title_id) {
|
||||
pending_install_entry_t *entry = find_pending_install_entry(title_id);
|
||||
if (entry)
|
||||
return entry;
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
if (g_pending_installs[i].state == INSTALL_TRACK_NONE)
|
||||
return &g_pending_installs[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint64_t next_pending_install_timeout_us(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return 0;
|
||||
|
||||
uint64_t next_deadline = 0;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED || entry->requested_at_us == 0)
|
||||
continue;
|
||||
|
||||
uint64_t deadline_us = entry->requested_at_us + INSTALL_WAIT_TIMEOUT_US;
|
||||
if (next_deadline == 0 || deadline_us < next_deadline)
|
||||
next_deadline = deadline_us;
|
||||
}
|
||||
|
||||
return next_deadline;
|
||||
}
|
||||
|
||||
static void schedule_pending_install_poll(uint64_t now_us) {
|
||||
if (g_submitted_install_count <= 0) {
|
||||
g_pending_install_poll_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_pending_install_poll_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = now_us == 0 ? 1 : now_us;
|
||||
}
|
||||
|
||||
static void schedule_queued_install_submit_retry(uint64_t now_us) {
|
||||
if (count_queued_installs() <= 0 || g_submitted_install_count > 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us =
|
||||
now_us == 0 ? 1 : now_us + INSTALL_SUBMIT_RETRY_US;
|
||||
}
|
||||
|
||||
static void clear_pending_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
|
||||
g_submitted_install_count--;
|
||||
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
|
||||
g_tracked_install_count--;
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
if (g_submitted_install_count <= 0)
|
||||
g_pending_install_poll_due_us = 0;
|
||||
if (g_tracked_install_count <= 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_title_install_pending(const char *title_id) {
|
||||
return find_pending_install_entry(title_id) != NULL;
|
||||
}
|
||||
|
||||
void sm_install_forget_pending_title(const char *title_id) {
|
||||
pending_install_entry_t *entry = find_pending_install_entry(title_id);
|
||||
if (entry)
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void append_batch_install_line(char *message, size_t message_size,
|
||||
const char *title_name,
|
||||
const char *title_id, int *shown_count) {
|
||||
if (!message || message_size == 0 || !title_name || !title_id || !shown_count)
|
||||
return;
|
||||
|
||||
size_t used = strlen(message);
|
||||
if (used >= message_size)
|
||||
return;
|
||||
|
||||
int written = snprintf(message + used, message_size - used, "\n%s (%s)",
|
||||
title_name, title_id);
|
||||
if (written < 0 || (size_t)written >= message_size - used)
|
||||
return;
|
||||
|
||||
(*shown_count)++;
|
||||
}
|
||||
|
||||
bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
bool has_src_snd0) {
|
||||
if (!candidate || candidate->title_id[0] == '\0' ||
|
||||
candidate->title_name[0] == '\0' || candidate->path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!runtime_config()->app_install_all_enabled)
|
||||
return false;
|
||||
|
||||
bool was_queue_empty = (count_queued_installs() <= 0);
|
||||
pending_install_entry_t *entry =
|
||||
reserve_pending_install_entry(candidate->title_id);
|
||||
if (!entry) {
|
||||
log_debug(" [REG] install queue full, cannot queue %s (%s)",
|
||||
candidate->title_name, candidate->title_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
install_track_state_t previous_state = entry->state;
|
||||
if (previous_state == INSTALL_TRACK_SUBMITTED)
|
||||
return true;
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
(void)strlcpy(entry->title_id, candidate->title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, candidate->title_name,
|
||||
sizeof(entry->title_name));
|
||||
(void)strlcpy(entry->source_path, candidate->path, sizeof(entry->source_path));
|
||||
(void)strlcpy(entry->manual_source_path, candidate->manual_source_path,
|
||||
sizeof(entry->manual_source_path));
|
||||
entry->manual = candidate->manual;
|
||||
entry->has_src_snd0 = has_src_snd0;
|
||||
entry->state = INSTALL_TRACK_QUEUED;
|
||||
if (previous_state == INSTALL_TRACK_NONE)
|
||||
g_tracked_install_count++;
|
||||
if (was_queue_empty) {
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
}
|
||||
schedule_queued_install_submit(monotonic_time_us());
|
||||
return true;
|
||||
}
|
||||
|
||||
static void finalize_pending_install_success(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Installed: %s (%s)", entry->title_name, entry->title_id);
|
||||
notify_game_installed_rich(entry->title_id);
|
||||
if (entry->has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(entry->title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
cache_game_entry(entry->source_path, entry->title_id, entry->title_name);
|
||||
if (entry->manual)
|
||||
sm_manual_note_installed(entry->manual_source_path, entry->title_id,
|
||||
entry->title_name);
|
||||
clear_register_attempts(entry->title_id);
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void finalize_pending_install_timeout(pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Install timeout after 5 minutes: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
notify_system("Install failed: %s (%s)\nTimed out after 5 minutes",
|
||||
entry->title_name, entry->title_id);
|
||||
(void)note_pending_install_failure(entry);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count <= 0)
|
||||
return;
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
bool app_db_titles_ready = get_app_db_title_list_cached(&app_db_titles);
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
|
||||
if (!app_db_titles_ready)
|
||||
log_debug(" [DB] app.db title list unavailable while polling installs");
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_SUBMITTED)
|
||||
continue;
|
||||
|
||||
if (app_db_titles_ready &&
|
||||
app_db_title_list_contains(&app_db_titles, entry->title_id)) {
|
||||
finalize_pending_install_success(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now_us != 0 && entry->requested_at_us != 0 &&
|
||||
now_us >= entry->requested_at_us &&
|
||||
now_us - entry->requested_at_us >= INSTALL_WAIT_TIMEOUT_US) {
|
||||
finalize_pending_install_timeout(entry);
|
||||
}
|
||||
}
|
||||
|
||||
schedule_pending_install_poll(now_us);
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return 0;
|
||||
|
||||
if (!install_queue_active())
|
||||
return 0;
|
||||
|
||||
if (g_submitted_install_count <= 0)
|
||||
return g_queued_install_submit_due_us;
|
||||
|
||||
uint64_t next_wake_us = next_pending_install_timeout_us();
|
||||
|
||||
if (g_pending_install_poll_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_pending_install_poll_due_us < next_wake_us)) {
|
||||
next_wake_us = g_pending_install_poll_due_us;
|
||||
}
|
||||
|
||||
if (g_queued_install_submit_due_us != 0 &&
|
||||
(next_wake_us == 0 || g_queued_install_submit_due_us < next_wake_us)) {
|
||||
next_wake_us = g_queued_install_submit_due_us;
|
||||
}
|
||||
|
||||
if (next_wake_us == 0)
|
||||
next_wake_us = now_us == 0 ? 1 : now_us + INSTALL_POLL_INTERVAL_US;
|
||||
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_install_poll_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
poll_pending_installs();
|
||||
}
|
||||
|
||||
void sm_install_service_pending(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
if (!install_queue_active())
|
||||
return;
|
||||
|
||||
sm_install_poll_pending();
|
||||
if (count_queued_installs() > 0 && !sm_install_submit_queued())
|
||||
sm_install_note_submit_failure();
|
||||
}
|
||||
|
||||
static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
|
||||
uint8_t failed_attempts = get_failed_mount_attempts(entry->title_id);
|
||||
if (failed_attempts == MAX_FAILED_MOUNT_ATTEMPTS) {
|
||||
log_debug(" [RETRY] limit reached (%u/%u): %s (%s)",
|
||||
(unsigned)failed_attempts,
|
||||
(unsigned)MAX_FAILED_MOUNT_ATTEMPTS, entry->title_name,
|
||||
entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static int count_queued_installs(void) {
|
||||
int queued_count = g_tracked_install_count - g_submitted_install_count;
|
||||
return queued_count > 0 ? queued_count : 0;
|
||||
}
|
||||
|
||||
static void log_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Batch install request contains %d title(s):", queued_count);
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
log_debug(" - %s (%s)", entry->title_name, entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void notify_queued_install_batch(void) {
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0 || g_queued_install_batch_announced)
|
||||
return;
|
||||
|
||||
char message[3075];
|
||||
int shown_count = 0;
|
||||
snprintf(message, sizeof(message), "Batch install queued (%d):", queued_count);
|
||||
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
const pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
append_batch_install_line(message, sizeof(message), entry->title_name,
|
||||
entry->title_id, &shown_count);
|
||||
}
|
||||
|
||||
if (shown_count < queued_count) {
|
||||
size_t used = strlen(message);
|
||||
if (used < sizeof(message)) {
|
||||
(void)snprintf(message + used, sizeof(message) - used, "\n... and %d more",
|
||||
queued_count - shown_count);
|
||||
}
|
||||
}
|
||||
|
||||
notify_system_info("%s", message);
|
||||
g_queued_install_batch_announced = true;
|
||||
}
|
||||
|
||||
static uint8_t note_pending_install_failure(const pending_install_entry_t *entry) {
|
||||
if (!entry)
|
||||
return 0;
|
||||
|
||||
uint8_t failed_attempts = bump_failed_mount_attempts(entry->title_id);
|
||||
log_batch_submit_retry_limit(entry);
|
||||
return failed_attempts;
|
||||
}
|
||||
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry) {
|
||||
if (!entry || entry->state != INSTALL_TRACK_QUEUED)
|
||||
return;
|
||||
|
||||
log_debug(" [REG] Dropping queued install after retry limit: %s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
clear_pending_install_entry(entry);
|
||||
}
|
||||
|
||||
bool sm_install_submit_queued(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
int queued_count = count_queued_installs();
|
||||
if (queued_count <= 0)
|
||||
return true;
|
||||
|
||||
if (g_submitted_install_count > 0)
|
||||
return true;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (g_queued_install_submit_due_us != 0 && now_us != 0 &&
|
||||
now_us < g_queued_install_submit_due_us) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
notify_system("Batch install failed for %d app(s).\ncode=0x%08X",
|
||||
queued_count, (uint32_t)res);
|
||||
g_queued_install_submit_failure_notified = true;
|
||||
}
|
||||
schedule_queued_install_submit_retry(now_us);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
entry->state = INSTALL_TRACK_SUBMITTED;
|
||||
entry->requested_at_us = now_us;
|
||||
g_submitted_install_count++;
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
}
|
||||
|
||||
invalidate_app_db_title_cache();
|
||||
schedule_pending_install_poll(now_us);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_install_note_submit_failure(void) {
|
||||
for (int i = 0; i < MAX_PENDING; i++) {
|
||||
pending_install_entry_t *entry = &g_pending_installs[i];
|
||||
if (entry->state != INSTALL_TRACK_QUEUED)
|
||||
continue;
|
||||
|
||||
if (note_pending_install_failure(entry) >= MAX_FAILED_MOUNT_ATTEMPTS)
|
||||
drop_queued_install_entry(entry);
|
||||
}
|
||||
}
|
||||
+420
-29
@@ -5,7 +5,9 @@
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
@@ -17,10 +19,13 @@
|
||||
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;
|
||||
@@ -28,8 +33,10 @@ typedef struct {
|
||||
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;
|
||||
@@ -39,6 +46,164 @@ typedef struct {
|
||||
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)
|
||||
@@ -139,6 +304,34 @@ static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out)
|
||||
*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;
|
||||
}
|
||||
@@ -149,6 +342,16 @@ static bool kstuff_sysentvec_is_enabled(intptr_t sysentvec_addr) {
|
||||
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
|
||||
@@ -156,7 +359,7 @@ static void set_kstuff_sysentvec_enabled(intptr_t sysentvec_addr, bool enabled)
|
||||
}
|
||||
|
||||
static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
|
||||
if (!g_kstuff.supported)
|
||||
if (!refresh_kstuff_support_state())
|
||||
return false;
|
||||
|
||||
bool ps5_enabled = kstuff_sysentvec_is_enabled(g_kstuff.sysentvec_ps5);
|
||||
@@ -168,6 +371,19 @@ static bool read_kstuff_enabled_state(bool *ps5_out, bool *ps4_out) {
|
||||
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;
|
||||
@@ -181,23 +397,92 @@ static void mark_restore_needed(void) {
|
||||
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) {
|
||||
bool *image_backed_out,
|
||||
uint32_t *autopause_delay_out,
|
||||
bool *autopause_delay_valid_out) {
|
||||
char source_path[MAX_PATH];
|
||||
bool image_backed =
|
||||
read_mount_link(title_id, source_path, sizeof(source_path)) &&
|
||||
is_under_image_mount_base(source_path);
|
||||
bool image_backed = false;
|
||||
(void)resolve_title_pause_source(title_id, source_path, &image_backed);
|
||||
|
||||
if (image_backed_out)
|
||||
*image_backed_out = 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 =
|
||||
image_backed ? runtime_config()->kstuff_pause_delay_image_seconds
|
||||
: runtime_config()->kstuff_pause_delay_direct_seconds;
|
||||
uint32_t override_delay_seconds = 0;
|
||||
if (get_kstuff_pause_delay_override_for_title(title_id,
|
||||
&override_delay_seconds)) {
|
||||
return override_delay_seconds;
|
||||
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;
|
||||
@@ -223,6 +508,12 @@ static void restore_kstuff_if_needed(const char *reason) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -244,7 +535,7 @@ static void restore_kstuff_if_needed(const char *reason) {
|
||||
}
|
||||
|
||||
static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
if (!entry || !entry->active || entry->pause_applied)
|
||||
if (!entry->active || entry->pause_applied)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
@@ -256,6 +547,15 @@ static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
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);
|
||||
@@ -267,6 +567,8 @@ static void maybe_apply_kstuff_pause_for_slot(kstuff_game_entry_t *entry) {
|
||||
|
||||
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");
|
||||
@@ -286,7 +588,13 @@ static void handle_kstuff_app_focus_change(uint32_t focused_app_id) {
|
||||
return;
|
||||
|
||||
if (focused_app_id != g_kstuff.game.app_id) {
|
||||
if (!g_kstuff.game.pause_applied || g_kstuff.game.focus_override_active)
|
||||
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();
|
||||
@@ -366,7 +674,7 @@ uint64_t sm_kstuff_game_next_wake_us(uint64_t now_us) {
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_supported(void) {
|
||||
return g_kstuff.supported;
|
||||
return refresh_kstuff_support_state();
|
||||
}
|
||||
|
||||
bool sm_kstuff_is_enabled(void) {
|
||||
@@ -375,12 +683,11 @@ bool sm_kstuff_is_enabled(void) {
|
||||
|
||||
static bool apply_kstuff_enabled_state(bool enabled, bool notify_user,
|
||||
bool *fully_applied_out) {
|
||||
if (!g_kstuff.supported)
|
||||
return false;
|
||||
|
||||
bool ps5_enabled = false;
|
||||
bool ps4_enabled = false;
|
||||
bool is_enabled = read_kstuff_enabled_state(&ps5_enabled, &ps4_enabled);
|
||||
if (!g_kstuff.supported)
|
||||
return false;
|
||||
bool fully_applied = kstuff_state_matches(enabled, ps5_enabled, ps4_enabled);
|
||||
if (fully_applied) {
|
||||
if (fully_applied_out)
|
||||
@@ -418,7 +725,7 @@ bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
|
||||
}
|
||||
|
||||
bool sm_kstuff_game_feature_enabled(void) {
|
||||
return runtime_config()->kstuff_game_auto_toggle && sm_kstuff_is_supported();
|
||||
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,
|
||||
@@ -433,12 +740,14 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_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));
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("tracked game handoff");
|
||||
if (restore_needed)
|
||||
finish_tracked_game_clear("tracked game handoff");
|
||||
}
|
||||
|
||||
if (is_kstuff_pause_disabled_for_title(title_id)) {
|
||||
@@ -448,20 +757,27 @@ void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
}
|
||||
|
||||
bool image_backed = false;
|
||||
uint32_t delay_seconds =
|
||||
get_pause_delay_seconds_for_title(title_id, &image_backed);
|
||||
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,
|
||||
@@ -477,15 +793,19 @@ 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 (!g_kstuff.game.active)
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("tracked game exit");
|
||||
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;
|
||||
|
||||
@@ -498,14 +818,65 @@ void sm_kstuff_game_poll(void) {
|
||||
}
|
||||
|
||||
void sm_kstuff_game_shutdown(void) {
|
||||
bool restore_needed = tracked_game_requires_restore();
|
||||
if (restore_needed && !g_kstuff.sleep_pause_restore &&
|
||||
runtime_sleep_mode_active() && should_stop_requested()) {
|
||||
restore_needed = false;
|
||||
g_kstuff.restore_on_empty = false;
|
||||
g_kstuff.restore_retry_us = 0;
|
||||
} else if (g_kstuff.sleep_pause_restore) {
|
||||
restore_needed = true;
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
}
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
restore_kstuff_if_needed("watcher shutdown");
|
||||
if (restore_needed) {
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("watcher shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_enter(void) {
|
||||
bool was_enabled = false;
|
||||
bool fully_disabled = false;
|
||||
|
||||
sm_mdbg_game_shutdown();
|
||||
memset(&g_kstuff.game, 0, sizeof(g_kstuff.game));
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
|
||||
if (!sm_kstuff_is_supported())
|
||||
return;
|
||||
|
||||
was_enabled = sm_kstuff_is_enabled();
|
||||
(void)apply_kstuff_enabled_state(false, false, &fully_disabled);
|
||||
if (!fully_disabled) {
|
||||
log_debug(" [KSTUFF] failed to pause for system sleep");
|
||||
return;
|
||||
}
|
||||
|
||||
g_kstuff.sleep_pause_restore = was_enabled;
|
||||
if (was_enabled)
|
||||
log_debug(" [KSTUFF] paused for system sleep");
|
||||
}
|
||||
|
||||
void sm_kstuff_sleep_leave(void) {
|
||||
if (!g_kstuff.sleep_pause_restore)
|
||||
return;
|
||||
|
||||
g_kstuff.sleep_pause_restore = false;
|
||||
mark_restore_needed();
|
||||
restore_kstuff_if_needed("system sleep");
|
||||
}
|
||||
|
||||
void sm_kstuff_on_config_reload(void) {
|
||||
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)) {
|
||||
@@ -513,8 +884,14 @@ void sm_kstuff_init(void) {
|
||||
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;
|
||||
}
|
||||
|
||||
g_kstuff.supported = true;
|
||||
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
|
||||
"direct_delay=%u)",
|
||||
runtime_config()->kstuff_pause_delay_image_seconds,
|
||||
@@ -525,5 +902,19 @@ 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;
|
||||
}
|
||||
+68
-31
@@ -1,4 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
@@ -6,6 +8,9 @@
|
||||
|
||||
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;
|
||||
@@ -60,10 +65,10 @@ static bool ensure_notification_icon_present(void) {
|
||||
}
|
||||
|
||||
void sm_notifications_init(void) {
|
||||
if (g_notifications_initialized)
|
||||
return;
|
||||
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
if (!g_notifications_initialized)
|
||||
g_notifications_initialized = ensure_notification_icon_present();
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
}
|
||||
|
||||
static void append_json_escaped(char *dst, size_t dst_size, const char *src) {
|
||||
@@ -138,7 +143,10 @@ static bool send_rich_notification(const char *message) {
|
||||
return false;
|
||||
|
||||
sm_notifications_init();
|
||||
if (!g_notifications_initialized)
|
||||
pthread_mutex_lock(&g_notifications_mutex);
|
||||
bool notifications_ready = g_notifications_initialized;
|
||||
pthread_mutex_unlock(&g_notifications_mutex);
|
||||
if (!notifications_ready)
|
||||
return false;
|
||||
|
||||
escaped_message[0] = '\0';
|
||||
@@ -157,8 +165,10 @@ static bool send_rich_notification(const char *message) {
|
||||
" \"rawData\": {\n"
|
||||
" \"viewTemplateType\": \"InteractiveToastTemplateB\",\n"
|
||||
" \"channelType\": \"ServiceFeedback\",\n"
|
||||
" \"bundleName\": \"ShadowMountPlusWelcome\",\n"
|
||||
" \"useCaseId\": \"IDC\",\n"
|
||||
" \"toastOverwriteType\": \"No\",\n"
|
||||
" \"soundEffect\": \"none\",\n"
|
||||
" \"toastOverwriteType\": \"InQueue\",\n"
|
||||
" \"isImmediate\": true,\n"
|
||||
" \"priority\": 100,\n"
|
||||
" \"viewData\": {\n"
|
||||
@@ -238,24 +248,38 @@ static void notify_system_plain_v(const char *fmt, va_list args) {
|
||||
notify_system_plain_message(message);
|
||||
}
|
||||
|
||||
static void log_to_file(const char *fmt, va_list args) {
|
||||
mkdir(LOG_DIR, 0777);
|
||||
FILE *fp = fopen(LOG_FILE, "a");
|
||||
if (fp) {
|
||||
va_list args_file;
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
|
||||
va_copy(args_file, args);
|
||||
fprintf(fp, "[%s] ", buffer);
|
||||
vfprintf(fp, fmt, args_file);
|
||||
fprintf(fp, "\n");
|
||||
va_end(args_file);
|
||||
fclose(fp);
|
||||
}
|
||||
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, ...) {
|
||||
@@ -266,13 +290,27 @@ void log_debug(const char *fmt, ...) {
|
||||
va_list args_copy;
|
||||
va_start(args, fmt);
|
||||
va_copy(args_copy, args);
|
||||
vprintf(fmt, args);
|
||||
printf("\n");
|
||||
log_to_file(fmt, args_copy);
|
||||
|
||||
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;
|
||||
@@ -355,10 +393,9 @@ void notify_system(const char *fmt, ...) {
|
||||
|
||||
void notify_image_mount_failed(const char *path, int mount_err) {
|
||||
if (g_last_error.valid && g_last_error.message[0] != '\0') {
|
||||
notify_system("Image mount failed: 0x%08X (%s)\n%s\n%s",
|
||||
(uint32_t)mount_err, strerror(mount_err),
|
||||
g_last_error.message,
|
||||
(g_last_error.path[0] != '\0') ? g_last_error.path : path);
|
||||
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;
|
||||
}
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include "sm_manual.h"
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
#define MANUAL_STATUS_CAPACITY 1024
|
||||
|
||||
typedef enum {
|
||||
MANUAL_STATUS_UNKNOWN = 0,
|
||||
MANUAL_STATUS_INSTALLED,
|
||||
MANUAL_STATUS_DELETED,
|
||||
} manual_status_t;
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
manual_status_t status;
|
||||
bool remove_from_list;
|
||||
} manual_status_entry_t;
|
||||
|
||||
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
|
||||
|
||||
static void trim_manual_line(char *line) {
|
||||
if (!line)
|
||||
return;
|
||||
|
||||
char *start = line;
|
||||
while (*start != '\0' && isspace((unsigned char)*start))
|
||||
start++;
|
||||
if (start != line)
|
||||
memmove(line, start, strlen(start) + 1u);
|
||||
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && isspace((unsigned char)line[len - 1u])) {
|
||||
line[len - 1u] = '\0';
|
||||
len--;
|
||||
}
|
||||
while (len > 1 && line[len - 1u] == '/') {
|
||||
line[len - 1u] = '\0';
|
||||
len--;
|
||||
}
|
||||
}
|
||||
|
||||
static void drain_manual_file_line(FILE *f) {
|
||||
int ch;
|
||||
while ((ch = fgetc(f)) != EOF && ch != '\n') {
|
||||
}
|
||||
}
|
||||
|
||||
static void copy_manual_file_line_remainder(FILE *in, FILE *out,
|
||||
int *saved_errno) {
|
||||
int ch;
|
||||
while ((ch = fgetc(in)) != EOF) {
|
||||
if (saved_errno && *saved_errno == 0 && fputc(ch, out) == EOF)
|
||||
*saved_errno = errno;
|
||||
if (ch == '\n')
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx) {
|
||||
if (!visit)
|
||||
return false;
|
||||
|
||||
FILE *f = fopen(MANUAL_LIST_FILE, "r");
|
||||
if (!f) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [MANUAL] open failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[MAX_PATH + 64];
|
||||
bool completed = true;
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
bool truncated =
|
||||
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
|
||||
trim_manual_line(line);
|
||||
if (line[0] == '\0' || line[0] == '#') {
|
||||
if (truncated)
|
||||
drain_manual_file_line(f);
|
||||
continue;
|
||||
}
|
||||
if (truncated || strlen(line) >= MAX_PATH) {
|
||||
log_debug(" [MANUAL] path too long, ignored: %.80s", line);
|
||||
if (truncated)
|
||||
drain_manual_file_line(f);
|
||||
continue;
|
||||
}
|
||||
if (!visit(line, ctx)) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ferror(f)) {
|
||||
log_debug(" [MANUAL] read failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
completed = false;
|
||||
}
|
||||
fclose(f);
|
||||
return completed;
|
||||
}
|
||||
|
||||
static const char *manual_status_name(manual_status_t status) {
|
||||
switch (status) {
|
||||
case MANUAL_STATUS_INSTALLED:
|
||||
return "installed";
|
||||
case MANUAL_STATUS_DELETED:
|
||||
return "deleted";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static manual_status_t parse_manual_status_name(const char *name) {
|
||||
if (!name)
|
||||
return MANUAL_STATUS_UNKNOWN;
|
||||
if (strcasecmp(name, "installed") == 0)
|
||||
return MANUAL_STATUS_INSTALLED;
|
||||
if (strcasecmp(name, "deleted") == 0)
|
||||
return MANUAL_STATUS_DELETED;
|
||||
return MANUAL_STATUS_UNKNOWN;
|
||||
}
|
||||
|
||||
static bool parse_manual_status_line(char *line, manual_status_entry_t *entry) {
|
||||
char *status = strtok(line, "\t");
|
||||
char *title_id = strtok(NULL, "\t");
|
||||
char *path = strtok(NULL, "\t");
|
||||
char *title_name = strtok(NULL, "\n");
|
||||
if (!status || !title_id || !path)
|
||||
return false;
|
||||
|
||||
trim_manual_line(status);
|
||||
trim_manual_line(title_id);
|
||||
trim_manual_line(path);
|
||||
if (title_name)
|
||||
trim_manual_line(title_name);
|
||||
|
||||
manual_status_t parsed_status = parse_manual_status_name(status);
|
||||
if (parsed_status == MANUAL_STATUS_UNKNOWN || title_id[0] == '\0' ||
|
||||
path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->status = parsed_status;
|
||||
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
if (title_name && title_name[0] != '\0')
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
return true;
|
||||
}
|
||||
|
||||
static int load_manual_status(void) {
|
||||
FILE *f = fopen(MANUAL_STATUS_FILE, "r");
|
||||
if (!f) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [MANUAL] status open failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
char line[MAX_PATH + MAX_TITLE_NAME + MAX_TITLE_ID + 64];
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
if (count >= MANUAL_STATUS_CAPACITY) {
|
||||
log_debug(" [MANUAL] status limit reached (%d)",
|
||||
MANUAL_STATUS_CAPACITY);
|
||||
break;
|
||||
}
|
||||
manual_status_entry_t entry;
|
||||
if (!parse_manual_status_line(line, &entry))
|
||||
continue;
|
||||
g_manual_status_entries[count++] = entry;
|
||||
}
|
||||
|
||||
if (ferror(f))
|
||||
log_debug(" [MANUAL] status read failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
fclose(f);
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool write_manual_status(int entry_count) {
|
||||
mkdir(LOG_DIR, 0777);
|
||||
|
||||
char tmp_path[MAX_PATH];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_STATUS_FILE);
|
||||
FILE *f = fopen(tmp_path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [MANUAL] status write open failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
const manual_status_entry_t *entry = &g_manual_status_entries[i];
|
||||
if (entry->status == MANUAL_STATUS_UNKNOWN || entry->path[0] == '\0' ||
|
||||
entry->title_id[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (fprintf(f, "%s\t%s\t%s\t%s\n", manual_status_name(entry->status),
|
||||
entry->title_id, entry->path, entry->title_name) < 0) {
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [MANUAL] status write failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rename(tmp_path, MANUAL_STATUS_FILE) != 0) {
|
||||
log_debug(" [MANUAL] status rename failed for %s: %s",
|
||||
MANUAL_STATUS_FILE, strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int find_manual_status_entry(int entry_count, const char *path,
|
||||
const char *title_id) {
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
if (path && path[0] != '\0' &&
|
||||
strcmp(g_manual_status_entries[i].path, path) == 0) {
|
||||
return i;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_manual_status_entries[i].title_id, title_id) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
|
||||
FILE *in = fopen(MANUAL_LIST_FILE, "r");
|
||||
if (!in) {
|
||||
if (errno == ENOENT)
|
||||
return true;
|
||||
log_debug(" [MANUAL] list rewrite open failed for %s: %s",
|
||||
MANUAL_LIST_FILE, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
char tmp_path[MAX_PATH];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", MANUAL_LIST_FILE);
|
||||
FILE *out = fopen(tmp_path, "w");
|
||||
if (!out) {
|
||||
log_debug(" [MANUAL] list rewrite temp open failed for %s: %s", tmp_path,
|
||||
strerror(errno));
|
||||
fclose(in);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
int saved_errno = 0;
|
||||
char line[MAX_PATH + 64];
|
||||
while (fgets(line, sizeof(line), in) != NULL) {
|
||||
bool truncated =
|
||||
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
|
||||
char normalized[MAX_PATH + 64];
|
||||
(void)strlcpy(normalized, line, sizeof(normalized));
|
||||
trim_manual_line(normalized);
|
||||
if (!truncated && normalized[0] != '\0' && normalized[0] != '#') {
|
||||
bool remove_line = false;
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
if (g_manual_status_entries[i].remove_from_list &&
|
||||
strcmp(g_manual_status_entries[i].path, normalized) == 0) {
|
||||
remove_line = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remove_line) {
|
||||
changed = true;
|
||||
if (truncated)
|
||||
drain_manual_file_line(in);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (fputs(line, out) == EOF) {
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
if (truncated)
|
||||
copy_manual_file_line_remainder(in, out, &saved_errno);
|
||||
}
|
||||
if (ferror(in) && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
fclose(in);
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [MANUAL] list rewrite failed for %s: %s", MANUAL_LIST_FILE,
|
||||
strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
(void)unlink(tmp_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
|
||||
log_debug(" [MANUAL] list rewrite rename failed for %s: %s",
|
||||
MANUAL_LIST_FILE, strerror(errno));
|
||||
(void)unlink(tmp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_manual_reconcile_deleted_titles(
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready) {
|
||||
if (!app_db_titles_ready || !app_db_titles)
|
||||
return true;
|
||||
|
||||
int entry_count = load_manual_status();
|
||||
if (entry_count <= 0)
|
||||
return true;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < entry_count; i++) {
|
||||
manual_status_entry_t *entry = &g_manual_status_entries[i];
|
||||
if (entry->status != MANUAL_STATUS_INSTALLED ||
|
||||
entry->title_id[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
if (app_db_title_list_contains(app_db_titles, entry->title_id))
|
||||
continue;
|
||||
|
||||
entry->status = MANUAL_STATUS_DELETED;
|
||||
entry->remove_from_list = true;
|
||||
sm_install_forget_pending_title(entry->title_id);
|
||||
clear_register_attempts(entry->title_id);
|
||||
clear_failed_mount_attempts(entry->title_id);
|
||||
changed = true;
|
||||
log_debug(" [MANUAL] title removed from app.db, disabling manual entry: "
|
||||
"%s (%s)",
|
||||
entry->title_name, entry->title_id);
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return true;
|
||||
|
||||
if (!rewrite_manual_list_without_deleted_entries(entry_count))
|
||||
return false;
|
||||
return write_manual_status(entry_count);
|
||||
}
|
||||
|
||||
void sm_manual_note_installed(const char *manual_source_path,
|
||||
const char *title_id,
|
||||
const char *title_name) {
|
||||
if (!manual_source_path || manual_source_path[0] == '\0' || !title_id ||
|
||||
title_id[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int entry_count = load_manual_status();
|
||||
int index =
|
||||
find_manual_status_entry(entry_count, manual_source_path, title_id);
|
||||
if (index < 0) {
|
||||
if (entry_count >= MANUAL_STATUS_CAPACITY) {
|
||||
log_debug(" [MANUAL] status full, cannot record %s (%s)",
|
||||
manual_source_path, title_id);
|
||||
return;
|
||||
}
|
||||
index = entry_count++;
|
||||
memset(&g_manual_status_entries[index], 0,
|
||||
sizeof(g_manual_status_entries[index]));
|
||||
}
|
||||
|
||||
manual_status_entry_t *entry = &g_manual_status_entries[index];
|
||||
manual_status_t previous_status = entry->status;
|
||||
bool unchanged = previous_status == MANUAL_STATUS_INSTALLED &&
|
||||
strcmp(entry->path, manual_source_path) == 0 &&
|
||||
strcmp(entry->title_id, title_id) == 0 &&
|
||||
(!title_name || title_name[0] == '\0' ||
|
||||
strcmp(entry->title_name, title_name) == 0);
|
||||
if (unchanged)
|
||||
return;
|
||||
|
||||
entry->status = MANUAL_STATUS_INSTALLED;
|
||||
(void)strlcpy(entry->path, manual_source_path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->title_id, title_id, sizeof(entry->title_id));
|
||||
if (title_name && title_name[0] != '\0')
|
||||
(void)strlcpy(entry->title_name, title_name, sizeof(entry->title_name));
|
||||
|
||||
if (!write_manual_status(entry_count))
|
||||
return;
|
||||
if (previous_status != MANUAL_STATUS_INSTALLED) {
|
||||
log_debug(" [MANUAL] installed status recorded: %s (%s) from %s",
|
||||
entry->title_name, entry->title_id, entry->path);
|
||||
}
|
||||
}
|
||||
+664
@@ -0,0 +1,664 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
int sceKernelDebugGetPrivateLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
int sceKernelDebugGetSdkLogText(void *buffer, size_t buffer_size,
|
||||
char **text, uint64_t *text_size);
|
||||
uint64_t sceKernelDebugGetLogBufferSize(void) __asm__("C49jelxiaVE");
|
||||
int mdbg_call(void *cmd, void *req, void *res);
|
||||
|
||||
#define MDBG_CMD_TYPE_SERVICE 1ull
|
||||
#define MDBG_CMD_PROCESS_STATE 30ull
|
||||
#define MDBG_SUBCMD_FLAGS 2ull
|
||||
|
||||
#define MDBG_FLAG_EXCEPTION_STOP 0x00080000ull
|
||||
#define MDBG_AUTOTUNE_WINDOW_US (300ull * 1000000ull)
|
||||
#define MDBG_LOG_LINE_BUFFER_SIZE 512u
|
||||
#define MDBG_RTLD_ERROR_PREFIX_SIZE 32u
|
||||
#ifndef MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_USE_PRIVATE_LOG_TEXT 0
|
||||
#endif
|
||||
#ifndef MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define MDBG_SKIP_PRIVILEGE_ELEVATION 0
|
||||
#endif
|
||||
|
||||
#if MDBG_USE_PRIVATE_LOG_TEXT
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetPrivateLogText
|
||||
#else
|
||||
#define MDBG_FETCH_LOG_TEXT sceKernelDebugGetSdkLogText
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint64_t type;
|
||||
uint64_t cmd;
|
||||
} mdbg_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t pid;
|
||||
int64_t subcmd;
|
||||
uint64_t arg;
|
||||
uint64_t reserved[5];
|
||||
} mdbg_req_t;
|
||||
|
||||
typedef struct {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
uint64_t reserved[2];
|
||||
} mdbg_res_t;
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
bool log_monitoring_active;
|
||||
bool pause_seen;
|
||||
pid_t pid;
|
||||
uint32_t pause_delay_seconds;
|
||||
uint64_t monitor_deadline_us;
|
||||
uint64_t pause_time_us;
|
||||
uint64_t next_poll_us;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char rtld_error_prefix[MDBG_RTLD_ERROR_PREFIX_SIZE];
|
||||
} mdbg_game_state_t;
|
||||
|
||||
typedef struct {
|
||||
bool privilege_probe_done;
|
||||
bool privilege_ready;
|
||||
size_t log_buffer_size;
|
||||
size_t log_snapshot_length;
|
||||
size_t log_line_length;
|
||||
char *log_snapshot;
|
||||
char *log_storage;
|
||||
char log_line[MDBG_LOG_LINE_BUFFER_SIZE];
|
||||
mdbg_game_state_t game;
|
||||
} sm_mdbg_state_t;
|
||||
|
||||
static sm_mdbg_state_t g_mdbg;
|
||||
|
||||
static bool sm_mdbg_enabled(void);
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
static int elevate_to_coredump(void);
|
||||
#endif
|
||||
static bool ensure_mdbg_privileges(void);
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out);
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out);
|
||||
static bool is_mdbg_process_gone_error(int ret);
|
||||
static void reset_log_line_buffer(void);
|
||||
static void reset_log_snapshot(void);
|
||||
static void free_log_buffers(void);
|
||||
static bool ensure_log_buffers(void);
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out);
|
||||
static size_t find_log_overlap(const char *current, size_t current_len);
|
||||
static void update_log_snapshot(const char *text, size_t text_len);
|
||||
static void clear_tracked_game(void);
|
||||
static bool matches_tracked_rtld_error(const char *text);
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error);
|
||||
static void handle_pre_pause_failure(const char *reason);
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us);
|
||||
static void process_log_line(const char *line, uint64_t now_us);
|
||||
static void append_log_char(char ch, uint64_t now_us);
|
||||
static void poll_log_monitor(uint64_t now_us);
|
||||
static void start_log_monitoring(void);
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
|
||||
|
||||
static bool sm_mdbg_enabled(void) {
|
||||
return runtime_config()->kstuff_crash_detection_enabled &&
|
||||
runtime_config()->kstuff_game_auto_toggle;
|
||||
}
|
||||
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
#define SCE_AUTHID_COREDUMP 0x4800000000000006ull
|
||||
static int elevate_to_coredump(void) {
|
||||
static const uint8_t k_priv_caps[16] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
||||
pid_t pid = getpid();
|
||||
|
||||
if (kernel_set_ucred_authid(pid, SCE_AUTHID_COREDUMP) < 0)
|
||||
return -1;
|
||||
if (kernel_set_ucred_caps(pid, k_priv_caps) < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool ensure_mdbg_privileges(void) {
|
||||
if (g_mdbg.privilege_probe_done)
|
||||
return g_mdbg.privilege_ready;
|
||||
|
||||
g_mdbg.privilege_probe_done = true;
|
||||
#if MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
g_mdbg.privilege_ready = true;
|
||||
log_debug(" [MDBG] coredump privilege probe skipped");
|
||||
#else
|
||||
g_mdbg.privilege_ready = elevate_to_coredump() == 0;
|
||||
if (g_mdbg.privilege_ready) {
|
||||
log_debug(" [MDBG] coredump privileges enabled");
|
||||
} else {
|
||||
log_debug(" [MDBG] failed to enable coredump privileges; mdbg polling "
|
||||
"disabled, log monitoring disabled");
|
||||
}
|
||||
#endif
|
||||
|
||||
return g_mdbg.privilege_ready;
|
||||
}
|
||||
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
int64_t *status_out, uint64_t *value_out) {
|
||||
mdbg_cmd_t cmd = {MDBG_CMD_TYPE_SERVICE, MDBG_CMD_PROCESS_STATE};
|
||||
mdbg_req_t req = {
|
||||
.pid = pid,
|
||||
.subcmd = (int64_t)subcmd,
|
||||
.arg = arg,
|
||||
};
|
||||
mdbg_res_t res = {0};
|
||||
int ret = mdbg_call(&cmd, &req, &res);
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
if (status_out)
|
||||
*status_out = res.status;
|
||||
if (value_out)
|
||||
*value_out = res.value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_mdbg_flags(pid_t pid, uint64_t *flags_out) {
|
||||
int64_t status;
|
||||
uint64_t value;
|
||||
int ret = mdbg_call_raw(pid, MDBG_SUBCMD_FLAGS, 0, &status, &value);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
if (status != 0)
|
||||
return (int)status;
|
||||
if (flags_out)
|
||||
*flags_out = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool is_mdbg_process_gone_error(int ret) {
|
||||
return ret == -ESRCH || ret == -ENOENT || ret == ESRCH || ret == ENOENT;
|
||||
}
|
||||
|
||||
static void reset_log_line_buffer(void) {
|
||||
g_mdbg.log_line_length = 0;
|
||||
g_mdbg.log_line[0] = '\0';
|
||||
}
|
||||
|
||||
static void reset_log_snapshot(void) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void free_log_buffers(void) {
|
||||
free(g_mdbg.log_snapshot);
|
||||
free(g_mdbg.log_storage);
|
||||
g_mdbg.log_snapshot = NULL;
|
||||
g_mdbg.log_storage = NULL;
|
||||
g_mdbg.log_buffer_size = 0;
|
||||
reset_log_snapshot();
|
||||
}
|
||||
|
||||
static bool ensure_log_buffers(void) {
|
||||
if (g_mdbg.log_storage && g_mdbg.log_snapshot && g_mdbg.log_buffer_size != 0)
|
||||
return true;
|
||||
|
||||
free_log_buffers();
|
||||
|
||||
const char *title_id =
|
||||
g_mdbg.game.title_id[0] != '\0' ? g_mdbg.game.title_id : "?";
|
||||
uint64_t raw_size = sceKernelDebugGetLogBufferSize();
|
||||
if (raw_size == 0) {
|
||||
log_debug(" [MDBG] invalid log buffer size for %s: 0x%016" PRIx64,
|
||||
title_id,
|
||||
raw_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t buffer_size = (size_t)raw_size;
|
||||
char *storage = malloc(buffer_size);
|
||||
char *snapshot = malloc(buffer_size);
|
||||
if (!storage || !snapshot) {
|
||||
free(storage);
|
||||
free(snapshot);
|
||||
log_debug(" [MDBG] failed to allocate log buffers for %s: size=0x%zx",
|
||||
title_id,
|
||||
buffer_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mdbg.log_storage = storage;
|
||||
g_mdbg.log_snapshot = snapshot;
|
||||
g_mdbg.log_buffer_size = buffer_size;
|
||||
reset_log_snapshot();
|
||||
log_debug(" [MDBG] log monitor ready: %s buffer_size=0x%zx", title_id,
|
||||
buffer_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int fetch_log_text(const char **text_out, size_t *text_len_out) {
|
||||
char *raw_text = NULL;
|
||||
uint64_t raw_len = 0;
|
||||
|
||||
*text_out = NULL;
|
||||
*text_len_out = 0;
|
||||
|
||||
int ret =
|
||||
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
|
||||
&raw_len);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
if (!raw_text || raw_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (raw_len > g_mdbg.log_buffer_size)
|
||||
return -EOVERFLOW;
|
||||
|
||||
*text_out = raw_text;
|
||||
*text_len_out = (size_t)raw_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t find_log_overlap(const char *current, size_t current_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
|
||||
current_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t max_overlap = g_mdbg.log_snapshot_length < current_len
|
||||
? g_mdbg.log_snapshot_length
|
||||
: current_len;
|
||||
|
||||
const char *search = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length -
|
||||
max_overlap;
|
||||
const char *end = g_mdbg.log_snapshot + g_mdbg.log_snapshot_length;
|
||||
while (search < end) {
|
||||
const char *candidate =
|
||||
memchr(search, current[0], (size_t)(end - search));
|
||||
if (!candidate)
|
||||
break;
|
||||
|
||||
size_t overlap = (size_t)(end - candidate);
|
||||
if (!memcmp(candidate, current, overlap))
|
||||
return overlap;
|
||||
|
||||
search = candidate + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void update_log_snapshot(const char *text, size_t text_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_buffer_size == 0)
|
||||
return;
|
||||
|
||||
if (!text || text_len == 0) {
|
||||
g_mdbg.log_snapshot_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text_len > g_mdbg.log_buffer_size)
|
||||
text_len = g_mdbg.log_buffer_size;
|
||||
|
||||
memcpy(g_mdbg.log_snapshot, text, text_len);
|
||||
g_mdbg.log_snapshot_length = text_len;
|
||||
}
|
||||
|
||||
static void clear_tracked_game(void) {
|
||||
memset(&g_mdbg.game, 0, sizeof(g_mdbg.game));
|
||||
free_log_buffers();
|
||||
}
|
||||
|
||||
static bool matches_tracked_rtld_error(const char *text) {
|
||||
return text &&
|
||||
g_mdbg.game.rtld_error_prefix[0] != '\0' &&
|
||||
strstr(text, g_mdbg.game.rtld_error_prefix) != NULL;
|
||||
}
|
||||
|
||||
static void summarize_failure_reason(const char *reason, char *summary_out,
|
||||
size_t summary_out_size,
|
||||
bool is_rtld_error) {
|
||||
if (!summary_out || summary_out_size == 0)
|
||||
return;
|
||||
|
||||
summary_out[0] = '\0';
|
||||
if (!reason || reason[0] == '\0')
|
||||
return;
|
||||
|
||||
if (is_rtld_error) {
|
||||
const char *open_paren = strrchr(reason, '(');
|
||||
const char *close_paren =
|
||||
open_paren ? strchr(open_paren + 1, ')') : NULL;
|
||||
if (open_paren && close_paren && close_paren > open_paren + 1) {
|
||||
int written = snprintf(summary_out, summary_out_size,
|
||||
"can't load module %.*s after KStuff pause",
|
||||
(int)(close_paren - open_paren - 1),
|
||||
open_paren + 1);
|
||||
if (written > 0 && (size_t)written < summary_out_size)
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, "can't load module after KStuff pause",
|
||||
summary_out_size);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)strlcpy(summary_out, reason, summary_out_size);
|
||||
}
|
||||
|
||||
static void handle_pre_pause_failure(const char *reason) {
|
||||
log_debug(" [MDBG] %s crashed before kstuff auto-pause%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed before KStuff pause: %s. KStuff is not to "
|
||||
"blame.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void handle_post_pause_failure(const char *reason, uint64_t now_us) {
|
||||
if (!g_mdbg.game.pause_seen || g_mdbg.game.pause_time_us == 0 ||
|
||||
now_us < g_mdbg.game.pause_time_us) {
|
||||
handle_pre_pause_failure(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t post_pause_us = now_us - g_mdbg.game.pause_time_us;
|
||||
if (post_pause_us > MDBG_AUTOTUNE_WINDOW_US) {
|
||||
log_debug(" [MDBG] %s crashed %us after kstuff pause; autotune skipped%s%s",
|
||||
g_mdbg.game.title_id, (unsigned)(post_pause_us / 1000000ull),
|
||||
reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("App crashed after KStuff pause: %s. Autotune skipped.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
char reason_summary[128];
|
||||
bool is_rtld_error = matches_tracked_rtld_error(reason);
|
||||
summarize_failure_reason(reason, reason_summary, sizeof(reason_summary),
|
||||
is_rtld_error);
|
||||
|
||||
uint32_t tuned_delay_seconds = 0;
|
||||
if (upsert_kstuff_autotune_pause_delay(g_mdbg.game.title_id,
|
||||
g_mdbg.game.pause_delay_seconds,
|
||||
&tuned_delay_seconds)) {
|
||||
log_debug(" [MDBG] autotune pause delay updated: %s=%us",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
if (reason_summary[0] != '\0')
|
||||
log_debug(" [MDBG] autotune trigger: %s", reason_summary);
|
||||
if (is_rtld_error) {
|
||||
notify_system_info("%s: %s. Delay increased to %us.\nLaunch the game again.",
|
||||
g_mdbg.game.title_id,
|
||||
reason_summary[0] != '\0' ? reason_summary
|
||||
: "Can't load module after "
|
||||
"KStuff pause",
|
||||
tuned_delay_seconds);
|
||||
} else {
|
||||
notify_system_info("Crash detected after KStuff pause: pause delay for %s "
|
||||
"increased to %us. Launch the game again.",
|
||||
g_mdbg.game.title_id, tuned_delay_seconds);
|
||||
}
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] failed to persist autotune pause delay for %s%s%s",
|
||||
g_mdbg.game.title_id, reason ? ": " : "", reason ? reason : "");
|
||||
notify_system_info("Crash detected after KStuff pause: %s. Failed to update "
|
||||
"autotune delay.",
|
||||
g_mdbg.game.title_id);
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
static void process_log_line(const char *line, uint64_t now_us) {
|
||||
if (!matches_tracked_rtld_error(line))
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] log load error for %s: %s", g_mdbg.game.title_id, line);
|
||||
handle_post_pause_failure(line, now_us);
|
||||
}
|
||||
|
||||
static void append_log_char(char ch, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (g_mdbg.log_line_length == 0)
|
||||
return;
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.log_line_length + 1u >= sizeof(g_mdbg.log_line)) {
|
||||
g_mdbg.log_line[g_mdbg.log_line_length] = '\0';
|
||||
process_log_line(g_mdbg.log_line, now_us);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
g_mdbg.log_line[g_mdbg.log_line_length++] = ch;
|
||||
}
|
||||
|
||||
static void poll_log_monitor(uint64_t now_us) {
|
||||
if (!g_mdbg.game.active || !g_mdbg.game.log_monitoring_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
size_t skip = 0;
|
||||
if (g_mdbg.log_snapshot_length != 0 && text_len >= g_mdbg.log_snapshot_length &&
|
||||
!memcmp(g_mdbg.log_snapshot, text, g_mdbg.log_snapshot_length)) {
|
||||
skip = g_mdbg.log_snapshot_length;
|
||||
} else {
|
||||
skip = find_log_overlap(text, text_len);
|
||||
if (skip == 0 && g_mdbg.log_snapshot_length != 0) {
|
||||
log_debug(" [MDBG] log stream reset or truncated for %s",
|
||||
g_mdbg.game.title_id);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = skip; i < text_len; ++i) {
|
||||
append_log_char(text[i], now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
}
|
||||
|
||||
static void start_log_monitoring(void) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_log_buffers()) {
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_mdbg.game.log_monitoring_active = true;
|
||||
|
||||
const char *text = NULL;
|
||||
size_t text_len = 0;
|
||||
int ret = fetch_log_text(&text, &text_len);
|
||||
if (ret < 0) {
|
||||
log_debug(" [MDBG] initial log snapshot failed for %s: 0x%08x",
|
||||
g_mdbg.game.title_id, ret);
|
||||
g_mdbg.game.log_monitoring_active = false;
|
||||
reset_log_snapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
update_log_snapshot(text, text_len);
|
||||
reset_log_line_buffer();
|
||||
}
|
||||
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us) {
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
log_debug(" [MDBG] crash-candidate: %s pid=%ld flags=0x%08" PRIx64,
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid, flags);
|
||||
|
||||
handle_post_pause_failure("crash-candidate", now_us);
|
||||
}
|
||||
|
||||
void sm_mdbg_init(void) {
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_shutdown(void) {
|
||||
free_log_buffers();
|
||||
memset(&g_mdbg, 0, sizeof(g_mdbg));
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id) {
|
||||
if (pid <= 0 || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
if (!sm_mdbg_enabled()) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_mdbg.game.active && g_mdbg.game.pid != pid) {
|
||||
log_debug(" [MDBG] replacing tracked game pid=%ld (%s) with pid=%ld (%s)",
|
||||
(long)g_mdbg.game.pid, g_mdbg.game.title_id, (long)pid, title_id);
|
||||
}
|
||||
|
||||
clear_tracked_game();
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
g_mdbg.game.active = true;
|
||||
g_mdbg.game.pid = pid;
|
||||
g_mdbg.game.next_poll_us = now_us;
|
||||
(void)strlcpy(g_mdbg.game.title_id, title_id, sizeof(g_mdbg.game.title_id));
|
||||
int rtld_prefix_len =
|
||||
snprintf(g_mdbg.game.rtld_error_prefix,
|
||||
sizeof(g_mdbg.game.rtld_error_prefix),
|
||||
"[rtld] <%ld> ERROR", (long)pid);
|
||||
if (rtld_prefix_len <= 0 ||
|
||||
(size_t)rtld_prefix_len >= sizeof(g_mdbg.game.rtld_error_prefix)) {
|
||||
g_mdbg.game.rtld_error_prefix[0] = '\0';
|
||||
}
|
||||
|
||||
log_debug(" [MDBG] tracking crash-candidate state: %s pid=%ld app_id=0x%08X",
|
||||
g_mdbg.game.title_id, (long)pid, app_id);
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_kstuff_pause(pid_t pid, uint64_t pause_time_us,
|
||||
uint32_t pause_delay_seconds) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
g_mdbg.game.pause_seen = true;
|
||||
g_mdbg.game.pause_time_us = pause_time_us;
|
||||
g_mdbg.game.monitor_deadline_us =
|
||||
g_mdbg.game.pause_time_us + MDBG_AUTOTUNE_WINDOW_US;
|
||||
g_mdbg.game.pause_delay_seconds = pause_delay_seconds;
|
||||
g_mdbg.game.next_poll_us = pause_time_us;
|
||||
start_log_monitoring();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_on_exit(pid_t pid) {
|
||||
if (!g_mdbg.game.active || g_mdbg.game.pid != pid)
|
||||
return;
|
||||
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
void sm_mdbg_game_shutdown(void) {
|
||||
clear_tracked_game();
|
||||
}
|
||||
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us) {
|
||||
(void)now_us;
|
||||
if (!sm_mdbg_enabled())
|
||||
return 0;
|
||||
if (!g_mdbg.game.active)
|
||||
return 0;
|
||||
|
||||
uint64_t next_wake_us = g_mdbg.game.next_poll_us;
|
||||
if (g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
(next_wake_us == 0 || g_mdbg.game.monitor_deadline_us < next_wake_us)) {
|
||||
next_wake_us = g_mdbg.game.monitor_deadline_us;
|
||||
}
|
||||
return next_wake_us;
|
||||
}
|
||||
|
||||
void sm_mdbg_poll(void) {
|
||||
if (!sm_mdbg_enabled())
|
||||
return;
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (now_us < g_mdbg.game.next_poll_us) {
|
||||
return;
|
||||
}
|
||||
if (g_mdbg.game.pause_seen && g_mdbg.game.monitor_deadline_us != 0 &&
|
||||
now_us >= g_mdbg.game.monitor_deadline_us) {
|
||||
log_debug(" [MDBG] crash monitoring window expired for %s pid=%ld",
|
||||
g_mdbg.game.title_id, (long)g_mdbg.game.pid);
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_mdbg_privileges()) {
|
||||
g_mdbg.game.next_poll_us = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
poll_log_monitor(now_us);
|
||||
if (!g_mdbg.game.active)
|
||||
return;
|
||||
|
||||
g_mdbg.game.next_poll_us = now_us + GAME_LIFECYCLE_POLL_INTERVAL_US;
|
||||
|
||||
uint64_t flags = 0;
|
||||
int ret = query_mdbg_flags(g_mdbg.game.pid, &flags);
|
||||
if (is_mdbg_process_gone_error(ret)) {
|
||||
clear_tracked_game();
|
||||
return;
|
||||
}
|
||||
if (ret != 0)
|
||||
return;
|
||||
|
||||
if ((flags & MDBG_FLAG_EXCEPTION_STOP) == 0)
|
||||
return;
|
||||
|
||||
handle_crash_candidate(flags, now_us);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#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) {
|
||||
@@ -17,9 +18,9 @@ const char *attach_backend_name(attach_backend_t backend) {
|
||||
}
|
||||
|
||||
// --- Device Node Wait and Source Stability ---
|
||||
static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
for (int i = 0; i < LVD_NODE_WAIT_RETRIES; i++) {
|
||||
if ((access(devname, F_OK) == 0) == should_exist)
|
||||
if (path_exists(devname) == should_exist)
|
||||
return true;
|
||||
sceKernelUsleep(LVD_NODE_WAIT_US);
|
||||
}
|
||||
@@ -43,7 +44,7 @@ bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
|
||||
int *unit_out) {
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (!dev_path || strncmp(dev_path, prefix, prefix_len) != 0)
|
||||
if (strncmp(dev_path, prefix, prefix_len) != 0)
|
||||
return false;
|
||||
|
||||
char *end = NULL;
|
||||
@@ -58,9 +59,6 @@ static bool parse_unit_from_dev_path(const char *dev_path, const char *prefix,
|
||||
|
||||
bool resolve_device_from_mount(const char *mount_point,
|
||||
attach_backend_t *backend_out, int *unit_out) {
|
||||
if (!mount_point || !backend_out || !unit_out)
|
||||
return false;
|
||||
|
||||
*backend_out = ATTACH_BACKEND_NONE;
|
||||
*unit_out = -1;
|
||||
|
||||
@@ -106,8 +104,6 @@ bool resolve_device_from_mount(const char *mount_point,
|
||||
}
|
||||
|
||||
static bool is_path_mountpoint(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct statfs sfs;
|
||||
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
|
||||
}
|
||||
|
||||
+83
-30
@@ -1,5 +1,6 @@
|
||||
#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"
|
||||
@@ -11,6 +12,8 @@ struct PathStateEntry {
|
||||
uint8_t image_mount_attempts;
|
||||
bool missing_param_limit_logged;
|
||||
bool image_mount_limit_logged;
|
||||
bool backport_mount_blocked;
|
||||
bool manual_missing_source_logged;
|
||||
bool game_info_cached;
|
||||
bool game_info_valid;
|
||||
time_t game_info_mtime;
|
||||
@@ -41,8 +44,6 @@ static void rebuild_path_state_hash(void) {
|
||||
}
|
||||
|
||||
static struct PathStateEntry *find_path_state(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return NULL;
|
||||
uint32_t slot = sm_fnv1a32(path) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_path_state_hash[slot];
|
||||
@@ -57,9 +58,6 @@ static struct PathStateEntry *find_path_state(const char *path) {
|
||||
}
|
||||
|
||||
static struct PathStateEntry *create_path_state(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid) {
|
||||
@@ -72,7 +70,8 @@ static struct PathStateEntry *create_path_state(const char *path) {
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid)
|
||||
continue;
|
||||
if (access(g_path_state[k].path, F_OK) != 0) {
|
||||
if (!path_exists(g_path_state[k].path) &&
|
||||
!g_path_state[k].manual_missing_source_logged) {
|
||||
evict_k = k;
|
||||
break;
|
||||
}
|
||||
@@ -83,6 +82,8 @@ static struct PathStateEntry *create_path_state(const char *path) {
|
||||
continue;
|
||||
if (g_path_state[k].missing_param_attempts == 0 &&
|
||||
g_path_state[k].image_mount_attempts == 0 &&
|
||||
!g_path_state[k].backport_mount_blocked &&
|
||||
!g_path_state[k].manual_missing_source_logged &&
|
||||
!g_path_state[k].game_info_cached) {
|
||||
evict_k = k;
|
||||
break;
|
||||
@@ -123,10 +124,7 @@ static struct PathStateEntry *get_or_create_path_state(const char *path) {
|
||||
|
||||
bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
char *out_id, char *out_name, bool *valid_out) {
|
||||
if (valid_out)
|
||||
*valid_out = false;
|
||||
if (!path || !param_st)
|
||||
return false;
|
||||
*valid_out = false;
|
||||
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry || !entry->game_info_cached ||
|
||||
@@ -136,28 +134,19 @@ bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (valid_out)
|
||||
*valid_out = entry->game_info_valid;
|
||||
*valid_out = entry->game_info_valid;
|
||||
if (!entry->game_info_valid)
|
||||
return true;
|
||||
|
||||
if (out_id)
|
||||
(void)strlcpy(out_id, entry->game_title_id, MAX_TITLE_ID);
|
||||
if (out_name)
|
||||
(void)strlcpy(out_name, entry->game_title_name, MAX_TITLE_NAME);
|
||||
(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) {
|
||||
if (!path || !param_st)
|
||||
return;
|
||||
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
entry->game_info_cached = true;
|
||||
entry->game_info_valid = valid;
|
||||
entry->game_info_mtime = param_st->st_mtime;
|
||||
@@ -172,7 +161,32 @@ void prune_path_state(void) {
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
if (!g_path_state[k].valid || g_path_state[k].path[0] == '\0')
|
||||
continue;
|
||||
if (access(g_path_state[k].path, F_OK) == 0)
|
||||
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;
|
||||
@@ -195,11 +209,6 @@ void record_missing_param_failure(const char *path) {
|
||||
return;
|
||||
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry) {
|
||||
log_debug(" [SCAN] missing/invalid param.json: %s", path);
|
||||
notify_system("Missing/invalid param.json:\n%s", path);
|
||||
return;
|
||||
}
|
||||
if (entry->missing_param_attempts < UINT8_MAX)
|
||||
entry->missing_param_attempts++;
|
||||
|
||||
@@ -230,8 +239,6 @@ bool is_image_mount_limited(const char *path) {
|
||||
|
||||
uint8_t bump_image_mount_attempts(const char *path) {
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return MAX_IMAGE_MOUNT_ATTEMPTS;
|
||||
if (entry->image_mount_attempts < UINT8_MAX)
|
||||
entry->image_mount_attempts++;
|
||||
if (entry->image_mount_attempts >= MAX_IMAGE_MOUNT_ATTEMPTS &&
|
||||
@@ -251,3 +258,49 @@ void clear_image_mount_attempts(const char *path) {
|
||||
entry->image_mount_attempts = 0;
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
|
||||
bool is_backport_mount_blocked(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
return entry ? entry->backport_mount_blocked : false;
|
||||
}
|
||||
|
||||
void note_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
if (entry->backport_mount_blocked)
|
||||
return;
|
||||
entry->backport_mount_blocked = true;
|
||||
}
|
||||
|
||||
void clear_backport_mount_failure(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->backport_mount_blocked = false;
|
||||
}
|
||||
|
||||
bool note_manual_missing_source_once(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry || entry->manual_missing_source_logged)
|
||||
return false;
|
||||
entry->manual_missing_source_logged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear_manual_missing_source(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->manual_missing_source_logged = false;
|
||||
}
|
||||
+38
-8
@@ -3,28 +3,58 @@
|
||||
#include "sm_paths.h"
|
||||
|
||||
const char *get_filename_component(const char *path) {
|
||||
if (!path)
|
||||
return "";
|
||||
const char *base = strrchr(path, '/');
|
||||
if (!base)
|
||||
base = strrchr(path, '\\');
|
||||
return base ? base + 1 : path;
|
||||
}
|
||||
|
||||
bool 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) {
|
||||
if (!path)
|
||||
return false;
|
||||
size_t image_prefix_len = strlen(IMAGE_MOUNT_BASE);
|
||||
return (strncmp(path, IMAGE_MOUNT_BASE, image_prefix_len) == 0 &&
|
||||
path[image_prefix_len] == '/');
|
||||
}
|
||||
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
|
||||
if (!scan_path || scan_path[0] == '\0')
|
||||
bool is_pfsc_image_mount_base_or_child(const char *path) {
|
||||
return path_matches_root_or_child(path, PFSC_IMAGE_MOUNT_BASE);
|
||||
}
|
||||
|
||||
bool path_matches_root_or_child(const char *path, const char *root) {
|
||||
if (!path || !root || root[0] == '\0')
|
||||
return false;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
bool is_usb_storage_path(const char *path) {
|
||||
static const char *usb_roots[] = {
|
||||
"/mnt/usb0", "/mnt/usb1", "/mnt/usb2", "/mnt/usb3",
|
||||
"/mnt/usb4", "/mnt/usb5", "/mnt/usb6", "/mnt/usb7",
|
||||
"/mnt/ext0",
|
||||
};
|
||||
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(usb_roots) / sizeof(usb_roots[0]); i++) {
|
||||
if (path_matches_root_or_child(path, usb_roots[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool build_backports_root_path(const char *scan_path, char out[MAX_PATH]) {
|
||||
if (is_under_image_mount_base(scan_path))
|
||||
return false;
|
||||
|
||||
snprintf(out, MAX_PATH, "%s/%s", scan_path, DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return true;
|
||||
int written = snprintf(out, MAX_PATH, "%s/%s", scan_path,
|
||||
DEFAULT_BACKPORTS_DIR_NAME);
|
||||
return written >= 0 && (size_t)written < MAX_PATH;
|
||||
}
|
||||
+758
-264
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,124 @@
|
||||
#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 scan_root_is_pfsc_mount_base =
|
||||
is_pfsc_image_mount_base_or_child(scan_root);
|
||||
bool skip_backports_root =
|
||||
(depth_from_root == 0u && !is_under_image_mount_base(scan_root));
|
||||
bool allow_image_file_visits =
|
||||
callbacks->on_image_file &&
|
||||
(!path_matches_root_or_child(scan_root, IMAGE_MOUNT_BASE) ||
|
||||
scan_root_is_pfsc_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_path(full_path, 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;
|
||||
}
|
||||
+1671
File diff suppressed because it is too large.
Load diff
+96
-46
@@ -7,10 +7,12 @@
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
|
||||
#define SHELLCORE_FLAG_WAITMODE_OR 2u
|
||||
#define SHELLCORE_FLAG_ALL_BITS UINT64_MAX
|
||||
#define SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS 0x0000000000200000ULL
|
||||
|
||||
typedef intptr_t sm_kernel_event_flag_t;
|
||||
|
||||
@@ -24,6 +26,7 @@ typedef struct {
|
||||
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;
|
||||
@@ -34,13 +37,17 @@ typedef struct {
|
||||
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},
|
||||
{.name = "SceSystemStateMgrInfo", .handle = -1},
|
||||
/* {.name = "SceSystemStateMgrStatus", .handle = -1},
|
||||
{.name = "SceSysCoreSuspend", .handle = -1},
|
||||
{.name = "SceLncUtilSystemStatus", .handle = -1},
|
||||
{.name = "SceShellCoreUtilCtrlFocus", .handle = -1},
|
||||
{.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},
|
||||
@@ -59,12 +66,31 @@ static bool g_shellcore_flag_start_ready = false;
|
||||
static bool g_shellcore_flag_start_success = false;
|
||||
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
|
||||
|
||||
#if 0
|
||||
static const shellcore_flag_bit_desc_t g_lnc_util_system_status_bits[] = {
|
||||
{0x0000000000000001ULL, "EXTRA_AUDIO_CPU_BUDGET_AVAILABLE"},
|
||||
{0x0000000000000002ULL, "SHELLUI_FG_GAME_BG_CPU_MODE"},
|
||||
static const shellcore_flag_bit_desc_t g_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"},
|
||||
@@ -99,28 +125,6 @@ static const shellcore_flag_bit_desc_t g_vshctl_internal_user_bits[] = {
|
||||
{0x0000000000000001ULL, "INTERNAL_CRONOS_USER"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
|
||||
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
|
||||
{0x0000000000000002ULL, "MEDIA_PLAYBACK_MODE"},
|
||||
{0x0000000000000004ULL, "BD_DRIVE_READY"},
|
||||
{0x0000000000000008ULL, "TICK_MUSIC_PLAYBACK"},
|
||||
{0x0000000000000010ULL, "TICK_VIDEO_PLAYBACK"},
|
||||
{0x0000000000000020ULL, "TICK_0x20"},
|
||||
{0x0000000000000040ULL, "TICK_PARTY_CHAT"},
|
||||
{0x0000000000000100ULL, "TURN_OFF_REQUEST"},
|
||||
{0x0000000000000200ULL, "REBOOT_REQUEST"},
|
||||
{0x0000000000000400ULL, "ENTER_STANDBY_REQUEST"},
|
||||
{0x0000000000010000ULL, "START_SHUTDOWN_TIMER"},
|
||||
{0x0000000000020000ULL, "START_REBOOT_TIMER"},
|
||||
{0x0000000000040000ULL, "START_STANDBY_TIMER"},
|
||||
{0x0000000000080000ULL, "EXTEND_SHUTDOWN_TIMER"},
|
||||
{0x0000000000100000ULL, "CANCEL_SHUTDOWN_TIMER"},
|
||||
{0x0000000000200000ULL, "SHELLUI_SHUTDOWN_IN_PROGRESS"},
|
||||
{0x0000000000400000ULL, "EXTEND_SHUTDOWN_TIMER_POST_AUTOUPDATE"},
|
||||
{0x0000000001000000ULL, "BIGAPP_LAUNCH_READY"},
|
||||
{0x0000000004000000ULL, "VSH_AUTO_UPDATE_VERIFY_DONE"},
|
||||
{0x0000000008000000ULL, "START_REBOOT_TIMER_FAST"},
|
||||
};
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_boot_status_bits[] = {
|
||||
{0x0000000000000002ULL, "BIT_0x2"},
|
||||
@@ -167,7 +171,6 @@ static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
if (!flag || !flag->name)
|
||||
return NULL;
|
||||
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceLncUtilSystemStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_lnc_util_system_status_bits) /
|
||||
@@ -176,6 +179,14 @@ static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
return g_lnc_util_system_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_system_state_mgr_status_bits) /
|
||||
sizeof(g_system_state_mgr_status_bits[0]);
|
||||
}
|
||||
return g_system_state_mgr_status_bits;
|
||||
}
|
||||
#if 0
|
||||
if (strcmp(flag->name, "SceShellCoreUtilPowerControl") == 0) {
|
||||
if (count_out)
|
||||
*count_out = sizeof(g_power_control_bits) / sizeof(g_power_control_bits[0]);
|
||||
@@ -198,13 +209,6 @@ static const shellcore_flag_bit_desc_t *get_shellcore_flag_bits(
|
||||
return g_vshctl_internal_user_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
if (count_out) {
|
||||
*count_out = sizeof(g_system_state_mgr_status_bits) /
|
||||
sizeof(g_system_state_mgr_status_bits[0]);
|
||||
}
|
||||
return g_system_state_mgr_status_bits;
|
||||
}
|
||||
|
||||
if (strcmp(flag->name, "SceBootStatusFlags") == 0) {
|
||||
if (count_out)
|
||||
@@ -504,6 +508,14 @@ static void set_shellcore_flag_start_result(bool success) {
|
||||
pthread_mutex_unlock(&g_shellcore_flag_mutex);
|
||||
}
|
||||
|
||||
static void enter_sleep_mode_and_cleanup(const char *reason) {
|
||||
if (request_runtime_sleep_mode(true, reason)) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
if (!flag)
|
||||
return;
|
||||
@@ -560,13 +572,20 @@ static void close_shellcore_flags(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static size_t open_shellcore_flags(void) {
|
||||
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,
|
||||
@@ -577,6 +596,8 @@ static size_t open_shellcore_flags(void) {
|
||||
|
||||
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);
|
||||
|
||||
@@ -591,7 +612,9 @@ static size_t open_shellcore_flags(void) {
|
||||
opened_count++;
|
||||
}
|
||||
|
||||
return 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) {
|
||||
@@ -599,7 +622,10 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
int rc;
|
||||
bool changed = false;
|
||||
bool entered_shutdown_on_going = false;
|
||||
bool entered_main_on_standby = false;
|
||||
bool entered_suspend_on_going = false;
|
||||
bool entered_resume_working = false;
|
||||
bool entered_shellui_shutdown_in_progress = false;
|
||||
bool is_system_state_mgr_info = false;
|
||||
unsigned current_state = 0;
|
||||
unsigned previous_state = 0;
|
||||
@@ -635,10 +661,25 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SHUTDOWN_ON_GOING);
|
||||
entered_main_on_standby =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_MAIN_ON_STANDBY);
|
||||
entered_suspend_on_going =
|
||||
current_state == SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING &&
|
||||
(!flag->has_last_pattern ||
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_SUSPEND_ON_GOING);
|
||||
entered_resume_working =
|
||||
flag->has_last_pattern &&
|
||||
current_state == SYSTEM_STATE_MGR_STATE_WORKING &&
|
||||
previous_state != SYSTEM_STATE_MGR_STATE_WORKING;
|
||||
} else if (strcmp(flag->name, "SceSystemStateMgrStatus") == 0) {
|
||||
entered_shellui_shutdown_in_progress =
|
||||
(result_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) != 0 &&
|
||||
(!flag->has_last_pattern ||
|
||||
(flag->last_pattern &
|
||||
SYSTEM_STATE_MGR_STATUS_SHELLUI_SHUTDOWN_IN_PROGRESS) == 0);
|
||||
}
|
||||
|
||||
flag->last_pattern = result_pattern;
|
||||
@@ -653,18 +694,27 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
if (entered_shutdown_on_going) {
|
||||
request_shutdown_stop("SceSystemStateMgrInfo=SHUTDOWN_ON_GOING");
|
||||
}
|
||||
if (entered_main_on_standby || entered_suspend_on_going) {
|
||||
const char *sleep_reason =
|
||||
entered_main_on_standby ? "SceSystemStateMgrInfo=MAIN_ON_STANDBY"
|
||||
: "SceSystemStateMgrInfo=SUSPEND_ON_GOING";
|
||||
enter_sleep_mode_and_cleanup(sleep_reason);
|
||||
}
|
||||
if (entered_shellui_shutdown_in_progress) {
|
||||
enter_sleep_mode_and_cleanup(
|
||||
"SceSystemStateMgrStatus=SHELLUI_SHUTDOWN_IN_PROGRESS");
|
||||
}
|
||||
if (entered_resume_working) {
|
||||
request_scan_now("SceSystemStateMgrInfo=WORKING");
|
||||
request_runtime_sleep_mode(false, "SceSystemStateMgrInfo=WORKING");
|
||||
}
|
||||
}
|
||||
|
||||
static void *shellcore_flag_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
|
||||
size_t opened_count = open_shellcore_flags();
|
||||
// The active monitor currently depends on both configured flags opening:
|
||||
// AppFocus plus SceSystemStateMgrInfo.
|
||||
if (opened_count < 2) {
|
||||
size_t opened_count = 0;
|
||||
if (!open_shellcore_flags(&opened_count)) {
|
||||
if (opened_count != 0)
|
||||
close_shellcore_flags();
|
||||
set_shellcore_flag_start_result(false);
|
||||
|
||||
+7
-1
@@ -4,6 +4,12 @@
|
||||
#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;
|
||||
@@ -20,7 +26,7 @@ bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
return false;
|
||||
}
|
||||
|
||||
double root_diff = difftime(now, st.st_mtime);
|
||||
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)
|
||||
|
||||
+16
-10
@@ -33,8 +33,6 @@ static void rebuild_title_state_hash(void) {
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *find_title_state(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
uint32_t slot = sm_fnv1a32(title_id) & (STATE_HASH_SIZE - 1u);
|
||||
for (uint32_t i = 0; i < STATE_HASH_SIZE; i++) {
|
||||
uint16_t idx = g_title_state_hash[slot];
|
||||
@@ -49,9 +47,6 @@ static struct TitleStateEntry *find_title_state(const char *title_id) {
|
||||
}
|
||||
|
||||
static struct TitleStateEntry *create_title_state(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return NULL;
|
||||
|
||||
int slot_k = -1;
|
||||
for (int k = 0; k < TITLE_STATE_CAPACITY; k++) {
|
||||
if (!g_title_state[k].valid) {
|
||||
@@ -124,17 +119,30 @@ uint8_t get_register_attempts(const char *title_id) {
|
||||
|
||||
void mark_register_attempted(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (entry && entry->register_attempts < UINT8_MAX)
|
||||
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 || entry->duplicate_notified_once)
|
||||
if (entry->duplicate_notified_once)
|
||||
return;
|
||||
entry->duplicate_notified_once = true;
|
||||
notify_system("Duplicate %s detected:\n%s\n%s", title_id, path_a, path_b);
|
||||
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) {
|
||||
@@ -150,8 +158,6 @@ void clear_failed_mount_attempts(const char *title_id) {
|
||||
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id) {
|
||||
struct TitleStateEntry *entry = get_or_create_title_state(title_id);
|
||||
if (!entry)
|
||||
return MAX_FAILED_MOUNT_ATTEMPTS;
|
||||
if (entry->mount_reg_attempts < UINT8_MAX)
|
||||
entry->mount_reg_attempts++;
|
||||
return entry->mount_reg_attempts;
|
||||
|
||||
Reference in new issue
Block a user