mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 05:40:07 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22bacfe168 | ||
|
|
4085fcd8ca | ||
|
|
eb3c611ebf | ||
|
|
9f0242044d | ||
|
|
c4e9839fe5 | ||
|
|
73b26ba3fb | ||
|
|
5691f25ece | ||
|
|
9cda9717ea | ||
|
|
e24a444147 | ||
|
|
03ad6d2e45 | ||
|
|
6d59555b66 | ||
|
|
7300dc1830 | ||
|
|
ad5afc5689 | ||
|
|
6d8622c829 | ||
|
|
9f85a3b421 | ||
|
|
0fb241cba5 | ||
|
|
09589b7ae2 | ||
|
|
8566c0294c | ||
|
|
56dd338abd | ||
|
|
090723879c | ||
|
|
16873e01f2 |
No files matched your search
@@ -10,3 +10,6 @@ mkexfat_macos.sh
|
||||
mkufs2.sh
|
||||
New-OsfExfatImage.ps1
|
||||
README.md
|
||||
docs/socket-api.md
|
||||
docs/openapi.yaml
|
||||
tools/api_test.py
|
||||
@@ -47,6 +47,7 @@ jobs:
|
||||
automake \
|
||||
build-essential \
|
||||
clang-18 \
|
||||
cmake \
|
||||
curl \
|
||||
git \
|
||||
libarchive-tools \
|
||||
@@ -56,6 +57,8 @@ jobs:
|
||||
meson \
|
||||
pacman-package-manager \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3-yaml \
|
||||
xxd \
|
||||
zip
|
||||
|
||||
@@ -65,12 +68,29 @@ jobs:
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 openlibm package
|
||||
working-directory: pacbrew-repo/openlibm
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 json-c package
|
||||
working-directory: pacbrew-repo/json-c
|
||||
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: Check API tools and manifest
|
||||
run: |
|
||||
python3 -m py_compile tools/api_test.py
|
||||
python3 -c 'import pathlib, yaml; doc = yaml.safe_load(pathlib.Path("docs/openapi.yaml").read_text()); assert doc["openapi"] == "3.1.2" and len(doc["paths"]) == 5'
|
||||
|
||||
- name: Build payload
|
||||
run: |
|
||||
make clean all PS5_SCE_STUBS_DIR="$GITHUB_WORKSPACE/ps5-sdk/sce_stubs"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
*.o
|
||||
*.elf
|
||||
*.so
|
||||
*.pyc
|
||||
__pycache__/
|
||||
daemon.lock
|
||||
debug.log
|
||||
notify.txt
|
||||
|
||||
@@ -3,22 +3,25 @@ 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)
|
||||
# Compiler and dependency flags
|
||||
JSON_C_ROOT := $(PS5_PAYLOAD_SDK)/target/user/homebrew
|
||||
JSON_CFLAGS := -I$(JSON_C_ROOT)/include
|
||||
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 := -flto=thin -Wl,--gc-sections
|
||||
|
||||
# Standard Libraries Only
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3
|
||||
# Libraries
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lsqlite3 $(JSON_C_ROOT)/lib/libjson-c.a -lm
|
||||
PS5_SCE_STUBS_DIR ?= $(PS5_PAYLOAD_SDK)/src/sce_stubs
|
||||
KERNEL_SYS_STUB_SO := src/libkernel_sys_ext.so
|
||||
KERNEL_SYS_STUB_SRCS := $(PS5_SCE_STUBS_DIR)/libkernel_sys.c src/libkernel_sys_ext.c
|
||||
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
OBJS := $(SRCS:.c=.o)
|
||||
ASM_SRCS := src/sm_shellcore_bridge.S
|
||||
OBJS := $(SRCS:.c=.o) $(ASM_SRCS:.S=.o)
|
||||
HEADERS := $(wildcard include/*.h)
|
||||
|
||||
# Targets
|
||||
@@ -40,8 +43,13 @@ src/notify_icon_asset.c: smp_icon.png
|
||||
src/config_ini_example_asset.c: config.ini.example
|
||||
xxd -i $< > $@
|
||||
|
||||
src/sm_api_service.o: CFLAGS += $(JSON_CFLAGS)
|
||||
|
||||
src/%.o: src/%.c $(HEADERS)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
src/%.o: src/%.S
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
rm -f shadowmountplus.elf api-test.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
@@ -54,9 +54,14 @@ This file is optional. If it does not exist, ShadowMountPlus creates it from the
|
||||
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`)
|
||||
- `api_bind_address=<IPv4>` (HTTP/JSON API bind address; default: `127.0.0.1`; use `0.0.0.0` for network access)
|
||||
- `api_port=<1..65535>` (HTTP/JSON API port; default: `10101`)
|
||||
- `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+`)
|
||||
- `app_install_all=1|0` (`1` stages new titles and submits them through the stock batch `sceAppInstUtilAppInstallAll`; default: `0` on every firmware. On FW `12.00+`, the default per-title path is provided by the SceShellCore TitleDir bridge.)
|
||||
- `auto_remove_missing_games=1|0` (`1` automatically removes games from the system library when their source is no longer available; default: `0`)
|
||||
- `auto_remove_games_with_dlc=1|0` (`1` allows automatic removal of games that have installed DLC; default: `0`)
|
||||
- `auto_remove_missing_delay_seconds=<1..86400>` (how long a source must remain unavailable before removal; default: `300`)
|
||||
- `image_ro=<image_filename>` (repeatable; force read-only mode for this image filename)
|
||||
- `image_rw=<image_filename>` (repeatable; force read-write mode for this image filename)
|
||||
- `image_sector=<image_filename>:<sector_size>` (repeatable; force sector size for this image filename)
|
||||
@@ -88,6 +93,9 @@ Supported keys (all optional):
|
||||
- `md_exfat_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512`)
|
||||
|
||||
The public API routes and JSON schemas are documented in [docs/socket-api.md](docs/socket-api.md)
|
||||
and available as an [OpenAPI manifest](docs/openapi.yaml).
|
||||
|
||||
Per-image mode override behavior:
|
||||
- Match is done by image file name (without path).
|
||||
- File names with spaces are supported.
|
||||
@@ -116,6 +124,7 @@ Scan path behavior:
|
||||
- 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`).
|
||||
- With `auto_remove_missing_games=1`, a game is removed from the system library only after its source remains unavailable for `auto_remove_missing_delay_seconds`. If the source becomes available again before removal, the pending removal is cancelled.
|
||||
- Direct folder installs use `<game>/sce_sys` for this check; image and backport sources use the target path itself.
|
||||
|
||||
Backport overlay behavior:
|
||||
@@ -124,7 +133,7 @@ Backport overlay behavior:
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- If `/mnt/sandbox/<TITLE_ID>_XXX/app0/fakelib` exists while the game is running, ShadowMount+ mounts it into that game's sandbox `common/lib`.
|
||||
- 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.
|
||||
@@ -207,7 +216,7 @@ mkpfs pack folder --verify --no-compress --no-adjust-output-file-extension --ver
|
||||
'./pfs_image.dat'
|
||||
```
|
||||
|
||||
Then pack the nested image into a compressed PFS container:
|
||||
Then pack the nested image (**pfs_image.dat**) into a compressed PFS container:
|
||||
|
||||
```bash
|
||||
mkpfs pack file --verify --version PS5 --inode-bits 32 \
|
||||
|
||||
+26
-1
@@ -21,6 +21,13 @@
|
||||
# Default: 0
|
||||
# quiet_mode=0
|
||||
|
||||
# Public HTTP/JSON API listener:
|
||||
# The loopback default accepts connections only from software running on the PS5.
|
||||
# Use 0.0.0.0 explicitly if remote devices on the network must connect.
|
||||
# Defaults: 127.0.0.1:10101
|
||||
# api_bind_address=127.0.0.1
|
||||
# api_port=10101
|
||||
|
||||
# Global mount mode:
|
||||
# 1/true/yes/on/ro -> read-only mounts
|
||||
# 0/false/no/off/rw -> read-write mounts
|
||||
@@ -39,6 +46,24 @@
|
||||
# Default: 0 on FW below 12.00, forced to 1 on FW 12.00 and newer
|
||||
# app_install_all=0
|
||||
|
||||
# Automatically remove games from the system library when their source
|
||||
# is no longer available:
|
||||
# 1/true/yes/on -> remove after the configured missing delay
|
||||
# 0/false/no/off -> keep the registration (default)
|
||||
# Default: 0
|
||||
# auto_remove_missing_games=0
|
||||
|
||||
# Allow automatic removal of games that have installed DLC:
|
||||
# 1/true/yes/on -> allow removal
|
||||
# 0/false/no/off -> keep games with installed DLC (default)
|
||||
# Default: 0
|
||||
# auto_remove_games_with_dlc=0
|
||||
|
||||
# How long a source must remain unavailable before removal (seconds),
|
||||
# range: 1..86400
|
||||
# Default: 300
|
||||
# auto_remove_missing_delay_seconds=300
|
||||
|
||||
# Scan depth relative to each scan root:
|
||||
# 1 -> scan only first-level subdirectories
|
||||
# 2 -> also scan one additional nested level
|
||||
@@ -67,7 +92,7 @@
|
||||
# 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
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
openapi: 3.1.2
|
||||
info:
|
||||
title: ShadowMount HTTP API
|
||||
version: "1"
|
||||
description: |
|
||||
HTTP/JSON API exposed by the ShadowMountPlus payload. The service has no
|
||||
authentication and should only be exposed on a trusted network.
|
||||
servers:
|
||||
- url: http://127.0.0.1:10101
|
||||
description: Default loopback endpoint on the PS5
|
||||
- url: http://{address}:{port}
|
||||
description: Configured network endpoint
|
||||
variables:
|
||||
address:
|
||||
default: 192.168.1.50
|
||||
description: PS5 IPv4 address or hostname
|
||||
port:
|
||||
default: "10101"
|
||||
description: Value of api_port
|
||||
security: []
|
||||
tags:
|
||||
- name: Service
|
||||
- name: Images
|
||||
- name: Games
|
||||
|
||||
paths:
|
||||
/api/v1/version:
|
||||
options:
|
||||
operationId: preflightVersion
|
||||
summary: CORS preflight
|
||||
tags: [Service]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: getVersion
|
||||
summary: Get API version and capabilities
|
||||
tags: [Service]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/VersionSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/images:
|
||||
options:
|
||||
operationId: preflightImages
|
||||
summary: CORS preflight
|
||||
tags: [Images]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: listImages
|
||||
summary: Get the complete image snapshot
|
||||
tags: [Images]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ImagesSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games:
|
||||
options:
|
||||
operationId: preflightGames
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: listGames
|
||||
summary: Get the complete game snapshot
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/EmptyRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/GamesSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games/mount:
|
||||
options:
|
||||
operationId: preflightMountGame
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: mountGame
|
||||
summary: Mount a managed game
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TitleRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MountSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/components/responses/Busy"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
/api/v1/games/unmount:
|
||||
options:
|
||||
operationId: preflightUnmountGame
|
||||
summary: CORS preflight
|
||||
tags: [Games]
|
||||
responses:
|
||||
"204":
|
||||
$ref: "#/components/responses/Preflight"
|
||||
post:
|
||||
operationId: unmountGame
|
||||
summary: Unmount a managed game
|
||||
tags: [Games]
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/TitleRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/UnmountSuccess"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/components/responses/Busy"
|
||||
"411":
|
||||
$ref: "#/components/responses/LengthRequired"
|
||||
"413":
|
||||
$ref: "#/components/responses/PayloadTooLarge"
|
||||
"415":
|
||||
$ref: "#/components/responses/UnsupportedMediaType"
|
||||
"431":
|
||||
$ref: "#/components/responses/HeadersTooLarge"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplemented"
|
||||
|
||||
components:
|
||||
requestBodies:
|
||||
EmptyRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example: {}
|
||||
TitleRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TitleRequest"
|
||||
|
||||
headers:
|
||||
AllowOrigin:
|
||||
description: Browser origins allowed to access the API
|
||||
schema:
|
||||
type: string
|
||||
const: "*"
|
||||
AllowMethods:
|
||||
description: Methods allowed by CORS preflight
|
||||
schema:
|
||||
type: string
|
||||
const: POST, OPTIONS
|
||||
AllowHeaders:
|
||||
description: Request headers allowed by CORS preflight
|
||||
schema:
|
||||
type: string
|
||||
const: Content-Type
|
||||
AllowPrivateNetwork:
|
||||
description: Private Network Access permission
|
||||
schema:
|
||||
type: string
|
||||
const: "true"
|
||||
MaxAge:
|
||||
description: Number of seconds for which the preflight may be cached
|
||||
schema:
|
||||
type: integer
|
||||
const: 86400
|
||||
|
||||
responses:
|
||||
Preflight:
|
||||
description: CORS preflight accepted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
Access-Control-Allow-Methods:
|
||||
$ref: "#/components/headers/AllowMethods"
|
||||
Access-Control-Allow-Headers:
|
||||
$ref: "#/components/headers/AllowHeaders"
|
||||
Access-Control-Allow-Private-Network:
|
||||
$ref: "#/components/headers/AllowPrivateNetwork"
|
||||
Access-Control-Max-Age:
|
||||
$ref: "#/components/headers/MaxAge"
|
||||
|
||||
VersionSuccess:
|
||||
description: API version and capabilities
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VersionResponse"
|
||||
|
||||
ImagesSuccess:
|
||||
description: Complete image snapshot
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ImagesResponse"
|
||||
|
||||
GamesSuccess:
|
||||
description: Complete game snapshot
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GamesResponse"
|
||||
|
||||
MountSuccess:
|
||||
description: Game mounted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MountResponse"
|
||||
|
||||
UnmountSuccess:
|
||||
description: Game unmounted
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UnmountResponse"
|
||||
|
||||
BadRequest:
|
||||
description: Invalid HTTP request, JSON body or title ID
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
NotFound:
|
||||
description: Managed title tracker was not found
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
Busy:
|
||||
description: Runtime state does not currently permit the operation
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
status: 16
|
||||
error: Device busy
|
||||
LengthRequired:
|
||||
description: Content-Length is required
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
PayloadTooLarge:
|
||||
description: JSON request body exceeds 4096 bytes
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
UnsupportedMediaType:
|
||||
description: Content-Type is not application/json
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
HeadersTooLarge:
|
||||
description: HTTP headers exceed 8192 bytes
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
InternalError:
|
||||
description: Internal failure
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
NotImplemented:
|
||||
description: Unsupported transfer encoding or runtime operation
|
||||
headers:
|
||||
Access-Control-Allow-Origin:
|
||||
$ref: "#/components/headers/AllowOrigin"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
schemas:
|
||||
TitleId:
|
||||
type: string
|
||||
pattern: "^(CUSA|PPSA)[0-9]{5}$"
|
||||
examples: [PPSA12345]
|
||||
|
||||
TitleRequest:
|
||||
type: object
|
||||
required: [title_id]
|
||||
properties:
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
|
||||
VersionResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, api_version, shadowmount_version, capabilities]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
api_version:
|
||||
type: integer
|
||||
const: 1
|
||||
shadowmount_version:
|
||||
type: string
|
||||
capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [list_images, list_games, mount_game, unmount_game]
|
||||
example: [list_images, list_games, mount_game, unmount_game]
|
||||
|
||||
Image:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- path
|
||||
- mount_point
|
||||
- size
|
||||
- mtime_sec
|
||||
- mtime_nsec
|
||||
- unit_id
|
||||
- backend
|
||||
- complete
|
||||
- source_available
|
||||
- mapped
|
||||
- mounted
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
mount_point:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 0
|
||||
mtime_sec:
|
||||
type: integer
|
||||
format: int64
|
||||
mtime_nsec:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 999999999
|
||||
unit_id:
|
||||
type: integer
|
||||
minimum: -1
|
||||
backend:
|
||||
type: string
|
||||
enum: [LVD, MD, UNKNOWN]
|
||||
complete:
|
||||
type: boolean
|
||||
source_available:
|
||||
type: boolean
|
||||
mapped:
|
||||
type: boolean
|
||||
mounted:
|
||||
type: boolean
|
||||
|
||||
ImagesResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, count, images]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
images:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Image"
|
||||
|
||||
Game:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- path
|
||||
- image_path
|
||||
- title_id
|
||||
- title_name
|
||||
- installed
|
||||
- managed
|
||||
- mounted
|
||||
- image_backed
|
||||
- source_available
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
image_path:
|
||||
type: string
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
title_name:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
managed:
|
||||
type: boolean
|
||||
mounted:
|
||||
type: boolean
|
||||
image_backed:
|
||||
type: boolean
|
||||
source_available:
|
||||
type: boolean
|
||||
|
||||
GamesResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, count, games]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
games:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Game"
|
||||
|
||||
MountResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, title_id, mounted]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
mounted:
|
||||
type: boolean
|
||||
const: true
|
||||
|
||||
UnmountResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, title_id, mounted]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
const: 0
|
||||
title_id:
|
||||
$ref: "#/components/schemas/TitleId"
|
||||
mounted:
|
||||
type: boolean
|
||||
const: false
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, error]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Positive errno value
|
||||
error:
|
||||
type: string
|
||||
@@ -0,0 +1,95 @@
|
||||
# ShadowMount HTTP/JSON API v1
|
||||
|
||||
The machine-readable OpenAPI 3.1 description is available in
|
||||
[`openapi.yaml`](openapi.yaml).
|
||||
|
||||
ShadowMount exposes its public API over HTTP/TCP. The default endpoint is
|
||||
`127.0.0.1:10101`, so only software running on the PS5 can connect. Set
|
||||
`api_bind_address=0.0.0.0` to allow clients on the network, or select a
|
||||
specific PS5 IPv4 address. `api_port` accepts values from 1 through 65535.
|
||||
Both settings are applied on runtime config reload.
|
||||
|
||||
The API has no authentication. Bind it to a network-facing address only on a
|
||||
trusted network.
|
||||
|
||||
The internal `/system_tmp/shadowmount.sock` Unix socket remains private to the
|
||||
SceShellCore bridge and must not be used by third-party applications.
|
||||
|
||||
The HTTP service owns and monitors its listener. If initial `socket/bind/listen`
|
||||
or a later `accept` fails, it retries with an interruptible 250 ms to 5 s
|
||||
backoff. Only the transition into recovery and the successful recovery are
|
||||
logged. Shutdown and config reload interrupt the wait immediately.
|
||||
|
||||
Every API operation must use `POST`, include `Content-Type: application/json`
|
||||
and contain one JSON object. Every response is a JSON object. Connections are
|
||||
closed after one response; chunked request bodies are not supported. The
|
||||
maximum request body is 4096 bytes.
|
||||
|
||||
All responses include `Access-Control-Allow-Origin: *`. Browser preflight
|
||||
requests using `OPTIONS` are supported and allow `POST`, the `Content-Type`
|
||||
header and Private Network Access. This permits calls from any browser origin;
|
||||
the listener still needs `api_bind_address=0.0.0.0` for access from another
|
||||
device.
|
||||
|
||||
## Routes
|
||||
|
||||
| Route | Request | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `/api/v1/version` | `{}` | API and ShadowMount versions plus capabilities |
|
||||
| `/api/v1/images` | `{}` | Complete image snapshot |
|
||||
| `/api/v1/games` | `{}` | Complete game snapshot |
|
||||
| `/api/v1/games/mount` | `{"title_id":"PPSA12345"}` | Mount a managed game |
|
||||
| `/api/v1/games/unmount` | `{"title_id":"PPSA12345"}` | Unmount a managed game |
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -sS http://127.0.0.1:10101/api/v1/version \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
|
||||
curl -sS http://127.0.0.1:10101/api/v1/games \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
## Responses
|
||||
|
||||
Every response contains numeric `status`. It is zero on success, or a positive
|
||||
errno value on failure. Error responses also contain `error` and use an
|
||||
appropriate HTTP status such as 400, 404, 409, 413 or 415.
|
||||
|
||||
A successful version response has this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 0,
|
||||
"api_version": 1,
|
||||
"shadowmount_version": "1.7",
|
||||
"capabilities": ["list_images", "list_games", "mount_game", "unmount_game"]
|
||||
}
|
||||
```
|
||||
|
||||
List responses contain `count` and the complete `images` or `games` array.
|
||||
Image items expose `path`, `mount_point`, `size`, modification time, `unit_id`,
|
||||
`backend`, `complete`, `source_available`, `mapped` and `mounted`. Game items
|
||||
expose `path`, `image_path`, `title_id`, `title_name`, `installed`, `managed`,
|
||||
`mounted`, `image_backed` and `source_available`.
|
||||
|
||||
Mount mutations remain conservative. They return HTTP 409 with `status` set to
|
||||
`EBUSY` while a game is active, while ShellCore owns another prepared title,
|
||||
during suspend, or while a conflicting mount/release is in progress. Clients
|
||||
must not retry this response in a tight loop. HTTP 404 with `ENOENT` means the
|
||||
title has no ShadowMount `mount.lnk` tracker.
|
||||
|
||||
## Test client
|
||||
|
||||
Run the host-side Python client while ShadowMount is active:
|
||||
|
||||
```sh
|
||||
python3 tools/api_test.py 192.168.1.50 10101
|
||||
```
|
||||
|
||||
It validates CORS preflight, the version response and complete image/game
|
||||
snapshots, then selects the first safe managed game for a mount/unmount cycle.
|
||||
The mutation test is skipped if there is no candidate or the runtime is busy.
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_API_PROTOCOL_H
|
||||
#define SM_API_PROTOCOL_H
|
||||
|
||||
#define SM_API_DEFAULT_BIND_ADDRESS "127.0.0.1"
|
||||
#define SM_API_DEFAULT_PORT 10101u
|
||||
#define SM_API_VERSION 1u
|
||||
#define SM_API_MAX_HTTP_HEADER_SIZE 8192u
|
||||
#define SM_API_MAX_JSON_BODY_SIZE 4096u
|
||||
|
||||
#define SM_API_ROUTE_VERSION "/api/v1/version"
|
||||
#define SM_API_ROUTE_IMAGES "/api/v1/images"
|
||||
#define SM_API_ROUTE_GAMES "/api/v1/games"
|
||||
#define SM_API_ROUTE_MOUNT "/api/v1/games/mount"
|
||||
#define SM_API_ROUTE_UNMOUNT "/api/v1/games/unmount"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_API_SERVICE_H
|
||||
#define SM_API_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
bool sm_api_service_start(void);
|
||||
void sm_api_service_stop(void);
|
||||
bool sm_api_service_reconfigure(void);
|
||||
|
||||
#endif
|
||||
@@ -22,6 +22,8 @@ void free_app_db_title_list(struct AppDbTitleList *list);
|
||||
void invalidate_app_db_title_cache(void);
|
||||
// Copy the cached app.db title list into caller-owned storage, loading it if needed.
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
|
||||
// Copy titles whose metadata path matches /user/app/PPSA*/sce_sys.
|
||||
bool get_app_db_auto_remove_title_list(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);
|
||||
|
||||
|
||||
+21
-1
@@ -5,6 +5,8 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
// Check whether a title is present in the installed app set.
|
||||
bool is_installed(const char *title_id);
|
||||
// Check whether /user/appmeta/<TITLE_ID>/param.json exists.
|
||||
@@ -13,6 +15,18 @@ bool has_appmeta_data(const char *title_id);
|
||||
bool is_data_mounted(const char *title_id);
|
||||
// Read the mount.lnk source path for a title tracked under /user/app.
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size);
|
||||
// Atomically persist the mount.lnk source path for a title.
|
||||
bool write_mount_link(const char *title_id, const char *source_path);
|
||||
// Read the optional backing image path retained for an image-backed title.
|
||||
bool read_mount_image_link(const char *title_id, char *out, size_t out_size);
|
||||
// Persist the outer backing-image path for an already staged title.
|
||||
bool write_mount_image_link(const char *title_id, const char *image_path);
|
||||
bool read_mount_image_chain(
|
||||
const char *title_id,
|
||||
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out);
|
||||
bool write_mount_image_chain(
|
||||
const char *title_id,
|
||||
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count);
|
||||
// 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);
|
||||
@@ -26,6 +40,12 @@ int remount_system_ex(void);
|
||||
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);
|
||||
// Drop runtime layers for a title while preserving its metadata/link files.
|
||||
bool unmount_title_runtime_layers(const char *title_id);
|
||||
// Same operation without expected EBUSY diagnostics during a bounded retry.
|
||||
bool unmount_title_runtime_layers_quiet(const char *title_id);
|
||||
// Release every managed title layer while preserving link metadata.
|
||||
bool unmount_all_title_runtime_layers(void);
|
||||
// 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,
|
||||
@@ -36,7 +56,7 @@ 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);
|
||||
bool shutdown_title_mounts(void);
|
||||
// Remove stale mount links and optionally restore image-backed mounts.
|
||||
void cleanup_mount_links(const char *removed_source_root,
|
||||
bool unmount_system_ex_bind);
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
#define SM_GAME_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
} sm_game_cache_snapshot_entry_t;
|
||||
|
||||
typedef bool (*game_cache_iter_fn)(const char *path, const char *title_id,
|
||||
const char *title_name,
|
||||
@@ -14,12 +23,22 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
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);
|
||||
// Cancel pending removal when the same title is discovered at a valid source.
|
||||
void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
const char *title_name);
|
||||
// Reconcile app.db auto-remove candidates and uninstall expired missing entries.
|
||||
void reconcile_missing_app_db_games(void);
|
||||
// Reset pending missing timers after relevant runtime config changes.
|
||||
void reset_missing_game_cache_timers(void);
|
||||
// Look up a cached game entry by path or title ID.
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out);
|
||||
// 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);
|
||||
// Allocate a stable snapshot of all cached games. Caller frees it.
|
||||
bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out,
|
||||
size_t *count_out);
|
||||
// Remove a game cache entry by path.
|
||||
void clear_cached_game(const char *path);
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// Read title ID and title name from a mounted game directory.
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name);
|
||||
// Accept canonical PS4/PS5 game title IDs used in filesystem paths.
|
||||
bool is_supported_game_title_id(const char *title_id);
|
||||
// Check whether a directory contains sce_sys/param.json and stat it.
|
||||
bool directory_has_param_json(const char *dir_path, struct stat *param_st_out);
|
||||
|
||||
|
||||
+8
-1
@@ -11,6 +11,13 @@ void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Unmount an image mount point and detach its backing device.
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend);
|
||||
// Unmount a runtime image without deleting persistent title metadata links.
|
||||
bool unmount_runtime_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Release one cached runtime image mount. An absent live mount is harmless.
|
||||
bool release_runtime_image_mount(const char *file_path);
|
||||
// Release all discovery/runtime image mounts without deleting title metadata.
|
||||
bool release_runtime_image_mounts(void);
|
||||
// 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.
|
||||
@@ -22,7 +29,7 @@ bool shutdown_image_mounts(void);
|
||||
// Remove empty directories left under the image mount root.
|
||||
void cleanup_mount_dirs(void);
|
||||
// Mount an image file if it is stable and not currently rate-limited.
|
||||
bool maybe_mount_image_file(const char *full_path, const char *name,
|
||||
bool maybe_mount_image_file(const char *full_path, const char *display_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,
|
||||
|
||||
@@ -20,6 +20,9 @@ bool cache_image_mount(const char *path, const char *mount_point,
|
||||
bool cache_image_source_mapping(const char *path, const char *mount_point);
|
||||
// Return a cached image mount entry by index.
|
||||
bool get_image_cache_entry(int index, image_cache_entry_t *entry_out);
|
||||
// Find a cached source path with one cache lock acquisition.
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
@@ -30,5 +33,16 @@ bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve the image whose mount point owns a mounted child path.
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size);
|
||||
// Resolve every owning image from the outermost source to the innermost one.
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]);
|
||||
// Resolve only the outermost owning image source.
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef SM_IMAGE_INDEX_H
|
||||
#define SM_IMAGE_INDEX_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
typedef void (*sm_image_index_title_visitor_t)(const char *title_id);
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int64_t size;
|
||||
int64_t mtime_sec;
|
||||
int32_t mtime_nsec;
|
||||
bool complete;
|
||||
} sm_image_index_snapshot_entry_t;
|
||||
|
||||
// Return true when a complete unchanged index entry can be reused instead of
|
||||
// opening the image, visiting every retained title ID before returning.
|
||||
bool sm_image_index_visit_ready_titles(
|
||||
const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready,
|
||||
sm_image_index_title_visitor_t visitor);
|
||||
// Return true when a title is mapped to an existing backing image.
|
||||
bool sm_image_index_has_source_for_title(const char *title_id);
|
||||
// Start a new fingerprint generation before mounting an image.
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st);
|
||||
// Associate a discovered mounted game with its outer backing image. Returns
|
||||
// false when an image-backed title could not be retained in the index.
|
||||
bool sm_image_index_record_game(const char *game_path, const char *title_id);
|
||||
// Commit one fingerprint after its mounted content was scanned.
|
||||
void sm_image_index_complete_scan(const char *path);
|
||||
// Atomically persist accumulated index changes at the scan-cycle boundary.
|
||||
void sm_image_index_flush(void);
|
||||
// Drop persistent entries whose backing image disappeared.
|
||||
void sm_image_index_prune(void);
|
||||
// Allocate a stable snapshot of all known backing images. Caller frees it.
|
||||
bool sm_image_index_snapshot(sm_image_index_snapshot_entry_t **entries_out,
|
||||
size_t *count_out);
|
||||
|
||||
#endif
|
||||
@@ -24,5 +24,7 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
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);
|
||||
// Return true while an install still needs its staged mounts.
|
||||
bool sm_install_has_pending_work(void);
|
||||
|
||||
#endif
|
||||
+8
-1
@@ -4,11 +4,13 @@
|
||||
#define DEFAULT_SCAN_INTERVAL_US 15000000u
|
||||
#define DEFAULT_SCAN_DEPTH 1u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS 300u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS 25u
|
||||
#define DEFAULT_KSTUFF_PAUSE_DELAY_DIRECT_SECONDS 15u
|
||||
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_IMAGE_MOUNTS 256
|
||||
#define MAX_IMAGE_MOUNTS 2048
|
||||
#define MAX_IMAGE_TITLES 8192
|
||||
#define MAX_IMAGE_MODE_RULES 128
|
||||
#define MAX_KSTUFF_TITLE_RULES 128
|
||||
#define MAX_FAKELIB_EXCLUDE_RULES 128
|
||||
@@ -22,11 +24,13 @@
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define MAX_LAYERED_UNMOUNT_ATTEMPTS 4
|
||||
#define MAX_IMAGE_CHAIN_DEPTH 8
|
||||
|
||||
#define LVD_RELEASE_WAIT_MAX_US 15000000u
|
||||
#define LVD_RELEASE_WAIT_POLL_US 500000u
|
||||
#define GAME_LIFECYCLE_POLL_INTERVAL_US 250000u
|
||||
#define GAME_APPINFO_LOOKUP_TIMEOUT_US 1000000u
|
||||
#define GAME_SLEEP_EXIT_TIMEOUT_US 5000000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define MIN_SCAN_DEPTH 1u
|
||||
@@ -34,6 +38,8 @@
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
#define MAX_STABILITY_WAIT_SECONDS 3600u
|
||||
#define MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS 1u
|
||||
#define MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS 86400u
|
||||
#define MAX_KSTUFF_PAUSE_DELAY_SECONDS 3600u
|
||||
|
||||
#define APP_DB_QUERY_BUSY_RETRIES 3
|
||||
@@ -45,6 +51,7 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_API_BIND_ADDRESS 16
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef SM_MOUNT_DIAG_H
|
||||
#define SM_MOUNT_DIAG_H
|
||||
|
||||
// Log read-only evidence explaining why unmount(path) returned EBUSY.
|
||||
void sm_mount_diag_log_busy(const char *mount_point);
|
||||
|
||||
#endif
|
||||
@@ -13,6 +13,7 @@
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
#define MANUAL_STATUS_FILE "/data/shadowmount/manual.status"
|
||||
#define IMAGE_INDEX_FILE_PATH "/data/shadowmount/image_index.bin"
|
||||
#define APPMETA_BASE "/user/appmeta"
|
||||
#define APP_BASE "/user/app"
|
||||
#define KSTUFF_NOAUTOMOUNT_FILE "/data/.kstuff_noautomount"
|
||||
|
||||
@@ -41,9 +41,16 @@ static inline uint32_t sm_firmware_major_version(void) {
|
||||
return ((major_bcd >> 4) & 0xFu) * 10u + (major_bcd & 0xFu);
|
||||
}
|
||||
|
||||
static inline bool sm_set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
// --- SDK Imports ---
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallAll(void);
|
||||
int sceAppInstUtilAppInstallAll(void *reserved);
|
||||
int sceAppInstUtilAppUnInstall(const char *title_id);
|
||||
int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
|
||||
int sceKernelUsleep(unsigned int microseconds);
|
||||
|
||||
+8
-2
@@ -21,7 +21,13 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
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);
|
||||
// Ensure a stable backport overlay is mounted when one is present.
|
||||
// Absence is harmless; an unusable or failed required overlay returns false.
|
||||
bool mount_backport_overlay_for_title(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
bool *unstable_found_out);
|
||||
// Remove discovery title/image mounts while preserving persistent metadata.
|
||||
bool release_scan_runtime_mounts(void);
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,8 @@
|
||||
bool sm_scanner_init(void);
|
||||
// Wake a blocked scanner wait. Safe to call from signal context.
|
||||
void sm_scanner_wake(void);
|
||||
// Return true after scanner-owned USB directory descriptors are closed.
|
||||
bool sm_scanner_usb_watches_suspended(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.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef SM_SHELLCORE_HOOKS_H
|
||||
#define SM_SHELLCORE_HOOKS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Install/remove the fail-open SceShellCore lifecycle bridge.
|
||||
bool sm_shellcore_hooks_start(void);
|
||||
void sm_shellcore_hooks_stop(void);
|
||||
// Return whether the post-onAppExit lifecycle hook is installed.
|
||||
bool sm_shellcore_app_exit_hook_active(void);
|
||||
|
||||
// Return whether this firmware uses the internal TitleDir bridge.
|
||||
bool sm_shellcore_install_bridge_enabled(void);
|
||||
|
||||
// Invoke the internal TitleDir path through one armed AppInstallAll dispatch.
|
||||
// Returns false when the bridge is unavailable or was not consumed.
|
||||
bool sm_shellcore_install_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef SM_SHELLCORE_OFFSETS_H
|
||||
#define SM_SHELLCORE_OFFSETS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP = 0,
|
||||
SM_SHELLCORE_TARGET_APP_EXIT,
|
||||
SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
SM_SHELLCORE_TARGET_COUNT
|
||||
} sm_shellcore_target_t;
|
||||
|
||||
typedef struct {
|
||||
uintptr_t offset;
|
||||
uint8_t patch_size;
|
||||
} sm_shellcore_target_offset_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t firmware;
|
||||
const char *name;
|
||||
sm_shellcore_target_offset_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
} sm_shellcore_firmware_offsets_t;
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *
|
||||
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware);
|
||||
const char *sm_shellcore_target_name(sm_shellcore_target_t target);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef SM_SHELLCORE_PROTOCOL_H
|
||||
#define SM_SHELLCORE_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
#define SM_SHELLCORE_SOCKET_PATH "/system_tmp/shadowmount.sock"
|
||||
#define SM_SHELLCORE_PROTOCOL_MAGIC 0x534d5343u
|
||||
#define SM_SHELLCORE_PROTOCOL_VERSION 1u
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_REQUEST_LAUNCH = 1,
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED = 2,
|
||||
SM_SHELLCORE_REQUEST_APP_EXITED = 3
|
||||
} sm_shellcore_request_op_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t operation;
|
||||
union {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
struct {
|
||||
uint32_t app_id;
|
||||
uint8_t reserved[MAX_TITLE_ID - sizeof(uint32_t)];
|
||||
} app_exit;
|
||||
} payload;
|
||||
} sm_shellcore_request_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t status;
|
||||
} sm_shellcore_response_t;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SM_SHELLCORE_REMOTE_H
|
||||
#define SM_SHELLCORE_REMOTE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "sm_shellcore_offsets.h"
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
uintptr_t image_base;
|
||||
uintptr_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
const sm_shellcore_firmware_offsets_t *offsets;
|
||||
} sm_shellcore_remote_t;
|
||||
|
||||
// Resolve and validate every lifecycle target in a running SceShellCore.
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out);
|
||||
// Read or write another process while handling post-8.20 write restrictions.
|
||||
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
|
||||
size_t size);
|
||||
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
|
||||
size_t size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef SM_SHELLCORE_SERVICE_H
|
||||
#define SM_SHELLCORE_SERVICE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Start/stop the local Unix-socket owner used by the SceShellCore bridge.
|
||||
bool sm_shellcore_service_start(void);
|
||||
void sm_shellcore_service_stop(void);
|
||||
|
||||
// Release transient title/image mounts after launch failure or app exit.
|
||||
bool sm_shellcore_release_title_runtime(const char *title_id);
|
||||
// Public API entry points. Return 0 or a positive errno value.
|
||||
int sm_shellcore_mount_title_runtime(const char *title_id);
|
||||
int sm_shellcore_unmount_title_runtime(const char *title_id);
|
||||
// Bind the prepared managed title to the app id stored in LncApplication.
|
||||
void sm_shellcore_service_bind_prepared_app(const char *title_id,
|
||||
uint32_t app_id);
|
||||
// Publish process exit while ShellCore finishes its own app-exit cleanup.
|
||||
// Returns true when a managed prepared title transitioned to exit-pending.
|
||||
bool sm_shellcore_service_note_game_exit(const char *title_id);
|
||||
// Ensure that a managed title has its image/nullfs/backport runtime stack.
|
||||
// Unmanaged stock titles are treated as already ready.
|
||||
bool sm_shellcore_ensure_title_runtime(const char *title_id);
|
||||
// Return true when the named title owns the prepared runtime mount.
|
||||
bool sm_shellcore_service_title_is_prepared(const char *title_id);
|
||||
// Return true while ShellCore owns a title mount prepared for launch or exit.
|
||||
bool sm_shellcore_service_has_prepared_mount(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef SM_SOCKET_IO_H
|
||||
#define SM_SOCKET_IO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
bool sm_socket_read_full(int fd, void *buffer, size_t size);
|
||||
bool sm_socket_write_full(int fd, const void *buffer, size_t size);
|
||||
|
||||
#endif
|
||||
+5
-1
@@ -109,19 +109,23 @@ typedef struct runtime_config {
|
||||
bool mount_read_only;
|
||||
bool force_mount;
|
||||
bool app_install_all_enabled;
|
||||
bool app_install_all_forced;
|
||||
bool auto_remove_missing_games;
|
||||
bool auto_remove_games_with_dlc;
|
||||
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 api_bind_address[MAX_API_BIND_ADDRESS];
|
||||
uint32_t api_port;
|
||||
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 auto_remove_missing_delay_seconds;
|
||||
uint32_t kstuff_pause_delay_image_seconds;
|
||||
uint32_t kstuff_pause_delay_direct_seconds;
|
||||
attach_backend_t exfat_backend;
|
||||
|
||||
+82
-19
@@ -5,9 +5,12 @@
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_api_service.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_shellcore_flags.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
@@ -77,6 +80,8 @@ void install_signal_handlers(void) {
|
||||
sigaction(SIGHUP, &sa, NULL);
|
||||
sigaction(SIGQUIT, &sa, NULL);
|
||||
sigaction(SIGABRT, &sa, NULL);
|
||||
sa.sa_handler = SIG_IGN;
|
||||
sigaction(SIGPIPE, &sa, NULL);
|
||||
}
|
||||
|
||||
bool should_stop_requested(void) {
|
||||
@@ -250,12 +255,23 @@ pid_t find_pid_by_name(const char *name, bool exclude_self) {
|
||||
pid_t found_pid = 0;
|
||||
uint8_t *ptr = buf;
|
||||
uint8_t *end = buf + buf_size;
|
||||
while (ptr < end) {
|
||||
int ki_structsize = *(int *)ptr;
|
||||
pid_t ki_pid = *(pid_t *)&ptr[KINFO_PID_OFFSET];
|
||||
while ((size_t)(end - ptr) >= sizeof(int)) {
|
||||
int ki_structsize = 0;
|
||||
memcpy(&ki_structsize, ptr, sizeof(ki_structsize));
|
||||
if (ki_structsize <= KINFO_TDNAME_OFFSET ||
|
||||
(size_t)ki_structsize > (size_t)(end - ptr)) {
|
||||
found_pid = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
pid_t ki_pid = 0;
|
||||
memcpy(&ki_pid, &ptr[KINFO_PID_OFFSET], sizeof(ki_pid));
|
||||
const char *ki_tdname = (const char *)&ptr[KINFO_TDNAME_OFFSET];
|
||||
size_t tdname_size = (size_t)ki_structsize - KINFO_TDNAME_OFFSET;
|
||||
ptr += ki_structsize;
|
||||
if ((!exclude_self || ki_pid != mypid) && strcmp(ki_tdname, name) == 0) {
|
||||
if ((!exclude_self || ki_pid != mypid) &&
|
||||
strnlen(ki_tdname, tdname_size) < tdname_size &&
|
||||
strcmp(ki_tdname, name) == 0) {
|
||||
found_pid = ki_pid;
|
||||
break;
|
||||
}
|
||||
@@ -321,20 +337,21 @@ static void log_non_empty_scan_paths(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void ensure_kstuff_noautomount_file(void) {
|
||||
static bool ensure_kstuff_noautomount_file(void) {
|
||||
if (path_exists(KSTUFF_NOAUTOMOUNT_FILE))
|
||||
return;
|
||||
return true;
|
||||
|
||||
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;
|
||||
int fd = open(KSTUFF_NOAUTOMOUNT_FILE, O_WRONLY | O_CREAT, 0666);
|
||||
if (fd < 0) {
|
||||
printf("[KSTUFF] Failed to create %s: %s\n", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
(void)close(fd);
|
||||
|
||||
printf("[KSTUFF] Failed to create %s: %s\n", KSTUFF_NOAUTOMOUNT_FILE,
|
||||
strerror(errno));
|
||||
printf("[KSTUFF] Created automount sentinel: %s\n",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_buffer_to_fd(int fd, const unsigned char *buf, size_t size) {
|
||||
@@ -381,7 +398,19 @@ static void ensure_runtime_config_file(void) {
|
||||
printf("[CFG] Created default config from template: %s\n", CONFIG_FILE);
|
||||
}
|
||||
|
||||
static void cleanup_kstuff_noautomount_files(void) {
|
||||
static void cleanup_kstuff_noautomount_file(void) {
|
||||
pid_t replacement_pid = find_pid_by_name(PAYLOAD_NAME, true);
|
||||
if (replacement_pid > 0) {
|
||||
log_debug("[KSTUFF] replacement instance pid=%ld; keeping %s",
|
||||
(long)replacement_pid, KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
}
|
||||
if (replacement_pid < 0) {
|
||||
log_debug("[KSTUFF] process enumeration unavailable; keeping %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlink(KSTUFF_NOAUTOMOUNT_FILE) == 0) {
|
||||
log_debug("[KSTUFF] removed shutdown sentinel: %s",
|
||||
KSTUFF_NOAUTOMOUNT_FILE);
|
||||
@@ -428,21 +457,31 @@ int main(void) {
|
||||
|
||||
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");
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
if (!ensure_kstuff_noautomount_file()) {
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
if (existing_pid > 0) {
|
||||
printf("[RESTART] Another instance is already running.\n");
|
||||
if (!wait_for_existing_instance_exit(existing_pid)) {
|
||||
printf("[KSTUFF] Keeping automount sentinel after failed handoff.\n");
|
||||
sceUserServiceTerminate();
|
||||
return 0;
|
||||
}
|
||||
restarted_previous_instance = true;
|
||||
}
|
||||
// Older builds removed the sentinel unconditionally during shutdown.
|
||||
// Recreate it after handoff before starting any mount work.
|
||||
if (!ensure_kstuff_noautomount_file()) {
|
||||
sceUserServiceTerminate();
|
||||
return 1;
|
||||
}
|
||||
syscall(SYS_thr_set_name, -1, PAYLOAD_NAME);
|
||||
|
||||
if (remove(KILL_FILE) == 0) {
|
||||
@@ -473,6 +512,13 @@ int main(void) {
|
||||
log_debug(" [SHELLFLAG] monitor unavailable");
|
||||
sm_mdbg_init();
|
||||
sm_kstuff_init();
|
||||
if (!sm_shellcore_service_start())
|
||||
log_debug(" [SHELLCORE] Unix socket service unavailable: %s",
|
||||
strerror(errno));
|
||||
else {
|
||||
if (!sm_shellcore_hooks_start())
|
||||
log_debug(" [SHELLCORE] lifecycle hooks unavailable; stock behavior kept");
|
||||
}
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
|
||||
@@ -500,9 +546,11 @@ int main(void) {
|
||||
}
|
||||
|
||||
log_debug("[STARTUP] cleanup_staged_mount_links begin");
|
||||
runtime_mount_state_lock();
|
||||
cleanup_staged_mount_links();
|
||||
log_debug("[STARTUP] cleanup_duplicate_title_mounts begin");
|
||||
cleanup_duplicate_title_mounts();
|
||||
runtime_mount_state_unlock();
|
||||
if (!app_db_run_startup_maintenance())
|
||||
log_debug(" [DB] startup snd0info maintenance unavailable");
|
||||
log_debug("[STARTUP] scanner startup sync begin");
|
||||
@@ -511,19 +559,34 @@ int main(void) {
|
||||
goto shutdown;
|
||||
}
|
||||
log_debug("[STARTUP] scanner startup sync done");
|
||||
if (!sm_api_service_start())
|
||||
log_debug(" [API] HTTP/JSON service unavailable: %s", strerror(errno));
|
||||
sm_scanner_run_loop();
|
||||
|
||||
shutdown:
|
||||
sm_api_service_stop();
|
||||
sm_shellcore_hooks_stop();
|
||||
sm_shellcore_service_stop();
|
||||
sm_shellcore_flags_stop();
|
||||
stop_game_lifecycle_watcher();
|
||||
sm_scanner_shutdown();
|
||||
sm_kstuff_shutdown();
|
||||
sm_mdbg_shutdown();
|
||||
cleanup_kstuff_noautomount_files();
|
||||
shutdown_title_mounts();
|
||||
if (!shutdown_image_mounts()) {
|
||||
bool title_mounts_released = shutdown_title_mounts();
|
||||
bool image_mounts_released = false;
|
||||
if (title_mounts_released) {
|
||||
image_mounts_released = shutdown_image_mounts();
|
||||
} else {
|
||||
log_debug("[SHUTDOWN] image teardown skipped while title layers remain");
|
||||
}
|
||||
if (!image_mounts_released) {
|
||||
log_debug("[SHUTDOWN] some image mounts or devices were not fully released");
|
||||
}
|
||||
if (title_mounts_released && image_mounts_released) {
|
||||
cleanup_kstuff_noautomount_file();
|
||||
} else {
|
||||
log_debug("[KSTUFF] keeping automount disabled after incomplete teardown");
|
||||
}
|
||||
shutdown_app_db();
|
||||
|
||||
if (atomic_load_explicit(&g_shutdown_on_going_stop_requested,
|
||||
|
||||
@@ -0,0 +1,934 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <json-c/json.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_api_protocol.h"
|
||||
#include "sm_api_service.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_device.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_socket_io.h"
|
||||
|
||||
#ifndef SHADOWMOUNT_VERSION
|
||||
#define SHADOWMOUNT_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
#define SM_API_ROUTE_SIZE 128u
|
||||
#define SM_API_RETRY_INITIAL_MS 250u
|
||||
#define SM_API_RETRY_MAX_MS 5000u
|
||||
#define SM_API_CORS_HEADERS \
|
||||
"Access-Control-Allow-Origin: *\r\n" \
|
||||
"Access-Control-Allow-Methods: POST, OPTIONS\r\n" \
|
||||
"Access-Control-Allow-Headers: Content-Type\r\n" \
|
||||
"Access-Control-Allow-Private-Network: true\r\n"
|
||||
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t retry_cond;
|
||||
bool started;
|
||||
bool stop_requested;
|
||||
int listen_fd;
|
||||
int client_fd;
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
uint16_t port;
|
||||
} sm_api_service_state_t;
|
||||
|
||||
typedef struct {
|
||||
char route[SM_API_ROUTE_SIZE];
|
||||
char *storage;
|
||||
char *body;
|
||||
size_t body_size;
|
||||
bool preflight;
|
||||
} sm_http_request_t;
|
||||
|
||||
static sm_api_service_state_t g_api = {
|
||||
.mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.retry_cond = PTHREAD_COND_INITIALIZER,
|
||||
.listen_fd = -1,
|
||||
.client_fd = -1,
|
||||
};
|
||||
|
||||
static int open_http_listener(const char *bind_address, uint16_t port) {
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
int reuse = 1;
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
|
||||
struct sockaddr_in address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sin_len = sizeof(address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, bind_address, &address.sin_addr) != 1) {
|
||||
close(fd);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(fd, 4) != 0) {
|
||||
int saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static uint32_t next_retry_delay_ms(uint32_t current) {
|
||||
if (current >= SM_API_RETRY_MAX_MS)
|
||||
return SM_API_RETRY_MAX_MS;
|
||||
if (current > SM_API_RETRY_MAX_MS / 2u)
|
||||
return SM_API_RETRY_MAX_MS;
|
||||
return current * 2u;
|
||||
}
|
||||
|
||||
static bool wait_before_listener_retry(uint32_t delay_ms) {
|
||||
struct timespec deadline;
|
||||
if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) {
|
||||
sceKernelUsleep(SM_API_RETRY_INITIAL_MS * 1000u);
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
bool keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return keep_running;
|
||||
}
|
||||
|
||||
deadline.tv_sec += (time_t)(delay_ms / 1000u);
|
||||
deadline.tv_nsec += (long)(delay_ms % 1000u) * 1000000l;
|
||||
if (deadline.tv_nsec >= 1000000000l) {
|
||||
deadline.tv_sec++;
|
||||
deadline.tv_nsec -= 1000000000l;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
int wait_status = 0;
|
||||
while (!g_api.stop_requested) {
|
||||
wait_status =
|
||||
pthread_cond_timedwait(&g_api.retry_cond, &g_api.mutex, &deadline);
|
||||
if (wait_status != 0)
|
||||
break;
|
||||
}
|
||||
bool keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (keep_running && wait_status != 0 && wait_status != ETIMEDOUT) {
|
||||
sceKernelUsleep(SM_API_RETRY_INITIAL_MS * 1000u);
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
keep_running = !g_api.stop_requested;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
}
|
||||
return keep_running;
|
||||
}
|
||||
|
||||
static bool accept_error_needs_wait(int error) {
|
||||
return error == EAGAIN || error == EWOULDBLOCK || error == EMFILE ||
|
||||
error == ENFILE || error == ENOBUFS || error == ENOMEM;
|
||||
}
|
||||
|
||||
static const char *http_reason_phrase(int status) {
|
||||
switch (status) {
|
||||
case 200:
|
||||
return "OK";
|
||||
case 400:
|
||||
return "Bad Request";
|
||||
case 404:
|
||||
return "Not Found";
|
||||
case 405:
|
||||
return "Method Not Allowed";
|
||||
case 409:
|
||||
return "Conflict";
|
||||
case 411:
|
||||
return "Length Required";
|
||||
case 413:
|
||||
return "Payload Too Large";
|
||||
case 415:
|
||||
return "Unsupported Media Type";
|
||||
case 431:
|
||||
return "Request Header Fields Too Large";
|
||||
case 500:
|
||||
return "Internal Server Error";
|
||||
case 501:
|
||||
return "Not Implemented";
|
||||
default:
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
static bool send_json_text(int fd, int http_status, const char *body,
|
||||
size_t body_size) {
|
||||
char header[512];
|
||||
int count = snprintf(header, sizeof(header),
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
SM_API_CORS_HEADERS
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Cache-Control: no-store\r\n\r\n",
|
||||
http_status, http_reason_phrase(http_status), body_size);
|
||||
if (count < 0 || (size_t)count >= sizeof(header))
|
||||
return false;
|
||||
return sm_socket_write_full(fd, header, (size_t)count) &&
|
||||
sm_socket_write_full(fd, body, body_size);
|
||||
}
|
||||
|
||||
static void send_preflight_response(int fd) {
|
||||
static const char response[] =
|
||||
"HTTP/1.1 204 No Content\r\n"
|
||||
SM_API_CORS_HEADERS
|
||||
"Access-Control-Max-Age: 86400\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n\r\n";
|
||||
(void)sm_socket_write_full(fd, response, sizeof(response) - 1u);
|
||||
}
|
||||
|
||||
static bool send_json_object(int fd, int http_status,
|
||||
struct json_object *response) {
|
||||
const char *body =
|
||||
json_object_to_json_string_ext(response, JSON_C_TO_STRING_PLAIN);
|
||||
if (!body)
|
||||
return false;
|
||||
return send_json_text(fd, http_status, body, strlen(body));
|
||||
}
|
||||
|
||||
static bool add_json_value(struct json_object *object, const char *key,
|
||||
struct json_object *value) {
|
||||
if (!value)
|
||||
return false;
|
||||
if (json_object_object_add_ex(object, key, value,
|
||||
JSON_C_OBJECT_ADD_CONSTANT_KEY) == 0) {
|
||||
return true;
|
||||
}
|
||||
json_object_put(value);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool add_json_int(struct json_object *object, const char *key,
|
||||
int64_t value) {
|
||||
return add_json_value(object, key, json_object_new_int64(value));
|
||||
}
|
||||
|
||||
static bool add_json_bool(struct json_object *object, const char *key,
|
||||
bool value) {
|
||||
return add_json_value(object, key, json_object_new_boolean(value));
|
||||
}
|
||||
|
||||
static bool add_json_string(struct json_object *object, const char *key,
|
||||
const char *value) {
|
||||
return add_json_value(object, key, json_object_new_string(value));
|
||||
}
|
||||
|
||||
static bool append_json_string(struct json_object *array, const char *value) {
|
||||
struct json_object *item = json_object_new_string(value);
|
||||
if (!item)
|
||||
return false;
|
||||
if (json_object_array_add(array, item) == 0)
|
||||
return true;
|
||||
json_object_put(item);
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct json_object *new_status_response(int status) {
|
||||
struct json_object *response = json_object_new_object();
|
||||
if (response && !add_json_int(response, "status", status)) {
|
||||
json_object_put(response);
|
||||
return NULL;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
static void send_out_of_memory_response(int fd) {
|
||||
static const char body[] =
|
||||
"{\"status\":12,\"error\":\"out of memory\"}";
|
||||
(void)send_json_text(fd, 500, body, sizeof(body) - 1u);
|
||||
}
|
||||
|
||||
static void send_error_response(int fd, int http_status, int status,
|
||||
const char *message) {
|
||||
struct json_object *response = new_status_response(status);
|
||||
if (!response) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_string(response, "error", message)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, http_status, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static char *find_http_header_end(char *buffer, size_t size) {
|
||||
if (size < 4u)
|
||||
return NULL;
|
||||
for (size_t i = 0; i + 3u < size; ++i) {
|
||||
if (buffer[i] == '\r' && buffer[i + 1u] == '\n' &&
|
||||
buffer[i + 2u] == '\r' && buffer[i + 3u] == '\n') {
|
||||
return &buffer[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *trim_http_space(char *value) {
|
||||
while (*value == ' ' || *value == '\t')
|
||||
value++;
|
||||
size_t size = strlen(value);
|
||||
while (size > 0u && (value[size - 1u] == ' ' || value[size - 1u] == '\t'))
|
||||
value[--size] = '\0';
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool is_json_content_type(const char *value) {
|
||||
static const char media_type[] = "application/json";
|
||||
size_t size = sizeof(media_type) - 1u;
|
||||
if (strncasecmp(value, media_type, size) != 0)
|
||||
return false;
|
||||
value += size;
|
||||
while (*value == ' ' || *value == '\t')
|
||||
value++;
|
||||
return *value == '\0' || *value == ';';
|
||||
}
|
||||
|
||||
static bool parse_content_length(const char *value, size_t *size_out) {
|
||||
if (!value || !size_out || value[0] == '\0' || value[0] == '-')
|
||||
return false;
|
||||
errno = 0;
|
||||
char *end = NULL;
|
||||
unsigned long long parsed = strtoull(value, &end, 10);
|
||||
if (errno != 0 || end == value || *end != '\0' || parsed > SIZE_MAX)
|
||||
return false;
|
||||
*size_out = (size_t)parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool reject_http_request(char *buffer, int http_status,
|
||||
const char *error, int *http_status_out,
|
||||
const char **error_out) {
|
||||
free(buffer);
|
||||
*http_status_out = http_status;
|
||||
*error_out = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool receive_http_request(int fd, sm_http_request_t *request,
|
||||
int *http_status_out,
|
||||
const char **error_out) {
|
||||
size_t capacity = SM_API_MAX_HTTP_HEADER_SIZE + SM_API_MAX_JSON_BODY_SIZE + 1u;
|
||||
char *buffer = malloc(capacity);
|
||||
if (!buffer)
|
||||
return reject_http_request(NULL, 500, "out of memory", http_status_out,
|
||||
error_out);
|
||||
|
||||
size_t received = 0;
|
||||
char *header_end = NULL;
|
||||
while (!header_end) {
|
||||
if (received >= SM_API_MAX_HTTP_HEADER_SIZE)
|
||||
return reject_http_request(buffer, 431, "HTTP headers are too large",
|
||||
http_status_out, error_out);
|
||||
ssize_t count = recv(fd, buffer + received,
|
||||
SM_API_MAX_HTTP_HEADER_SIZE - received, 0);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return reject_http_request(buffer, 400, "incomplete HTTP request",
|
||||
http_status_out, error_out);
|
||||
received += (size_t)count;
|
||||
header_end = find_http_header_end(buffer, received);
|
||||
}
|
||||
|
||||
size_t header_size = (size_t)(header_end - buffer) + 4u;
|
||||
*header_end = '\0';
|
||||
char *line_end = strstr(buffer, "\r\n");
|
||||
if (!line_end)
|
||||
return reject_http_request(buffer, 400, "invalid HTTP request line",
|
||||
http_status_out, error_out);
|
||||
*line_end = '\0';
|
||||
|
||||
char method[8];
|
||||
char version[16];
|
||||
char extra;
|
||||
if (sscanf(buffer, "%7s %127s %15s %c", method, request->route, version,
|
||||
&extra) != 3 ||
|
||||
(strcmp(version, "HTTP/1.1") != 0 && strcmp(version, "HTTP/1.0") != 0))
|
||||
return reject_http_request(buffer, 400, "invalid HTTP request line",
|
||||
http_status_out, error_out);
|
||||
if (strcmp(method, "POST") == 0) {
|
||||
request->preflight = false;
|
||||
} else if (strcmp(method, "OPTIONS") == 0) {
|
||||
request->preflight = true;
|
||||
} else {
|
||||
return reject_http_request(buffer, 405,
|
||||
"only POST and OPTIONS requests are supported",
|
||||
http_status_out, error_out);
|
||||
}
|
||||
|
||||
bool has_content_length = false;
|
||||
bool has_json_content_type = false;
|
||||
bool has_transfer_encoding = false;
|
||||
size_t body_size = 0;
|
||||
char *line = line_end + 2;
|
||||
while (line < header_end && line[0] != '\0') {
|
||||
char *next = strstr(line, "\r\n");
|
||||
if (!next)
|
||||
next = header_end;
|
||||
*next = '\0';
|
||||
char *colon = strchr(line, ':');
|
||||
if (!colon)
|
||||
return reject_http_request(buffer, 400, "invalid HTTP header",
|
||||
http_status_out, error_out);
|
||||
*colon = '\0';
|
||||
char *name = trim_http_space(line);
|
||||
char *value = trim_http_space(colon + 1);
|
||||
if (strcasecmp(name, "Content-Length") == 0) {
|
||||
if (has_content_length || !parse_content_length(value, &body_size))
|
||||
return reject_http_request(buffer, 400,
|
||||
"invalid Content-Length header",
|
||||
http_status_out, error_out);
|
||||
has_content_length = true;
|
||||
} else if (strcasecmp(name, "Content-Type") == 0) {
|
||||
has_json_content_type = is_json_content_type(value);
|
||||
} else if (strcasecmp(name, "Transfer-Encoding") == 0) {
|
||||
has_transfer_encoding = true;
|
||||
}
|
||||
line = next + 2;
|
||||
}
|
||||
|
||||
if (has_transfer_encoding)
|
||||
return reject_http_request(buffer, 501,
|
||||
"Transfer-Encoding is not supported",
|
||||
http_status_out, error_out);
|
||||
if (request->preflight) {
|
||||
request->storage = buffer;
|
||||
return true;
|
||||
}
|
||||
if (!has_content_length)
|
||||
return reject_http_request(buffer, 411, "Content-Length is required",
|
||||
http_status_out, error_out);
|
||||
if (!has_json_content_type)
|
||||
return reject_http_request(buffer, 415,
|
||||
"Content-Type must be application/json",
|
||||
http_status_out, error_out);
|
||||
if (body_size > SM_API_MAX_JSON_BODY_SIZE)
|
||||
return reject_http_request(buffer, 413, "JSON request body is too large",
|
||||
http_status_out, error_out);
|
||||
|
||||
size_t total_size = header_size + body_size;
|
||||
while (received < total_size) {
|
||||
ssize_t count = recv(fd, buffer + received, total_size - received, 0);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return reject_http_request(buffer, 400,
|
||||
"incomplete JSON request body",
|
||||
http_status_out, error_out);
|
||||
received += (size_t)count;
|
||||
}
|
||||
buffer[total_size] = '\0';
|
||||
request->storage = buffer;
|
||||
request->body = buffer + header_size;
|
||||
request->body_size = body_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct json_object *parse_request_json(const sm_http_request_t *request) {
|
||||
struct json_tokener *tokener = json_tokener_new_ex(16);
|
||||
if (!tokener)
|
||||
return NULL;
|
||||
json_tokener_set_flags(tokener, JSON_TOKENER_STRICT | JSON_TOKENER_VALIDATE_UTF8);
|
||||
struct json_object *root =
|
||||
json_tokener_parse_ex(tokener, request->body, (int)request->body_size);
|
||||
enum json_tokener_error error = json_tokener_get_error(tokener);
|
||||
size_t parsed_size = json_tokener_get_parse_end(tokener);
|
||||
while (parsed_size < request->body_size &&
|
||||
isspace((unsigned char)request->body[parsed_size])) {
|
||||
parsed_size++;
|
||||
}
|
||||
bool valid = error == json_tokener_success && root != NULL &&
|
||||
parsed_size == request->body_size &&
|
||||
json_object_is_type(root, json_type_object);
|
||||
json_tokener_free(tokener);
|
||||
if (!valid) {
|
||||
if (root)
|
||||
json_object_put(root);
|
||||
return NULL;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
static const char *get_title_id(struct json_object *request) {
|
||||
struct json_object *value = NULL;
|
||||
if (!json_object_object_get_ex(request, "title_id", &value) ||
|
||||
!json_object_is_type(value, json_type_string)) {
|
||||
return NULL;
|
||||
}
|
||||
const char *title_id = json_object_get_string(value);
|
||||
return is_supported_game_title_id(title_id) ? title_id : NULL;
|
||||
}
|
||||
|
||||
static void handle_version(int fd) {
|
||||
struct json_object *response = new_status_response(0);
|
||||
struct json_object *capabilities = json_object_new_array();
|
||||
if (!response || !capabilities) {
|
||||
if (response)
|
||||
json_object_put(response);
|
||||
if (capabilities)
|
||||
json_object_put(capabilities);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_int(response, "api_version", SM_API_VERSION) ||
|
||||
!add_json_string(response, "shadowmount_version", SHADOWMOUNT_VERSION) ||
|
||||
!append_json_string(capabilities, "list_images") ||
|
||||
!append_json_string(capabilities, "list_games") ||
|
||||
!append_json_string(capabilities, "mount_game") ||
|
||||
!append_json_string(capabilities, "unmount_game")) {
|
||||
json_object_put(capabilities);
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_value(response, "capabilities", capabilities)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static struct json_object *image_to_json(const void *entry) {
|
||||
const sm_image_index_snapshot_entry_t *source = entry;
|
||||
struct json_object *item = json_object_new_object();
|
||||
if (!item)
|
||||
return NULL;
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(source->path, mount_point);
|
||||
bool source_available = path_exists(source->path);
|
||||
bool mapped = false;
|
||||
bool mounted = false;
|
||||
int unit_id = -1;
|
||||
attach_backend_t backend = ATTACH_BACKEND_NONE;
|
||||
|
||||
image_cache_entry_t cached;
|
||||
if (find_image_cache_entry(source->path, &cached, NULL)) {
|
||||
mapped = true;
|
||||
(void)strlcpy(mount_point, cached.mount_point, sizeof(mount_point));
|
||||
unit_id = cached.unit_id;
|
||||
backend = cached.backend;
|
||||
mounted = cached.unit_id >= 0 && cached.backend != ATTACH_BACKEND_NONE &&
|
||||
is_active_image_mount_point(cached.mount_point);
|
||||
}
|
||||
|
||||
if (!add_json_string(item, "path", source->path) ||
|
||||
!add_json_string(item, "mount_point", mount_point) ||
|
||||
!add_json_int(item, "size", source->size) ||
|
||||
!add_json_int(item, "mtime_sec", source->mtime_sec) ||
|
||||
!add_json_int(item, "mtime_nsec", source->mtime_nsec) ||
|
||||
!add_json_int(item, "unit_id", unit_id) ||
|
||||
!add_json_string(item, "backend", attach_backend_name(backend)) ||
|
||||
!add_json_bool(item, "complete", source->complete) ||
|
||||
!add_json_bool(item, "source_available", source_available) ||
|
||||
!add_json_bool(item, "mapped", mapped) ||
|
||||
!add_json_bool(item, "mounted", mounted)) {
|
||||
json_object_put(item);
|
||||
return NULL;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
static struct json_object *game_to_json(const void *entry) {
|
||||
const sm_game_cache_snapshot_entry_t *source = entry;
|
||||
struct json_object *item = json_object_new_object();
|
||||
if (!item)
|
||||
return NULL;
|
||||
|
||||
char managed_path[MAX_PATH];
|
||||
char image_path[MAX_PATH] = {0};
|
||||
bool installed = is_installed(source->title_id);
|
||||
bool mounted = is_data_mounted(source->title_id);
|
||||
bool managed =
|
||||
read_mount_link(source->title_id, managed_path, sizeof(managed_path));
|
||||
bool image_backed = read_mount_image_link(source->title_id, image_path,
|
||||
sizeof(image_path));
|
||||
bool source_available = path_exists(source->path) ||
|
||||
(image_path[0] != '\0' && path_exists(image_path));
|
||||
|
||||
if (!add_json_string(item, "path", source->path) ||
|
||||
!add_json_string(item, "image_path", image_path) ||
|
||||
!add_json_string(item, "title_id", source->title_id) ||
|
||||
!add_json_string(item, "title_name", source->title_name) ||
|
||||
!add_json_bool(item, "installed", installed) ||
|
||||
!add_json_bool(item, "managed", managed) ||
|
||||
!add_json_bool(item, "mounted", mounted) ||
|
||||
!add_json_bool(item, "image_backed", image_backed) ||
|
||||
!add_json_bool(item, "source_available", source_available)) {
|
||||
json_object_put(item);
|
||||
return NULL;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
typedef struct json_object *(*snapshot_to_json_fn)(const void *entry);
|
||||
|
||||
static void send_snapshot_response(int fd, void *snapshot, size_t count,
|
||||
size_t entry_size, const char *array_key,
|
||||
snapshot_to_json_fn to_json) {
|
||||
struct json_object *response = new_status_response(0);
|
||||
struct json_object *items = json_object_new_array_ext((int)count);
|
||||
if (!response || !items) {
|
||||
if (response)
|
||||
json_object_put(response);
|
||||
if (items)
|
||||
json_object_put(items);
|
||||
free(snapshot);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned char *entries = snapshot;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
struct json_object *item = to_json(entries + i * entry_size);
|
||||
if (!item || json_object_array_add(items, item) != 0) {
|
||||
if (item)
|
||||
json_object_put(item);
|
||||
json_object_put(items);
|
||||
json_object_put(response);
|
||||
free(snapshot);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
free(snapshot);
|
||||
|
||||
if (!add_json_int(response, "count", (int64_t)count)) {
|
||||
json_object_put(items);
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_value(response, array_key, items)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static void handle_images(int fd) {
|
||||
sm_image_index_snapshot_entry_t *snapshot = NULL;
|
||||
size_t count = 0;
|
||||
if (!sm_image_index_snapshot(&snapshot, &count)) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
send_snapshot_response(fd, snapshot, count, sizeof(*snapshot), "images",
|
||||
image_to_json);
|
||||
}
|
||||
|
||||
static void handle_games(int fd) {
|
||||
sm_game_cache_snapshot_entry_t *snapshot = NULL;
|
||||
size_t count = 0;
|
||||
if (!sm_game_cache_snapshot(&snapshot, &count)) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
send_snapshot_response(fd, snapshot, count, sizeof(*snapshot), "games",
|
||||
game_to_json);
|
||||
}
|
||||
|
||||
static int operation_http_status(int status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 200;
|
||||
case EINVAL:
|
||||
return 400;
|
||||
case ENOENT:
|
||||
return 404;
|
||||
case EBUSY:
|
||||
return 409;
|
||||
case ENOTSUP:
|
||||
return 501;
|
||||
default:
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_mount_operation(int fd, struct json_object *request,
|
||||
bool mount) {
|
||||
const char *title_id = get_title_id(request);
|
||||
if (!title_id) {
|
||||
send_error_response(fd, 400, EINVAL,
|
||||
"title_id must be a valid PS4 or PS5 title ID");
|
||||
return;
|
||||
}
|
||||
|
||||
int status = mount ? sm_shellcore_mount_title_runtime(title_id)
|
||||
: sm_shellcore_unmount_title_runtime(title_id);
|
||||
log_debug(" [API] HTTP %s game: title=%s status=%d",
|
||||
mount ? "mount" : "unmount", title_id, status);
|
||||
if (status != 0) {
|
||||
send_error_response(fd, operation_http_status(status), status,
|
||||
strerror(status));
|
||||
return;
|
||||
}
|
||||
|
||||
struct json_object *response = new_status_response(0);
|
||||
if (!response) {
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
if (!add_json_string(response, "title_id", title_id) ||
|
||||
!add_json_bool(response, "mounted", mount)) {
|
||||
json_object_put(response);
|
||||
send_out_of_memory_response(fd);
|
||||
return;
|
||||
}
|
||||
(void)send_json_object(fd, 200, response);
|
||||
json_object_put(response);
|
||||
}
|
||||
|
||||
static void handle_client(int fd) {
|
||||
sm_http_request_t request;
|
||||
memset(&request, 0, sizeof(request));
|
||||
int http_status = 400;
|
||||
const char *error = "invalid HTTP request";
|
||||
if (!receive_http_request(fd, &request, &http_status, &error)) {
|
||||
send_error_response(fd, http_status,
|
||||
http_status == 500 ? ENOMEM : EPROTO, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.preflight) {
|
||||
send_preflight_response(fd);
|
||||
free(request.storage);
|
||||
return;
|
||||
}
|
||||
|
||||
struct json_object *json = parse_request_json(&request);
|
||||
if (!json) {
|
||||
send_error_response(fd, 400, EINVAL,
|
||||
"request body must be one valid JSON object");
|
||||
free(request.storage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(request.route, SM_API_ROUTE_VERSION) == 0) {
|
||||
handle_version(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_IMAGES) == 0) {
|
||||
handle_images(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_GAMES) == 0) {
|
||||
handle_games(fd);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_MOUNT) == 0) {
|
||||
handle_mount_operation(fd, json, true);
|
||||
} else if (strcmp(request.route, SM_API_ROUTE_UNMOUNT) == 0) {
|
||||
handle_mount_operation(fd, json, false);
|
||||
} else {
|
||||
send_error_response(fd, 404, ENOENT, "unknown API route");
|
||||
}
|
||||
|
||||
json_object_put(json);
|
||||
free(request.storage);
|
||||
}
|
||||
|
||||
static void *service_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
(void)strlcpy(bind_address, g_api.bind_address, sizeof(bind_address));
|
||||
uint16_t port = g_api.port;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
int listen_fd = -1;
|
||||
uint32_t retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
bool outage_logged = false;
|
||||
while (true) {
|
||||
if (listen_fd < 0) {
|
||||
listen_fd = open_http_listener(bind_address, port);
|
||||
if (listen_fd < 0) {
|
||||
int open_error = errno;
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP listener unavailable at %s:%u: %s; "
|
||||
"recovery scheduled",
|
||||
bind_address, (unsigned)port, strerror(open_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(listen_fd);
|
||||
break;
|
||||
}
|
||||
g_api.listen_fd = listen_fd;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (outage_logged) {
|
||||
log_debug(" [API] HTTP listener recovered: http://%s:%u/api/v1",
|
||||
bind_address, (unsigned)port);
|
||||
} else {
|
||||
log_debug(" [API] HTTP/JSON ready: http://%s:%u/api/v1 (v%u)",
|
||||
bind_address, (unsigned)port, SM_API_VERSION);
|
||||
}
|
||||
outage_logged = false;
|
||||
retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
}
|
||||
|
||||
int client = accept(listen_fd, NULL, NULL);
|
||||
if (client < 0) {
|
||||
int accept_error = errno;
|
||||
if (accept_error == EINTR || accept_error == ECONNABORTED ||
|
||||
accept_error == EPROTO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (accept_error_needs_wait(accept_error)) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP accept paused: %s; recovery scheduled",
|
||||
strerror(accept_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool owns_listener = g_api.listen_fd == listen_fd;
|
||||
if (owns_listener)
|
||||
g_api.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
if (owns_listener)
|
||||
close(listen_fd);
|
||||
listen_fd = -1;
|
||||
if (!outage_logged) {
|
||||
log_debug(" [API] HTTP listener lost: %s; recovery scheduled",
|
||||
strerror(accept_error));
|
||||
outage_logged = true;
|
||||
}
|
||||
if (!wait_before_listener_retry(retry_delay_ms))
|
||||
break;
|
||||
retry_delay_ms = next_retry_delay_ms(retry_delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (outage_logged) {
|
||||
log_debug(" [API] HTTP accept recovered");
|
||||
outage_logged = false;
|
||||
}
|
||||
retry_delay_ms = SM_API_RETRY_INITIAL_MS;
|
||||
|
||||
struct timeval timeout = {.tv_sec = 5, .tv_usec = 0};
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.stop_requested) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(client);
|
||||
break;
|
||||
}
|
||||
g_api.client_fd = client;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
handle_client(client);
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.client_fd == client)
|
||||
g_api.client_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_api_service_start(void) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
char bind_address[MAX_API_BIND_ADDRESS];
|
||||
(void)strlcpy(bind_address, cfg->api_bind_address, sizeof(bind_address));
|
||||
uint16_t port = (uint16_t)cfg->api_port;
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (g_api.started) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return true;
|
||||
}
|
||||
g_api.listen_fd = -1;
|
||||
g_api.client_fd = -1;
|
||||
g_api.stop_requested = false;
|
||||
(void)strlcpy(g_api.bind_address, bind_address,
|
||||
sizeof(g_api.bind_address));
|
||||
g_api.port = port;
|
||||
int rc = pthread_create(&g_api.thread, NULL, service_thread_main, NULL);
|
||||
if (rc != 0) {
|
||||
g_api.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
errno = rc;
|
||||
return false;
|
||||
}
|
||||
g_api.started = true;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_api_service_stop(void) {
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
if (!g_api.started) {
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
return;
|
||||
}
|
||||
g_api.stop_requested = true;
|
||||
int fd = g_api.listen_fd;
|
||||
int client_fd = g_api.client_fd;
|
||||
g_api.listen_fd = -1;
|
||||
pthread_cond_broadcast(&g_api.retry_cond);
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
|
||||
if (client_fd >= 0)
|
||||
(void)shutdown(client_fd, SHUT_RDWR);
|
||||
if (fd >= 0) {
|
||||
(void)shutdown(fd, SHUT_RDWR);
|
||||
close(fd);
|
||||
}
|
||||
pthread_join(g_api.thread, NULL);
|
||||
|
||||
pthread_mutex_lock(&g_api.mutex);
|
||||
g_api.started = false;
|
||||
g_api.stop_requested = false;
|
||||
g_api.client_fd = -1;
|
||||
pthread_mutex_unlock(&g_api.mutex);
|
||||
}
|
||||
|
||||
bool sm_api_service_reconfigure(void) {
|
||||
sm_api_service_stop();
|
||||
return sm_api_service_start();
|
||||
}
|
||||
+76
-47
@@ -14,10 +14,13 @@ 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_auto_remove_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_auto_remove_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_auto_remove_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;
|
||||
|
||||
@@ -49,10 +52,13 @@ 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_auto_remove_cache);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_auto_remove_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_auto_remove_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
@@ -418,6 +424,28 @@ static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_auto_remove_title_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE metaDataPath GLOB '/user/app/PPSA*/sce_sys' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"auto-remove title query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "auto-remove title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db auto-remove title list: %d entries",
|
||||
list->count);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
@@ -444,8 +472,10 @@ static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list)
|
||||
void invalidate_app_db_title_cache(void) {
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
g_app_db_title_cache_ready = false;
|
||||
g_app_db_auto_remove_cache_ready = false;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_ready = false;
|
||||
g_app_db_title_cache_mtime = 0;
|
||||
g_app_db_auto_remove_cache_mtime = 0;
|
||||
g_app_db_blocked_uninstall_ppsa_cache_mtime = 0;
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
}
|
||||
@@ -470,34 +500,54 @@ static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
return true;
|
||||
}
|
||||
|
||||
typedef bool (*load_app_db_title_list_fn)(struct AppDbTitleList *list);
|
||||
|
||||
static bool get_cached_app_db_title_list(
|
||||
struct AppDbTitleList *list_out, struct AppDbTitleList *cache,
|
||||
bool *cache_ready, time_t *cache_mtime, load_app_db_title_list_fn load_fn) {
|
||||
free_app_db_title_list(list_out);
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
if (!*cache_ready ||
|
||||
(app_db_stat_rc == 0 && *cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 && load_fn(&fresh)) {
|
||||
free_app_db_title_list(cache);
|
||||
*cache = fresh;
|
||||
*cache_mtime = st.st_mtime;
|
||||
*cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!*cache_ready)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return copy_app_db_title_list(list_out, cache);
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
free_app_db_title_list(list_out);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
bool copied = get_cached_app_db_title_list(
|
||||
list_out, &g_app_db_title_cache, &g_app_db_title_cache_ready,
|
||||
&g_app_db_title_cache_mtime, load_app_db_title_list);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return copied;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
int app_db_stat_rc = stat(APP_DB_PATH, &st);
|
||||
bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
if (!g_app_db_title_cache_ready ||
|
||||
(app_db_stat_rc == 0 && g_app_db_title_cache_mtime != st.st_mtime)) {
|
||||
struct AppDbTitleList fresh = {0};
|
||||
if (app_db_stat_rc == 0 && load_app_db_title_list(&fresh)) {
|
||||
free_app_db_title_list(&g_app_db_title_cache);
|
||||
g_app_db_title_cache = fresh;
|
||||
g_app_db_title_cache_mtime = st.st_mtime;
|
||||
g_app_db_title_cache_ready = true;
|
||||
} else {
|
||||
free_app_db_title_list(&fresh);
|
||||
if (!g_app_db_title_cache_ready) {
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool copied = copy_app_db_title_list(list_out, &g_app_db_title_cache);
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
bool copied = get_cached_app_db_title_list(
|
||||
list_out, &g_app_db_auto_remove_cache,
|
||||
&g_app_db_auto_remove_cache_ready, &g_app_db_auto_remove_cache_mtime,
|
||||
load_app_db_auto_remove_title_list);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return copied;
|
||||
}
|
||||
@@ -506,33 +556,12 @@ 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);
|
||||
bool loaded = get_cached_app_db_title_list(
|
||||
list_out, &g_app_db_blocked_uninstall_ppsa_cache,
|
||||
&g_app_db_blocked_uninstall_ppsa_cache_ready,
|
||||
&g_app_db_blocked_uninstall_ppsa_cache_mtime,
|
||||
load_app_db_blocked_uninstall_ppsa_list);
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
return loaded;
|
||||
}
|
||||
+77
-20
@@ -1,4 +1,9 @@
|
||||
#include "sm_platform.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_api_protocol.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_limits.h"
|
||||
@@ -98,7 +103,6 @@ static bool lookup_kstuff_delay_override_in_file(const char *path,
|
||||
static bool upsert_kstuff_delay_override_in_file(const char *path,
|
||||
const char *title_id,
|
||||
uint32_t delay_seconds);
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state);
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
@@ -152,19 +156,6 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void apply_firmware_runtime_overrides(runtime_config_state_t *state) {
|
||||
if (!state)
|
||||
return;
|
||||
|
||||
if (sm_firmware_major_version() >= 12u) {
|
||||
state->cfg.app_install_all_enabled = true;
|
||||
state->cfg.app_install_all_forced = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state->cfg.app_install_all_forced = false;
|
||||
}
|
||||
|
||||
static const runtime_config_state_t *active_runtime_state(void) {
|
||||
return &g_runtime_state_slots[atomic_load_explicit(&g_runtime_state_active_index,
|
||||
memory_order_acquire)];
|
||||
@@ -250,18 +241,24 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
state->cfg.force_mount = false;
|
||||
state->cfg.app_install_all_enabled = false;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
state->cfg.auto_remove_missing_games = false;
|
||||
state->cfg.auto_remove_games_with_dlc = false;
|
||||
state->cfg.backport_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_enabled = true;
|
||||
state->cfg.global_fakelib_mount_first = true;
|
||||
state->cfg.kstuff_game_auto_toggle = true;
|
||||
state->cfg.kstuff_crash_detection_enabled = true;
|
||||
state->cfg.legacy_recursive_scan_forced = false;
|
||||
(void)strlcpy(state->cfg.api_bind_address, SM_API_DEFAULT_BIND_ADDRESS,
|
||||
sizeof(state->cfg.api_bind_address));
|
||||
state->cfg.api_port = SM_API_DEFAULT_PORT;
|
||||
(void)strlcpy(state->cfg.global_fakelib_path, DEFAULT_GLOBAL_FAKELIB_PATH,
|
||||
sizeof(state->cfg.global_fakelib_path));
|
||||
state->cfg.scan_depth = DEFAULT_SCAN_DEPTH;
|
||||
state->cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
|
||||
state->cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
|
||||
state->cfg.auto_remove_missing_delay_seconds =
|
||||
DEFAULT_AUTO_REMOVE_MISSING_DELAY_SECONDS;
|
||||
state->cfg.kstuff_pause_delay_image_seconds =
|
||||
DEFAULT_KSTUFF_PAUSE_DELAY_IMAGE_SECONDS;
|
||||
state->cfg.kstuff_pause_delay_direct_seconds =
|
||||
@@ -276,7 +273,6 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules));
|
||||
clear_kstuff_title_rules(state);
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
apply_firmware_runtime_overrides(state);
|
||||
}
|
||||
|
||||
static config_file_stamp_t read_config_file_stamp(void) {
|
||||
@@ -1171,6 +1167,29 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_bind_address") == 0) {
|
||||
struct in_addr address;
|
||||
if (strlen(value) >= sizeof(state->cfg.api_bind_address) ||
|
||||
inet_pton(AF_INET, value, &address) != 1) {
|
||||
log_debug(" [CFG] invalid API IPv4 address at line %d: %s=%s",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
(void)strlcpy(state->cfg.api_bind_address, value,
|
||||
sizeof(state->cfg.api_bind_address));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_port") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 == 0 || u32 > 65535u) {
|
||||
log_debug(" [CFG] invalid API port at line %d: %s=%s (range: 1..65535)",
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.api_port = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "mount_read_only") == 0 ||
|
||||
strcasecmp(key, "read_only") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
@@ -1196,7 +1215,24 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
state->cfg.app_install_all_enabled = bval;
|
||||
state->cfg.app_install_all_forced = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_missing_games") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_missing_games = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_games_with_dlc") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_games_with_dlc = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1368,6 +1404,22 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "auto_remove_missing_delay_seconds") == 0 ||
|
||||
strcasecmp(key, "auto_remove_missing_delay_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) ||
|
||||
u32 < MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS ||
|
||||
u32 > MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS) {
|
||||
log_debug(" [CFG] invalid auto-remove delay at line %d: %s=%s "
|
||||
"(range: %u..%u)",
|
||||
line_no, key, value,
|
||||
(unsigned)MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS,
|
||||
(unsigned)MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS);
|
||||
continue;
|
||||
}
|
||||
state->cfg.auto_remove_missing_delay_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "kstuff_pause_delay_image_seconds") == 0 ||
|
||||
strcasecmp(key, "kstuff_pause_delay_image_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_KSTUFF_PAUSE_DELAY_SECONDS) {
|
||||
@@ -1469,7 +1521,6 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
log_debug(" [CFG] recursive_scan=1 is deprecated; forcing scan_depth=2");
|
||||
}
|
||||
|
||||
apply_firmware_runtime_overrides(state);
|
||||
|
||||
int image_rule_count = 0;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
@@ -1484,7 +1535,9 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d ro=%d force=%d "
|
||||
"app_install_all=%d app_install_all_forced=%d scan_depth=%u "
|
||||
"app_install_all=%d auto_remove_missing_games=%d "
|
||||
"auto_remove_games_with_dlc=%d auto_remove_missing_delay_s=%u "
|
||||
"api=%s:%u scan_depth=%u "
|
||||
"legacy_recursive_scan_forced=%d backport_fakelib=%d "
|
||||
"global_fakelib=%d global_fakelib_priority=%s "
|
||||
"global_fakelib_path=%s global_fakelib_exclude=%u "
|
||||
@@ -1498,7 +1551,11 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.mount_read_only ? 1 : 0,
|
||||
state->cfg.force_mount ? 1 : 0,
|
||||
state->cfg.app_install_all_enabled ? 1 : 0,
|
||||
state->cfg.app_install_all_forced ? 1 : 0, state->cfg.scan_depth,
|
||||
state->cfg.auto_remove_missing_games ? 1 : 0,
|
||||
state->cfg.auto_remove_games_with_dlc ? 1 : 0,
|
||||
state->cfg.auto_remove_missing_delay_seconds,
|
||||
state->cfg.api_bind_address, state->cfg.api_port,
|
||||
state->cfg.scan_depth,
|
||||
state->cfg.legacy_recursive_scan_forced ? 1 : 0,
|
||||
state->cfg.backport_fakelib_enabled ? 1 : 0,
|
||||
state->cfg.global_fakelib_enabled ? 1 : 0,
|
||||
|
||||
+20
-7
@@ -3,6 +3,7 @@
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_diag.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#include <pthread.h>
|
||||
@@ -52,15 +53,19 @@ static bool mount_fakelib_overlay(const char *title_id,
|
||||
|
||||
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 ||
|
||||
if (unmount(mount_path, 0) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
log_debug(" [FAKELIB] %s libraries unmounted: %s -> %s", layer->label,
|
||||
layer->source_path, mount_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] %s unmount failed for %s: %s", layer->label,
|
||||
mount_path, strerror(errno));
|
||||
int unmount_errno = errno;
|
||||
if (unmount_errno == EBUSY)
|
||||
sm_mount_diag_log_busy(mount_path);
|
||||
log_debug(" [FAKELIB] %s unmount deferred for %s: %s", layer->label,
|
||||
mount_path, strerror(unmount_errno));
|
||||
errno = unmount_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -108,8 +113,10 @@ static bool resolve_sandbox_context(const char *title_id,
|
||||
continue;
|
||||
|
||||
errno = 0;
|
||||
long idx_long = strtol(suffix, NULL, 10);
|
||||
if (errno == ERANGE || idx_long > INT_MAX)
|
||||
char *suffix_end = NULL;
|
||||
long idx_long = strtol(suffix, &suffix_end, 10);
|
||||
if (errno == ERANGE || !suffix_end || *suffix_end != '\0' ||
|
||||
idx_long > INT_MAX)
|
||||
continue;
|
||||
int idx = (int)idx_long;
|
||||
if (idx < best_index)
|
||||
@@ -125,11 +132,17 @@ static bool resolve_sandbox_context(const char *title_id,
|
||||
return false;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
snprintf(source_path, sizeof(source_path), "/mnt/sandbox/%s/app0/fakelib",
|
||||
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))
|
||||
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);
|
||||
|
||||
+269
-72
@@ -4,11 +4,16 @@
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_diag.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path);
|
||||
static bool unmount_controlled_mount_stack_impl(const char *path,
|
||||
bool diagnose_busy);
|
||||
|
||||
// --- FILESYSTEM ---
|
||||
bool is_installed(const char *title_id) {
|
||||
char path[MAX_PATH];
|
||||
@@ -87,7 +92,7 @@ typedef bool (*title_app_dir_iter_fn)(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx);
|
||||
|
||||
static void for_each_title_app_dir(const char *open_error_context,
|
||||
static bool 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(APP_BASE);
|
||||
@@ -95,23 +100,30 @@ static void for_each_title_app_dir(const char *open_error_context,
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [LINK] open %s failed%s: %s", APP_BASE,
|
||||
open_error_context ? open_error_context : "", strerror(errno));
|
||||
return;
|
||||
return errno == ENOENT;
|
||||
}
|
||||
|
||||
bool completed = true;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (stop_on_signal && should_stop_requested())
|
||||
if (stop_on_signal && should_stop_requested()) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
if (!resolve_title_app_dir(entry, NULL, 0))
|
||||
continue;
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(entry->d_name, &paths);
|
||||
if (!fn(entry->d_name, &paths, ctx))
|
||||
if (!fn(entry->d_name, &paths, ctx)) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
if (closedir(d) != 0)
|
||||
completed = false;
|
||||
return completed;
|
||||
}
|
||||
|
||||
bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
@@ -124,6 +136,131 @@ bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
return read_mount_link_file(paths.mount_link, out, out_size);
|
||||
}
|
||||
|
||||
static bool write_link_lines_atomic(const char *path,
|
||||
const char *const *lines,
|
||||
size_t count) {
|
||||
char temp_path[MAX_PATH];
|
||||
int written = snprintf(temp_path, sizeof(temp_path), "%s.tmp", path);
|
||||
if (written < 0 || (size_t)written >= sizeof(temp_path))
|
||||
return false;
|
||||
|
||||
FILE *file = fopen(temp_path, "w");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!lines[i] || lines[i][0] == '\0' ||
|
||||
fprintf(file, "%s%s", lines[i], i + 1u < count ? "\n" : "") < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok && fflush(file) != 0)
|
||||
ok = false;
|
||||
if (ok && fsync(fileno(file)) != 0)
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (ok && rename(temp_path, path) == 0)
|
||||
return true;
|
||||
|
||||
int saved_errno = errno != 0 ? errno : EIO;
|
||||
(void)unlink(temp_path);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool write_mount_link(const char *title_id, const char *source_path) {
|
||||
if (!title_id || title_id[0] == '\0' || !source_path ||
|
||||
source_path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(title_id, &paths);
|
||||
const char *lines[] = {source_path};
|
||||
return write_link_lines_atomic(paths.mount_link, lines, 1);
|
||||
}
|
||||
|
||||
bool read_mount_image_link(const char *title_id, char *out, size_t out_size) {
|
||||
if (!title_id || !out || out_size == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
|
||||
title_link_paths_t paths;
|
||||
build_title_link_paths(title_id, &paths);
|
||||
return read_mount_link_file(paths.mount_image_link, out, out_size);
|
||||
}
|
||||
|
||||
bool write_mount_image_link(const char *title_id, const char *image_path) {
|
||||
if (!title_id || title_id[0] == '\0' || !image_path ||
|
||||
image_path[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH] = {{0}};
|
||||
(void)strlcpy(chain[0], image_path, sizeof(chain[0]));
|
||||
return write_mount_image_chain(title_id, chain, 1);
|
||||
}
|
||||
|
||||
bool read_mount_image_chain(
|
||||
const char *title_id,
|
||||
char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *count_out) {
|
||||
if (!title_id || !paths || !count_out)
|
||||
return false;
|
||||
*count_out = 0;
|
||||
|
||||
title_link_paths_t link_paths;
|
||||
build_title_link_paths(title_id, &link_paths);
|
||||
FILE *file = fopen(link_paths.mount_image_link, "r");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
while (*count_out < MAX_IMAGE_CHAIN_DEPTH &&
|
||||
fgets(paths[*count_out], MAX_PATH, file)) {
|
||||
size_t len = strcspn(paths[*count_out], "\r\n");
|
||||
if (paths[*count_out][len] == '\0' && len + 1u >= MAX_PATH) {
|
||||
int next = fgetc(file);
|
||||
if (next != EOF && next != '\n' && next != '\r') {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
paths[*count_out][len] = '\0';
|
||||
if (len == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
(*count_out)++;
|
||||
}
|
||||
if (ok && *count_out == MAX_IMAGE_CHAIN_DEPTH && fgetc(file) != EOF)
|
||||
ok = false;
|
||||
if (ferror(file))
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (!ok)
|
||||
*count_out = 0;
|
||||
return ok && *count_out > 0;
|
||||
}
|
||||
|
||||
bool write_mount_image_chain(
|
||||
const char *title_id,
|
||||
const char paths[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t count) {
|
||||
if (!title_id || title_id[0] == '\0' || !paths || count == 0 ||
|
||||
count > MAX_IMAGE_CHAIN_DEPTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
title_link_paths_t link_paths;
|
||||
build_title_link_paths(title_id, &link_paths);
|
||||
const char *lines[MAX_IMAGE_CHAIN_DEPTH];
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
lines[i] = paths[i];
|
||||
return write_link_lines_atomic(link_paths.mount_image_link, lines, count);
|
||||
}
|
||||
|
||||
bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
size_t app_dir_size) {
|
||||
if (!entry)
|
||||
@@ -159,14 +296,18 @@ bool resolve_title_app_dir(const struct dirent *entry, char *app_dir,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool source_path_needs_cleanup(const char *source_path,
|
||||
bool *tried_image_recovery) {
|
||||
static bool source_path_needs_cleanup(const char *source_path) {
|
||||
if (is_under_image_mount_base(source_path)) {
|
||||
char image_source_path[MAX_PATH];
|
||||
bool has_image_source = resolve_image_source_from_mount_cache(
|
||||
source_path, image_source_path, sizeof(image_source_path));
|
||||
if (has_image_source && !path_exists(image_source_path))
|
||||
return true;
|
||||
if (has_image_source) {
|
||||
if (!path_exists(image_source_path))
|
||||
return true;
|
||||
// The image mount point is intentionally absent outside install/launch.
|
||||
if (!path_exists(source_path))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path_exists(source_path))
|
||||
@@ -182,11 +323,6 @@ static bool source_path_needs_cleanup(const char *source_path,
|
||||
if (path_exists(eboot_path))
|
||||
return false;
|
||||
|
||||
if (!*tried_image_recovery && is_under_image_mount_base(source_path)) {
|
||||
cleanup_stale_image_mounts();
|
||||
*tried_image_recovery = true;
|
||||
}
|
||||
|
||||
return !path_exists(eboot_path);
|
||||
}
|
||||
|
||||
@@ -459,7 +595,8 @@ static bool inspect_title_stack(const char *title_id, const char *source_path,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_top_controlled_layer(const char *path) {
|
||||
static bool unmount_top_controlled_layer_impl(const char *path,
|
||||
bool diagnose_busy) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
@@ -476,13 +613,22 @@ static bool unmount_top_controlled_layer(const char *path) {
|
||||
|
||||
if (unmount(path, 0) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
if (unmount(path, MNT_FORCE) == 0 || errno == ENOENT || errno == EINVAL)
|
||||
return true;
|
||||
int unmount_errno = errno;
|
||||
if (unmount_errno == EBUSY && diagnose_busy)
|
||||
sm_mount_diag_log_busy(path);
|
||||
|
||||
log_debug(" [LINK] unmount failed for %s: %s", path, strerror(errno));
|
||||
if (unmount_errno != EBUSY || diagnose_busy) {
|
||||
log_debug(" [LINK] unmount deferred for %s: %s", path,
|
||||
strerror(unmount_errno));
|
||||
}
|
||||
errno = unmount_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_top_controlled_layer(const char *path) {
|
||||
return unmount_top_controlled_layer_impl(path, true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -501,6 +647,60 @@ bool rollback_title_nullfs_mount(const char *title_id, const char *src_path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_title_runtime_layers_impl(const char *title_id,
|
||||
bool diagnose_busy) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
char path[MAX_PATH];
|
||||
int written = snprintf(path, sizeof(path), "/system_ex/app/%s", title_id);
|
||||
if (written < 0 || (size_t)written >= sizeof(path))
|
||||
return false;
|
||||
|
||||
if (!unmount_controlled_mount_stack_impl(path, diagnose_busy))
|
||||
return false;
|
||||
log_debug(" [LINK] runtime layers released: %s", title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unmount_title_runtime_layers(const char *title_id) {
|
||||
return unmount_title_runtime_layers_impl(title_id, true);
|
||||
}
|
||||
|
||||
bool unmount_title_runtime_layers_quiet(const char *title_id) {
|
||||
return unmount_title_runtime_layers_impl(title_id, false);
|
||||
}
|
||||
|
||||
static bool unmount_managed_title_entry(
|
||||
const char *title_id, const title_link_paths_t *paths, void *ctx) {
|
||||
bool *all_released = (bool *)ctx;
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path,
|
||||
sizeof(source_path))) {
|
||||
return true;
|
||||
}
|
||||
title_mount_state_t state;
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
if (!state.has_our_nullfs) {
|
||||
return true;
|
||||
}
|
||||
if (!unmount_title_runtime_layers(title_id))
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unmount_all_title_runtime_layers(void) {
|
||||
bool all_released = true;
|
||||
if (!for_each_title_app_dir(" during runtime mount cleanup", false,
|
||||
unmount_managed_title_entry, &all_released)) {
|
||||
all_released = false;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
|
||||
bool reconcile_title_backport_mount(const char *title_id, const char *src_path,
|
||||
const char *expected_backport_path,
|
||||
bool *overlay_active_out) {
|
||||
@@ -581,12 +781,13 @@ bool mount_backport_overlay(const char *mount_point,
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
static bool unmount_controlled_mount_stack_impl(const char *path,
|
||||
bool diagnose_busy) {
|
||||
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS * 4; i++) {
|
||||
struct statfs mount_st;
|
||||
if (!get_top_mount(path, &mount_st))
|
||||
return true;
|
||||
if (!unmount_top_controlled_layer(path))
|
||||
if (!unmount_top_controlled_layer_impl(path, diagnose_busy))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -594,27 +795,14 @@ static bool unmount_controlled_mount_stack(const char *path) {
|
||||
return !get_top_mount(path, &mount_st);
|
||||
}
|
||||
|
||||
static bool unmount_controlled_mount_stack(const char *path) {
|
||||
return unmount_controlled_mount_stack_impl(path, true);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -755,7 +943,7 @@ int copy_file(const char *src, const char *dst) {
|
||||
return copy_param_json_rewrite(src, dst);
|
||||
}
|
||||
|
||||
char buf[8192];
|
||||
char buf[64 * 1024];
|
||||
FILE *fs = fopen(src, "rb");
|
||||
if (!fs)
|
||||
return -1;
|
||||
@@ -880,8 +1068,7 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
return false;
|
||||
}
|
||||
if (!path_exists(src_eboot)) {
|
||||
bool tried_image_recovery = false;
|
||||
if (source_path_needs_cleanup(src_path, &tried_image_recovery)) {
|
||||
if (source_path_needs_cleanup(src_path)) {
|
||||
log_debug(" [LINK] source eboot.bin missing for %s: %s", title_id,
|
||||
src_eboot);
|
||||
return false;
|
||||
@@ -945,20 +1132,16 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (unmount(dst, 0) != 0 && errno != ENOENT && errno != EINVAL)
|
||||
(void)unmount(dst, MNT_FORCE);
|
||||
if (!unmount_top_controlled_layer(dst))
|
||||
log_debug(" [LINK] nullfs rollback deferred during sleep: %s", dst);
|
||||
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) {
|
||||
if (unmount(dst, MNT_FORCE) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
log_debug(" [LINK] failed to rollback empty nullfs mount %s: %s", dst,
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
if (!unmount_top_controlled_layer(dst))
|
||||
log_debug(" [LINK] empty nullfs rollback deferred: %s", dst);
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] nullfs mounted: %s -> %s", src_path, dst);
|
||||
@@ -968,9 +1151,9 @@ bool mount_title_nullfs(const char *title_id, const char *src_path) {
|
||||
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;
|
||||
bool preserve_mount_links;
|
||||
} cleanup_mount_links_ctx_t;
|
||||
|
||||
static bool cleanup_mount_links_entry(const char *title_id,
|
||||
@@ -1014,12 +1197,10 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
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);
|
||||
source_path_needs_cleanup(source_path);
|
||||
} else {
|
||||
should_remove = (has_image_source && !path_exists(image_source_path)) ||
|
||||
source_path_needs_cleanup(source_path,
|
||||
&ctx->tried_image_recovery);
|
||||
source_path_needs_cleanup(source_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,17 +1223,19 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
stack_matches_link = state.has_nullfs_from_root;
|
||||
}
|
||||
|
||||
if (inspected && stack_matches_link && !top_is_ours) {
|
||||
if (stack_matches_link && !top_is_ours) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] keeping mount link for %s: stack still active but "
|
||||
"top layer is not ours", state.system_ex_path);
|
||||
} else if (inspected && top_is_ours && stack_matches_link) {
|
||||
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
|
||||
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
} else if (top_is_ours && stack_matches_link) {
|
||||
if (!ctx->preserve_mount_links) {
|
||||
if (rename(paths->mount_link, paths->staged_mount_link) != 0) {
|
||||
log_debug(" [LINK] stage failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
return true;
|
||||
}
|
||||
mount_link_staged = true;
|
||||
}
|
||||
mount_link_staged = true;
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
keep_mount_link = true;
|
||||
log_debug(" [LINK] failed to unmount mount stack for %s",
|
||||
@@ -1066,6 +1249,9 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->preserve_mount_links)
|
||||
keep_mount_link = true;
|
||||
|
||||
if (!keep_mount_link) {
|
||||
int unlink_res = mount_link_staged ? unlink(paths->staged_mount_link)
|
||||
: unlink(paths->mount_link);
|
||||
@@ -1084,8 +1270,7 @@ static bool cleanup_mount_links_entry(const char *title_id,
|
||||
}
|
||||
|
||||
if (mount_link_staged && rename(paths->staged_mount_link, paths->mount_link) == 0) {
|
||||
log_debug(" [LINK] restored mount link after failed cleanup: %s",
|
||||
paths->mount_link);
|
||||
log_debug(" [LINK] restored staged mount link: %s", paths->mount_link);
|
||||
} else if (mount_link_staged) {
|
||||
log_debug(" [LINK] restore failed for %s: %s", paths->mount_link,
|
||||
strerror(errno));
|
||||
@@ -1098,9 +1283,9 @@ void cleanup_mount_links(const char *removed_source_root,
|
||||
cleanup_mount_links_ctx_t ctx = {
|
||||
.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,
|
||||
.preserve_mount_links = false,
|
||||
};
|
||||
for_each_title_app_dir("", !unmount_system_ex_bind, cleanup_mount_links_entry,
|
||||
&ctx);
|
||||
@@ -1113,9 +1298,9 @@ void cleanup_mount_links_for_source_unmount(const char *source_root) {
|
||||
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,
|
||||
.preserve_mount_links = false,
|
||||
};
|
||||
for_each_title_app_dir(" during source unmount cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
@@ -1125,9 +1310,9 @@ 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,
|
||||
.preserve_mount_links = true,
|
||||
};
|
||||
for_each_title_app_dir(" during USB suspend cleanup", false,
|
||||
cleanup_mount_links_entry, &ctx);
|
||||
@@ -1136,7 +1321,7 @@ void cleanup_usb_mount_links_for_suspend(void) {
|
||||
static bool shutdown_title_mounts_entry(const char *title_id,
|
||||
const title_link_paths_t *paths,
|
||||
void *ctx) {
|
||||
(void)ctx;
|
||||
bool *all_released = (bool *)ctx;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link_file(paths->mount_link, source_path, sizeof(source_path))) {
|
||||
@@ -1144,20 +1329,32 @@ 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 ||
|
||||
!title_stack_top_is_managed(&state)) {
|
||||
if (!inspect_title_stack(title_id, source_path, NULL, &state)) {
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
if (!state.has_our_nullfs)
|
||||
return true;
|
||||
if (!title_stack_top_is_managed(&state)) {
|
||||
log_debug(" [LINK] shutdown stack blocked by unmanaged top for %s",
|
||||
state.system_ex_path);
|
||||
*all_released = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!unmount_controlled_mount_stack(state.system_ex_path)) {
|
||||
log_debug(" [LINK] failed to unmount shutdown stack for %s",
|
||||
state.system_ex_path);
|
||||
*all_released = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown_title_mounts(void) {
|
||||
for_each_title_app_dir(" during shutdown", false,
|
||||
shutdown_title_mounts_entry, NULL);
|
||||
bool shutdown_title_mounts(void) {
|
||||
bool all_released = true;
|
||||
if (!for_each_title_app_dir(" during shutdown", false,
|
||||
shutdown_title_mounts_entry, &all_released)) {
|
||||
all_released = false;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
+317
-23
@@ -1,11 +1,16 @@
|
||||
#include "sm_platform.h"
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
struct GameCache {
|
||||
@@ -13,10 +18,34 @@ struct GameCache {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char owning_scan_root[MAX_PATH];
|
||||
uint64_t missing_since_us;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct GameCache g_game_cache[MAX_PENDING];
|
||||
static pthread_mutex_t g_game_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static bool game_cache_source_exists(const struct GameCache *entry) {
|
||||
return entry->path[0] != '\0' && path_exists(entry->path);
|
||||
}
|
||||
|
||||
static bool title_is_protected_by_dlc(const char *title_id,
|
||||
bool remove_games_with_dlc) {
|
||||
if (remove_games_with_dlc)
|
||||
return false;
|
||||
|
||||
char addcont_path[MAX_PATH];
|
||||
int written = snprintf(addcont_path, sizeof(addcont_path),
|
||||
"/user/addcont/%s", title_id);
|
||||
if (written <= 0 || (size_t)written >= sizeof(addcont_path))
|
||||
return true;
|
||||
|
||||
struct stat st;
|
||||
if (stat(addcont_path, &st) == 0)
|
||||
return S_ISDIR(st.st_mode);
|
||||
|
||||
return errno != ENOENT && errno != ENOTDIR;
|
||||
}
|
||||
|
||||
static bool resolve_game_cache_owning_scan_root(const char *path,
|
||||
char owning_scan_root[MAX_PATH]) {
|
||||
@@ -56,6 +85,7 @@ static void write_game_cache_slot(struct GameCache *entry, const char *path,
|
||||
(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->missing_since_us = 0;
|
||||
entry->valid = true;
|
||||
}
|
||||
|
||||
@@ -66,6 +96,27 @@ static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
|
||||
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
|
||||
}
|
||||
|
||||
static void start_missing_timer(struct GameCache *entry, uint64_t now_us) {
|
||||
if (entry->missing_since_us != 0 || now_us == 0)
|
||||
return;
|
||||
|
||||
entry->missing_since_us = now_us;
|
||||
if (entry->path[0] != '\0') {
|
||||
log_debug(" [CACHE] source missing, auto-remove timer started: %s (%s)",
|
||||
entry->title_id, entry->path);
|
||||
} else {
|
||||
log_debug(" [CACHE] source missing, auto-remove timer started: %s",
|
||||
entry->title_id);
|
||||
}
|
||||
}
|
||||
|
||||
static bool missing_timer_expired(const struct GameCache *entry,
|
||||
uint64_t now_us, uint64_t delay_us) {
|
||||
return entry->valid && entry->missing_since_us != 0 &&
|
||||
now_us >= entry->missing_since_us &&
|
||||
now_us - entry->missing_since_us >= delay_us;
|
||||
}
|
||||
|
||||
static void clear_game_cache_slot(int index, const char *reason) {
|
||||
if (index < 0 || index >= MAX_PENDING || !g_game_cache[index].valid)
|
||||
return;
|
||||
@@ -89,6 +140,7 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
@@ -98,6 +150,7 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
}
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,40 +158,218 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
if (!g_game_cache[k].valid) {
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id, title_name,
|
||||
owning_scan_root);
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void prune_game_cache(void) {
|
||||
static void prune_game_cache_entries(const char *root) {
|
||||
const bool auto_remove = runtime_config()->auto_remove_missing_games;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path_exists(g_game_cache[k].path))
|
||||
if (root) {
|
||||
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;
|
||||
}
|
||||
|
||||
struct GameCache *entry = &g_game_cache[k];
|
||||
if (game_cache_source_exists(entry)) {
|
||||
if (entry->missing_since_us != 0) {
|
||||
entry->missing_since_us = 0;
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)",
|
||||
entry->title_id, entry->path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (auto_remove)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void prune_game_cache(void) { prune_game_cache_entries(NULL); }
|
||||
|
||||
void prune_game_cache_for_root(const char *root) {
|
||||
if (!root || root[0] == '\0') {
|
||||
prune_game_cache();
|
||||
return;
|
||||
}
|
||||
|
||||
prune_game_cache_entries(root);
|
||||
}
|
||||
|
||||
void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
const char *title_name) {
|
||||
if (!path || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us == 0)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].title_id, title_id) != 0)
|
||||
continue;
|
||||
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
write_game_cache_slot(&g_game_cache[k], path, title_id,
|
||||
title_name ? title_name : "", owning_scan_root);
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s (%s)",
|
||||
title_id, path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
static void reconcile_missing_game_cache(
|
||||
const struct AppDbTitleList *auto_remove_titles, uint64_t now_us,
|
||||
bool remove_games_with_dlc) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
struct GameCache *entry = &g_game_cache[k];
|
||||
if (!entry->valid || game_cache_source_exists(entry) ||
|
||||
app_db_title_list_contains(auto_remove_titles, entry->title_id)) {
|
||||
continue;
|
||||
}
|
||||
clear_game_cache_slot(k, "source removed");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
|
||||
bool cache_full_logged = false;
|
||||
for (int i = 0; i < auto_remove_titles->count; ++i) {
|
||||
const char *title_id = auto_remove_titles->ids[i];
|
||||
bool protected_by_dlc =
|
||||
title_is_protected_by_dlc(title_id, remove_games_with_dlc);
|
||||
bool image_source_exists =
|
||||
!protected_by_dlc && sm_image_index_has_source_for_title(title_id);
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
int entry_index = -1;
|
||||
int free_index = -1;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (!g_game_cache[k].valid) {
|
||||
if (free_index < 0)
|
||||
free_index = k;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
entry_index = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (protected_by_dlc) {
|
||||
if (entry_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[entry_index];
|
||||
if (game_cache_source_exists(entry))
|
||||
entry->missing_since_us = 0;
|
||||
else
|
||||
clear_game_cache_slot(entry_index, NULL);
|
||||
}
|
||||
} else if (entry_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[entry_index];
|
||||
if (game_cache_source_exists(entry) || image_source_exists) {
|
||||
if (entry->missing_since_us != 0) {
|
||||
log_debug(" [CACHE] source restored, auto-remove cancelled: %s",
|
||||
title_id);
|
||||
}
|
||||
if (entry->path[0] == '\0')
|
||||
clear_game_cache_slot(entry_index, NULL);
|
||||
else
|
||||
entry->missing_since_us = 0;
|
||||
} else {
|
||||
start_missing_timer(entry, now_us);
|
||||
}
|
||||
} else if (!image_source_exists && free_index >= 0) {
|
||||
struct GameCache *entry = &g_game_cache[free_index];
|
||||
write_game_cache_slot(entry, "", title_id, "", "");
|
||||
start_missing_timer(entry, now_us);
|
||||
} else if (!image_source_exists && !cache_full_logged) {
|
||||
log_debug(" [CACHE] auto-remove tracking full; remaining titles deferred");
|
||||
cache_full_logged = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void reconcile_missing_app_db_games(void) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->auto_remove_missing_games)
|
||||
return;
|
||||
|
||||
struct AppDbTitleList auto_remove_titles = {0};
|
||||
if (!get_app_db_auto_remove_title_list(&auto_remove_titles)) {
|
||||
log_debug(" [REG] app.db auto-remove title list unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t now_us = monotonic_time_us();
|
||||
const uint64_t delay_us =
|
||||
(uint64_t)cfg->auto_remove_missing_delay_seconds * 1000000ull;
|
||||
reconcile_missing_game_cache(&auto_remove_titles, now_us,
|
||||
cfg->auto_remove_games_with_dlc);
|
||||
|
||||
for (;;) {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
title_id[0] = '\0';
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
const struct GameCache *entry = &g_game_cache[k];
|
||||
if (!missing_timer_expired(entry, now_us, delay_us))
|
||||
continue;
|
||||
|
||||
(void)strlcpy(title_id, entry->title_id, sizeof(title_id));
|
||||
clear_game_cache_slot(k, NULL);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
|
||||
if (title_id[0] == '\0')
|
||||
break;
|
||||
|
||||
if (!app_db_title_list_contains(&auto_remove_titles, title_id))
|
||||
continue;
|
||||
if (title_is_protected_by_dlc(title_id,
|
||||
cfg->auto_remove_games_with_dlc))
|
||||
continue;
|
||||
|
||||
int res = sceAppInstUtilAppUnInstall(title_id);
|
||||
if (res == 0) {
|
||||
sm_install_forget_pending_title(title_id);
|
||||
invalidate_app_db_title_cache();
|
||||
log_debug(" [REG] auto-remove requested: %s", title_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [REG] auto-remove failed (not retried): %s code=0x%08X",
|
||||
title_id, (uint32_t)res);
|
||||
}
|
||||
|
||||
free_app_db_title_list(&auto_remove_titles);
|
||||
}
|
||||
|
||||
void reset_missing_game_cache_timers(void) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
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");
|
||||
if (g_game_cache[k].path[0] == '\0')
|
||||
clear_game_cache_slot(k, NULL);
|
||||
else
|
||||
g_game_cache[k].missing_since_us = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
|
||||
void for_each_cached_game_entry(const char *root, game_cache_iter_fn fn,
|
||||
@@ -147,52 +378,115 @@ void for_each_cached_game_entry(const char *root, game_cache_iter_fn 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;
|
||||
struct GameCache entry;
|
||||
bool valid = false;
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0) {
|
||||
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) {
|
||||
(void)strlcpy(entry.path, g_game_cache[k].path, sizeof(entry.path));
|
||||
(void)strlcpy(entry.title_id, g_game_cache[k].title_id,
|
||||
sizeof(entry.title_id));
|
||||
(void)strlcpy(entry.title_name, g_game_cache[k].title_name,
|
||||
sizeof(entry.title_name));
|
||||
(void)strlcpy(entry.owning_scan_root,
|
||||
has_owning_scan_root ? g_game_cache[k].owning_scan_root
|
||||
: "",
|
||||
sizeof(entry.owning_scan_root));
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
if (!valid)
|
||||
continue;
|
||||
if (!fn(entry.path, entry.title_id, entry.title_name,
|
||||
entry.owning_scan_root[0] != '\0' ? entry.owning_scan_root : NULL,
|
||||
ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_game_cache_snapshot(sm_game_cache_snapshot_entry_t **entries_out,
|
||||
size_t *count_out) {
|
||||
if (!entries_out || !count_out)
|
||||
return false;
|
||||
*entries_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
size_t count = 0;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (g_game_cache[k].valid && g_game_cache[k].missing_since_us == 0)
|
||||
count++;
|
||||
}
|
||||
|
||||
sm_game_cache_snapshot_entry_t *entries = NULL;
|
||||
if (count > 0) {
|
||||
entries = calloc(count, sizeof(*entries));
|
||||
if (!entries) {
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t copied = 0;
|
||||
for (int k = 0; k < MAX_PENDING; ++k) {
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0)
|
||||
continue;
|
||||
sm_game_cache_snapshot_entry_t *entry = &entries[copied++];
|
||||
(void)strlcpy(entry->path, g_game_cache[k].path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->title_id, g_game_cache[k].title_id,
|
||||
sizeof(entry->title_id));
|
||||
(void)strlcpy(entry->title_name, g_game_cache[k].title_name,
|
||||
sizeof(entry->title_name));
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
*entries_out = entries;
|
||||
*count_out = copied;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_cached_game(const char *path, const char *title_id,
|
||||
const char **existing_path_out) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = NULL;
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
continue;
|
||||
if (path && strcmp(g_game_cache[k].path, path) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
if (title_id && title_id[0] != '\0' &&
|
||||
strcmp(g_game_cache[k].title_id, title_id) == 0) {
|
||||
if (existing_path_out)
|
||||
*existing_path_out = g_game_cache[k].path;
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear_cached_game(const char *path) {
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
if (!g_game_cache[k].valid)
|
||||
if (!g_game_cache[k].valid || g_game_cache[k].missing_since_us != 0)
|
||||
continue;
|
||||
if (strcmp(g_game_cache[k].path, path) != 0)
|
||||
continue;
|
||||
clear_game_cache_slot(k, "removed from duplicate tracking");
|
||||
}
|
||||
pthread_mutex_unlock(&g_game_cache_mutex);
|
||||
}
|
||||
+192
-56
@@ -1,6 +1,5 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/event.h>
|
||||
@@ -8,13 +7,19 @@
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_kstuff.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_mdbg.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
|
||||
#define MAX_PENDING_GAME_EXEC_CANDIDATES 32
|
||||
@@ -32,17 +37,19 @@ static pending_game_launch_t
|
||||
g_pending_game_launches[MAX_PENDING_GAME_EXEC_CANDIDATES];
|
||||
static pthread_t g_game_lifecycle_thread;
|
||||
static bool g_game_lifecycle_thread_started = false;
|
||||
static volatile sig_atomic_t g_game_lifecycle_stop_requested = 0;
|
||||
static atomic_bool g_game_lifecycle_stop_requested = false;
|
||||
static int g_game_lifecycle_wake_pipe[2] = {-1, -1};
|
||||
static pthread_mutex_t g_game_lifecycle_start_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_game_lifecycle_start_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_game_lifecycle_start_ready = false;
|
||||
static bool g_game_lifecycle_start_success = false;
|
||||
static _Atomic pid_t g_active_game_pid = 0;
|
||||
static pthread_mutex_t g_active_game_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
// Title storage is owned exclusively by the lifecycle thread. Other threads
|
||||
// consume only the atomically published pid through the public state helper.
|
||||
static char g_active_game_title_id[MAX_TITLE_ID];
|
||||
|
||||
static bool register_game_exit_watch(int kq, pid_t pid);
|
||||
static void handle_game_exit(pid_t pid);
|
||||
|
||||
static void publish_active_game_pid(pid_t pid) {
|
||||
pid_t previous_pid = atomic_exchange(&g_active_game_pid, pid);
|
||||
@@ -52,33 +59,40 @@ static void publish_active_game_pid(pid_t pid) {
|
||||
}
|
||||
|
||||
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;
|
||||
if (pid <= 0 || atomic_load(&g_active_game_pid) != pid ||
|
||||
g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id_out, g_active_game_title_id, MAX_TITLE_ID);
|
||||
g_active_game_title_id[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_supported_title_id(const char *title_id) {
|
||||
return strncmp(title_id, "PPSA", 4) == 0 || strncmp(title_id, "CUSA", 4) == 0;
|
||||
static bool snapshot_active_game(pid_t pid, char title_id[MAX_TITLE_ID]) {
|
||||
title_id[0] = '\0';
|
||||
if (pid <= 0 || g_active_game_title_id[0] == '\0')
|
||||
return false;
|
||||
(void)strlcpy(title_id, g_active_game_title_id, MAX_TITLE_ID);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool title_is_usb_backed(const char *title_id, const char *source_path,
|
||||
bool has_mount_link) {
|
||||
char image_path[MAX_PATH];
|
||||
if (read_mount_image_link(title_id, image_path, sizeof(image_path)) &&
|
||||
is_usb_storage_path(image_path)) {
|
||||
return true;
|
||||
}
|
||||
return has_mount_link && is_usb_storage_path(source_path);
|
||||
}
|
||||
|
||||
static uint64_t min_nonzero_u64(uint64_t a, uint64_t b) {
|
||||
@@ -166,9 +180,19 @@ static bool is_process_alive(pid_t pid) {
|
||||
|
||||
static bool dispatch_game_launch(int kq, pid_t pid, uint64_t exec_time_us,
|
||||
const char *title_id, uint32_t app_id) {
|
||||
if (!sm_shellcore_ensure_title_runtime(title_id)) {
|
||||
return false;
|
||||
}
|
||||
sm_shellcore_service_bind_prepared_app(title_id, app_id);
|
||||
|
||||
if (!register_game_exit_watch(kq, pid)) {
|
||||
log_debug(" [GAME] skipping launch tracking for %s pid=%ld without exit watch",
|
||||
title_id, (long)pid);
|
||||
if (sm_shellcore_service_title_is_prepared(title_id) &&
|
||||
!sm_shellcore_release_title_runtime(title_id)) {
|
||||
log_debug(" [SHELLCORE] failed to roll back untracked runtime: %s",
|
||||
title_id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -205,7 +229,7 @@ static void drain_game_lifecycle_wake_pipe(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
static bool drain_game_lifecycle_events_nowait(int kq) {
|
||||
struct kevent events[16];
|
||||
struct timespec timeout;
|
||||
memset(&timeout, 0, sizeof(timeout));
|
||||
@@ -221,17 +245,15 @@ static bool discard_game_lifecycle_events_nowait(int kq) {
|
||||
}
|
||||
if (nev == 0)
|
||||
return true;
|
||||
for (int i = 0; i < nev; ++i) {
|
||||
if (events[i].filter == EVFILT_PROC &&
|
||||
(events[i].fflags & NOTE_EXIT) != 0) {
|
||||
handle_game_exit((pid_t)events[i].ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void set_game_lifecycle_start_result(bool success) {
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_success = success;
|
||||
@@ -296,6 +318,12 @@ static pending_game_launch_t *queue_pending_game_launch(pid_t pid,
|
||||
static void defer_confirmed_game_launch_retry(pid_t pid, uint64_t exec_time_us,
|
||||
uint64_t now_us,
|
||||
const char *title_id) {
|
||||
if (now_us == 0) {
|
||||
log_debug(" [GAME] cannot schedule launch retry without monotonic clock: "
|
||||
"%s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
return;
|
||||
}
|
||||
clear_all_pending_game_launches();
|
||||
queue_pending_game_launch(pid, exec_time_us, now_us);
|
||||
|
||||
@@ -358,13 +386,28 @@ static void poll_game_modules(int kq) {
|
||||
if (resolve_game_title_id(entry->pid, title_id, &app_id)) {
|
||||
pid_t pid = entry->pid;
|
||||
uint64_t exec_time_us = entry->exec_time_us;
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (is_supported_game_title_id(title_id)) {
|
||||
if (entry->deadline_us == 0 || now_us == 0 ||
|
||||
now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] runtime preparation timed out for %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
clear_pending_game_launch(entry);
|
||||
break;
|
||||
}
|
||||
if (dispatch_game_launch(kq, pid, exec_time_us, title_id, app_id)) {
|
||||
clear_pending_game_launch(entry);
|
||||
clear_all_pending_game_launches();
|
||||
break;
|
||||
}
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, now_us, title_id);
|
||||
uint64_t retry_now_us = monotonic_time_us();
|
||||
if (retry_now_us == 0 || retry_now_us >= entry->deadline_us) {
|
||||
log_debug(" [GAME] runtime preparation timed out for %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
clear_pending_game_launch(entry);
|
||||
} else {
|
||||
defer_confirmed_game_launch_retry(pid, exec_time_us, retry_now_us,
|
||||
title_id);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
clear_pending_game_launch(entry);
|
||||
@@ -405,7 +448,7 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
uint32_t app_id = 0;
|
||||
if (resolve_game_title_id(pid, title_id, &app_id)) {
|
||||
if (is_supported_title_id(title_id)) {
|
||||
if (is_supported_game_title_id(title_id)) {
|
||||
if (dispatch_game_launch(kq, pid, now_us, title_id, app_id))
|
||||
clear_all_pending_game_launches();
|
||||
else
|
||||
@@ -417,7 +460,8 @@ static void handle_game_exec(int kq, pid_t pid) {
|
||||
if (!is_process_alive(pid))
|
||||
return;
|
||||
|
||||
queue_pending_game_launch(pid, now_us, now_us);
|
||||
if (now_us != 0)
|
||||
queue_pending_game_launch(pid, now_us, now_us);
|
||||
}
|
||||
|
||||
static void handle_game_exit(pid_t pid) {
|
||||
@@ -426,11 +470,20 @@ static void handle_game_exit(pid_t pid) {
|
||||
pending_game_launch_t *entry = find_pending_game_launch(pid);
|
||||
if (entry)
|
||||
clear_pending_game_launch(entry);
|
||||
if (atomic_load(&g_active_game_pid) == pid)
|
||||
if (had_active_title)
|
||||
publish_active_game_pid(0);
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
sm_kstuff_game_on_exit(pid);
|
||||
if (had_active_title) {
|
||||
if (sm_shellcore_service_note_game_exit(title_id)) {
|
||||
if (sm_shellcore_app_exit_hook_active()) {
|
||||
log_debug(" [SHELLCORE] runtime release awaiting onAppExit: %s",
|
||||
title_id);
|
||||
} else if (!sm_shellcore_release_title_runtime(title_id)) {
|
||||
log_debug(" [SHELLCORE] no app-exit hook; runtime release deferred: %s",
|
||||
title_id);
|
||||
}
|
||||
}
|
||||
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",
|
||||
@@ -438,21 +491,69 @@ static void handle_game_exit(pid_t pid) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
static bool terminate_usb_game_for_sleep(int kq, pid_t pid,
|
||||
const char *title_id) {
|
||||
if (kill(pid, SIGKILL) != 0) {
|
||||
if (errno == ESRCH) {
|
||||
handle_game_exit(pid);
|
||||
return true;
|
||||
}
|
||||
log_debug(" [SLEEP] failed to kill USB game: %s pid=%ld: %s", title_id,
|
||||
(long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SLEEP] killing USB game before suspend: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
|
||||
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);
|
||||
uint64_t deadline_us =
|
||||
now_us == 0 ? 0 : now_us + GAME_SLEEP_EXIT_TIMEOUT_US;
|
||||
while (!atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
struct kevent event;
|
||||
struct timespec timeout = {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = (long)GAME_LIFECYCLE_POLL_INTERVAL_US * 1000L,
|
||||
};
|
||||
int nev = kevent(kq, NULL, 0, &event, 1, &timeout);
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SLEEP] NOTE_EXIT wait failed for %s pid=%ld: %s",
|
||||
title_id, (long)pid, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (nev > 0) {
|
||||
if (event.filter == EVFILT_READ &&
|
||||
event.ident == (uintptr_t)g_game_lifecycle_wake_pipe[0]) {
|
||||
drain_game_lifecycle_wake_pipe();
|
||||
} else if (event.filter == EVFILT_PROC &&
|
||||
(event.fflags & NOTE_EXIT) != 0) {
|
||||
pid_t exited_pid = (pid_t)event.ident;
|
||||
handle_game_exit(exited_pid);
|
||||
if (exited_pid == pid) {
|
||||
log_debug(" [SLEEP] USB game stopped: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
if (deadline_us != 0 && now_us != 0 && now_us >= deadline_us) {
|
||||
if (!is_process_alive(pid)) {
|
||||
handle_game_exit(pid);
|
||||
log_debug(" [SLEEP] USB game exited without NOTE_EXIT: %s pid=%ld",
|
||||
title_id, (long)pid);
|
||||
return true;
|
||||
}
|
||||
log_debug(" [SLEEP] timed out waiting NOTE_EXIT: %s pid=%ld", title_id,
|
||||
(long)pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void *game_lifecycle_watcher_main(void *arg) {
|
||||
@@ -497,14 +598,42 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
|
||||
bool sleep_cleanup_done = false;
|
||||
pid_t suspended_game_pid = 0;
|
||||
while (!g_game_lifecycle_stop_requested && !should_stop_requested()) {
|
||||
while (!atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
if (runtime_sleep_mode_active()) {
|
||||
if (!sleep_cleanup_done) {
|
||||
if (!sleep_cleanup_done && sm_scanner_usb_watches_suspended()) {
|
||||
suspended_game_pid = atomic_load(&g_active_game_pid);
|
||||
char usb_game_title_id[MAX_TITLE_ID];
|
||||
bool active_game = snapshot_active_game(suspended_game_pid,
|
||||
usb_game_title_id);
|
||||
char source_path[MAX_PATH];
|
||||
bool has_mount_link =
|
||||
active_game && read_mount_link(usb_game_title_id, source_path,
|
||||
sizeof(source_path));
|
||||
bool usb_game =
|
||||
active_game && title_is_usb_backed(
|
||||
usb_game_title_id, source_path, has_mount_link);
|
||||
clear_all_pending_game_launches();
|
||||
sm_fakelib_game_shutdown();
|
||||
sm_kstuff_sleep_enter();
|
||||
publish_active_game(0, NULL);
|
||||
bool usb_cleanup_allowed = true;
|
||||
if (usb_game) {
|
||||
usb_cleanup_allowed = terminate_usb_game_for_sleep(
|
||||
kq, suspended_game_pid, usb_game_title_id);
|
||||
if (usb_cleanup_allowed) {
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
}
|
||||
if (usb_cleanup_allowed) {
|
||||
runtime_mount_state_lock();
|
||||
unmount_usb_sources_for_suspend();
|
||||
runtime_mount_state_unlock();
|
||||
} else {
|
||||
log_debug("[SLEEP] USB cleanup skipped while game process remains");
|
||||
}
|
||||
// Keep a live non-USB game published while it is suspended so scanner
|
||||
// work cannot race its unchanged runtime mount.
|
||||
sleep_cleanup_done = true;
|
||||
}
|
||||
|
||||
@@ -523,10 +652,13 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
continue;
|
||||
}
|
||||
if (sleep_cleanup_done) {
|
||||
if (!discard_game_lifecycle_events_nowait(kq))
|
||||
if (!drain_game_lifecycle_events_nowait(kq))
|
||||
break;
|
||||
sleep_cleanup_done = false;
|
||||
restore_suspended_game_if_alive(kq, suspended_game_pid);
|
||||
if (runtime_sleep_mode_active())
|
||||
continue;
|
||||
if (suspended_game_pid > 0 && is_process_alive(suspended_game_pid))
|
||||
handle_game_exec(kq, suspended_game_pid);
|
||||
sm_kstuff_sleep_leave();
|
||||
suspended_game_pid = 0;
|
||||
}
|
||||
@@ -538,7 +670,8 @@ static void *game_lifecycle_watcher_main(void *arg) {
|
||||
if (nev < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (g_game_lifecycle_stop_requested)
|
||||
if (atomic_load_explicit(&g_game_lifecycle_stop_requested,
|
||||
memory_order_relaxed))
|
||||
break;
|
||||
log_debug(" [GAME] kevent wait failed: %s", strerror(errno));
|
||||
break;
|
||||
@@ -578,15 +711,16 @@ bool start_game_lifecycle_watcher(void) {
|
||||
log_debug(" [GAME] wake pipe creation failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
if (!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[0]) ||
|
||||
!sm_set_fd_nonblocking(g_game_lifecycle_wake_pipe[1])) {
|
||||
log_debug(" [GAME] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_game_lifecycle_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, false,
|
||||
memory_order_relaxed);
|
||||
pthread_mutex_lock(&g_game_lifecycle_start_mutex);
|
||||
g_game_lifecycle_start_ready = false;
|
||||
g_game_lifecycle_start_success = false;
|
||||
@@ -632,12 +766,14 @@ void stop_game_lifecycle_watcher(void) {
|
||||
if (!g_game_lifecycle_thread_started)
|
||||
return;
|
||||
|
||||
g_game_lifecycle_stop_requested = 1;
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, true,
|
||||
memory_order_relaxed);
|
||||
wake_game_lifecycle_watcher();
|
||||
|
||||
(void)pthread_join(g_game_lifecycle_thread, NULL);
|
||||
g_game_lifecycle_thread_started = false;
|
||||
g_game_lifecycle_stop_requested = 0;
|
||||
atomic_store_explicit(&g_game_lifecycle_stop_requested, false,
|
||||
memory_order_relaxed);
|
||||
publish_active_game(0, NULL);
|
||||
close_game_lifecycle_wake_pipe();
|
||||
}
|
||||
|
||||
+28
-2
@@ -6,8 +6,13 @@
|
||||
// --- Game Metadata Parsing (param.json) ---
|
||||
static int extract_json_string(const char *json, const char *key, char *out,
|
||||
size_t out_size) {
|
||||
if (!json || !key || !out || out_size == 0)
|
||||
return -1;
|
||||
|
||||
char search[64];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
int written = snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
if (written < 0 || (size_t)written >= sizeof(search))
|
||||
return -1;
|
||||
const char *p = strstr(json, search);
|
||||
if (!p)
|
||||
return -1;
|
||||
@@ -27,9 +32,27 @@ static int extract_json_string(const char *json, const char *key, char *out,
|
||||
i++;
|
||||
}
|
||||
out[i] = '\0';
|
||||
if (p[i] != '"') {
|
||||
out[0] = '\0';
|
||||
return -4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool is_supported_game_title_id(const char *title_id) {
|
||||
if (!title_id || strlen(title_id) != 9u)
|
||||
return false;
|
||||
if (strncmp(title_id, "PPSA", 4u) != 0 &&
|
||||
strncmp(title_id, "CUSA", 4u) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 4u; i < 9u; ++i) {
|
||||
if (!isdigit((unsigned char)title_id[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
char *out_id, char *out_name) {
|
||||
out_id[0] = '\0';
|
||||
@@ -77,7 +100,7 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
int res = extract_json_string(buf, "titleId", out_id, MAX_TITLE_ID);
|
||||
if (res != 0)
|
||||
res = extract_json_string(buf, "title_id", out_id, MAX_TITLE_ID);
|
||||
if (res == 0) {
|
||||
if (res == 0 && is_supported_game_title_id(out_id)) {
|
||||
const char *en_ptr = strstr(buf, "\"en-US\"");
|
||||
const char *search_start = en_ptr ? en_ptr : buf;
|
||||
if (extract_json_string(search_start, "titleName", out_name,
|
||||
@@ -86,6 +109,9 @@ bool get_game_info(const char *base_path, const struct stat *param_st,
|
||||
if (out_name[0] == '\0')
|
||||
(void)strlcpy(out_name, out_id, MAX_TITLE_NAME);
|
||||
valid = true;
|
||||
} else {
|
||||
out_id[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
}
|
||||
free(buf);
|
||||
|
||||
|
||||
+147
-31
@@ -515,15 +515,20 @@ static const image_backend_ops_t *get_image_backend_ops(attach_backend_t backend
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool is_cached_image_path(const char *file_path) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (get_image_cache_entry(k, &cached_entry) &&
|
||||
strcmp(cached_entry.path, file_path) == 0) {
|
||||
return true;
|
||||
}
|
||||
static bool get_cached_image_mount(const char *file_path,
|
||||
image_cache_entry_t *entry_out,
|
||||
int *index_out) {
|
||||
image_cache_entry_t entry;
|
||||
int index = -1;
|
||||
if (!find_image_cache_entry(file_path, &entry, &index) ||
|
||||
entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (entry_out)
|
||||
*entry_out = entry;
|
||||
if (index_out)
|
||||
*index_out = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool directory_has_visible_entries(const char *path) {
|
||||
@@ -567,7 +572,7 @@ static bool reject_mounted_image_io(const char *file_path,
|
||||
log_debug(" [IMG][%s] unreadable or damaged mount (%s -> %s, %s): %s",
|
||||
attach_backend_name(attach_backend), devname, mount_point, stage,
|
||||
strerror(io_err));
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
errno = io_err;
|
||||
return false;
|
||||
}
|
||||
@@ -586,7 +591,8 @@ static bool prepare_image_mount_retry(const image_cache_entry_t *cached_entry,
|
||||
log_debug(" [IMG][%s] mount unreadable, retrying: %s -> %s: %s",
|
||||
attach_backend_name(cached_entry->backend), source_path,
|
||||
mount_point, strerror(root_err));
|
||||
if (!unmount_image(source_path, cached_entry->unit_id, cached_entry->backend))
|
||||
if (!unmount_runtime_image(source_path, cached_entry->unit_id,
|
||||
cached_entry->backend))
|
||||
return false;
|
||||
} else {
|
||||
log_debug(" [IMG][%s] mount lost, retrying: %s -> %s",
|
||||
@@ -617,7 +623,8 @@ static bool reuse_existing_image_mount(const char *file_path,
|
||||
log_debug(" [IMG][%s] Existing mount unreadable, reattaching: %s -> %s: %s",
|
||||
attach_backend_name(existing_backend), file_path, mount_point,
|
||||
strerror(root_err));
|
||||
if (!unmount_image(file_path, existing_unit, existing_backend)) {
|
||||
if (!unmount_runtime_image(file_path, existing_unit,
|
||||
existing_backend)) {
|
||||
if (cache_failed_out)
|
||||
*cache_failed_out = true;
|
||||
return false;
|
||||
@@ -657,10 +664,26 @@ static bool handle_cached_or_existing_image_mount_locked(
|
||||
if (runtime_sleep_mode_active())
|
||||
return true;
|
||||
|
||||
if (is_cached_image_path(file_path)) {
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
return true;
|
||||
image_cache_entry_t cached_entry;
|
||||
int cached_index = -1;
|
||||
if (get_cached_image_mount(file_path, &cached_entry, &cached_index)) {
|
||||
int root_err = 0;
|
||||
if (is_active_image_mount_point(mount_point) &&
|
||||
is_image_mount_root_accessible(mount_point, &root_err)) {
|
||||
if (success_out)
|
||||
*success_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] cached mount is not active, reattaching: %s -> "
|
||||
"%s",
|
||||
attach_backend_name(cached_entry.backend), file_path,
|
||||
mount_point);
|
||||
if (!unmount_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
return true;
|
||||
}
|
||||
invalidate_image_cache_entry(cached_index);
|
||||
}
|
||||
|
||||
bool cache_failed = false;
|
||||
@@ -914,7 +937,7 @@ static bool validate_mounted_image(const char *file_path, image_fs_type_t fs_typ
|
||||
min_device_sector);
|
||||
}
|
||||
sm_error_mark_notified();
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
@@ -1008,7 +1031,7 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
return false;
|
||||
}
|
||||
if (runtime_sleep_mode_active()) {
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
@@ -1030,7 +1053,7 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
(unsigned)MAX_IMAGE_MOUNTS);
|
||||
log_debug(" [IMG] image cache full, rolling back mount: %s -> %s",
|
||||
file_path, mount_point);
|
||||
(void)unmount_image(file_path, unit_id, attach_backend);
|
||||
(void)unmount_runtime_image(file_path, unit_id, attach_backend);
|
||||
errno = ENOSPC;
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
@@ -1039,20 +1062,31 @@ bool mount_image(const char *file_path, image_fs_type_t fs_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool unmount_child_image_mounts_for_container(const char *mount_point) {
|
||||
static bool unmount_child_image_mounts_for_container(
|
||||
const char *mount_point, bool cleanup_source_state) {
|
||||
bool all_unmounted = true;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
if (!path_matches_root_or_child(cached_entry.path, mount_point))
|
||||
continue;
|
||||
|
||||
log_debug(" [IMG] container child unmount: %s source=%s",
|
||||
mount_point, cached_entry.path);
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
bool unmounted = cleanup_source_state
|
||||
? unmount_image(cached_entry.path,
|
||||
cached_entry.unit_id,
|
||||
cached_entry.backend)
|
||||
: unmount_runtime_image(cached_entry.path,
|
||||
cached_entry.unit_id,
|
||||
cached_entry.backend);
|
||||
if (unmounted) {
|
||||
invalidate_image_cache_entry(k);
|
||||
continue;
|
||||
}
|
||||
@@ -1065,7 +1099,9 @@ static bool unmount_child_image_mounts_for_container(const char *mount_point) {
|
||||
return all_unmounted;
|
||||
}
|
||||
|
||||
bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend) {
|
||||
static bool unmount_image_impl(const char *file_path, int unit_id,
|
||||
attach_backend_t backend,
|
||||
bool cleanup_source_state) {
|
||||
char mount_point[MAX_PATH];
|
||||
image_fs_type_t fs_type = detect_image_fs_type_for_path(file_path, NULL);
|
||||
build_image_mount_point_for_fs(file_path, fs_type, mount_point);
|
||||
@@ -1085,14 +1121,16 @@ bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend)
|
||||
resolved_unit);
|
||||
|
||||
if (fs_type == IMAGE_FS_PFSC_CONTAINER &&
|
||||
!unmount_child_image_mounts_for_container(mount_point)) {
|
||||
!unmount_child_image_mounts_for_container(mount_point,
|
||||
cleanup_source_state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove mount.lnk and unmount /system_ex/app/<titleid> that point to this
|
||||
// source before unmounting the virtual disk itself.
|
||||
cleanup_mount_links_for_source_unmount(mount_point);
|
||||
clear_cached_game(mount_point);
|
||||
if (cleanup_source_state) {
|
||||
// Permanent source cleanup removes title links before releasing the image.
|
||||
cleanup_mount_links_for_source_unmount(mount_point);
|
||||
clear_cached_game(mount_point);
|
||||
}
|
||||
|
||||
// Unmount stacked layers (unionfs over image fs).
|
||||
for (int i = 0; i < MAX_LAYERED_UNMOUNT_ATTEMPTS; i++) {
|
||||
@@ -1152,10 +1190,72 @@ bool unmount_image(const char *file_path, int unit_id, attach_backend_t backend)
|
||||
return detach_ok;
|
||||
}
|
||||
|
||||
bool unmount_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
return unmount_image_impl(file_path, unit_id, backend, true);
|
||||
}
|
||||
|
||||
bool unmount_runtime_image(const char *file_path, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
return unmount_image_impl(file_path, unit_id, backend, false);
|
||||
}
|
||||
|
||||
bool release_runtime_image_mount(const char *file_path) {
|
||||
image_cache_entry_t entry;
|
||||
int index = -1;
|
||||
if (!find_image_cache_entry(file_path, &entry, &index) ||
|
||||
entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0) {
|
||||
return true;
|
||||
}
|
||||
if (!unmount_runtime_image(entry.path, entry.unit_id, entry.backend))
|
||||
return false;
|
||||
invalidate_image_cache_entry(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool release_runtime_image_mounts(void) {
|
||||
bool all_released = true;
|
||||
for (int pass = 0; pass < MAX_LAYERED_UNMOUNT_ATTEMPTS; ++pass) {
|
||||
bool any_remaining = false;
|
||||
bool progress = false;
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; ++k) {
|
||||
image_cache_entry_t entry;
|
||||
if (!get_image_cache_entry(k, &entry))
|
||||
continue;
|
||||
if (entry.backend == ATTACH_BACKEND_NONE || entry.unit_id < 0)
|
||||
continue;
|
||||
if (unmount_runtime_image(entry.path, entry.unit_id, entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
progress = true;
|
||||
} else {
|
||||
any_remaining = true;
|
||||
all_released = false;
|
||||
}
|
||||
}
|
||||
if (!any_remaining)
|
||||
return true;
|
||||
if (!progress)
|
||||
break;
|
||||
}
|
||||
return all_released;
|
||||
}
|
||||
|
||||
void cleanup_stale_image_mounts(void) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
|
||||
bool has_cached_mount = false;
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; ++k) {
|
||||
image_cache_entry_t entry;
|
||||
if (get_image_cache_entry(k, &entry) &&
|
||||
entry.backend != ATTACH_BACKEND_NONE && entry.unit_id >= 0) {
|
||||
has_cached_mount = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_cached_mount)
|
||||
return;
|
||||
|
||||
log_debug(" [IMG] stale image cleanup begin");
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
image_cache_entry_t cached_entry;
|
||||
@@ -1163,6 +1263,10 @@ void cleanup_stale_image_mounts(void) {
|
||||
return;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [IMG][%s] stale cleanup check: slot=%d source=%s mount=%s",
|
||||
attach_backend_name(cached_entry.backend), k, cached_entry.path,
|
||||
@@ -1213,6 +1317,10 @@ void cleanup_stale_image_mounts_for_root(const char *root) {
|
||||
return;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
if (!path_matches_root_or_child(cached_entry.path, root) &&
|
||||
!path_matches_root_or_child(cached_entry.mount_point, root)) {
|
||||
continue;
|
||||
@@ -1254,13 +1362,17 @@ bool unmount_usb_image_mounts_for_suspend(void) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
if (!is_usb_storage_path(cached_entry.path))
|
||||
continue;
|
||||
|
||||
log_debug(" [IMG][%s] USB suspend unmount: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
if (unmount_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
log_debug(" [IMG][%s] USB suspend unmounted: %s",
|
||||
attach_backend_name(cached_entry.backend), cached_entry.path);
|
||||
invalidate_image_cache_entry(k);
|
||||
@@ -1359,9 +1471,13 @@ bool shutdown_image_mounts(void) {
|
||||
image_cache_entry_t cached_entry;
|
||||
if (!get_image_cache_entry(k, &cached_entry))
|
||||
continue;
|
||||
if (cached_entry.backend == ATTACH_BACKEND_NONE ||
|
||||
cached_entry.unit_id < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unmount_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
if (unmount_runtime_image(cached_entry.path, cached_entry.unit_id,
|
||||
cached_entry.backend)) {
|
||||
invalidate_image_cache_entry(k);
|
||||
progress = true;
|
||||
continue;
|
||||
|
||||
+121
-7
@@ -15,6 +15,16 @@ struct ImageCache {
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void copy_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
}
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
@@ -55,6 +65,18 @@ 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) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
if (entry_index >= 0) {
|
||||
const struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
// A metadata-only path mapping must never retarget a live mount entry.
|
||||
// The live entry owns its actual filesystem mount point until release.
|
||||
if (entry->backend != ATTACH_BACKEND_NONE && entry->unit_id >= 0 &&
|
||||
(strcmp(entry->path, path) != 0 ||
|
||||
strcmp(entry->mount_point, mount_point) != 0)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
bool ok = upsert_image_source_mapping(path, mount_point) >= 0;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return ok;
|
||||
@@ -83,13 +105,25 @@ bool get_image_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
(void)strlcpy(entry_out->path, g_image_cache[index].path,
|
||||
sizeof(entry_out->path));
|
||||
(void)strlcpy(entry_out->mount_point, g_image_cache[index].mount_point,
|
||||
sizeof(entry_out->mount_point));
|
||||
entry_out->unit_id = g_image_cache[index].unit_id;
|
||||
entry_out->backend = g_image_cache[index].backend;
|
||||
copy_cache_entry(index, entry_out);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
int *index_out) {
|
||||
if (!path || !entry_out)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_cache_index(path, NULL);
|
||||
if (index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
copy_cache_entry(index, entry_out);
|
||||
if (index_out)
|
||||
*index_out = index;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -140,3 +174,83 @@ bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int find_owning_cache_index(const char *path) {
|
||||
int best_index = -1;
|
||||
size_t best_length = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_cache[i].valid)
|
||||
continue;
|
||||
size_t length = strlen(g_image_cache[i].mount_point);
|
||||
if (length <= best_length || strncmp(path, g_image_cache[i].mount_point,
|
||||
length) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (path[length] != '\0' && path[length] != '/')
|
||||
continue;
|
||||
best_index = i;
|
||||
best_length = length;
|
||||
}
|
||||
return best_index;
|
||||
}
|
||||
|
||||
static size_t find_owning_chain_indices(const char *path,
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH]) {
|
||||
char current[MAX_PATH];
|
||||
(void)strlcpy(current, path, sizeof(current));
|
||||
|
||||
size_t count = 0;
|
||||
while (count < MAX_IMAGE_CHAIN_DEPTH) {
|
||||
int index = find_owning_cache_index(current);
|
||||
if (index < 0)
|
||||
break;
|
||||
indices[count++] = index;
|
||||
(void)strlcpy(current, g_image_cache[index].path, sizeof(current));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_owning_image_source_from_mount_cache(const char *path,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_owning_cache_index(path);
|
||||
if (index >= 0)
|
||||
(void)strlcpy(path_out, g_image_cache[index].path, path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return index >= 0;
|
||||
}
|
||||
|
||||
size_t resolve_image_source_chain_from_mount_cache(
|
||||
const char *path,
|
||||
char chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH]) {
|
||||
if (!path || !chain)
|
||||
return 0;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
(void)strlcpy(chain[i], g_image_cache[indices[count - i - 1u]].path,
|
||||
MAX_PATH);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool resolve_outermost_image_source_from_mount_cache(
|
||||
const char *path, char *path_out, size_t path_out_size) {
|
||||
if (!path || !path_out || path_out_size == 0)
|
||||
return false;
|
||||
|
||||
int indices[MAX_IMAGE_CHAIN_DEPTH];
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
size_t count = find_owning_chain_indices(path, indices);
|
||||
if (count > 0)
|
||||
(void)strlcpy(path_out, g_image_cache[indices[count - 1u]].path,
|
||||
path_out_size);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return count > 0;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#define IMAGE_INDEX_FILE IMAGE_INDEX_FILE_PATH
|
||||
#define IMAGE_INDEX_TEMP_FILE LOG_DIR "/image_index.bin.tmp"
|
||||
#define IMAGE_INDEX_MAGIC 0x58444953u
|
||||
#define IMAGE_INDEX_VERSION 3u
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int64_t size;
|
||||
int64_t mtime_sec;
|
||||
int32_t mtime_nsec;
|
||||
uint8_t complete;
|
||||
uint8_t valid;
|
||||
uint8_t reserved[2];
|
||||
} image_index_entry_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t image_index;
|
||||
uint8_t valid;
|
||||
uint8_t reserved;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
} image_index_title_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t entry_capacity;
|
||||
uint32_t title_capacity;
|
||||
} image_index_header_t;
|
||||
|
||||
static image_index_entry_t g_image_index[MAX_IMAGE_MOUNTS];
|
||||
static image_index_title_t g_image_titles[MAX_IMAGE_TITLES];
|
||||
static bool g_image_index_loaded;
|
||||
static bool g_image_index_dirty;
|
||||
static pthread_mutex_t g_image_index_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static bool stamp_matches(const image_index_entry_t *entry,
|
||||
const struct stat *st) {
|
||||
return entry->size == (int64_t)st->st_size &&
|
||||
entry->mtime_sec == (int64_t)st->st_mtim.tv_sec &&
|
||||
entry->mtime_nsec == (int32_t)st->st_mtim.tv_nsec;
|
||||
}
|
||||
|
||||
static int find_entry(const char *path) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && strcmp(g_image_index[i].path, path) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void clear_entry_titles(int entry_index) {
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index) {
|
||||
memset(&g_image_titles[i], 0, sizeof(g_image_titles[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_entry(int entry_index) {
|
||||
clear_entry_titles(entry_index);
|
||||
memset(&g_image_index[entry_index], 0, sizeof(g_image_index[entry_index]));
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
static bool save_index(void) {
|
||||
if (!g_image_index_dirty)
|
||||
return true;
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_TEMP_FILE, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
image_index_header_t header = {
|
||||
.magic = IMAGE_INDEX_MAGIC,
|
||||
.version = IMAGE_INDEX_VERSION,
|
||||
.entry_capacity = MAX_IMAGE_MOUNTS,
|
||||
.title_capacity = MAX_IMAGE_TITLES,
|
||||
};
|
||||
bool ok = fwrite(&header, sizeof(header), 1, file) == 1 &&
|
||||
fwrite(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fwrite(g_image_titles, sizeof(g_image_titles), 1, file) == 1 &&
|
||||
fflush(file) == 0;
|
||||
if (ok && fsync(fileno(file)) != 0)
|
||||
ok = false;
|
||||
if (fclose(file) != 0)
|
||||
ok = false;
|
||||
if (!ok || rename(IMAGE_INDEX_TEMP_FILE, IMAGE_INDEX_FILE) != 0) {
|
||||
int saved_errno = errno;
|
||||
if (saved_errno == 0)
|
||||
saved_errno = EIO;
|
||||
(void)unlink(IMAGE_INDEX_TEMP_FILE);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
g_image_index_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool loaded_index_is_valid(void) {
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
const image_index_entry_t *entry = &g_image_index[i];
|
||||
if (!entry->valid)
|
||||
continue;
|
||||
if (entry->complete > 1 || entry->path[0] == '\0' ||
|
||||
memchr(entry->path, '\0', sizeof(entry->path)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (!title->valid)
|
||||
continue;
|
||||
if (title->image_index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_index[title->image_index].valid ||
|
||||
title->title_id[0] == '\0' ||
|
||||
memchr(title->title_id, '\0', sizeof(title->title_id)) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void load_index(void) {
|
||||
if (g_image_index_loaded)
|
||||
return;
|
||||
g_image_index_loaded = true;
|
||||
|
||||
FILE *file = fopen(IMAGE_INDEX_FILE, "rb");
|
||||
if (!file)
|
||||
return;
|
||||
image_index_header_t header;
|
||||
bool ok = fread(&header, sizeof(header), 1, file) == 1 &&
|
||||
header.magic == IMAGE_INDEX_MAGIC &&
|
||||
header.version == IMAGE_INDEX_VERSION &&
|
||||
header.entry_capacity == MAX_IMAGE_MOUNTS &&
|
||||
header.title_capacity == MAX_IMAGE_TITLES &&
|
||||
fread(g_image_index, sizeof(g_image_index), 1, file) == 1 &&
|
||||
fread(g_image_titles, sizeof(g_image_titles), 1, file) == 1;
|
||||
(void)fclose(file);
|
||||
if (ok)
|
||||
ok = loaded_index_is_valid();
|
||||
if (!ok) {
|
||||
memset(g_image_index, 0, sizeof(g_image_index));
|
||||
memset(g_image_titles, 0, sizeof(g_image_titles));
|
||||
(void)unlink(IMAGE_INDEX_FILE);
|
||||
log_debug(" [IMGIDX] invalid cache discarded: %s", IMAGE_INDEX_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
static int reserve_entry(const char *path) {
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index >= 0)
|
||||
return entry_index;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (!g_image_index[i].valid) {
|
||||
g_image_index[i].valid = 1;
|
||||
(void)strlcpy(g_image_index[i].path, path, sizeof(g_image_index[i].path));
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void begin_scan_locked(const char *path, const struct stat *st) {
|
||||
int entry_index = reserve_entry(path);
|
||||
if (entry_index < 0) {
|
||||
log_debug(" [IMGIDX] cache full, scanning without fingerprint: %s", path);
|
||||
return;
|
||||
}
|
||||
clear_entry_titles(entry_index);
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
entry->size = (int64_t)st->st_size;
|
||||
entry->mtime_sec = (int64_t)st->st_mtim.tv_sec;
|
||||
entry->mtime_nsec = (int32_t)st->st_mtim.tv_nsec;
|
||||
entry->complete = 0;
|
||||
entry->valid = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
|
||||
static bool cached_titles_ready(int entry_index,
|
||||
const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready) {
|
||||
const char *image_path = g_image_index[entry_index].path;
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (!g_image_titles[i].valid ||
|
||||
g_image_titles[i].image_index != (uint16_t)entry_index) {
|
||||
continue;
|
||||
}
|
||||
const char *title_id = g_image_titles[i].title_id;
|
||||
if (!is_installed(title_id) || !has_appmeta_data(title_id))
|
||||
return false;
|
||||
char runtime_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (!read_mount_link(title_id, runtime_path, sizeof(runtime_path)) ||
|
||||
!is_under_image_mount_base(runtime_path) ||
|
||||
!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (app_db_titles_ready &&
|
||||
!app_db_title_list_contains(app_db_titles, title_id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_image_index_visit_ready_titles(
|
||||
const char *path, const struct stat *st,
|
||||
const struct AppDbTitleList *app_db_titles, bool app_db_titles_ready,
|
||||
sm_image_index_title_visitor_t visitor) {
|
||||
if (!path || !st || !visitor)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
bool ready = entry_index >= 0 && g_image_index[entry_index].complete &&
|
||||
stamp_matches(&g_image_index[entry_index], st) &&
|
||||
cached_titles_ready(entry_index, app_db_titles,
|
||||
app_db_titles_ready);
|
||||
if (ready) {
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (title->valid && title->image_index == (uint16_t)entry_index)
|
||||
visitor(title->title_id);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
bool sm_image_index_has_source_for_title(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
const image_index_title_t *title = &g_image_titles[i];
|
||||
if (!title->valid || strcmp(title->title_id, title_id) != 0)
|
||||
continue;
|
||||
|
||||
const image_index_entry_t *entry = &g_image_index[title->image_index];
|
||||
found = entry->valid && path_exists(entry->path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return found;
|
||||
}
|
||||
|
||||
void sm_image_index_begin_scan(const char *path, const struct stat *st) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
begin_scan_locked(path, st);
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
bool sm_image_index_record_game(const char *game_path, const char *title_id) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
char image_path[MAX_PATH];
|
||||
if (!resolve_outermost_image_source_from_mount_cache(
|
||||
game_path, image_path, sizeof(image_path))) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
int entry_index = find_entry(image_path);
|
||||
if (entry_index < 0) {
|
||||
struct stat st;
|
||||
if (stat(image_path, &st) != 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
begin_scan_locked(image_path, &st);
|
||||
entry_index = find_entry(image_path);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
char tracked_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (is_installed(title_id) &&
|
||||
read_mount_link(title_id, tracked_path, sizeof(tracked_path)) &&
|
||||
strcmp(tracked_path, game_path) == 0 &&
|
||||
(!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) &&
|
||||
!write_mount_image_link(title_id, image_path)) {
|
||||
log_debug(" [IMGIDX] failed to repair image link: %s -> %s", title_id,
|
||||
image_path);
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid &&
|
||||
g_image_titles[i].image_index == (uint16_t)entry_index &&
|
||||
strcmp(g_image_titles[i].title_id, title_id) == 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < MAX_IMAGE_TITLES; ++i) {
|
||||
if (g_image_titles[i].valid)
|
||||
continue;
|
||||
g_image_titles[i].valid = 1;
|
||||
g_image_titles[i].image_index = (uint16_t)entry_index;
|
||||
(void)strlcpy(g_image_titles[i].title_id, title_id,
|
||||
sizeof(g_image_titles[i].title_id));
|
||||
g_image_index_dirty = true;
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return true;
|
||||
}
|
||||
log_debug(" [IMGIDX] title cache full: %s", title_id);
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
void sm_image_index_complete_scan(const char *path) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return;
|
||||
}
|
||||
image_index_entry_t *entry = &g_image_index[entry_index];
|
||||
if (!entry->complete) {
|
||||
entry->complete = 1;
|
||||
g_image_index_dirty = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
void sm_image_index_flush(void) {
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
if (!save_index())
|
||||
log_debug(" [IMGIDX] failed to persist scan index: %s", strerror(errno));
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
void sm_image_index_prune(void) {
|
||||
struct stat st;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid && stat(g_image_index[i].path, &st) != 0 &&
|
||||
errno == ENOENT) {
|
||||
clear_entry(i);
|
||||
}
|
||||
}
|
||||
if (!save_index())
|
||||
log_debug(" [IMGIDX] failed to persist prune: %s", strerror(errno));
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
}
|
||||
|
||||
bool sm_image_index_snapshot(sm_image_index_snapshot_entry_t **entries_out,
|
||||
size_t *count_out) {
|
||||
if (!entries_out || !count_out)
|
||||
return false;
|
||||
*entries_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
size_t count = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
if (g_image_index[i].valid)
|
||||
count++;
|
||||
}
|
||||
|
||||
sm_image_index_snapshot_entry_t *entries = NULL;
|
||||
if (count > 0) {
|
||||
entries = calloc(count, sizeof(*entries));
|
||||
if (!entries) {
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t copied = 0;
|
||||
for (int i = 0; i < MAX_IMAGE_MOUNTS; ++i) {
|
||||
const image_index_entry_t *source = &g_image_index[i];
|
||||
if (!source->valid)
|
||||
continue;
|
||||
sm_image_index_snapshot_entry_t *entry = &entries[copied++];
|
||||
(void)strlcpy(entry->path, source->path, sizeof(entry->path));
|
||||
entry->size = source->size;
|
||||
entry->mtime_sec = source->mtime_sec;
|
||||
entry->mtime_nsec = source->mtime_nsec;
|
||||
entry->complete = source->complete != 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
*entries_out = entries;
|
||||
*count_out = copied;
|
||||
return true;
|
||||
}
|
||||
+47
-56
@@ -14,6 +14,7 @@
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
@@ -61,29 +62,6 @@ static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool write_link_file(const char *path, const char *value) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
log_debug(" [LINK] open failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int saved_errno = 0;
|
||||
if (fprintf(f, "%s", value) < 0)
|
||||
saved_errno = errno;
|
||||
if (fflush(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(f) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
log_debug(" [LINK] write failed for %s: %s", path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_appmeta_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
@@ -139,11 +117,11 @@ static bool copy_optional_trophy_metadata_file(const char *src_sce_sys,
|
||||
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)
|
||||
if (!path_exists(src_path))
|
||||
return true;
|
||||
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_base, relative_path);
|
||||
if (access(dst_path, F_OK) == 0)
|
||||
if (path_exists(dst_path))
|
||||
return true;
|
||||
|
||||
if (copy_file(src_path, dst_path) == 0)
|
||||
@@ -182,6 +160,13 @@ static bool update_trophy_metadata(const char *title_id,
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void update_registered_title_snd0info(const char *title_id) {
|
||||
sceKernelUsleep(200000);
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
|
||||
// --- Install/Remount Action ---
|
||||
static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
@@ -193,7 +178,8 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
char src_snd0[MAX_PATH];
|
||||
char image_source_path[MAX_PATH];
|
||||
char image_source_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_source_count = 0;
|
||||
bool has_image_source = false;
|
||||
bool appmeta_missing = false;
|
||||
bool metadata_restaged = false;
|
||||
@@ -206,8 +192,9 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(user_app_dir, sizeof(user_app_dir), "%s/%s", APP_BASE, title_id);
|
||||
|
||||
if (is_under_image_mount_base(src_path)) {
|
||||
has_image_source = resolve_image_source_from_mount_cache(
|
||||
src_path, image_source_path, sizeof(image_source_path));
|
||||
image_source_count = resolve_image_source_chain_from_mount_cache(
|
||||
src_path, image_source_chain);
|
||||
has_image_source = image_source_count > 0;
|
||||
if (!has_image_source) {
|
||||
log_debug(" [LINK] image source lookup failed for %s: %s", title_id,
|
||||
src_path);
|
||||
@@ -289,7 +276,8 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
mkdir(APP_BASE, 0777);
|
||||
mkdir(user_app_dir, 0777);
|
||||
snprintf(lnk_path, sizeof(lnk_path), "%s/mount.lnk", user_app_dir);
|
||||
if (!write_link_file(lnk_path, src_path)) {
|
||||
if (!write_mount_link(title_id, src_path)) {
|
||||
log_debug(" [LINK] write failed for %s: %s", lnk_path, strerror(errno));
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
@@ -301,17 +289,18 @@ 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_mount_image_chain(title_id, image_source_chain,
|
||||
image_source_count)) {
|
||||
(void)unlink(lnk_path);
|
||||
(void)rollback_title_nullfs_mount(title_id, src_path);
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s", img_lnk_path,
|
||||
image_source_path);
|
||||
if (!cache_image_source_mapping(image_source_path, src_path)) {
|
||||
log_debug(" [LINK] mount_img.lnk created: %s -> %s layers=%zu",
|
||||
img_lnk_path, image_source_chain[0], image_source_count);
|
||||
if (!cache_image_source_mapping(image_source_chain[0], src_path)) {
|
||||
log_debug(" [LINK] image source cache update failed: %s -> %s",
|
||||
src_path, image_source_path);
|
||||
image_source_chain[0], src_path);
|
||||
}
|
||||
} else if (unlink(img_lnk_path) != 0 && errno != ENOENT) {
|
||||
log_debug(" [LINK] remove failed for %s: %s", img_lnk_path,
|
||||
@@ -342,38 +331,40 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
}
|
||||
|
||||
// REGISTER
|
||||
app_install_title_dir_fn_t app_install_title_dir_fn =
|
||||
resolve_app_install_title_dir();
|
||||
if (!app_install_title_dir_fn) {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system("Register failed: %s (%s)\nAppInstallTitleDir unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
int res = 0;
|
||||
if (sm_shellcore_install_bridge_enabled()) {
|
||||
if (!sm_shellcore_install_title_dir(title_id, APP_BASE "/", &res)) {
|
||||
log_debug(" [REG] internal AppInstallTitleDir bridge unavailable");
|
||||
notify_system("Register failed: %s (%s)\nTitleDir bridge unavailable",
|
||||
title_name, title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
} else {
|
||||
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);
|
||||
res = app_install_title_dir_fn(title_id, APP_BASE "/", NULL);
|
||||
}
|
||||
|
||||
mark_register_attempted(title_id);
|
||||
int res = app_install_title_dir_fn(title_id, APP_BASE "/", 0);
|
||||
sceKernelUsleep(200000);
|
||||
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
log_debug(" [REG] Installed NEW!");
|
||||
notify_game_installed_rich(title_id);
|
||||
if (has_src_snd0) {
|
||||
int snd0_updates = update_snd0info(title_id);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
if (has_src_snd0)
|
||||
update_registered_title_snd0info(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);
|
||||
if (snd0_updates >= 0)
|
||||
log_debug(" [DB] snd0info updated rows=%d", snd0_updates);
|
||||
}
|
||||
if (has_src_snd0)
|
||||
update_registered_title_snd0info(title_id);
|
||||
// Silent on restore/remount to avoid spam
|
||||
} else {
|
||||
log_debug(" [REG] FAIL: 0x%x", res);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "sm_log.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
|
||||
@@ -49,6 +50,10 @@ static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
}
|
||||
|
||||
bool sm_install_has_pending_work(void) {
|
||||
return 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')
|
||||
@@ -280,6 +285,8 @@ static void poll_pending_installs(void) {
|
||||
if (g_submitted_install_count == 0)
|
||||
schedule_queued_install_submit(now_us);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] deferred release after install completion");
|
||||
}
|
||||
|
||||
uint64_t sm_install_next_wake_us(uint64_t now_us) {
|
||||
@@ -425,7 +432,7 @@ bool sm_install_submit_queued(void) {
|
||||
log_queued_install_batch();
|
||||
notify_queued_install_batch();
|
||||
|
||||
int res = sceAppInstUtilAppInstallAll();
|
||||
int res = sceAppInstUtilAppInstallAll(NULL);
|
||||
if (res != 0) {
|
||||
log_debug(" [REG] Batch install request failed: 0x%x", res);
|
||||
if (!g_queued_install_submit_failure_notified) {
|
||||
|
||||
@@ -292,8 +292,6 @@ static bool rewrite_manual_list_without_deleted_entries(int entry_count) {
|
||||
}
|
||||
if (remove_line) {
|
||||
changed = true;
|
||||
if (truncated)
|
||||
drain_manual_file_line(in);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -105,7 +105,18 @@ bool resolve_device_from_mount(const char *mount_point,
|
||||
|
||||
static bool is_path_mountpoint(const char *path) {
|
||||
struct statfs sfs;
|
||||
return (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0);
|
||||
if (statfs(path, &sfs) == 0 && strcmp(sfs.f_mntonname, path) == 0)
|
||||
return true;
|
||||
|
||||
// An unreadable filesystem can make statfs(path) fail while it is still in
|
||||
// the mount table. Treat it as active until unmount removes that entry.
|
||||
struct statfs *mntbuf = NULL;
|
||||
int mntcount = getmntinfo(&mntbuf, MNT_NOWAIT);
|
||||
for (int i = 0; i < mntcount && mntbuf; ++i) {
|
||||
if (strcmp(mntbuf[i].f_mntonname, path) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_active_image_mount_point(const char *path) {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/vnode.h>
|
||||
|
||||
#include "sm_log.h"
|
||||
#include "sm_mount_diag.h"
|
||||
#include "sm_path_utils.h"
|
||||
|
||||
#define MOUNT_DIAG_MAX_ROOTS 16u
|
||||
#define MOUNT_DIAG_MAX_VM_HITS 32u
|
||||
#define MOUNT_DIAG_MAX_VNODE_HITS 16u
|
||||
|
||||
typedef struct {
|
||||
char paths[MOUNT_DIAG_MAX_ROOTS][MAX_PATH];
|
||||
size_t count;
|
||||
} mount_diag_roots_t;
|
||||
|
||||
static void *read_sysctl_snapshot(const int *mib, u_int mib_count,
|
||||
size_t *size_out) {
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
size_t needed = 0;
|
||||
if (sysctl(mib, mib_count, NULL, &needed, NULL, 0) != 0)
|
||||
return NULL;
|
||||
if (needed == 0) {
|
||||
*size_out = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t capacity = needed + needed / 8u + 4096u;
|
||||
uint8_t *buffer = malloc(capacity);
|
||||
if (!buffer)
|
||||
return NULL;
|
||||
|
||||
size_t used = capacity;
|
||||
if (sysctl(mib, mib_count, buffer, &used, NULL, 0) == 0) {
|
||||
*size_out = used;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
int snapshot_errno = errno;
|
||||
free(buffer);
|
||||
if (snapshot_errno != ENOMEM) {
|
||||
errno = snapshot_errno;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool copy_record_path(char out[MAX_PATH], const uint8_t *record,
|
||||
size_t record_size, size_t path_offset) {
|
||||
out[0] = '\0';
|
||||
if (record_size <= path_offset)
|
||||
return false;
|
||||
|
||||
const char *path = (const char *)(record + path_offset);
|
||||
size_t available = record_size - path_offset;
|
||||
size_t length = strnlen(path, available);
|
||||
if (length == 0 || length == available)
|
||||
return false;
|
||||
|
||||
(void)strlcpy(out, path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void copy_process_name(char out[TDNAMLEN + 1], const uint8_t *record,
|
||||
size_t record_size) {
|
||||
size_t name_offset = offsetof(struct kinfo_proc, ki_tdname);
|
||||
out[0] = '\0';
|
||||
if (record_size <= name_offset)
|
||||
return;
|
||||
|
||||
const char *name = (const char *)(record + name_offset);
|
||||
size_t available = record_size - name_offset;
|
||||
size_t length = strnlen(name, available);
|
||||
if (length == available)
|
||||
return;
|
||||
if (length > TDNAMLEN)
|
||||
length = TDNAMLEN;
|
||||
memcpy(out, name, length);
|
||||
out[length] = '\0';
|
||||
}
|
||||
|
||||
static bool add_diag_root(mount_diag_roots_t *roots, const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
for (size_t i = 0; i < roots->count; ++i) {
|
||||
if (strcmp(roots->paths[i], path) == 0)
|
||||
return true;
|
||||
}
|
||||
if (roots->count >= MOUNT_DIAG_MAX_ROOTS)
|
||||
return false;
|
||||
(void)strlcpy(roots->paths[roots->count++], path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool path_matches_diag_roots(const mount_diag_roots_t *roots,
|
||||
const char *path) {
|
||||
for (size_t i = 0; i < roots->count; ++i) {
|
||||
if (path_matches_root_or_child(path, roots->paths[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *normalized_mount_source(const char *source) {
|
||||
return strncmp(source, "<above>:", 8) == 0 ? source + 8 : source;
|
||||
}
|
||||
|
||||
static void collect_mount_dependencies(const char *mount_point,
|
||||
mount_diag_roots_t *roots,
|
||||
size_t *dependency_count_out) {
|
||||
(void)add_diag_root(roots, mount_point);
|
||||
|
||||
struct statfs *mounts = NULL;
|
||||
int mount_count = getmntinfo(&mounts, MNT_NOWAIT);
|
||||
if (mount_count <= 0 || !mounts) {
|
||||
log_debug(" [MOUNT-BUSY] mount table unavailable for %s: %s",
|
||||
mount_point, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t dependency_count = 0;
|
||||
for (int i = 0; i < mount_count; ++i) {
|
||||
const char *source = normalized_mount_source(mounts[i].f_mntfromname);
|
||||
const char *target = mounts[i].f_mntonname;
|
||||
bool consumes_mount = path_matches_root_or_child(source, mount_point);
|
||||
bool child_mount = strcmp(target, mount_point) != 0 &&
|
||||
path_matches_root_or_child(target, mount_point);
|
||||
if (!consumes_mount && !child_mount)
|
||||
continue;
|
||||
|
||||
dependency_count++;
|
||||
log_debug(" [MOUNT-BUSY] dependent type=%s from=%s on=%s flags=0x%lX",
|
||||
mounts[i].f_fstypename, source, target,
|
||||
(unsigned long)mounts[i].f_flags);
|
||||
if (consumes_mount)
|
||||
(void)add_diag_root(roots, target);
|
||||
}
|
||||
*dependency_count_out = dependency_count;
|
||||
}
|
||||
|
||||
static void log_process_vm_refs(pid_t pid, const char *name,
|
||||
const mount_diag_roots_t *roots,
|
||||
size_t *hit_count) {
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_VMMAP, pid};
|
||||
size_t data_size = 0;
|
||||
uint8_t *data = read_sysctl_snapshot(mib, 4, &data_size);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
uint8_t *cursor = data;
|
||||
uint8_t *end = data + data_size;
|
||||
while ((size_t)(end - cursor) >= sizeof(int)) {
|
||||
int record_size = 0;
|
||||
memcpy(&record_size, cursor, sizeof(record_size));
|
||||
if (record_size <= 0 || (size_t)record_size > (size_t)(end - cursor))
|
||||
break;
|
||||
|
||||
const struct kinfo_vmentry *entry =
|
||||
(const struct kinfo_vmentry *)cursor;
|
||||
if ((size_t)record_size > offsetof(struct kinfo_vmentry, kve_path) &&
|
||||
entry->kve_type == KVME_TYPE_VNODE) {
|
||||
char path[MAX_PATH];
|
||||
if (copy_record_path(path, cursor, (size_t)record_size,
|
||||
offsetof(struct kinfo_vmentry, kve_path)) &&
|
||||
path_matches_diag_roots(roots, path)) {
|
||||
if (*hit_count < MOUNT_DIAG_MAX_VM_HITS) {
|
||||
log_debug(" [MOUNT-BUSY] pid=%ld name=%s vm=0x%llX-0x%llX "
|
||||
"prot=0x%X path=%s",
|
||||
(long)pid, name, (unsigned long long)entry->kve_start,
|
||||
(unsigned long long)entry->kve_end,
|
||||
entry->kve_protection, path);
|
||||
}
|
||||
(*hit_count)++;
|
||||
}
|
||||
}
|
||||
cursor += record_size;
|
||||
}
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
static void log_all_process_vm_refs(const mount_diag_roots_t *roots,
|
||||
size_t *hit_count_out) {
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
|
||||
size_t data_size = 0;
|
||||
uint8_t *data = read_sysctl_snapshot(mib, 4, &data_size);
|
||||
if (!data) {
|
||||
log_debug(" [MOUNT-BUSY] process list unavailable: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
size_t hit_count = 0;
|
||||
uint8_t *cursor = data;
|
||||
uint8_t *end = data + data_size;
|
||||
while ((size_t)(end - cursor) >= sizeof(int)) {
|
||||
int record_size = 0;
|
||||
memcpy(&record_size, cursor, sizeof(record_size));
|
||||
if (record_size <= 0 || (size_t)record_size > (size_t)(end - cursor))
|
||||
break;
|
||||
if ((size_t)record_size <= offsetof(struct kinfo_proc, ki_tdname))
|
||||
break;
|
||||
|
||||
const struct kinfo_proc *process = (const struct kinfo_proc *)cursor;
|
||||
char process_name[TDNAMLEN + 1];
|
||||
copy_process_name(process_name, cursor, (size_t)record_size);
|
||||
const char *name = process_name[0] ? process_name : "?";
|
||||
if (process->ki_pid > 0)
|
||||
log_process_vm_refs(process->ki_pid, name, roots, &hit_count);
|
||||
cursor += record_size;
|
||||
}
|
||||
|
||||
free(data);
|
||||
*hit_count_out = hit_count;
|
||||
if (hit_count > MOUNT_DIAG_MAX_VM_HITS) {
|
||||
log_debug(" [MOUNT-BUSY] %zu additional VM references suppressed",
|
||||
hit_count - MOUNT_DIAG_MAX_VM_HITS);
|
||||
}
|
||||
}
|
||||
|
||||
static void log_vnode_refs(const char *mount_point, size_t *hit_count_out) {
|
||||
struct stat root_stat;
|
||||
if (stat(mount_point, &root_stat) != 0)
|
||||
return;
|
||||
|
||||
int mib[2] = {CTL_KERN, KERN_VNODE};
|
||||
size_t data_size = 0;
|
||||
uint8_t *data = read_sysctl_snapshot(mib, 2, &data_size);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
void *mount_address = NULL;
|
||||
uint8_t *cursor = data;
|
||||
uint8_t *end = data + data_size;
|
||||
while ((size_t)(end - cursor) >= sizeof(size_t)) {
|
||||
const struct xvnode *vnode = (const struct xvnode *)cursor;
|
||||
if (vnode->xv_size < sizeof(struct xvnode) ||
|
||||
vnode->xv_size > (size_t)(end - cursor)) {
|
||||
break;
|
||||
}
|
||||
if (vnode->xv_dev == root_stat.st_dev &&
|
||||
vnode->xv_ino == root_stat.st_ino) {
|
||||
mount_address = vnode->xv_mount;
|
||||
break;
|
||||
}
|
||||
cursor += vnode->xv_size;
|
||||
}
|
||||
|
||||
size_t hit_count = 0;
|
||||
size_t hold_only_count = 0;
|
||||
if (mount_address) {
|
||||
cursor = data;
|
||||
while ((size_t)(end - cursor) >= sizeof(size_t)) {
|
||||
const struct xvnode *vnode = (const struct xvnode *)cursor;
|
||||
if (vnode->xv_size < sizeof(struct xvnode) ||
|
||||
vnode->xv_size > (size_t)(end - cursor)) {
|
||||
break;
|
||||
}
|
||||
if (vnode->xv_mount == mount_address) {
|
||||
bool active = vnode->xv_usecount > 0 || vnode->xv_writecount > 0 ||
|
||||
vnode->xv_numoutput > 0;
|
||||
if (active) {
|
||||
if (hit_count < MOUNT_DIAG_MAX_VNODE_HITS) {
|
||||
log_debug(" [MOUNT-BUSY] vnode=%p dev=0x%llX ino=0x%llX "
|
||||
"use=%d hold=%d write=%d output=%ld",
|
||||
vnode->xv_vnode, (unsigned long long)vnode->xv_dev,
|
||||
(unsigned long long)vnode->xv_ino, vnode->xv_usecount,
|
||||
vnode->xv_holdcnt, vnode->xv_writecount,
|
||||
vnode->xv_numoutput);
|
||||
}
|
||||
hit_count++;
|
||||
} else if (vnode->xv_holdcnt > 0) {
|
||||
hold_only_count++;
|
||||
}
|
||||
}
|
||||
cursor += vnode->xv_size;
|
||||
}
|
||||
}
|
||||
|
||||
free(data);
|
||||
*hit_count_out = hit_count;
|
||||
if (hit_count > MOUNT_DIAG_MAX_VNODE_HITS) {
|
||||
log_debug(" [MOUNT-BUSY] %zu additional active vnodes suppressed",
|
||||
hit_count - MOUNT_DIAG_MAX_VNODE_HITS);
|
||||
}
|
||||
if (hold_only_count > 0) {
|
||||
log_debug(" [MOUNT-BUSY] hold-only cached vnodes=%zu", hold_only_count);
|
||||
}
|
||||
}
|
||||
|
||||
void sm_mount_diag_log_busy(const char *mount_point) {
|
||||
if (!mount_point || mount_point[0] == '\0')
|
||||
return;
|
||||
|
||||
int saved_errno = errno;
|
||||
log_debug(" [MOUNT-BUSY] diagnostic begin: %s", mount_point);
|
||||
|
||||
mount_diag_roots_t roots;
|
||||
memset(&roots, 0, sizeof(roots));
|
||||
size_t dependency_count = 0;
|
||||
collect_mount_dependencies(mount_point, &roots, &dependency_count);
|
||||
|
||||
size_t vm_hit_count = 0;
|
||||
log_all_process_vm_refs(&roots, &vm_hit_count);
|
||||
|
||||
size_t vnode_hit_count = 0;
|
||||
log_vnode_refs(mount_point, &vnode_hit_count);
|
||||
|
||||
if (dependency_count == 0 && vm_hit_count == 0 &&
|
||||
vnode_hit_count == 0) {
|
||||
log_debug(" [MOUNT-BUSY] no user-visible owner found; likely transient "
|
||||
"VFS, buffer-cache, or backing-device reference");
|
||||
}
|
||||
log_debug(" [MOUNT-BUSY] diagnostic end: dependencies=%zu vm_refs=%zu "
|
||||
"active_vnodes=%zu",
|
||||
dependency_count, vm_hit_count, vnode_hit_count);
|
||||
errno = saved_errno;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ struct PathStateEntry {
|
||||
bool game_info_cached;
|
||||
bool game_info_valid;
|
||||
time_t game_info_mtime;
|
||||
long game_info_mtime_nsec;
|
||||
off_t game_info_size;
|
||||
ino_t game_info_ino;
|
||||
char game_title_id[MAX_TITLE_ID];
|
||||
@@ -129,6 +130,7 @@ bool load_cached_game_info(const char *path, const struct stat *param_st,
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry || !entry->game_info_cached ||
|
||||
entry->game_info_mtime != param_st->st_mtime ||
|
||||
entry->game_info_mtime_nsec != param_st->st_mtim.tv_nsec ||
|
||||
entry->game_info_size != param_st->st_size ||
|
||||
entry->game_info_ino != param_st->st_ino) {
|
||||
return false;
|
||||
@@ -150,6 +152,7 @@ void store_cached_game_info(const char *path, const struct stat *param_st,
|
||||
entry->game_info_cached = true;
|
||||
entry->game_info_valid = valid;
|
||||
entry->game_info_mtime = param_st->st_mtime;
|
||||
entry->game_info_mtime_nsec = param_st->st_mtim.tv_nsec;
|
||||
entry->game_info_size = param_st->st_size;
|
||||
entry->game_info_ino = param_st->st_ino;
|
||||
(void)strlcpy(entry->game_title_id, valid ? title_id : "", MAX_TITLE_ID);
|
||||
|
||||
@@ -30,6 +30,8 @@ bool path_matches_root_or_child(const char *path, const char *root) {
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
return false;
|
||||
if (root_len == 1u && root[0] == '/')
|
||||
return path[0] == '/';
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
|
||||
+160
-49
@@ -4,6 +4,7 @@
|
||||
#include "sm_scan_tree.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_config_mount.h"
|
||||
@@ -16,17 +17,21 @@
|
||||
#include "sm_stability.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_image_cache.h"
|
||||
#include "sm_image_index.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_manual.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
|
||||
typedef struct {
|
||||
char discovered_param_roots[MAX_PENDING][MAX_PATH];
|
||||
char found_titles[MAX_PENDING][MAX_TITLE_ID];
|
||||
char checked_appmeta_titles[MAX_PENDING][MAX_TITLE_ID];
|
||||
char blocked_ppsa_uninstall_titles[MAX_PENDING][MAX_TITLE_ID];
|
||||
bool checked_appmeta_present[MAX_PENDING];
|
||||
int checked_appmeta_count;
|
||||
int blocked_ppsa_uninstall_count;
|
||||
int found_title_count;
|
||||
} scan_workspace_t;
|
||||
|
||||
// Reuse the largest transient scan buffer instead of placing ~512 KiB of path
|
||||
@@ -36,6 +41,23 @@ static scan_workspace_t g_scan_workspace;
|
||||
static void reset_scan_workspace(void) {
|
||||
g_scan_workspace.checked_appmeta_count = 0;
|
||||
g_scan_workspace.blocked_ppsa_uninstall_count = 0;
|
||||
g_scan_workspace.found_title_count = 0;
|
||||
}
|
||||
|
||||
static void note_found_title(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return;
|
||||
|
||||
for (int i = 0; i < g_scan_workspace.found_title_count; i++) {
|
||||
if (strcmp(g_scan_workspace.found_titles[i], title_id) == 0)
|
||||
return;
|
||||
}
|
||||
if (g_scan_workspace.found_title_count >= MAX_PENDING)
|
||||
return;
|
||||
|
||||
int slot = g_scan_workspace.found_title_count++;
|
||||
(void)strlcpy(g_scan_workspace.found_titles[slot], title_id,
|
||||
sizeof(g_scan_workspace.found_titles[slot]));
|
||||
}
|
||||
|
||||
static bool blocked_ppsa_uninstall_requested(const char *title_id) {
|
||||
@@ -233,6 +255,16 @@ static void notify_duplicate_scan_candidate(const char *title_id,
|
||||
notify_duplicate_title_once(title_id, ignored_path, existing_path);
|
||||
}
|
||||
|
||||
static bool defer_full_candidate_batch(int candidate_count,
|
||||
int max_candidates,
|
||||
bool *incomplete_found_out) {
|
||||
if (candidate_count < max_candidates)
|
||||
return false;
|
||||
if (incomplete_found_out)
|
||||
*incomplete_found_out = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static existing_directory_result_t handle_existing_directory_candidate(
|
||||
const char *full_path, const scan_app_db_context_t *app_db,
|
||||
const directory_candidate_info_t *info,
|
||||
@@ -267,9 +299,11 @@ static existing_directory_result_t handle_existing_directory_candidate(
|
||||
link_matches_source = installed && link_matches_source;
|
||||
bool source_valid = false;
|
||||
if (link_matches_source) {
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
if (!source_valid && mount_title_nullfs(info->title_id, full_path))
|
||||
source_valid = is_data_mounted(info->title_id);
|
||||
char eboot_path[MAX_PATH];
|
||||
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
|
||||
full_path);
|
||||
source_valid = written > 0 && (size_t)written < sizeof(eboot_path) &&
|
||||
path_exists(eboot_path);
|
||||
}
|
||||
|
||||
if (source_valid && appmeta_present) {
|
||||
@@ -327,9 +361,10 @@ static bool enqueue_directory_candidate(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (*candidate_count >= max_candidates) {
|
||||
log_debug(" [SKIP] candidate queue full (%d): %s (%s)", max_candidates,
|
||||
info->title_name, info->title_id);
|
||||
if (defer_full_candidate_batch(*candidate_count, max_candidates,
|
||||
unstable_found_out)) {
|
||||
log_debug(" [SKIP] candidate queue full (%d), deferring: %s (%s)",
|
||||
max_candidates, info->title_name, info->title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -365,6 +400,15 @@ static bool try_collect_candidate_for_directory(
|
||||
if (probe_result == DIRECTORY_CANDIDATE_DESCEND)
|
||||
return false;
|
||||
|
||||
note_found_title(info.title_id);
|
||||
|
||||
note_game_cache_source_seen(full_path, info.title_id, info.title_name);
|
||||
|
||||
if (!sm_image_index_record_game(full_path, info.title_id) &&
|
||||
unstable_found_out) {
|
||||
*unstable_found_out = true;
|
||||
}
|
||||
|
||||
int duplicate_candidate_index = find_scan_candidate_index_by_title_id(
|
||||
candidates, *candidate_count, info.title_id);
|
||||
const char *duplicate_candidate_path =
|
||||
@@ -449,6 +493,10 @@ static sm_scan_tree_dir_visit_t collect_candidate_directory_visit(
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
|
||||
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
|
||||
if (defer_full_candidate_batch(*ctx->candidate_count, ctx->max_candidates,
|
||||
ctx->unstable_found_out)) {
|
||||
return SM_SCAN_TREE_DIR_ABORT;
|
||||
}
|
||||
if (try_collect_candidate_for_directory(
|
||||
dir_path, ctx->candidates, ctx->max_candidates, ctx->candidate_count,
|
||||
ctx->app_db, ctx->discovered_param_roots,
|
||||
@@ -467,17 +515,41 @@ static bool collect_candidate_image_visit(const char *image_path,
|
||||
(void)depth_from_root;
|
||||
|
||||
collect_candidates_walk_ctx_t *ctx = (collect_candidates_walk_ctx_t *)ctx_ptr;
|
||||
if (!maybe_mount_image_file(image_path, image_name, ctx->unstable_found_out))
|
||||
if (defer_full_candidate_batch(*ctx->candidate_count, ctx->max_candidates,
|
||||
ctx->unstable_found_out)) {
|
||||
return false;
|
||||
}
|
||||
struct stat image_st;
|
||||
if (stat(image_path, &image_st) != 0)
|
||||
return true;
|
||||
bool persistent_image = !is_under_image_mount_base(image_path);
|
||||
if (persistent_image &&
|
||||
sm_image_index_visit_ready_titles(
|
||||
image_path, &image_st, ctx->app_db->titles,
|
||||
ctx->app_db->titles_ready, note_found_title)) {
|
||||
return true;
|
||||
}
|
||||
bool image_unstable = false;
|
||||
if (!maybe_mount_image_file(image_path, image_name, &image_unstable)) {
|
||||
if (image_unstable && ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
return true;
|
||||
}
|
||||
if (persistent_image)
|
||||
sm_image_index_begin_scan(image_path, &image_st);
|
||||
|
||||
if (ctx->manual_source_path && is_pfsc_image_mount_base_or_child(image_path)) {
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, ctx->manual_source_path, ctx->candidates,
|
||||
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
|
||||
ctx->discovered_param_roots, ctx->discovered_param_root_count,
|
||||
ctx->unstable_found_out);
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, ctx->manual_source_path, ctx->candidates,
|
||||
ctx->max_candidates, ctx->candidate_count, ctx->app_db,
|
||||
ctx->discovered_param_roots, ctx->discovered_param_root_count,
|
||||
&image_unstable);
|
||||
if (image_unstable && ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
if (persistent_image && !image_unstable && !should_stop_requested() &&
|
||||
!runtime_sleep_mode_active()) {
|
||||
sm_image_index_complete_scan(image_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -534,6 +606,10 @@ static void collect_scan_candidates_from_manual_path(
|
||||
int *discovered_param_root_count, bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
if (defer_full_candidate_batch(*candidate_count, max_candidates,
|
||||
unstable_found_out)) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(manual_path, &st) != 0) {
|
||||
@@ -546,15 +622,31 @@ static void collect_scan_candidates_from_manual_path(
|
||||
const char *name = get_filename_component(manual_path);
|
||||
if (S_ISREG(st.st_mode) &&
|
||||
is_supported_image_file_path(manual_path, name)) {
|
||||
if (!maybe_mount_image_file(manual_path, name, unstable_found_out))
|
||||
if (sm_image_index_visit_ready_titles(
|
||||
manual_path, &st, app_db->titles, app_db->titles_ready,
|
||||
note_found_title)) {
|
||||
return;
|
||||
}
|
||||
bool image_unstable = false;
|
||||
if (!maybe_mount_image_file(manual_path, name, &image_unstable)) {
|
||||
if (image_unstable && unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
return;
|
||||
}
|
||||
sm_image_index_begin_scan(manual_path, &st);
|
||||
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(manual_path, mount_point);
|
||||
collect_scan_candidates_from_manual_root(
|
||||
mount_point, manual_path, candidates, max_candidates, candidate_count,
|
||||
app_db, discovered_param_roots, discovered_param_root_count,
|
||||
unstable_found_out);
|
||||
&image_unstable);
|
||||
if (image_unstable && unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
if (!image_unstable && !should_stop_requested() &&
|
||||
!runtime_sleep_mode_active()) {
|
||||
sm_image_index_complete_scan(manual_path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -653,43 +745,39 @@ static bool resolve_backport_path(const char *title_id,
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
bool *unstable_found_out;
|
||||
} backport_overlay_ctx_t;
|
||||
|
||||
static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *title_name,
|
||||
const char *owning_scan_root,
|
||||
void *ctx_ptr) {
|
||||
(void)title_name;
|
||||
|
||||
bool mount_backport_overlay_for_title(const char *source_path,
|
||||
const char *title_id,
|
||||
const char *owning_scan_root,
|
||||
bool *unstable_found_out) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
backport_overlay_ctx_t *ctx = (backport_overlay_ctx_t *)ctx_ptr;
|
||||
char backport_path[MAX_PATH];
|
||||
if (!resolve_backport_path(title_id, owning_scan_root, backport_path)) {
|
||||
return true;
|
||||
}
|
||||
if (is_backport_mount_blocked(backport_path))
|
||||
return true;
|
||||
return false;
|
||||
|
||||
bool overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
&overlay_active) ||
|
||||
overlay_active) {
|
||||
&overlay_active)) {
|
||||
return false;
|
||||
}
|
||||
if (overlay_active) {
|
||||
return true;
|
||||
}
|
||||
if (!wait_for_stability_fast(backport_path, "BKP")) {
|
||||
if (ctx->unstable_found_out)
|
||||
*ctx->unstable_found_out = true;
|
||||
return true;
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = true;
|
||||
return false;
|
||||
}
|
||||
overlay_active = false;
|
||||
if (!reconcile_title_backport_mount(title_id, source_path, backport_path,
|
||||
&overlay_active) ||
|
||||
overlay_active) {
|
||||
&overlay_active)) {
|
||||
return false;
|
||||
}
|
||||
if (overlay_active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -698,35 +786,56 @@ static bool mount_backport_overlay_for_cached_game(const char *source_path,
|
||||
title_id);
|
||||
if (!mount_backport_overlay(system_ex_path, backport_path, title_id)) {
|
||||
note_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
clear_backport_mount_failure(backport_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void mount_backport_overlays(bool *unstable_found_out) {
|
||||
backport_overlay_ctx_t ctx = {
|
||||
.unstable_found_out = unstable_found_out,
|
||||
};
|
||||
for_each_cached_game_entry(NULL, mount_backport_overlay_for_cached_game, &ctx);
|
||||
bool release_scan_runtime_mounts(void) {
|
||||
if (runtime_sleep_mode_active())
|
||||
return false;
|
||||
if (sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount()) {
|
||||
return true;
|
||||
}
|
||||
runtime_mount_state_lock();
|
||||
if (sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount()) {
|
||||
runtime_mount_state_unlock();
|
||||
return true;
|
||||
}
|
||||
if (!unmount_all_title_runtime_layers()) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
bool released = release_runtime_image_mounts();
|
||||
runtime_mount_state_unlock();
|
||||
return released;
|
||||
}
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
void cleanup_lost_sources_before_scan(void) {
|
||||
// 1) Drop stale game cache entries for deleted sources.
|
||||
prune_game_cache();
|
||||
// 2) Drop stale/broken mount links and unmount stale /system_ex stacks.
|
||||
cleanup_mount_links(NULL, true);
|
||||
// 3) Unmount stale image mounts for deleted image files.
|
||||
// 2) Recover or remove stale image mounts before validating their title links.
|
||||
cleanup_stale_image_mounts();
|
||||
// 3) Drop stale/broken mount links and unmount stale /system_ex stacks.
|
||||
runtime_mount_state_lock();
|
||||
cleanup_mount_links(NULL, true);
|
||||
runtime_mount_state_unlock();
|
||||
sm_image_index_prune();
|
||||
// 4) Drop stale path-state entries.
|
||||
prune_path_state();
|
||||
}
|
||||
|
||||
void cleanup_lost_sources_for_scan_root(const char *scan_root) {
|
||||
prune_game_cache_for_root(scan_root);
|
||||
cleanup_mount_links(scan_root, true);
|
||||
cleanup_stale_image_mounts_for_root(scan_root);
|
||||
runtime_mount_state_lock();
|
||||
cleanup_mount_links(scan_root, true);
|
||||
runtime_mount_state_unlock();
|
||||
sm_image_index_prune();
|
||||
prune_path_state_for_root(scan_root);
|
||||
}
|
||||
|
||||
@@ -847,7 +956,8 @@ int collect_scan_candidates_for_scan_root(const char *scan_root,
|
||||
unstable_found_out);
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
*total_found_out = g_scan_workspace.found_title_count;
|
||||
sm_image_index_flush();
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
@@ -896,7 +1006,8 @@ int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
blocked_ppsa_titles_ready);
|
||||
|
||||
if (total_found_out)
|
||||
*total_found_out = discovered_param_root_count;
|
||||
*total_found_out = g_scan_workspace.found_title_count;
|
||||
sm_image_index_flush();
|
||||
free_app_db_title_list(&blocked_ppsa_titles);
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
return candidate_count;
|
||||
|
||||
+4
-1
@@ -94,7 +94,10 @@ bool sm_scan_tree_walk(const char *scan_root, const char *dir_path,
|
||||
}
|
||||
|
||||
char full_path[MAX_PATH];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
int written =
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
|
||||
if (written < 0 || (size_t)written >= sizeof(full_path))
|
||||
continue;
|
||||
|
||||
bool is_dir = false;
|
||||
bool is_regular = false;
|
||||
|
||||
+138
-99
@@ -1,15 +1,18 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_api_service.h"
|
||||
#include "sm_appdb.h"
|
||||
#include "sm_fakelib.h"
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_game_cache.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_hash.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_install.h"
|
||||
@@ -23,6 +26,7 @@
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scan_tree.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
@@ -37,9 +41,7 @@
|
||||
typedef enum {
|
||||
SCANNER_WATCH_SCAN_ROOT = 0,
|
||||
SCANNER_WATCH_SCAN_ROOT_PARENT,
|
||||
SCANNER_WATCH_SCAN_BACKPORT_ROOT,
|
||||
SCANNER_WATCH_SCAN_SUBDIR,
|
||||
SCANNER_WATCH_SCAN_IMAGE_FILE,
|
||||
} scanner_watch_kind_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -66,6 +68,7 @@ typedef struct {
|
||||
} scanner_root_state_t;
|
||||
|
||||
static int g_scanner_wake_pipe[2] = {-1, -1};
|
||||
static atomic_bool g_usb_watches_suspended = ATOMIC_VAR_INIT(true);
|
||||
static int g_scanner_config_fd = -1;
|
||||
static int g_scanner_manual_fd = -1;
|
||||
static volatile sig_atomic_t g_scanner_wake_write_fd = -1;
|
||||
@@ -109,14 +112,6 @@ static void schedule_manual_probe(uint64_t now_us) {
|
||||
g_scanner_manual_probe_due_us = now_us + SCANNER_MANUAL_PROBE_INTERVAL_US;
|
||||
}
|
||||
|
||||
static bool set_fd_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0)
|
||||
return false;
|
||||
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
static void reset_scanner_root_states(void) {
|
||||
memset(g_scanner_root_states, 0, sizeof(g_scanner_root_states));
|
||||
}
|
||||
@@ -364,11 +359,7 @@ static bool register_scanner_watch_entry(int kq, int scan_root_index,
|
||||
const char *path,
|
||||
scanner_watch_kind_t kind,
|
||||
uint8_t depth) {
|
||||
int open_flags = O_RDONLY;
|
||||
if (kind != SCANNER_WATCH_SCAN_IMAGE_FILE)
|
||||
open_flags |= O_DIRECTORY;
|
||||
|
||||
int fd = open(path, open_flags);
|
||||
int fd = open(path, O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) {
|
||||
if (errno != ENOENT && errno != ENOTDIR) {
|
||||
log_debug(" [SCAN] watcher open failed for %s: %s", path,
|
||||
@@ -557,7 +548,6 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
scanner_watch_kind_t *kind_out) {
|
||||
switch (entry->kind) {
|
||||
case SCANNER_WATCH_SCAN_ROOT:
|
||||
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
|
||||
case SCANNER_WATCH_SCAN_SUBDIR:
|
||||
(void)strlcpy(rebuild_path, entry->path, MAX_PATH);
|
||||
*rebuild_depth_out = entry->depth;
|
||||
@@ -569,13 +559,6 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
*rebuild_depth_out = 0;
|
||||
*kind_out = SCANNER_WATCH_SCAN_ROOT;
|
||||
return true;
|
||||
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
||||
if (!build_parent_directory_path(entry->path, rebuild_path))
|
||||
return false;
|
||||
*rebuild_depth_out = (entry->depth > 0u) ? (uint8_t)(entry->depth - 1u) : 0u;
|
||||
*kind_out =
|
||||
(entry->depth <= 1u) ? SCANNER_WATCH_SCAN_ROOT : SCANNER_WATCH_SCAN_SUBDIR;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -584,11 +567,25 @@ static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry
|
||||
typedef struct {
|
||||
int kq;
|
||||
int scan_root_index;
|
||||
unsigned int scan_depth;
|
||||
} register_watch_tree_ctx_t;
|
||||
|
||||
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
|
||||
register_watch_tree_ctx_t *ctx = (register_watch_tree_ctx_t *)ctx_ptr;
|
||||
if (depth_from_root > 0u) {
|
||||
// Runtime image descendants can be active PFS/PFSC mounts. Keeping a
|
||||
// directory fd open there pins the mounted vnode and makes unmount return
|
||||
// EBUSY. The managed mount-base watcher is sufficient: mounted images are
|
||||
// read-only and every discovery pass rebuilds this tree.
|
||||
if (is_under_image_mount_base(dir_path))
|
||||
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
|
||||
if (depth_from_root >= ctx->scan_depth ||
|
||||
directory_has_param_json(dir_path, NULL)) {
|
||||
return SM_SCAN_TREE_DIR_SKIP_DESCEND;
|
||||
}
|
||||
}
|
||||
|
||||
scanner_watch_kind_t kind =
|
||||
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
|
||||
: SCANNER_WATCH_SCAN_SUBDIR;
|
||||
@@ -600,18 +597,6 @@ static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
||||
return SM_SCAN_TREE_DIR_DESCEND;
|
||||
}
|
||||
|
||||
static bool register_watch_image_visit(const char *image_path,
|
||||
const char *image_name,
|
||||
unsigned int depth_from_root,
|
||||
void *ctx_ptr) {
|
||||
(void)image_name;
|
||||
|
||||
register_watch_tree_ctx_t *ctx = (register_watch_tree_ctx_t *)ctx_ptr;
|
||||
return register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, image_path,
|
||||
SCANNER_WATCH_SCAN_IMAGE_FILE,
|
||||
(uint8_t)depth_from_root);
|
||||
}
|
||||
|
||||
static bool register_scan_root_parent_watch(int kq, int scan_root_index,
|
||||
const char *scan_root) {
|
||||
char parent_path[MAX_PATH];
|
||||
@@ -642,24 +627,17 @@ static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.scan_depth = scan_depth,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
.on_image_file = NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
||||
&walk_ctx)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char backport_root[MAX_PATH];
|
||||
if (build_backports_root_path(scan_root, backport_root)) {
|
||||
if (!register_scanner_watch_entry(kq, scan_root_index, backport_root,
|
||||
SCANNER_WATCH_SCAN_BACKPORT_ROOT, 1u)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!register_scan_root_parent_watch(kq, scan_root_index, scan_root))
|
||||
return false;
|
||||
|
||||
@@ -678,18 +656,6 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
return rebuild_scan_root_watch_tree(kq, scan_root_index);
|
||||
}
|
||||
|
||||
if (rebuild_kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT) {
|
||||
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
||||
return false;
|
||||
if (!register_scanner_watch_entry(kq, scan_root_index, rebuild_path,
|
||||
SCANNER_WATCH_SCAN_BACKPORT_ROOT,
|
||||
rebuild_depth)) {
|
||||
return false;
|
||||
}
|
||||
clear_scan_root_watch_tree_state(scan_root_index);
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_root);
|
||||
|
||||
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
||||
@@ -702,10 +668,11 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
register_watch_tree_ctx_t walk_ctx = {
|
||||
.kq = kq,
|
||||
.scan_root_index = scan_root_index,
|
||||
.scan_depth = scan_depth,
|
||||
};
|
||||
sm_scan_tree_callbacks_t callbacks = {
|
||||
.on_directory = register_watch_directory_visit,
|
||||
.on_image_file = register_watch_image_visit,
|
||||
.on_image_file = NULL,
|
||||
};
|
||||
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
|
||||
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {
|
||||
@@ -724,6 +691,30 @@ static bool rebuild_all_scan_root_watch_trees(int kq) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool suspend_usb_scan_root_watch_trees(void) {
|
||||
bool removed_any = false;
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
if (!is_usb_storage_path(get_scan_path(i)))
|
||||
continue;
|
||||
while (g_scanner_root_watch_heads[i] != SCANNER_WATCH_INDEX_NONE) {
|
||||
remove_scanner_watch_entry_at(g_scanner_root_watch_heads[i]);
|
||||
removed_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !removed_any || rebuild_scanner_watch_fd_index();
|
||||
}
|
||||
|
||||
static bool resume_usb_scan_root_watch_trees(int kq) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
if (!is_usb_storage_path(get_scan_path(i)))
|
||||
continue;
|
||||
if (!rebuild_scan_root_watch_tree(kq, i))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void clear_all_dirty_scan_roots(void) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
g_scanner_root_states[i].dirty = false;
|
||||
@@ -759,11 +750,7 @@ static void schedule_scan_root_dirty(int scan_root_index, uint64_t now_us,
|
||||
state->ready_after_us = ready_after_us;
|
||||
}
|
||||
|
||||
static bool scanner_event_requires_consistency_cleanup(
|
||||
const scanner_watch_entry_t *entry, uint32_t fflags) {
|
||||
if (entry->kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT)
|
||||
return false;
|
||||
|
||||
static bool scanner_event_requires_consistency_cleanup(uint32_t fflags) {
|
||||
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
}
|
||||
|
||||
@@ -775,14 +762,10 @@ static bool scanner_event_requires_watch_tree_refresh(
|
||||
switch (entry->kind) {
|
||||
case SCANNER_WATCH_SCAN_ROOT:
|
||||
return (fflags & tree_change_flags) != 0;
|
||||
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
|
||||
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
case SCANNER_WATCH_SCAN_SUBDIR:
|
||||
return entry->depth <
|
||||
get_scan_depth_for_root(get_scan_path(entry->scan_root_index)) &&
|
||||
(fflags & tree_change_flags) != 0;
|
||||
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
||||
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -811,9 +794,7 @@ static void schedule_scan_root_watch_tree_rebuild(
|
||||
if (strcmp(state->watch_tree_rebuild_path, rebuild_path) == 0) {
|
||||
if (rebuild_depth < state->watch_tree_rebuild_depth)
|
||||
state->watch_tree_rebuild_depth = rebuild_depth;
|
||||
if (rebuild_kind == SCANNER_WATCH_SCAN_ROOT ||
|
||||
(rebuild_kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT &&
|
||||
state->watch_tree_rebuild_kind != SCANNER_WATCH_SCAN_ROOT)) {
|
||||
if (rebuild_kind == SCANNER_WATCH_SCAN_ROOT) {
|
||||
state->watch_tree_rebuild_kind = rebuild_kind;
|
||||
}
|
||||
return;
|
||||
@@ -922,12 +903,23 @@ static bool apply_runtime_config_reload_effects(int kq,
|
||||
const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg,
|
||||
bool scan_topology_changed) {
|
||||
if (strcmp(old_cfg->api_bind_address, new_cfg->api_bind_address) != 0 ||
|
||||
old_cfg->api_port != new_cfg->api_port) {
|
||||
if (!sm_api_service_reconfigure())
|
||||
log_debug(" [CFG] HTTP API reconfigure failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
if (old_cfg->backport_fakelib_enabled &&
|
||||
fakelib_runtime_config_changed(old_cfg, new_cfg))
|
||||
sm_fakelib_game_shutdown();
|
||||
|
||||
sm_kstuff_on_config_reload();
|
||||
|
||||
if (old_cfg->auto_remove_missing_games !=
|
||||
new_cfg->auto_remove_missing_games) {
|
||||
reset_missing_game_cache_timers();
|
||||
}
|
||||
|
||||
if (scan_topology_changed) {
|
||||
clear_scanner_watch_entries();
|
||||
reset_scanner_root_states();
|
||||
@@ -985,9 +977,10 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
mount_backport_overlays(&unstable_found);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
reconcile_missing_app_db_games();
|
||||
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] some discovery mounts remain busy");
|
||||
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = unstable_found;
|
||||
@@ -1024,9 +1017,8 @@ static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
mount_backport_overlays(&unstable_found);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
if (!sm_install_has_pending_work() && !release_scan_runtime_mounts())
|
||||
log_debug(" [IMG] some discovery mounts remain busy");
|
||||
|
||||
if (unstable_found_out)
|
||||
*unstable_found_out = unstable_found;
|
||||
@@ -1073,7 +1065,8 @@ static int find_due_dirty_scan_root(uint64_t now_us) {
|
||||
}
|
||||
|
||||
static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
uint64_t full_resync_due_us) {
|
||||
uint64_t full_resync_due_us,
|
||||
bool include_scan_work) {
|
||||
uint64_t next_deadline = full_resync_due_us;
|
||||
uint64_t install_wake_us = sm_install_next_wake_us(now_us);
|
||||
|
||||
@@ -1093,7 +1086,7 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_config_probe_due_us;
|
||||
}
|
||||
|
||||
if (g_scanner_manual_scan_due_us != 0 &&
|
||||
if (include_scan_work && g_scanner_manual_scan_due_us != 0 &&
|
||||
(next_deadline == 0 || g_scanner_manual_scan_due_us < next_deadline)) {
|
||||
next_deadline = g_scanner_manual_scan_due_us;
|
||||
}
|
||||
@@ -1103,12 +1096,14 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_manual_probe_due_us;
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
||||
if (!state->dirty)
|
||||
continue;
|
||||
if (next_deadline == 0 || state->ready_after_us < next_deadline)
|
||||
next_deadline = state->ready_after_us;
|
||||
if (include_scan_work) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
||||
if (!state->dirty)
|
||||
continue;
|
||||
if (next_deadline == 0 || state->ready_after_us < next_deadline)
|
||||
next_deadline = state->ready_after_us;
|
||||
}
|
||||
}
|
||||
|
||||
return next_deadline;
|
||||
@@ -1222,7 +1217,7 @@ static bool process_scanner_events(int kq, const struct timespec *timeout,
|
||||
|
||||
bool immediate =
|
||||
(event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
||||
if (scanner_event_requires_consistency_cleanup(watch_entry, event->fflags))
|
||||
if (scanner_event_requires_consistency_cleanup(event->fflags))
|
||||
schedule_scan_root_cleanup(watch_entry->scan_root_index);
|
||||
schedule_scan_root_dirty(watch_entry->scan_root_index, now_us, immediate);
|
||||
|
||||
@@ -1285,14 +1280,16 @@ bool sm_scanner_init(void) {
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
|
||||
if (pipe(g_scanner_wake_pipe) != 0) {
|
||||
log_debug(" [SCAN] wake pipe creation failed: %s", strerror(errno));
|
||||
close_scanner_wake_pipe();
|
||||
return false;
|
||||
}
|
||||
if (!set_fd_nonblocking(g_scanner_wake_pipe[0]) ||
|
||||
!set_fd_nonblocking(g_scanner_wake_pipe[1])) {
|
||||
if (!sm_set_fd_nonblocking(g_scanner_wake_pipe[0]) ||
|
||||
!sm_set_fd_nonblocking(g_scanner_wake_pipe[1])) {
|
||||
log_debug(" [SCAN] wake pipe nonblocking setup failed: %s",
|
||||
strerror(errno));
|
||||
close_scanner_wake_pipe();
|
||||
@@ -1324,6 +1321,10 @@ void sm_scanner_wake(void) {
|
||||
(void)write((int)wake_fd, &token, sizeof(token));
|
||||
}
|
||||
|
||||
bool sm_scanner_usb_watches_suspended(void) {
|
||||
return atomic_load_explicit(&g_usb_watches_suspended, memory_order_acquire);
|
||||
}
|
||||
|
||||
bool sm_scanner_run_startup_sync(void) {
|
||||
while (!should_stop_requested()) {
|
||||
while (runtime_sleep_mode_active() && !should_stop_requested())
|
||||
@@ -1369,9 +1370,13 @@ void sm_scanner_run_loop(void) {
|
||||
|
||||
register_config_file_watch(kq, monotonic_time_us());
|
||||
register_manual_file_watch(kq, monotonic_time_us());
|
||||
atomic_store_explicit(&g_usb_watches_suspended, false,
|
||||
memory_order_release);
|
||||
if (!rebuild_all_scan_root_watch_trees(kq)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
close_scanner_config_file();
|
||||
close_scanner_manual_file();
|
||||
request_scanner_shutdown("scanner watcher initialization failed");
|
||||
@@ -1381,6 +1386,7 @@ void sm_scanner_run_loop(void) {
|
||||
uint64_t next_full_resync_us =
|
||||
monotonic_time_us() + scanner_full_resync_interval_us();
|
||||
bool was_sleeping = false;
|
||||
bool scan_work_blocked = false;
|
||||
|
||||
while (true) {
|
||||
if (should_stop_requested()) {
|
||||
@@ -1389,7 +1395,19 @@ void sm_scanner_run_loop(void) {
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
was_sleeping = true;
|
||||
if (!was_sleeping) {
|
||||
if (!suspend_usb_scan_root_watch_trees()) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("USB watcher suspend failed");
|
||||
return;
|
||||
}
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
wake_game_lifecycle_watcher();
|
||||
was_sleeping = true;
|
||||
log_debug("[SLEEP] USB scanner watches suspended");
|
||||
}
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_scanner_wake_pipe[0], &readfds);
|
||||
@@ -1407,17 +1425,29 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
if (was_sleeping) {
|
||||
was_sleeping = false;
|
||||
if (!discard_scanner_events_nowait(kq)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("scanner stale event drain failed");
|
||||
return;
|
||||
}
|
||||
if (!resume_usb_scan_root_watch_trees(kq)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
request_scanner_shutdown("USB watcher resume failed");
|
||||
return;
|
||||
}
|
||||
atomic_store_explicit(&g_usb_watches_suspended, false,
|
||||
memory_order_release);
|
||||
was_sleeping = false;
|
||||
log_debug("[SLEEP] USB scanner watches resumed");
|
||||
}
|
||||
|
||||
bool game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
char scan_reason[128];
|
||||
if (consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
if (!game_mount_busy &&
|
||||
consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, scan_reason, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -1444,10 +1474,14 @@ void sm_scanner_run_loop(void) {
|
||||
}
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
if (sm_game_lifecycle_has_active_game()) {
|
||||
game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
if (game_mount_busy) {
|
||||
next_full_resync_us = 0;
|
||||
} else if (next_full_resync_us == 0) {
|
||||
next_full_resync_us = now_us;
|
||||
scan_work_blocked = true;
|
||||
} else if (scan_work_blocked) {
|
||||
scan_work_blocked = false;
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
}
|
||||
|
||||
if (config_probe_due(now_us)) {
|
||||
@@ -1503,7 +1537,7 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (g_scanner_manual_scan_due_us != 0 &&
|
||||
if (!game_mount_busy && g_scanner_manual_scan_due_us != 0 &&
|
||||
now_us >= g_scanner_manual_scan_due_us) {
|
||||
g_scanner_manual_scan_due_us = 0;
|
||||
invalidate_app_db_title_cache();
|
||||
@@ -1539,7 +1573,8 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (next_full_resync_us != 0 && now_us >= next_full_resync_us) {
|
||||
if (!game_mount_busy && next_full_resync_us != 0 &&
|
||||
now_us >= next_full_resync_us) {
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, NULL, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -1565,14 +1600,16 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int cleanup_root_index = find_pending_cleanup_scan_root();
|
||||
int cleanup_root_index =
|
||||
game_mount_busy ? -1 : find_pending_cleanup_scan_root();
|
||||
if (cleanup_root_index >= 0) {
|
||||
g_scanner_root_states[cleanup_root_index].cleanup_pending = false;
|
||||
cleanup_lost_sources_for_scan_root(get_scan_path(cleanup_root_index));
|
||||
continue;
|
||||
}
|
||||
|
||||
int dirty_root_index = find_due_dirty_scan_root(now_us);
|
||||
int dirty_root_index =
|
||||
game_mount_busy ? -1 : find_due_dirty_scan_root(now_us);
|
||||
if (dirty_root_index >= 0) {
|
||||
bool cleanup_pending =
|
||||
g_scanner_root_states[dirty_root_index].cleanup_pending;
|
||||
@@ -1635,8 +1672,8 @@ void sm_scanner_run_loop(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint64_t deadline_us =
|
||||
compute_next_scan_deadline_us(now_us, next_full_resync_us);
|
||||
uint64_t deadline_us = compute_next_scan_deadline_us(
|
||||
now_us, next_full_resync_us, !game_mount_busy);
|
||||
struct timespec timeout;
|
||||
const struct timespec *timeout_ptr =
|
||||
build_wait_timeout(&timeout, now_us, deadline_us);
|
||||
@@ -1668,4 +1705,6 @@ void sm_scanner_shutdown(void) {
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/* Position-independent bridge copied into SceShellCore executable memory. */
|
||||
.text
|
||||
.p2align 4
|
||||
.global sm_shellcore_bridge_blob_start
|
||||
.global sm_shellcore_bridge_blob_end
|
||||
.global sm_shellcore_bridge_launch_hook
|
||||
.global sm_shellcore_bridge_app_exit_hook
|
||||
.global sm_shellcore_bridge_install_all_hook
|
||||
.global sm_shellcore_bridge_launch_trampoline
|
||||
.global sm_shellcore_bridge_app_exit_trampoline
|
||||
.global sm_shellcore_bridge_install_all_trampoline
|
||||
.global sm_shellcore_bridge_install_title_dir
|
||||
.global sm_shellcore_bridge_install_armed
|
||||
.global sm_shellcore_bridge_install_title_id
|
||||
.global sm_shellcore_bridge_install_dir
|
||||
.global sm_shellcore_bridge_socket
|
||||
.global sm_shellcore_bridge_connect
|
||||
.global sm_shellcore_bridge_read
|
||||
.global sm_shellcore_bridge_write
|
||||
.global sm_shellcore_bridge_close
|
||||
|
||||
sm_shellcore_bridge_blob_start:
|
||||
sm_shellcore_bridge_launch_hook:
|
||||
pushq %rbx
|
||||
pushq %r12
|
||||
movq %rsi, %r12
|
||||
pushfq
|
||||
pushq %rax
|
||||
pushq %rdi
|
||||
pushq %rsi
|
||||
pushq %rdx
|
||||
pushq %rcx
|
||||
pushq %r8
|
||||
pushq %r9
|
||||
pushq %r10
|
||||
pushq %r11
|
||||
subq $8, %rsp
|
||||
movl $1, %edi
|
||||
xorl %edx, %edx
|
||||
/* RSI still contains title_id. */
|
||||
call sm_shellcore_bridge_exchange
|
||||
addq $8, %rsp
|
||||
popq %r11
|
||||
popq %r10
|
||||
popq %r9
|
||||
popq %r8
|
||||
popq %rcx
|
||||
popq %rdx
|
||||
popq %rsi
|
||||
popq %rdi
|
||||
popq %rax
|
||||
popfq
|
||||
subq $8, %rsp
|
||||
call *sm_shellcore_bridge_launch_trampoline(%rip)
|
||||
addq $8, %rsp
|
||||
movq %rax, %rbx
|
||||
testl %eax, %eax
|
||||
jns 1f
|
||||
movl $2, %edi
|
||||
movq %r12, %rsi
|
||||
xorl %edx, %edx
|
||||
subq $8, %rsp
|
||||
call sm_shellcore_bridge_exchange
|
||||
addq $8, %rsp
|
||||
1:
|
||||
movq %rbx, %rax
|
||||
popq %r12
|
||||
popq %rbx
|
||||
ret
|
||||
|
||||
.p2align 4
|
||||
/*
|
||||
* LncManager::onAppExit(uint32_t app_id) owns the SysCore application-exit
|
||||
* event. Notify ShadowMount only after the original method returns, after its
|
||||
* firmware-specific exit cleanup. The app id is the second argument.
|
||||
*/
|
||||
sm_shellcore_bridge_app_exit_hook:
|
||||
pushq %rbx
|
||||
movl %esi, %ebx
|
||||
call *sm_shellcore_bridge_app_exit_trampoline(%rip)
|
||||
pushq %rax
|
||||
subq $8, %rsp
|
||||
movl $3, %edi
|
||||
xorl %esi, %esi
|
||||
movl %ebx, %edx
|
||||
call sm_shellcore_bridge_exchange
|
||||
addq $8, %rsp
|
||||
popq %rax
|
||||
popq %rbx
|
||||
ret
|
||||
|
||||
.p2align 4
|
||||
/*
|
||||
* FW 12+ removed the public AppInstallTitleDir RPC. AppInstallAll still
|
||||
* reaches this internal dispatcher, so one explicitly armed request is
|
||||
* redirected to the internal TitleDir implementation. Every ordinary
|
||||
* AppInstallAll call follows its original trampoline unchanged.
|
||||
*/
|
||||
.p2align 4
|
||||
sm_shellcore_bridge_install_all_hook:
|
||||
cmpb $0, sm_shellcore_bridge_install_armed(%rip)
|
||||
je 1f
|
||||
movb $0, sm_shellcore_bridge_install_armed(%rip)
|
||||
leaq sm_shellcore_bridge_install_title_id(%rip), %rdi
|
||||
leaq sm_shellcore_bridge_install_dir(%rip), %rsi
|
||||
movl $1, %edx
|
||||
xorl %ecx, %ecx
|
||||
jmp *sm_shellcore_bridge_install_title_dir(%rip)
|
||||
1:
|
||||
jmp *sm_shellcore_bridge_install_all_trampoline(%rip)
|
||||
|
||||
/* int exchange(uint16_t operation, const char *title_id, uint32_t app_id) */
|
||||
.p2align 4
|
||||
sm_shellcore_bridge_exchange:
|
||||
pushq %rbp
|
||||
movq %rsp, %rbp
|
||||
pushq %rbx
|
||||
pushq %r12
|
||||
subq $160, %rsp
|
||||
|
||||
/* Build the fixed 40-byte request. */
|
||||
movq $0, -64(%rbp)
|
||||
movq $0, -56(%rbp)
|
||||
movq $0, -48(%rbp)
|
||||
movq $0, -40(%rbp)
|
||||
movq $0, -32(%rbp)
|
||||
movl $0x534d5343, -64(%rbp)
|
||||
movw $1, -60(%rbp)
|
||||
movw %di, -58(%rbp)
|
||||
movl %edx, -56(%rbp)
|
||||
testq %rsi, %rsi
|
||||
je 2f
|
||||
leaq -56(%rbp), %rdi
|
||||
movl $31, %ecx
|
||||
1:
|
||||
movb (%rsi), %al
|
||||
testb %al, %al
|
||||
je 2f
|
||||
movb %al, (%rdi)
|
||||
incq %rsi
|
||||
incq %rdi
|
||||
decl %ecx
|
||||
jne 1b
|
||||
2:
|
||||
/* Use libkernel_sys wrappers. Direct syscalls from SceShellCore are fatal. */
|
||||
movl $1, %edi
|
||||
movl $1, %esi
|
||||
xorl %edx, %edx
|
||||
call *sm_shellcore_bridge_socket(%rip)
|
||||
testl %eax, %eax
|
||||
js 8f
|
||||
movl %eax, %ebx
|
||||
|
||||
/* connect(fd, &sockaddr, sockaddr.sun_len) */
|
||||
movl %ebx, %edi
|
||||
leaq sm_shellcore_bridge_sockaddr(%rip), %rsi
|
||||
movl $31, %edx
|
||||
call *sm_shellcore_bridge_connect(%rip)
|
||||
testl %eax, %eax
|
||||
jne 7f
|
||||
|
||||
/* write(fd, &request, sizeof(request)) */
|
||||
movl %ebx, %edi
|
||||
leaq -64(%rbp), %rsi
|
||||
movl $40, %edx
|
||||
call *sm_shellcore_bridge_write(%rip)
|
||||
cmpq $40, %rax
|
||||
jne 7f
|
||||
|
||||
/* Only status is needed by launch and launch-failure requests. */
|
||||
movl %ebx, %edi
|
||||
leaq -68(%rbp), %rsi
|
||||
movl $4, %edx
|
||||
call *sm_shellcore_bridge_read(%rip)
|
||||
cmpq $4, %rax
|
||||
jne 7f
|
||||
movl -68(%rbp), %r12d
|
||||
jmp 5f
|
||||
7:
|
||||
movl $5, %r12d
|
||||
5:
|
||||
movl %ebx, %edi
|
||||
call *sm_shellcore_bridge_close(%rip)
|
||||
movl %r12d, %eax
|
||||
jmp 9f
|
||||
8:
|
||||
movl $5, %eax
|
||||
9:
|
||||
addq $160, %rsp
|
||||
popq %r12
|
||||
popq %rbx
|
||||
popq %rbp
|
||||
ret
|
||||
|
||||
.p2align 3
|
||||
sm_shellcore_bridge_launch_trampoline:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_app_exit_trampoline:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_install_all_trampoline:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_install_title_dir:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_socket:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_connect:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_read:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_write:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_close:
|
||||
.quad 0
|
||||
sm_shellcore_bridge_sockaddr:
|
||||
.byte 31, 1
|
||||
.asciz "/system_tmp/shadowmount.sock"
|
||||
.zero 75
|
||||
.p2align 3
|
||||
sm_shellcore_bridge_install_armed:
|
||||
.byte 0
|
||||
sm_shellcore_bridge_install_title_id:
|
||||
.zero 32
|
||||
sm_shellcore_bridge_install_dir:
|
||||
.zero 1024
|
||||
sm_shellcore_bridge_blob_end:
|
||||
.section .note.GNU-stack,"",@progbits
|
||||
+13
-14
@@ -1,5 +1,7 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_game_lifecycle.h"
|
||||
@@ -7,7 +9,6 @@
|
||||
#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
|
||||
@@ -64,7 +65,7 @@ static pthread_cond_t g_shellcore_flag_cond = PTHREAD_COND_INITIALIZER;
|
||||
static bool g_shellcore_flag_thread_started = false;
|
||||
static bool g_shellcore_flag_start_ready = false;
|
||||
static bool g_shellcore_flag_start_success = false;
|
||||
static volatile sig_atomic_t g_shellcore_flag_stop_requested = 0;
|
||||
static atomic_bool g_shellcore_flag_stop_requested = false;
|
||||
|
||||
static const shellcore_flag_bit_desc_t g_system_state_mgr_status_bits[] = {
|
||||
{0x0000000000000001ULL, "CEC_ONE_TOUCH_PLAY_COMMAND"},
|
||||
@@ -509,11 +510,7 @@ static void set_shellcore_flag_start_result(bool success) {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
(void)request_runtime_sleep_mode(true, reason);
|
||||
}
|
||||
|
||||
static void reset_shellcore_flag_state(shellcore_flag_monitor_t *flag) {
|
||||
@@ -627,8 +624,6 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
bool entered_resume_working = false;
|
||||
bool entered_shellui_shutdown_in_progress = false;
|
||||
bool is_system_state_mgr_info = false;
|
||||
unsigned current_state = 0;
|
||||
unsigned previous_state = 0;
|
||||
|
||||
if (!flag || !flag->is_open)
|
||||
return;
|
||||
@@ -653,8 +648,8 @@ static void poll_shellcore_flag(shellcore_flag_monitor_t *flag) {
|
||||
|
||||
is_system_state_mgr_info = strcmp(flag->name, "SceSystemStateMgrInfo") == 0;
|
||||
if (is_system_state_mgr_info) {
|
||||
current_state = (unsigned)(result_pattern & 0xFFFFu);
|
||||
previous_state =
|
||||
unsigned current_state = (unsigned)(result_pattern & 0xFFFFu);
|
||||
unsigned previous_state =
|
||||
flag->has_last_pattern ? (unsigned)(flag->last_pattern & 0xFFFFu) : 0;
|
||||
|
||||
entered_shutdown_on_going =
|
||||
@@ -730,7 +725,9 @@ static void *shellcore_flag_thread_main(void *arg) {
|
||||
sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]));
|
||||
set_shellcore_flag_start_result(true);
|
||||
|
||||
while (!g_shellcore_flag_stop_requested && !should_stop_requested()) {
|
||||
while (!atomic_load_explicit(&g_shellcore_flag_stop_requested,
|
||||
memory_order_relaxed) &&
|
||||
!should_stop_requested()) {
|
||||
for (size_t i = 0; i < sizeof(g_shellcore_flags) / sizeof(g_shellcore_flags[0]);
|
||||
++i) {
|
||||
poll_shellcore_flag(&g_shellcore_flags[i]);
|
||||
@@ -749,7 +746,8 @@ bool sm_shellcore_flags_start(void) {
|
||||
if (g_shellcore_flag_thread_started)
|
||||
return g_shellcore_flag_start_success;
|
||||
|
||||
g_shellcore_flag_stop_requested = 0;
|
||||
atomic_store_explicit(&g_shellcore_flag_stop_requested, false,
|
||||
memory_order_relaxed);
|
||||
g_shellcore_flag_start_ready = false;
|
||||
g_shellcore_flag_start_success = false;
|
||||
close_shellcore_flags();
|
||||
@@ -780,7 +778,8 @@ void sm_shellcore_flags_stop(void) {
|
||||
if (!g_shellcore_flag_thread_started)
|
||||
return;
|
||||
|
||||
g_shellcore_flag_stop_requested = 1;
|
||||
atomic_store_explicit(&g_shellcore_flag_stop_requested, true,
|
||||
memory_order_relaxed);
|
||||
pthread_join(g_shellcore_flag_thread, NULL);
|
||||
g_shellcore_flag_thread_started = false;
|
||||
g_shellcore_flag_start_ready = false;
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_remote.h"
|
||||
|
||||
#define SHELLCORE_BASE_HOOK_COUNT 2u
|
||||
#define SHELLCORE_MAX_HOOK_COUNT 3u
|
||||
#define MAX_HOOK_PROLOGUE_SIZE 32u
|
||||
#define ABSOLUTE_JUMP_SIZE 12u
|
||||
|
||||
/*
|
||||
* Private payload-SDK ABI from src/crt/kernel.h. The public
|
||||
* <ps5/kernel.h> intentionally omits dynlib_obj_t/kernel_dynlib_obj(), but
|
||||
* kernel_dynlib_obj() copies the complete 0x180-byte object. Keep the local
|
||||
* definition layout-checked because ShellCore bridge placement needs the
|
||||
* module bounds and its executable eh_frame area.
|
||||
*/
|
||||
typedef struct {
|
||||
uintptr_t next;
|
||||
uintptr_t path;
|
||||
uintptr_t unknown0[2];
|
||||
uint32_t refcount;
|
||||
uintptr_t handle;
|
||||
uintptr_t mapbase;
|
||||
uintptr_t mapsize;
|
||||
uintptr_t textsize;
|
||||
uintptr_t database;
|
||||
uintptr_t datasize;
|
||||
uintptr_t unknown1;
|
||||
uintptr_t unknown1size;
|
||||
uintptr_t entry;
|
||||
uintptr_t unknown2;
|
||||
uintptr_t vaddrbase;
|
||||
uint32_t tlsindex;
|
||||
uintptr_t tlsinit;
|
||||
uintptr_t tlsinitsize;
|
||||
uintptr_t tlssize;
|
||||
uintptr_t tlsoffset;
|
||||
uintptr_t tlsalign;
|
||||
uintptr_t pltgot;
|
||||
uintptr_t unknown3[6];
|
||||
uintptr_t init;
|
||||
uintptr_t fini;
|
||||
uintptr_t eh_frame_hdr;
|
||||
uintptr_t eh_frame_hdr_size;
|
||||
uintptr_t eh_frame;
|
||||
uintptr_t eh_frame_size;
|
||||
int32_t status;
|
||||
int32_t flags;
|
||||
uintptr_t unknown4[5];
|
||||
uintptr_t dynsec;
|
||||
uintptr_t unknown5[6];
|
||||
} shellcore_dynlib_obj_t;
|
||||
|
||||
_Static_assert(offsetof(shellcore_dynlib_obj_t, mapbase) == 0x30,
|
||||
"unexpected dynlib_obj mapbase offset");
|
||||
_Static_assert(offsetof(shellcore_dynlib_obj_t, eh_frame) == 0x108,
|
||||
"unexpected dynlib_obj eh_frame offset");
|
||||
_Static_assert(sizeof(shellcore_dynlib_obj_t) == 0x180,
|
||||
"unexpected dynlib_obj size");
|
||||
|
||||
int kernel_dynlib_obj(pid_t pid, uint32_t handle, shellcore_dynlib_obj_t *obj);
|
||||
|
||||
extern const uint8_t sm_shellcore_bridge_blob_start[];
|
||||
extern const uint8_t sm_shellcore_bridge_blob_end[];
|
||||
extern const uint8_t sm_shellcore_bridge_launch_hook[];
|
||||
extern const uint8_t sm_shellcore_bridge_app_exit_hook[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_all_hook[];
|
||||
extern const uint8_t sm_shellcore_bridge_launch_trampoline[];
|
||||
extern const uint8_t sm_shellcore_bridge_app_exit_trampoline[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_all_trampoline[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_title_dir[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_armed[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_title_id[];
|
||||
extern const uint8_t sm_shellcore_bridge_install_dir[];
|
||||
extern const uint8_t sm_shellcore_bridge_socket[];
|
||||
extern const uint8_t sm_shellcore_bridge_connect[];
|
||||
extern const uint8_t sm_shellcore_bridge_read[];
|
||||
extern const uint8_t sm_shellcore_bridge_write[];
|
||||
extern const uint8_t sm_shellcore_bridge_close[];
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *slot;
|
||||
const char *name;
|
||||
} shellcore_import_t;
|
||||
|
||||
static const shellcore_import_t k_bridge_imports[] = {
|
||||
{sm_shellcore_bridge_socket, "socket"},
|
||||
{sm_shellcore_bridge_connect, "connect"},
|
||||
{sm_shellcore_bridge_read, "read"},
|
||||
{sm_shellcore_bridge_write, "write"},
|
||||
{sm_shellcore_bridge_close, "close"},
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool installed;
|
||||
sm_shellcore_remote_t remote;
|
||||
uintptr_t bridge_address;
|
||||
size_t hook_count;
|
||||
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT];
|
||||
uint8_t original[SHELLCORE_MAX_HOOK_COUNT][MAX_HOOK_PROLOGUE_SIZE];
|
||||
uint8_t original_size[SHELLCORE_MAX_HOOK_COUNT];
|
||||
} shellcore_hooks_state_t;
|
||||
|
||||
static shellcore_hooks_state_t g_hooks;
|
||||
static pthread_mutex_t g_install_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static uintptr_t remote_bridge_symbol(const uint8_t *symbol) {
|
||||
return g_hooks.bridge_address +
|
||||
(uintptr_t)(symbol - sm_shellcore_bridge_blob_start);
|
||||
}
|
||||
|
||||
static void build_absolute_jump(uint8_t jump[ABSOLUTE_JUMP_SIZE],
|
||||
uintptr_t destination) {
|
||||
jump[0] = 0x48;
|
||||
jump[1] = 0xb8;
|
||||
memcpy(jump + 2, &destination, sizeof(destination));
|
||||
jump[10] = 0xff;
|
||||
jump[11] = 0xe0;
|
||||
}
|
||||
|
||||
static bool patch_remote_jump(pid_t pid, uintptr_t source,
|
||||
uintptr_t destination, size_t patch_size) {
|
||||
if (patch_size < ABSOLUTE_JUMP_SIZE || patch_size > MAX_HOOK_PROLOGUE_SIZE) {
|
||||
return false;
|
||||
}
|
||||
uint8_t patch[MAX_HOOK_PROLOGUE_SIZE];
|
||||
memset(patch, 0x90, patch_size);
|
||||
build_absolute_jump(patch, destination);
|
||||
if (!sm_remote_process_write(pid, source, patch, patch_size))
|
||||
return false;
|
||||
uint8_t verify[MAX_HOOK_PROLOGUE_SIZE];
|
||||
return sm_remote_process_read(pid, source, verify, patch_size) &&
|
||||
memcmp(verify, patch, patch_size) == 0;
|
||||
}
|
||||
|
||||
static bool verify_remote_bytes(pid_t pid, uintptr_t address,
|
||||
const void *expected, size_t size) {
|
||||
uint8_t buffer[256];
|
||||
const uint8_t *bytes = (const uint8_t *)expected;
|
||||
while (size != 0) {
|
||||
size_t chunk = size < sizeof(buffer) ? size : sizeof(buffer);
|
||||
if (!sm_remote_process_read(pid, address, buffer, chunk) ||
|
||||
memcmp(buffer, bytes, chunk) != 0) {
|
||||
return false;
|
||||
}
|
||||
address += chunk;
|
||||
bytes += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool append_trampoline(uint8_t *bridge, size_t bridge_capacity,
|
||||
size_t *cursor, const uint8_t *original,
|
||||
size_t original_size, uintptr_t return_address,
|
||||
uintptr_t remote_base,
|
||||
uintptr_t *remote_trampoline_out) {
|
||||
size_t aligned = (*cursor + 15u) & ~(size_t)15u;
|
||||
size_t required = aligned + original_size + ABSOLUTE_JUMP_SIZE;
|
||||
if (required > bridge_capacity)
|
||||
return false;
|
||||
memcpy(bridge + aligned, original, original_size);
|
||||
build_absolute_jump(bridge + aligned + original_size, return_address);
|
||||
*remote_trampoline_out = remote_base + aligned;
|
||||
*cursor = required;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void set_bridge_pointer(uint8_t *bridge, const uint8_t *local_symbol,
|
||||
uintptr_t value) {
|
||||
size_t offset = (size_t)(local_symbol - sm_shellcore_bridge_blob_start);
|
||||
memcpy(bridge + offset, &value, sizeof(value));
|
||||
}
|
||||
|
||||
static bool resolve_bridge_imports(pid_t pid, uint8_t *bridge) {
|
||||
uint32_t handle = UINT32_MAX;
|
||||
if (kernel_dynlib_handle(pid, "libkernel_sys.sprx", &handle) != 0)
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(k_bridge_imports) / sizeof(k_bridge_imports[0]);
|
||||
++i) {
|
||||
uintptr_t address =
|
||||
(uintptr_t)kernel_dynlib_dlsym(pid, handle, k_bridge_imports[i].name);
|
||||
if (address == 0) {
|
||||
log_debug(" [SHELLCORE] failed to resolve libkernel %s",
|
||||
k_bridge_imports[i].name);
|
||||
return false;
|
||||
}
|
||||
set_bridge_pointer(bridge, k_bridge_imports[i].slot, address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static pid_t find_shellcore_pid(void) {
|
||||
pid_t pid = find_pid_by_name("SceShellCore", true);
|
||||
if (pid <= 0)
|
||||
pid = find_pid_by_name("SceShellCore.elf", true);
|
||||
return pid;
|
||||
}
|
||||
|
||||
bool sm_shellcore_install_bridge_enabled(void) {
|
||||
uint16_t firmware =
|
||||
(uint16_t)((kernel_get_fw_version() >> 16) & 0xffffu);
|
||||
return firmware >= 0x1200u;
|
||||
}
|
||||
|
||||
static bool install_hooks_for_pid(pid_t pid) {
|
||||
const size_t blob_size =
|
||||
(size_t)(sm_shellcore_bridge_blob_end - sm_shellcore_bridge_blob_start);
|
||||
const size_t bridge_capacity =
|
||||
blob_size + SHELLCORE_MAX_HOOK_COUNT *
|
||||
(MAX_HOOK_PROLOGUE_SIZE + ABSOLUTE_JUMP_SIZE + 16u);
|
||||
uint8_t *bridge = calloc(1, bridge_capacity);
|
||||
if (!bridge)
|
||||
return false;
|
||||
memcpy(bridge, sm_shellcore_bridge_blob_start, blob_size);
|
||||
|
||||
if (kill(pid, SIGSTOP) != 0) {
|
||||
free(bridge);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
sm_shellcore_remote_t remote;
|
||||
shellcore_dynlib_obj_t object;
|
||||
if (!sm_shellcore_remote_resolve(pid, &remote) ||
|
||||
kernel_dynlib_obj(pid, 0, &object) != 0) {
|
||||
log_debug(" [SHELLCORE] stopped-process layout resolution failed");
|
||||
goto done;
|
||||
}
|
||||
if (object.mapbase != remote.image_base ||
|
||||
object.mapbase > UINTPTR_MAX - object.mapsize ||
|
||||
object.eh_frame < object.mapbase ||
|
||||
object.eh_frame_size < bridge_capacity ||
|
||||
object.eh_frame > UINTPTR_MAX - bridge_capacity ||
|
||||
object.eh_frame + bridge_capacity > object.mapbase + object.mapsize) {
|
||||
log_debug(" [SHELLCORE] invalid layout: base=0x%lx map=0x%lx+0x%lx "
|
||||
"eh=0x%lx+0x%lx bridge=0x%zx",
|
||||
(unsigned long)remote.image_base, (unsigned long)object.mapbase,
|
||||
(unsigned long)object.mapsize, (unsigned long)object.eh_frame,
|
||||
(unsigned long)object.eh_frame_size, bridge_capacity);
|
||||
goto done;
|
||||
}
|
||||
log_debug(" [SHELLCORE] layout validated: map=0x%lx+0x%lx eh=0x%lx+0x%lx",
|
||||
(unsigned long)object.mapbase, (unsigned long)object.mapsize,
|
||||
(unsigned long)object.eh_frame,
|
||||
(unsigned long)object.eh_frame_size);
|
||||
|
||||
const bool install_hook_enabled = sm_shellcore_install_bridge_enabled();
|
||||
const size_t hook_count = install_hook_enabled
|
||||
? SHELLCORE_MAX_HOOK_COUNT
|
||||
: SHELLCORE_BASE_HOOK_COUNT;
|
||||
uintptr_t targets[SHELLCORE_MAX_HOOK_COUNT] = {
|
||||
remote.targets[SM_SHELLCORE_TARGET_LAUNCH_APP],
|
||||
remote.targets[SM_SHELLCORE_TARGET_APP_EXIT],
|
||||
remote.targets[SM_SHELLCORE_TARGET_INSTALL_ALL],
|
||||
};
|
||||
sm_shellcore_target_t target_ids[SHELLCORE_MAX_HOOK_COUNT] = {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP,
|
||||
SM_SHELLCORE_TARGET_APP_EXIT,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < hook_count; ++i) {
|
||||
uint8_t patch_size = remote.offsets->targets[target_ids[i]].patch_size;
|
||||
if (patch_size < ABSOLUTE_JUMP_SIZE ||
|
||||
patch_size > MAX_HOOK_PROLOGUE_SIZE ||
|
||||
!sm_remote_process_read(pid, targets[i], g_hooks.original[i],
|
||||
patch_size)) {
|
||||
goto done;
|
||||
}
|
||||
static const uint8_t function_prologue[] = {0x55, 0x48, 0x89, 0xe5};
|
||||
if (memcmp(g_hooks.original[i], function_prologue,
|
||||
sizeof(function_prologue)) != 0) {
|
||||
log_debug(" [SHELLCORE] unexpected prologue: %s at 0x%lx",
|
||||
sm_shellcore_target_name(target_ids[i]),
|
||||
(unsigned long)targets[i]);
|
||||
goto done;
|
||||
}
|
||||
g_hooks.original_size[i] = patch_size;
|
||||
}
|
||||
|
||||
size_t cursor = blob_size;
|
||||
uintptr_t launch_trampoline = 0;
|
||||
uintptr_t app_exit_trampoline = 0;
|
||||
uintptr_t install_all_trampoline = 0;
|
||||
if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[0],
|
||||
g_hooks.original_size[0],
|
||||
targets[0] + g_hooks.original_size[0], object.eh_frame,
|
||||
&launch_trampoline)) {
|
||||
goto done;
|
||||
}
|
||||
set_bridge_pointer(bridge, sm_shellcore_bridge_launch_trampoline,
|
||||
launch_trampoline);
|
||||
if (!append_trampoline(bridge, bridge_capacity, &cursor, g_hooks.original[1],
|
||||
g_hooks.original_size[1],
|
||||
targets[1] + g_hooks.original_size[1], object.eh_frame,
|
||||
&app_exit_trampoline)) {
|
||||
goto done;
|
||||
}
|
||||
set_bridge_pointer(bridge, sm_shellcore_bridge_app_exit_trampoline,
|
||||
app_exit_trampoline);
|
||||
if (install_hook_enabled) {
|
||||
if (!append_trampoline(bridge, bridge_capacity, &cursor,
|
||||
g_hooks.original[2], g_hooks.original_size[2],
|
||||
targets[2] + g_hooks.original_size[2],
|
||||
object.eh_frame, &install_all_trampoline)) {
|
||||
goto done;
|
||||
}
|
||||
set_bridge_pointer(bridge, sm_shellcore_bridge_install_all_trampoline,
|
||||
install_all_trampoline);
|
||||
set_bridge_pointer(
|
||||
bridge, sm_shellcore_bridge_install_title_dir,
|
||||
remote.targets[SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR]);
|
||||
}
|
||||
if (!resolve_bridge_imports(pid, bridge))
|
||||
goto done;
|
||||
|
||||
if (kernel_mprotect(pid, (intptr_t)object.mapbase, object.mapsize,
|
||||
PROT_READ | PROT_WRITE | PROT_EXEC) != 0 ||
|
||||
!sm_remote_process_write(pid, object.eh_frame, bridge, cursor) ||
|
||||
!verify_remote_bytes(pid, object.eh_frame, bridge, cursor)) {
|
||||
log_debug(" [SHELLCORE] bridge write verification failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
uintptr_t launch_hook =
|
||||
object.eh_frame + (uintptr_t)(sm_shellcore_bridge_launch_hook -
|
||||
sm_shellcore_bridge_blob_start);
|
||||
uintptr_t app_exit_hook =
|
||||
object.eh_frame + (uintptr_t)(sm_shellcore_bridge_app_exit_hook -
|
||||
sm_shellcore_bridge_blob_start);
|
||||
uintptr_t install_all_hook =
|
||||
object.eh_frame +
|
||||
(uintptr_t)(sm_shellcore_bridge_install_all_hook -
|
||||
sm_shellcore_bridge_blob_start);
|
||||
if (!patch_remote_jump(pid, targets[0], launch_hook,
|
||||
g_hooks.original_size[0]) ||
|
||||
!patch_remote_jump(pid, targets[1], app_exit_hook,
|
||||
g_hooks.original_size[1]) ||
|
||||
(install_hook_enabled &&
|
||||
!patch_remote_jump(pid, targets[2], install_all_hook,
|
||||
g_hooks.original_size[2]))) {
|
||||
goto rollback;
|
||||
}
|
||||
|
||||
g_hooks.remote = remote;
|
||||
g_hooks.bridge_address = object.eh_frame;
|
||||
g_hooks.hook_count = hook_count;
|
||||
memcpy(g_hooks.target_ids, target_ids,
|
||||
hook_count * sizeof(g_hooks.target_ids[0]));
|
||||
g_hooks.installed = true;
|
||||
ok = true;
|
||||
log_debug(" [SHELLCORE] hooks installed: fw=%s pid=%ld lifecycle=1 "
|
||||
"app_exit=1 install=%d",
|
||||
remote.offsets->name, (long)pid, install_hook_enabled ? 1 : 0);
|
||||
goto done;
|
||||
|
||||
rollback:
|
||||
for (size_t i = 0; i < hook_count; ++i) {
|
||||
(void)sm_remote_process_write(pid, targets[i], g_hooks.original[i],
|
||||
g_hooks.original_size[i]);
|
||||
}
|
||||
|
||||
done:
|
||||
free(bridge);
|
||||
(void)kill(pid, SIGCONT);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool sm_shellcore_hooks_start(void) {
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (g_hooks.installed)
|
||||
goto installed;
|
||||
memset(&g_hooks, 0, sizeof(g_hooks));
|
||||
pid_t pid = find_shellcore_pid();
|
||||
if (pid <= 0) {
|
||||
log_debug(" [SHELLCORE] process not found");
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
bool ok = install_hooks_for_pid(pid);
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return ok;
|
||||
|
||||
installed:
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_hooks_stop(void) {
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (!g_hooks.installed) {
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid = g_hooks.remote.pid;
|
||||
if (kill(pid, SIGSTOP) == 0) {
|
||||
for (size_t i = 0; i < g_hooks.hook_count; ++i) {
|
||||
sm_shellcore_target_t target = g_hooks.target_ids[i];
|
||||
(void)sm_remote_process_write(pid, g_hooks.remote.targets[target],
|
||||
g_hooks.original[i],
|
||||
g_hooks.original_size[i]);
|
||||
}
|
||||
(void)kill(pid, SIGCONT);
|
||||
}
|
||||
memset(&g_hooks, 0, sizeof(g_hooks));
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
}
|
||||
|
||||
bool sm_shellcore_app_exit_hook_active(void) {
|
||||
int saved_errno = errno;
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
bool active = g_hooks.installed && g_hooks.remote.pid > 0;
|
||||
if (active && kill(g_hooks.remote.pid, 0) != 0 && errno == ESRCH)
|
||||
active = false;
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
errno = saved_errno;
|
||||
return active;
|
||||
}
|
||||
|
||||
bool sm_shellcore_install_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out) {
|
||||
if (!title_id || !install_dir || !result_out)
|
||||
return false;
|
||||
|
||||
size_t title_len = strnlen(title_id, MAX_TITLE_ID);
|
||||
size_t dir_len = strnlen(install_dir, MAX_PATH);
|
||||
if (title_len == 0 || title_len >= MAX_TITLE_ID || dir_len == 0 ||
|
||||
dir_len >= MAX_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_install_mutex);
|
||||
if (!g_hooks.installed ||
|
||||
g_hooks.hook_count < SHELLCORE_MAX_HOOK_COUNT) {
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
char remote_title[MAX_TITLE_ID] = {0};
|
||||
char remote_dir[MAX_PATH] = {0};
|
||||
memcpy(remote_title, title_id, title_len);
|
||||
memcpy(remote_dir, install_dir, dir_len);
|
||||
|
||||
pid_t pid = g_hooks.remote.pid;
|
||||
uint8_t armed = 1;
|
||||
bool written =
|
||||
sm_remote_process_write(pid,
|
||||
remote_bridge_symbol(
|
||||
sm_shellcore_bridge_install_title_id),
|
||||
remote_title, sizeof(remote_title)) &&
|
||||
sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_dir),
|
||||
remote_dir, sizeof(remote_dir)) &&
|
||||
sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed),
|
||||
&armed, sizeof(armed));
|
||||
if (!written) {
|
||||
armed = 0;
|
||||
(void)sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
|
||||
sizeof(armed));
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int result = sceAppInstUtilAppInstallAll(NULL);
|
||||
uint8_t remaining = 1;
|
||||
bool read_back = sm_remote_process_read(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &remaining,
|
||||
sizeof(remaining));
|
||||
bool consumed = read_back && remaining == 0;
|
||||
if (!consumed) {
|
||||
armed = 0;
|
||||
(void)sm_remote_process_write(
|
||||
pid, remote_bridge_symbol(sm_shellcore_bridge_install_armed), &armed,
|
||||
sizeof(armed));
|
||||
}
|
||||
pthread_mutex_unlock(&g_install_mutex);
|
||||
|
||||
if (!consumed) {
|
||||
log_debug(" [SHELLCORE] AppInstallAll trigger was not consumed: %s",
|
||||
title_id);
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SHELLCORE] AppInstallAll bridge consumed: %s result=0x%08x",
|
||||
title_id, (uint32_t)result);
|
||||
*result_out = result;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "sm_shellcore_offsets.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
static const sm_shellcore_firmware_offsets_t g_shellcore_offsets[] = {
|
||||
#include "sm_shellcore_offsets.inc"
|
||||
};
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *
|
||||
sm_shellcore_offsets_for_firmware(uint32_t raw_firmware) {
|
||||
uint16_t firmware = (uint16_t)((raw_firmware >> 16) & 0xffffu);
|
||||
for (size_t i = 0;
|
||||
i < sizeof(g_shellcore_offsets) / sizeof(g_shellcore_offsets[0]); ++i) {
|
||||
if (g_shellcore_offsets[i].firmware == firmware)
|
||||
return &g_shellcore_offsets[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *sm_shellcore_target_name(sm_shellcore_target_t target) {
|
||||
switch (target) {
|
||||
case SM_SHELLCORE_TARGET_LAUNCH_APP:
|
||||
return "launchApp";
|
||||
case SM_SHELLCORE_TARGET_APP_EXIT:
|
||||
return "onAppExit";
|
||||
case SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR:
|
||||
return "AppInstallTitleDirMain";
|
||||
case SM_SHELLCORE_TARGET_INSTALL_ALL:
|
||||
return "AppInstallAll";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/* Generated by tools/generate_shellcore_offsets_from_files.py. */
|
||||
{
|
||||
.firmware = 0x0100u,
|
||||
.name = "1.00",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0101u,
|
||||
.name = "1.01",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0102u,
|
||||
.name = "1.02",
|
||||
.targets = {{.offset = 0x3465a0u, .patch_size = 12u}, {.offset = 0x3498e0u, .patch_size = 12u}, {.offset = 0x1adc50u, .patch_size = 12u}, {.offset = 0xa05c40u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0112u,
|
||||
.name = "1.12",
|
||||
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x349cf0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa063a0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0114u,
|
||||
.name = "1.14",
|
||||
.targets = {{.offset = 0x3469b0u, .patch_size = 12u}, {.offset = 0x349cf0u, .patch_size = 12u}, {.offset = 0x1adc30u, .patch_size = 12u}, {.offset = 0xa06660u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0200u,
|
||||
.name = "2.00",
|
||||
.targets = {{.offset = 0x3a4660u, .patch_size = 12u}, {.offset = 0x3a7cf0u, .patch_size = 12u}, {.offset = 0x1df8f0u, .patch_size = 12u}, {.offset = 0xaf3740u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0220u,
|
||||
.name = "2.20",
|
||||
.targets = {{.offset = 0x3a4930u, .patch_size = 12u}, {.offset = 0x3a7fc0u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3aa0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0225u,
|
||||
.name = "2.25",
|
||||
.targets = {{.offset = 0x3a4e80u, .patch_size = 12u}, {.offset = 0x3a8510u, .patch_size = 12u}, {.offset = 0x1dfb80u, .patch_size = 12u}, {.offset = 0xaf3fe0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0226u,
|
||||
.name = "2.26",
|
||||
.targets = {{.offset = 0x3a65f0u, .patch_size = 12u}, {.offset = 0x3a9c80u, .patch_size = 12u}, {.offset = 0x1e0d50u, .patch_size = 12u}, {.offset = 0xaf57a0u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0230u,
|
||||
.name = "2.30",
|
||||
.targets = {{.offset = 0x3a6d90u, .patch_size = 12u}, {.offset = 0x3aa420u, .patch_size = 12u}, {.offset = 0x1e0f00u, .patch_size = 12u}, {.offset = 0xaf6360u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0250u,
|
||||
.name = "2.50",
|
||||
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3ab7d0u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0270u,
|
||||
.name = "2.70",
|
||||
.targets = {{.offset = 0x3a8140u, .patch_size = 12u}, {.offset = 0x3ab7d0u, .patch_size = 12u}, {.offset = 0x1e0ca0u, .patch_size = 12u}, {.offset = 0xaf7700u, .patch_size = 14u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0300u,
|
||||
.name = "3.00",
|
||||
.targets = {{.offset = 0x3f1960u, .patch_size = 12u}, {.offset = 0x3f51e0u, .patch_size = 12u}, {.offset = 0x2138e0u, .patch_size = 12u}, {.offset = 0x212ab0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0310u,
|
||||
.name = "3.10",
|
||||
.targets = {{.offset = 0x3f19a0u, .patch_size = 12u}, {.offset = 0x3f5220u, .patch_size = 12u}, {.offset = 0x213920u, .patch_size = 12u}, {.offset = 0x212af0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0320u,
|
||||
.name = "3.20",
|
||||
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f52d0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0321u,
|
||||
.name = "3.21",
|
||||
.targets = {{.offset = 0x3f1a50u, .patch_size = 12u}, {.offset = 0x3f52d0u, .patch_size = 12u}, {.offset = 0x2139d0u, .patch_size = 12u}, {.offset = 0x212ba0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0400u,
|
||||
.name = "4.00",
|
||||
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x436460u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0403u,
|
||||
.name = "4.03",
|
||||
.targets = {{.offset = 0x432630u, .patch_size = 12u}, {.offset = 0x436460u, .patch_size = 12u}, {.offset = 0x226c20u, .patch_size = 12u}, {.offset = 0x225e40u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0450u,
|
||||
.name = "4.50",
|
||||
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x436bb0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0451u,
|
||||
.name = "4.51",
|
||||
.targets = {{.offset = 0x432d80u, .patch_size = 12u}, {.offset = 0x436bb0u, .patch_size = 12u}, {.offset = 0x227350u, .patch_size = 12u}, {.offset = 0x226570u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0500u,
|
||||
.name = "5.00",
|
||||
.targets = {{.offset = 0x498950u, .patch_size = 12u}, {.offset = 0x49c640u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0502u,
|
||||
.name = "5.02",
|
||||
.targets = {{.offset = 0x498940u, .patch_size = 12u}, {.offset = 0x49c630u, .patch_size = 12u}, {.offset = 0x2556a0u, .patch_size = 12u}, {.offset = 0x254900u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0510u,
|
||||
.name = "5.10",
|
||||
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49dfc0u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0550u,
|
||||
.name = "5.50",
|
||||
.targets = {{.offset = 0x4998f0u, .patch_size = 12u}, {.offset = 0x49dfc0u, .patch_size = 12u}, {.offset = 0x256570u, .patch_size = 12u}, {.offset = 0x2557d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0600u,
|
||||
.name = "6.00",
|
||||
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4cb8c0u, .patch_size = 12u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0602u,
|
||||
.name = "6.02",
|
||||
.targets = {{.offset = 0x4c7380u, .patch_size = 12u}, {.offset = 0x4cb8c0u, .patch_size = 12u}, {.offset = 0x26b9b0u, .patch_size = 12u}, {.offset = 0x271bb0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0650u,
|
||||
.name = "6.50",
|
||||
.targets = {{.offset = 0x4c73f0u, .patch_size = 12u}, {.offset = 0x4cb930u, .patch_size = 12u}, {.offset = 0x26ba20u, .patch_size = 12u}, {.offset = 0x271c20u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0700u,
|
||||
.name = "7.00",
|
||||
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x56e9b0u, .patch_size = 12u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0701u,
|
||||
.name = "7.01",
|
||||
.targets = {{.offset = 0x568920u, .patch_size = 12u}, {.offset = 0x56e9b0u, .patch_size = 12u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0720u,
|
||||
.name = "7.20",
|
||||
.targets = {{.offset = 0x568940u, .patch_size = 12u}, {.offset = 0x56e9d0u, .patch_size = 12u}, {.offset = 0x2852e0u, .patch_size = 12u}, {.offset = 0x28b690u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0740u,
|
||||
.name = "7.40",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0760u,
|
||||
.name = "7.60",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0761u,
|
||||
.name = "7.61",
|
||||
.targets = {{.offset = 0x56d430u, .patch_size = 12u}, {.offset = 0x5734c0u, .patch_size = 12u}, {.offset = 0x2896b0u, .patch_size = 12u}, {.offset = 0x28fa60u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0800u,
|
||||
.name = "8.00",
|
||||
.targets = {{.offset = 0x597eb0u, .patch_size = 12u}, {.offset = 0x59c640u, .patch_size = 12u}, {.offset = 0x29a730u, .patch_size = 12u}, {.offset = 0x2a12f0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0820u,
|
||||
.name = "8.20",
|
||||
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x59d710u, .patch_size = 12u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0840u,
|
||||
.name = "8.40",
|
||||
.targets = {{.offset = 0x598f80u, .patch_size = 12u}, {.offset = 0x59d710u, .patch_size = 12u}, {.offset = 0x29a9a0u, .patch_size = 12u}, {.offset = 0x2a1560u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0860u,
|
||||
.name = "8.60",
|
||||
.targets = {{.offset = 0x598d60u, .patch_size = 12u}, {.offset = 0x59d4f0u, .patch_size = 12u}, {.offset = 0x29a750u, .patch_size = 12u}, {.offset = 0x2a1310u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0900u,
|
||||
.name = "9.00",
|
||||
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x5d0a80u, .patch_size = 12u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0905u,
|
||||
.name = "9.05",
|
||||
.targets = {{.offset = 0x5cc0a0u, .patch_size = 12u}, {.offset = 0x5d0a80u, .patch_size = 12u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0920u,
|
||||
.name = "9.20",
|
||||
.targets = {{.offset = 0x5cbdc0u, .patch_size = 12u}, {.offset = 0x5d07a0u, .patch_size = 12u}, {.offset = 0x2b74d0u, .patch_size = 12u}, {.offset = 0x2be0e0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0940u,
|
||||
.name = "9.40",
|
||||
.targets = {{.offset = 0x5cc490u, .patch_size = 12u}, {.offset = 0x5d0e70u, .patch_size = 12u}, {.offset = 0x2b7ba0u, .patch_size = 12u}, {.offset = 0x2be7b0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x0960u,
|
||||
.name = "9.60",
|
||||
.targets = {{.offset = 0x5cc500u, .patch_size = 12u}, {.offset = 0x5d0ee0u, .patch_size = 12u}, {.offset = 0x2b7c10u, .patch_size = 12u}, {.offset = 0x2be820u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1000u,
|
||||
.name = "10.00",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1001u,
|
||||
.name = "10.01",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d1f00u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1020u,
|
||||
.name = "10.20",
|
||||
.targets = {{.offset = 0x5ca1a0u, .patch_size = 12u}, {.offset = 0x5ce820u, .patch_size = 12u}, {.offset = 0x2b6700u, .patch_size = 12u}, {.offset = 0x17d61c0u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1040u,
|
||||
.name = "10.40",
|
||||
.targets = {{.offset = 0x5ca0f0u, .patch_size = 12u}, {.offset = 0x5ce770u, .patch_size = 12u}, {.offset = 0x2b6760u, .patch_size = 12u}, {.offset = 0x17d61e0u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1060u,
|
||||
.name = "10.60",
|
||||
.targets = {{.offset = 0x5cb980u, .patch_size = 12u}, {.offset = 0x5d0000u, .patch_size = 12u}, {.offset = 0x2b80c0u, .patch_size = 12u}, {.offset = 0x17d7a60u, .patch_size = 16u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1100u,
|
||||
.name = "11.00",
|
||||
.targets = {{.offset = 0x62a940u, .patch_size = 12u}, {.offset = 0x62efe0u, .patch_size = 12u}, {.offset = 0x2bd880u, .patch_size = 12u}, {.offset = 0x2c4e60u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1120u,
|
||||
.name = "11.20",
|
||||
.targets = {{.offset = 0x62aab0u, .patch_size = 12u}, {.offset = 0x62f150u, .patch_size = 12u}, {.offset = 0x2bd930u, .patch_size = 12u}, {.offset = 0x2c4f10u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1140u,
|
||||
.name = "11.40",
|
||||
.targets = {{.offset = 0x62ba90u, .patch_size = 12u}, {.offset = 0x630130u, .patch_size = 12u}, {.offset = 0x2be910u, .patch_size = 12u}, {.offset = 0x2c5ef0u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1160u,
|
||||
.name = "11.60",
|
||||
.targets = {{.offset = 0x633500u, .patch_size = 12u}, {.offset = 0x637ba0u, .patch_size = 12u}, {.offset = 0x2c2c10u, .patch_size = 12u}, {.offset = 0x2ca1f0u, .patch_size = 15u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1200u,
|
||||
.name = "12.00",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1202u,
|
||||
.name = "12.02",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1220u,
|
||||
.name = "12.20",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1240u,
|
||||
.name = "12.40",
|
||||
.targets = {{.offset = 0x647410u, .patch_size = 12u}, {.offset = 0x64bac0u, .patch_size = 12u}, {.offset = 0x2e0050u, .patch_size = 12u}, {.offset = 0x2dd100u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1260u,
|
||||
.name = "12.60",
|
||||
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x64cd10u, .patch_size = 12u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
|
||||
},
|
||||
{
|
||||
.firmware = 0x1270u,
|
||||
.name = "12.70",
|
||||
.targets = {{.offset = 0x648660u, .patch_size = 12u}, {.offset = 0x64cd10u, .patch_size = 12u}, {.offset = 0x2e0320u, .patch_size = 12u}, {.offset = 0x2dd3d0u, .patch_size = 12u}},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <ps5/mdbg.h>
|
||||
|
||||
#include "sm_log.h"
|
||||
#include "sm_shellcore_remote.h"
|
||||
|
||||
#define X86_PAGE_FRAME 0x000ffffffffff000ull
|
||||
#define X86_PAGE_VALID 0x001ull
|
||||
#define X86_PAGE_LARGE 0x080ull
|
||||
|
||||
static uintptr_t resolve_vmspace_pmap(uintptr_t vmspace) {
|
||||
uint32_t version = kernel_get_fw_version() >> 16;
|
||||
if (version >= 0x0100u && version <= 0x0102u)
|
||||
return vmspace + 0x2c0u;
|
||||
if (version >= 0x0105u && version <= 0x0550u)
|
||||
return vmspace + 0x2e0u;
|
||||
if (version >= 0x0600u && version <= 0x1270u)
|
||||
return vmspace + 0x2e8u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool resolve_process_paging(pid_t pid, uint64_t *cr3_out,
|
||||
uint64_t *dmap_out) {
|
||||
uintptr_t process = (uintptr_t)kernel_get_proc(pid);
|
||||
if (!process)
|
||||
return false;
|
||||
uintptr_t vmspace = (uintptr_t)kernel_getlong(
|
||||
process + (uintptr_t)KERNEL_OFFSET_PROC_P_VMSPACE);
|
||||
if (!vmspace)
|
||||
return false;
|
||||
uintptr_t pmap = resolve_vmspace_pmap(vmspace);
|
||||
if (!pmap)
|
||||
return false;
|
||||
|
||||
uint64_t paging[2] = {0};
|
||||
if (kernel_copyout((intptr_t)(pmap + 32u), paging, sizeof(paging)) != 0)
|
||||
return false;
|
||||
if (!paging[0] || !paging[1])
|
||||
return false;
|
||||
|
||||
*cr3_out = paging[1];
|
||||
*dmap_out = paging[0] - paging[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t virtual_to_physical(uint64_t address, uint64_t dmap,
|
||||
uint64_t page_map,
|
||||
uint64_t *physical_limit_out) {
|
||||
page_map &= X86_PAGE_FRAME;
|
||||
for (int shift = 39; shift >= 12; shift -= 9) {
|
||||
uint64_t index = (address >> shift) & 0x1ffu;
|
||||
page_map = kernel_getlong((intptr_t)(dmap + page_map + index * 8u));
|
||||
if ((page_map & X86_PAGE_VALID) == 0)
|
||||
return UINT64_MAX;
|
||||
if ((page_map & X86_PAGE_LARGE) != 0 || shift == 12) {
|
||||
page_map &= (1ull << 52) - (1ull << shift);
|
||||
page_map |= address & ((1ull << shift) - 1ull);
|
||||
if (physical_limit_out)
|
||||
*physical_limit_out = (page_map | ((1ull << shift) - 1ull)) + 1ull;
|
||||
return page_map;
|
||||
}
|
||||
page_map &= X86_PAGE_FRAME;
|
||||
}
|
||||
return UINT64_MAX;
|
||||
}
|
||||
|
||||
bool sm_remote_process_read(pid_t pid, uintptr_t address, void *buffer,
|
||||
size_t size) {
|
||||
if (!buffer || size == 0)
|
||||
return false;
|
||||
return mdbg_copyout(pid, (intptr_t)address, buffer, size) == 0;
|
||||
}
|
||||
|
||||
bool sm_remote_process_write(pid_t pid, uintptr_t address, const void *buffer,
|
||||
size_t size) {
|
||||
if (!buffer || size == 0)
|
||||
return false;
|
||||
if ((kernel_get_fw_version() >> 16) <= 0x0820u)
|
||||
return mdbg_copyin(pid, buffer, (intptr_t)address, size) == 0;
|
||||
|
||||
void *probe = malloc(size);
|
||||
if (!probe)
|
||||
return false;
|
||||
bool faulted = sm_remote_process_read(pid, address, probe, size);
|
||||
free(probe);
|
||||
if (!faulted)
|
||||
return false;
|
||||
|
||||
uint64_t cr3 = 0;
|
||||
uint64_t dmap = 0;
|
||||
if (!resolve_process_paging(pid, &cr3, &dmap))
|
||||
return false;
|
||||
|
||||
const uint8_t *source = (const uint8_t *)buffer;
|
||||
while (size != 0) {
|
||||
uint64_t physical_limit = 0;
|
||||
uint64_t physical =
|
||||
virtual_to_physical(address, dmap, cr3, &physical_limit);
|
||||
if (physical == UINT64_MAX || physical_limit <= physical)
|
||||
return false;
|
||||
size_t chunk = (size_t)(physical_limit - physical);
|
||||
if (chunk > size)
|
||||
chunk = size;
|
||||
if (kernel_copyin(source, (intptr_t)(dmap + physical), chunk) != 0)
|
||||
return false;
|
||||
address += chunk;
|
||||
source += chunk;
|
||||
size -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out) {
|
||||
if (pid <= 0 || !remote_out)
|
||||
return false;
|
||||
memset(remote_out, 0, sizeof(*remote_out));
|
||||
|
||||
const sm_shellcore_firmware_offsets_t *firmware =
|
||||
sm_shellcore_offsets_for_firmware(kernel_get_fw_version());
|
||||
if (!firmware) {
|
||||
log_debug(" [SHELLCORE] unsupported firmware: 0x%08x",
|
||||
kernel_get_fw_version());
|
||||
return false;
|
||||
}
|
||||
|
||||
uintptr_t image_base = (uintptr_t)kernel_dynlib_mapbase_addr(pid, 0);
|
||||
if (!image_base) {
|
||||
log_debug(" [SHELLCORE] failed to resolve image base for pid=%ld",
|
||||
(long)pid);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int target = 0; target < SM_SHELLCORE_TARGET_COUNT; ++target) {
|
||||
remote_out->targets[target] = image_base + firmware->targets[target].offset;
|
||||
}
|
||||
|
||||
remote_out->pid = pid;
|
||||
remote_out->image_base = image_base;
|
||||
remote_out->offsets = firmware;
|
||||
log_debug(" [SHELLCORE] resolved fw=%s pid=%ld base=0x%lx", firmware->name,
|
||||
(long)pid, (unsigned long)image_base);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include "sm_filesystem.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_gameinfo.h"
|
||||
#include "sm_image.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_runtime.h"
|
||||
#include "sm_scan.h"
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_protocol.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_socket_io.h"
|
||||
|
||||
_Static_assert(sizeof(sm_shellcore_request_t) == 40,
|
||||
"unexpected ShellCore request size");
|
||||
_Static_assert(offsetof(sm_shellcore_request_t, payload) == 8,
|
||||
"unexpected ShellCore request payload offset");
|
||||
_Static_assert(offsetof(sm_shellcore_request_t, payload.app_exit.app_id) == 8,
|
||||
"unexpected ShellCore app-exit app id offset");
|
||||
_Static_assert(sizeof(sm_shellcore_response_t) == 4,
|
||||
"unexpected ShellCore response size");
|
||||
|
||||
typedef struct {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
bool started;
|
||||
bool stop_requested;
|
||||
int listen_fd;
|
||||
int client_fd;
|
||||
// Owner of the prepared runtime mount, not the lifecycle active-game state.
|
||||
char prepared_title_id[MAX_TITLE_ID];
|
||||
uint32_t prepared_app_id;
|
||||
bool prepared_game_exited;
|
||||
bool prepare_in_progress;
|
||||
bool launch_pending;
|
||||
bool release_in_progress;
|
||||
} shellcore_service_state_t;
|
||||
|
||||
static shellcore_service_state_t g_service = {
|
||||
.mutex = PTHREAD_MUTEX_INITIALIZER,
|
||||
.listen_fd = -1,
|
||||
.client_fd = -1,
|
||||
};
|
||||
|
||||
#define SHELLCORE_RELEASE_MAX_ATTEMPTS 5u
|
||||
#define SHELLCORE_RELEASE_RETRY_DELAY_US 150000u
|
||||
|
||||
static int claim_prepared_title(const char *title_id, bool launch_request,
|
||||
bool *claimed_out) {
|
||||
bool changed = false;
|
||||
if (claimed_out)
|
||||
*claimed_out = false;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (g_service.release_in_progress || g_service.prepare_in_progress) {
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return EBUSY;
|
||||
}
|
||||
if (g_service.prepared_title_id[0] == '\0') {
|
||||
(void)strlcpy(g_service.prepared_title_id, title_id,
|
||||
sizeof(g_service.prepared_title_id));
|
||||
g_service.prepared_app_id = 0;
|
||||
g_service.prepared_game_exited = false;
|
||||
g_service.prepare_in_progress = true;
|
||||
g_service.launch_pending = launch_request;
|
||||
changed = true;
|
||||
if (claimed_out)
|
||||
*claimed_out = true;
|
||||
} else if (strcmp(g_service.prepared_title_id, title_id) != 0) {
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return EBUSY;
|
||||
} else if (g_service.prepared_game_exited) {
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return EBUSY;
|
||||
} else {
|
||||
g_service.prepare_in_progress = true;
|
||||
if (launch_request)
|
||||
g_service.launch_pending = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (changed)
|
||||
sm_scanner_wake();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void clear_prepared_title(const char *title_id) {
|
||||
bool changed = false;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (strcmp(g_service.prepared_title_id, title_id) == 0) {
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
g_service.prepared_app_id = 0;
|
||||
g_service.prepared_game_exited = false;
|
||||
g_service.prepare_in_progress = false;
|
||||
g_service.launch_pending = false;
|
||||
changed = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (changed)
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
static void finish_prepared_title(const char *title_id,
|
||||
bool cancel_launch) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (strcmp(g_service.prepared_title_id, title_id) == 0) {
|
||||
g_service.prepare_in_progress = false;
|
||||
if (cancel_launch)
|
||||
g_service.launch_pending = false;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
}
|
||||
|
||||
static int begin_runtime_release(const char *title_id,
|
||||
bool public_request) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool different_title = g_service.prepared_title_id[0] != '\0' &&
|
||||
strcmp(g_service.prepared_title_id, title_id) != 0;
|
||||
bool blocked = different_title || g_service.release_in_progress ||
|
||||
g_service.prepare_in_progress ||
|
||||
(public_request && (g_service.launch_pending ||
|
||||
g_service.prepared_app_id != 0));
|
||||
if (!blocked)
|
||||
g_service.release_in_progress = true;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return blocked ? EBUSY : 0;
|
||||
}
|
||||
|
||||
static void end_runtime_release(void) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
g_service.release_in_progress = false;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
}
|
||||
|
||||
static bool shellcore_service_stopping(void) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool stopping = g_service.stop_requested;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return stopping;
|
||||
}
|
||||
|
||||
static bool find_required_image_layer(const char *root,
|
||||
const char *runtime_source,
|
||||
unsigned int depth,
|
||||
char image_path[MAX_PATH]) {
|
||||
DIR *dir = opendir(root);
|
||||
if (!dir)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
struct dirent *entry;
|
||||
while (!found && (entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
char path[MAX_PATH];
|
||||
int written = snprintf(path, sizeof(path), "%s/%s", root, entry->d_name);
|
||||
if (written < 0 || (size_t)written >= sizeof(path))
|
||||
continue;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0)
|
||||
continue;
|
||||
if (S_ISREG(st.st_mode) &&
|
||||
is_supported_image_file_path(path, entry->d_name)) {
|
||||
char mount_point[MAX_PATH];
|
||||
get_image_mount_point_for_source(path, mount_point);
|
||||
if (path_matches_root_or_child(runtime_source, mount_point)) {
|
||||
(void)strlcpy(image_path, path, MAX_PATH);
|
||||
found = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (depth > 0 && S_ISDIR(st.st_mode))
|
||||
found = find_required_image_layer(path, runtime_source, depth - 1u,
|
||||
image_path);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool repair_image_chain_for_runtime_source(
|
||||
const char *title_id, const char *runtime_source, const char *eboot_path,
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH], size_t *image_count) {
|
||||
while (*image_count < MAX_IMAGE_CHAIN_DEPTH) {
|
||||
char mounted_root[MAX_PATH];
|
||||
get_image_mount_point_for_source(image_chain[*image_count - 1u],
|
||||
mounted_root);
|
||||
char image_path[MAX_PATH];
|
||||
if (!find_required_image_layer(mounted_root, runtime_source,
|
||||
MAX_SCAN_DEPTH, image_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *name = strrchr(image_path, '/');
|
||||
name = name ? name + 1 : image_path;
|
||||
bool unstable = false;
|
||||
if (!maybe_mount_image_file(image_path, name, &unstable))
|
||||
return false;
|
||||
|
||||
(void)strlcpy(image_chain[*image_count], image_path, MAX_PATH);
|
||||
(*image_count)++;
|
||||
if (!write_mount_image_chain(title_id, image_chain, *image_count)) {
|
||||
log_debug(" [SHELLCORE] failed to persist repaired image chain: %s",
|
||||
title_id);
|
||||
} else {
|
||||
log_debug(" [SHELLCORE] image chain repaired: title=%s layers=%zu",
|
||||
title_id, *image_count);
|
||||
}
|
||||
|
||||
if (path_exists(eboot_path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prepare_image_source(const char *title_id,
|
||||
const char *runtime_source) {
|
||||
char eboot_path[MAX_PATH];
|
||||
int written = snprintf(eboot_path, sizeof(eboot_path), "%s/eboot.bin",
|
||||
runtime_source);
|
||||
if (written < 0 || (size_t)written >= sizeof(eboot_path))
|
||||
return false;
|
||||
if (path_exists(eboot_path))
|
||||
return true;
|
||||
if (!is_under_image_mount_base(runtime_source))
|
||||
return false;
|
||||
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_count = 0;
|
||||
if (!read_mount_image_chain(title_id, image_chain, &image_count))
|
||||
return false;
|
||||
// The USB filesystem can become available shortly after WORKING. Avoid
|
||||
// repeatedly entering the image attach path until its outer layer exists.
|
||||
if (!path_exists(image_chain[0]))
|
||||
return false;
|
||||
for (size_t i = 0; i < image_count; ++i) {
|
||||
const char *name = strrchr(image_chain[i], '/');
|
||||
name = name ? name + 1 : image_chain[i];
|
||||
bool unstable = false;
|
||||
if (!maybe_mount_image_file(image_chain[i], name, &unstable)) {
|
||||
log_debug(" [SHELLCORE] image chain mount failed: title=%s layer=%zu "
|
||||
"path=%s",
|
||||
title_id, i, image_chain[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (path_exists(eboot_path))
|
||||
return true;
|
||||
if (repair_image_chain_for_runtime_source(title_id, runtime_source,
|
||||
eboot_path, image_chain,
|
||||
&image_count))
|
||||
return true;
|
||||
log_debug(" [SHELLCORE] image chain ready but eboot missing: %s",
|
||||
eboot_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool prepare_title_runtime(const char *title_id,
|
||||
const char *source_path) {
|
||||
if (!prepare_image_source(title_id, source_path))
|
||||
return false;
|
||||
runtime_mount_state_lock();
|
||||
bool title_mounted = !runtime_sleep_mode_active() &&
|
||||
mount_title_nullfs(title_id, source_path);
|
||||
bool ready = title_mounted;
|
||||
if (ready)
|
||||
ready = mount_backport_overlay_for_title(source_path, title_id, NULL, NULL);
|
||||
if (title_mounted && !ready)
|
||||
(void)unmount_title_runtime_layers(title_id);
|
||||
runtime_mount_state_unlock();
|
||||
return ready;
|
||||
}
|
||||
|
||||
static bool release_title_runtime(const char *title_id,
|
||||
bool public_request);
|
||||
|
||||
static int mount_managed_title_runtime(const char *title_id,
|
||||
bool allow_unmanaged,
|
||||
bool launch_request) {
|
||||
if (!is_supported_game_title_id(title_id))
|
||||
return EINVAL;
|
||||
if (runtime_sleep_mode_active())
|
||||
return EBUSY;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
|
||||
return allow_unmanaged ? 0 : ENOENT;
|
||||
|
||||
bool claimed = false;
|
||||
int claim_status =
|
||||
claim_prepared_title(title_id, launch_request, &claimed);
|
||||
if (claim_status != 0)
|
||||
return claim_status;
|
||||
if (!claimed && is_data_mounted(title_id)) {
|
||||
finish_prepared_title(title_id, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
if (prepare_title_runtime(title_id, source_path)) {
|
||||
finish_prepared_title(title_id, false);
|
||||
return 0;
|
||||
}
|
||||
int prepare_errno = errno;
|
||||
if (claimed)
|
||||
clear_prepared_title(title_id);
|
||||
else
|
||||
finish_prepared_title(title_id, true);
|
||||
return prepare_errno == EBUSY ? EBUSY : EIO;
|
||||
}
|
||||
|
||||
bool sm_shellcore_ensure_title_runtime(const char *title_id) {
|
||||
return mount_managed_title_runtime(title_id, true, true) == 0;
|
||||
}
|
||||
|
||||
static int handle_launch_request(const char *title_id) {
|
||||
int status = mount_managed_title_runtime(title_id, false, true);
|
||||
if (status != 0)
|
||||
return status;
|
||||
log_debug(" [SHELLCORE] launch mount ready: %s", title_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sm_shellcore_mount_title_runtime(const char *title_id) {
|
||||
if (sm_game_lifecycle_has_active_game())
|
||||
return EBUSY;
|
||||
return mount_managed_title_runtime(title_id, false, false);
|
||||
}
|
||||
|
||||
int sm_shellcore_unmount_title_runtime(const char *title_id) {
|
||||
if (!is_supported_game_title_id(title_id))
|
||||
return EINVAL;
|
||||
if (sm_game_lifecycle_has_active_game())
|
||||
return EBUSY;
|
||||
|
||||
char source_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, source_path, sizeof(source_path)))
|
||||
return ENOENT;
|
||||
bool released = release_title_runtime(title_id, true);
|
||||
int status = released ? 0 : (errno == EBUSY ? EBUSY : EIO);
|
||||
return status;
|
||||
}
|
||||
|
||||
static bool release_backing_image(const char *title_id) {
|
||||
char image_chain[MAX_IMAGE_CHAIN_DEPTH][MAX_PATH];
|
||||
size_t image_count = 0;
|
||||
if (!read_mount_image_chain(title_id, image_chain, &image_count))
|
||||
return true;
|
||||
|
||||
for (size_t layer = image_count; layer > 0; --layer) {
|
||||
if (!release_runtime_image_mount(image_chain[layer - 1u]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool release_title_runtime(const char *title_id,
|
||||
bool public_request) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
int begin_status = begin_runtime_release(title_id, public_request);
|
||||
if (begin_status != 0) {
|
||||
errno = begin_status;
|
||||
return false;
|
||||
}
|
||||
log_debug(" [SHELLCORE] runtime release start: %s", title_id);
|
||||
bool released = false;
|
||||
int release_errno = 0;
|
||||
unsigned int attempts = public_request ? 1u : SHELLCORE_RELEASE_MAX_ATTEMPTS;
|
||||
unsigned int attempt = 0;
|
||||
for (; attempt < attempts; ++attempt) {
|
||||
bool diagnose_busy = attempt + 1u == attempts;
|
||||
runtime_mount_state_lock();
|
||||
released = diagnose_busy
|
||||
? unmount_title_runtime_layers(title_id)
|
||||
: unmount_title_runtime_layers_quiet(title_id);
|
||||
if (released)
|
||||
released = release_backing_image(title_id);
|
||||
release_errno = released ? 0 : errno;
|
||||
runtime_mount_state_unlock();
|
||||
|
||||
if (released || release_errno != EBUSY || attempt + 1u == attempts ||
|
||||
shellcore_service_stopping()) {
|
||||
break;
|
||||
}
|
||||
if (attempt == 0) {
|
||||
log_debug(" [SHELLCORE] runtime release busy; fast retry armed: %s "
|
||||
"attempts=%u delay_ms=%u",
|
||||
title_id, attempts - 1u,
|
||||
SHELLCORE_RELEASE_RETRY_DELAY_US / 1000u);
|
||||
}
|
||||
(void)sceKernelUsleep(SHELLCORE_RELEASE_RETRY_DELAY_US);
|
||||
}
|
||||
if (released && attempt > 0) {
|
||||
log_debug(" [SHELLCORE] runtime release completed after retry: %s "
|
||||
"attempt=%u",
|
||||
title_id, attempt + 1u);
|
||||
}
|
||||
// The lifecycle owner is gone even when a short-lived ShellCore sandbox
|
||||
// nullfs still pins the title stack. Leaving the title marked as prepared
|
||||
// would block scanner cleanup forever. The intact stack remains discoverable
|
||||
// through mount.lnk and is retried by the normal full-scan cleanup cycle.
|
||||
clear_prepared_title(title_id);
|
||||
end_runtime_release();
|
||||
if (!released && release_errno != 0)
|
||||
errno = release_errno;
|
||||
return released;
|
||||
}
|
||||
|
||||
bool sm_shellcore_release_title_runtime(const char *title_id) {
|
||||
return release_title_runtime(title_id, false);
|
||||
}
|
||||
|
||||
void sm_shellcore_service_bind_prepared_app(const char *title_id,
|
||||
uint32_t app_id) {
|
||||
if (!title_id || title_id[0] == '\0' || app_id == 0)
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (strcmp(g_service.prepared_title_id, title_id) == 0) {
|
||||
g_service.prepared_app_id = app_id;
|
||||
g_service.prepared_game_exited = false;
|
||||
g_service.prepare_in_progress = false;
|
||||
g_service.launch_pending = false;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_note_game_exit(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
bool changed = false;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool prepared = strcmp(g_service.prepared_title_id, title_id) == 0;
|
||||
if (prepared && !g_service.prepared_game_exited) {
|
||||
g_service.prepared_game_exited = true;
|
||||
changed = true;
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_has_prepared_mount(void) {
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
// Keep scanner cleanup blocked after process exit as well. Only the
|
||||
// post-onAppExit event (or the explicit no-hook fallback) owns the
|
||||
// transition that clears this state and releases the runtime stack.
|
||||
bool prepared = g_service.prepared_title_id[0] != '\0';
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_title_is_prepared(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool prepared = strcmp(g_service.prepared_title_id, title_id) == 0;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
static int handle_launch_failed_request(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return 0;
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool prepared = strcmp(g_service.prepared_title_id, title_id) == 0;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (!prepared)
|
||||
return 0;
|
||||
return sm_shellcore_release_title_runtime(title_id) ? 0 : EBUSY;
|
||||
}
|
||||
|
||||
static bool resolve_app_exit_title(uint32_t app_id,
|
||||
char title_id[MAX_TITLE_ID]) {
|
||||
if (app_id == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (g_service.prepared_app_id == app_id &&
|
||||
g_service.prepared_title_id[0] != '\0') {
|
||||
(void)strlcpy(title_id, g_service.prepared_title_id, MAX_TITLE_ID);
|
||||
}
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (title_id[0] == '\0')
|
||||
return false;
|
||||
|
||||
log_debug(" [SHELLCORE] onAppExit cleanup complete: %s app_id=0x%08X",
|
||||
title_id, app_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handle_client(int fd) {
|
||||
sm_shellcore_request_t request;
|
||||
sm_shellcore_response_t response;
|
||||
char release_title_id[MAX_TITLE_ID] = {0};
|
||||
if (!sm_socket_read_full(fd, &request, sizeof(request)))
|
||||
return;
|
||||
if (request.magic != SM_SHELLCORE_PROTOCOL_MAGIC ||
|
||||
request.version != SM_SHELLCORE_PROTOCOL_VERSION ||
|
||||
((request.operation == SM_SHELLCORE_REQUEST_LAUNCH ||
|
||||
request.operation == SM_SHELLCORE_REQUEST_LAUNCH_FAILED) &&
|
||||
strnlen(request.payload.title_id, sizeof(request.payload.title_id)) ==
|
||||
sizeof(request.payload.title_id))) {
|
||||
response.status = EPROTO;
|
||||
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH) {
|
||||
response.status = handle_launch_request(request.payload.title_id);
|
||||
} else if (request.operation == SM_SHELLCORE_REQUEST_LAUNCH_FAILED) {
|
||||
response.status = handle_launch_failed_request(request.payload.title_id);
|
||||
} else if (request.operation == SM_SHELLCORE_REQUEST_APP_EXITED) {
|
||||
(void)resolve_app_exit_title(request.payload.app_exit.app_id,
|
||||
release_title_id);
|
||||
response.status = 0;
|
||||
} else {
|
||||
response.status = ENOTSUP;
|
||||
}
|
||||
(void)sm_socket_write_full(fd, &response, sizeof(response));
|
||||
if (release_title_id[0] != '\0')
|
||||
(void)sm_shellcore_release_title_runtime(release_title_id);
|
||||
}
|
||||
|
||||
static void *service_thread_main(void *arg) {
|
||||
(void)arg;
|
||||
int listen_fd = g_service.listen_fd;
|
||||
while (true) {
|
||||
int client = accept(listen_fd, NULL, NULL);
|
||||
if (client < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
bool stopping = g_service.stop_requested;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (!stopping)
|
||||
log_debug(" [SHELLCORE] accept failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
struct timeval timeout = {.tv_sec = 2, .tv_usec = 0};
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
(void)setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (g_service.stop_requested) {
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
close(client);
|
||||
break;
|
||||
}
|
||||
g_service.client_fd = client;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
|
||||
handle_client(client);
|
||||
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
if (g_service.client_fd == client)
|
||||
g_service.client_fd = -1;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
close(client);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_shellcore_service_start(void) {
|
||||
if (g_service.started)
|
||||
return true;
|
||||
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
struct sockaddr_un address;
|
||||
memset(&address, 0, sizeof(address));
|
||||
address.sun_family = AF_UNIX;
|
||||
(void)strlcpy(address.sun_path, SM_SHELLCORE_SOCKET_PATH,
|
||||
sizeof(address.sun_path));
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) != 0 ||
|
||||
listen(fd, 4) != 0) {
|
||||
int saved_errno = errno;
|
||||
close(fd);
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_service.listen_fd = fd;
|
||||
g_service.client_fd = -1;
|
||||
g_service.stop_requested = false;
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
g_service.prepared_app_id = 0;
|
||||
g_service.prepared_game_exited = false;
|
||||
g_service.prepare_in_progress = false;
|
||||
g_service.launch_pending = false;
|
||||
g_service.release_in_progress = false;
|
||||
int rc = pthread_create(&g_service.thread, NULL, service_thread_main, NULL);
|
||||
if (rc != 0) {
|
||||
close(fd);
|
||||
g_service.listen_fd = -1;
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
errno = rc;
|
||||
return false;
|
||||
}
|
||||
g_service.started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_shellcore_service_stop(void) {
|
||||
if (!g_service.started)
|
||||
return;
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
g_service.stop_requested = true;
|
||||
int fd = g_service.listen_fd;
|
||||
int client_fd = g_service.client_fd;
|
||||
g_service.listen_fd = -1;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
if (client_fd >= 0)
|
||||
(void)shutdown(client_fd, SHUT_RDWR);
|
||||
if (fd >= 0)
|
||||
(void)shutdown(fd, SHUT_RDWR);
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
pthread_join(g_service.thread, NULL);
|
||||
pthread_mutex_lock(&g_service.mutex);
|
||||
g_service.started = false;
|
||||
g_service.stop_requested = false;
|
||||
g_service.client_fd = -1;
|
||||
g_service.prepared_title_id[0] = '\0';
|
||||
g_service.prepared_app_id = 0;
|
||||
g_service.prepared_game_exited = false;
|
||||
g_service.prepare_in_progress = false;
|
||||
g_service.launch_pending = false;
|
||||
g_service.release_in_progress = false;
|
||||
pthread_mutex_unlock(&g_service.mutex);
|
||||
(void)unlink(SM_SHELLCORE_SOCKET_PATH);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_socket_io.h"
|
||||
|
||||
bool sm_socket_read_full(int fd, void *buffer, size_t size) {
|
||||
uint8_t *bytes = (uint8_t *)buffer;
|
||||
while (size > 0) {
|
||||
ssize_t count = recv(fd, bytes, size, 0);
|
||||
if (count == 0)
|
||||
return false;
|
||||
if (count < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
bytes += (size_t)count;
|
||||
size -= (size_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_socket_write_full(int fd, const void *buffer, size_t size) {
|
||||
const uint8_t *bytes = (const uint8_t *)buffer;
|
||||
while (size > 0) {
|
||||
ssize_t count = send(fd, bytes, size, 0);
|
||||
if (count == 0)
|
||||
return false;
|
||||
if (count < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
bytes += (size_t)count;
|
||||
size -= (size_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a running ShadowMount HTTP/JSON API endpoint."""
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import http.client
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
|
||||
API_VERSION = 1
|
||||
MAX_RESPONSE_SIZE = 16 * 1024 * 1024
|
||||
|
||||
ROUTE_VERSION = "/api/v1/version"
|
||||
ROUTE_IMAGES = "/api/v1/images"
|
||||
ROUTE_GAMES = "/api/v1/games"
|
||||
ROUTE_MOUNT = "/api/v1/games/mount"
|
||||
ROUTE_UNMOUNT = "/api/v1/games/unmount"
|
||||
|
||||
|
||||
class ApiTestError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ApiClient:
|
||||
def __init__(self, address: str, port: int, timeout: float) -> None:
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
route: str,
|
||||
body: bytes = b"",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> Tuple[int, Dict[str, str], bytes]:
|
||||
connection = http.client.HTTPConnection(
|
||||
self.address, self.port, timeout=self.timeout
|
||||
)
|
||||
try:
|
||||
connection.request(method, route, body=body, headers=headers or {})
|
||||
response = connection.getresponse()
|
||||
response_body = response.read(MAX_RESPONSE_SIZE + 1)
|
||||
response_headers = {
|
||||
name.lower(): value.strip() for name, value in response.getheaders()
|
||||
}
|
||||
response_status = response.status
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if len(response_body) > MAX_RESPONSE_SIZE:
|
||||
raise ApiTestError("response body exceeds 16 MiB")
|
||||
if response_headers.get("access-control-allow-origin") != "*":
|
||||
raise ApiTestError(f"{method} {route}: missing CORS allow-origin header")
|
||||
|
||||
content_length = response_headers.get("content-length")
|
||||
if content_length is None:
|
||||
raise ApiTestError(f"{method} {route}: missing Content-Length header")
|
||||
try:
|
||||
declared_size = int(content_length, 10)
|
||||
except ValueError as error:
|
||||
raise ApiTestError(
|
||||
f"{method} {route}: invalid Content-Length header"
|
||||
) from error
|
||||
if declared_size != len(response_body):
|
||||
raise ApiTestError(
|
||||
f"{method} {route}: Content-Length is {declared_size}, "
|
||||
f"received {len(response_body)} bytes"
|
||||
)
|
||||
return response_status, response_headers, response_body
|
||||
|
||||
def post(self, route: str, payload: Dict[str, Any]) -> Tuple[int, Dict[str, Any]]:
|
||||
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
status, headers, response_body = self.request(
|
||||
"POST",
|
||||
route,
|
||||
body,
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": str(len(body)),
|
||||
"Connection": "close",
|
||||
},
|
||||
)
|
||||
if not headers.get("content-type", "").lower().startswith(
|
||||
"application/json"
|
||||
):
|
||||
raise ApiTestError(f"POST {route}: response is not JSON")
|
||||
try:
|
||||
response = json.loads(response_body.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise ApiTestError(f"POST {route}: invalid JSON response") from error
|
||||
if not isinstance(response, dict):
|
||||
raise ApiTestError(f"POST {route}: response must be a JSON object")
|
||||
return status, response
|
||||
|
||||
|
||||
def require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise ApiTestError(message)
|
||||
|
||||
|
||||
def integer_field(response: Dict[str, Any], key: str) -> int:
|
||||
value = response.get(key)
|
||||
if type(value) is not int:
|
||||
raise ApiTestError(f"response field {key!r} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def test_preflight(client: ApiClient) -> None:
|
||||
status, headers, body = client.request(
|
||||
"OPTIONS",
|
||||
ROUTE_VERSION,
|
||||
headers={
|
||||
"Origin": "http://example.test",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "Content-Type",
|
||||
"Access-Control-Request-Private-Network": "true",
|
||||
"Connection": "close",
|
||||
},
|
||||
)
|
||||
require(status == 204, f"CORS preflight returned HTTP {status}")
|
||||
require(not body, "CORS preflight returned a response body")
|
||||
require(
|
||||
headers.get("access-control-allow-methods") == "POST, OPTIONS",
|
||||
"CORS preflight returned invalid allowed methods",
|
||||
)
|
||||
require(
|
||||
headers.get("access-control-allow-headers") == "Content-Type",
|
||||
"CORS preflight returned invalid allowed headers",
|
||||
)
|
||||
require(
|
||||
headers.get("access-control-allow-private-network") == "true",
|
||||
"CORS private-network access is not enabled",
|
||||
)
|
||||
print("[API-TEST] CORS preflight=PASS")
|
||||
|
||||
|
||||
def test_version(client: ApiClient) -> None:
|
||||
http_status, response = client.post(ROUTE_VERSION, {})
|
||||
require(http_status == 200, f"version request returned HTTP {http_status}")
|
||||
require(integer_field(response, "status") == 0, "version request failed")
|
||||
require(
|
||||
integer_field(response, "api_version") == API_VERSION,
|
||||
"unsupported API version",
|
||||
)
|
||||
version = response.get("shadowmount_version")
|
||||
require(isinstance(version, str), "missing ShadowMount version")
|
||||
print(f"[API-TEST] ShadowMount={version} API={API_VERSION}")
|
||||
|
||||
|
||||
def test_list(client: ApiClient, route: str, key: str) -> list:
|
||||
http_status, response = client.post(route, {})
|
||||
require(http_status == 200, f"{key} request returned HTTP {http_status}")
|
||||
require(integer_field(response, "status") == 0, f"{key} request failed")
|
||||
count = integer_field(response, "count")
|
||||
items = response.get(key)
|
||||
require(count >= 0, f"{key} count is negative")
|
||||
require(isinstance(items, list), f"{key} field must be an array")
|
||||
require(len(items) == count, f"{key} count does not match array length")
|
||||
require(
|
||||
all(isinstance(item, dict) for item in items),
|
||||
f"{key} array contains a non-object item",
|
||||
)
|
||||
print(f"[API-TEST] {key}={count}")
|
||||
return items
|
||||
|
||||
|
||||
def find_mount_candidate(games: list) -> str:
|
||||
for game in games:
|
||||
title_id = game.get("title_id")
|
||||
if (
|
||||
game.get("managed") is True
|
||||
and game.get("source_available") is True
|
||||
and game.get("mounted") is False
|
||||
and isinstance(title_id, str)
|
||||
and title_id
|
||||
):
|
||||
return title_id
|
||||
return ""
|
||||
|
||||
|
||||
def title_operation(client: ApiClient, route: str, title_id: str) -> Tuple[int, int]:
|
||||
http_status, response = client.post(route, {"title_id": title_id})
|
||||
return http_status, integer_field(response, "status")
|
||||
|
||||
|
||||
def test_mount_cycle(client: ApiClient, title_id: str) -> str:
|
||||
if not title_id:
|
||||
print("[API-TEST] mount cycle=SKIP (no candidate)")
|
||||
return "SKIP"
|
||||
|
||||
http_status, status = title_operation(client, ROUTE_MOUNT, title_id)
|
||||
if http_status == 409 and status == errno.EBUSY:
|
||||
print(f"[API-TEST] mount cycle=SKIP ({title_id} is busy)")
|
||||
return "SKIP"
|
||||
require(
|
||||
http_status == 200 and status == 0,
|
||||
f"mount {title_id} failed: HTTP {http_status}, status {status}",
|
||||
)
|
||||
|
||||
http_status, status = title_operation(client, ROUTE_UNMOUNT, title_id)
|
||||
require(
|
||||
http_status == 200 and status == 0,
|
||||
f"unmount {title_id} failed: HTTP {http_status}, status {status}",
|
||||
)
|
||||
print(f"[API-TEST] mount cycle=PASS ({title_id})")
|
||||
return "PASS"
|
||||
|
||||
|
||||
def valid_port(value: str) -> int:
|
||||
try:
|
||||
port = int(value, 10)
|
||||
except ValueError as error:
|
||||
raise argparse.ArgumentTypeError("port must be an integer") from error
|
||||
if not 1 <= port <= 65535:
|
||||
raise argparse.ArgumentTypeError("port must be in range 1..65535")
|
||||
return port
|
||||
|
||||
|
||||
def positive_timeout(value: str) -> float:
|
||||
try:
|
||||
timeout = float(value)
|
||||
except ValueError as error:
|
||||
raise argparse.ArgumentTypeError("timeout must be a number") from error
|
||||
if timeout <= 0:
|
||||
raise argparse.ArgumentTypeError("timeout must be positive")
|
||||
return timeout
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("address", help="PS5 IPv4 address or hostname")
|
||||
parser.add_argument("port", type=valid_port, help="ShadowMount HTTP API port")
|
||||
parser.add_argument(
|
||||
"--timeout", type=positive_timeout, default=5.0, help="request timeout"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
client = ApiClient(args.address, args.port, args.timeout)
|
||||
try:
|
||||
test_preflight(client)
|
||||
test_version(client)
|
||||
images = test_list(client, ROUTE_IMAGES, "images")
|
||||
games = test_list(client, ROUTE_GAMES, "games")
|
||||
mount_result = test_mount_cycle(client, find_mount_candidate(games))
|
||||
except (ApiTestError, http.client.HTTPException, OSError) as error:
|
||||
print(f"[API-TEST] FAIL: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"[API-TEST] PASS: images={len(images)} games={len(games)} "
|
||||
f"mount={mount_result}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate SceShellCore lifecycle offsets from unpacked firmware files.
|
||||
|
||||
The generator identifies functions through semantic log strings, finds the
|
||||
containing function prologue and emits a compact firmware offset table. It is
|
||||
intentionally independent from IDA databases so the checked-in table can be
|
||||
reproduced from unpacked firmware files.
|
||||
|
||||
The runtime-release hook targets ``LncManager::onAppExit`` rather than an
|
||||
application/workspace destructor. ShellCore can destroy a launch-time
|
||||
``LncApplication`` object with the live app id, so destructor hooks produce a
|
||||
false exit event while the game is still running. ``onAppExit`` is the
|
||||
SysCore exit-event owner and returns after the firmware-specific exit cleanup
|
||||
path has run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from capstone import CS_ARCH_X86, CS_MODE_64, Cs
|
||||
|
||||
PROLOGUE = b"\x55\x48\x89\xe5"
|
||||
TARGET_NAMES = (
|
||||
"launch_app",
|
||||
"app_exit",
|
||||
"install_title_dir",
|
||||
"install_all",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadSegment:
|
||||
file_offset: int
|
||||
virtual_address: int
|
||||
file_size: int
|
||||
flags: int
|
||||
|
||||
|
||||
class ShellCore:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.data = path.read_bytes()
|
||||
header = struct.unpack_from("<16sHHIQQQIHHHHHH", self.data, 0)
|
||||
phoff, phentsize, phnum = header[5], header[9], header[10]
|
||||
self.loads: list[LoadSegment] = []
|
||||
self.executable: LoadSegment | None = None
|
||||
for index in range(phnum):
|
||||
values = struct.unpack_from(
|
||||
"<IIQQQQQQ", self.data, phoff + index * phentsize
|
||||
)
|
||||
p_type, flags, offset, address, _, file_size, _, _ = values
|
||||
if p_type != 1:
|
||||
continue
|
||||
segment = LoadSegment(offset, address, file_size, flags)
|
||||
self.loads.append(segment)
|
||||
if flags & 1:
|
||||
self.executable = segment
|
||||
if self.executable is None:
|
||||
raise ValueError(f"{path}: executable PT_LOAD not found")
|
||||
self._rip_references: list[tuple[int, int]] | None = None
|
||||
|
||||
def file_to_virtual(self, offset: int) -> int:
|
||||
for segment in self.loads:
|
||||
relative = offset - segment.file_offset
|
||||
if 0 <= relative < segment.file_size:
|
||||
return segment.virtual_address + relative
|
||||
raise ValueError(f"file offset 0x{offset:x} is not mapped")
|
||||
|
||||
def virtual_to_file(self, address: int) -> int:
|
||||
for segment in self.loads:
|
||||
relative = address - segment.virtual_address
|
||||
if 0 <= relative < segment.file_size:
|
||||
return segment.file_offset + relative
|
||||
raise ValueError(f"virtual address 0x{address:x} is not mapped")
|
||||
|
||||
def string_addresses(self, needle: bytes, use_string_start: bool) -> list[int]:
|
||||
result: list[int] = []
|
||||
offset = 0
|
||||
while True:
|
||||
offset = self.data.find(needle, offset)
|
||||
if offset < 0:
|
||||
return result
|
||||
string_offset = offset
|
||||
if use_string_start:
|
||||
string_offset = self.data.rfind(
|
||||
b"\0", max(0, offset - 256), offset
|
||||
) + 1
|
||||
result.append(self.file_to_virtual(string_offset))
|
||||
offset += 1
|
||||
|
||||
def rip_xrefs(self, targets: list[int], slop: int = 0) -> list[int]:
|
||||
if self._rip_references is None:
|
||||
segment = self.executable
|
||||
assert segment is not None
|
||||
code = self.data[
|
||||
segment.file_offset : segment.file_offset + segment.file_size
|
||||
]
|
||||
references: list[tuple[int, int]] = []
|
||||
for index in range(len(code) - 7):
|
||||
if code[index] not in (0x48, 0x4C):
|
||||
continue
|
||||
if code[index + 1] not in (0x8D, 0x8B):
|
||||
continue
|
||||
if code[index + 2] & 0xC7 != 0x05:
|
||||
continue
|
||||
displacement = struct.unpack_from("<i", code, index + 3)[0]
|
||||
instruction_address = segment.virtual_address + index
|
||||
references.append(
|
||||
(instruction_address, instruction_address + 7 + displacement)
|
||||
)
|
||||
self._rip_references = references
|
||||
|
||||
if slop == 0:
|
||||
exact_targets = set(targets)
|
||||
return [
|
||||
address
|
||||
for address, destination in self._rip_references
|
||||
if destination in exact_targets
|
||||
]
|
||||
return [
|
||||
address
|
||||
for address, destination in self._rip_references
|
||||
if any(
|
||||
target - slop <= destination <= target + slop
|
||||
for target in targets
|
||||
)
|
||||
]
|
||||
|
||||
def preceding_prologue(self, address: int, window: int = 0x40000) -> int:
|
||||
end = self.virtual_to_file(address)
|
||||
start = max(self.executable.file_offset, end - window) # type: ignore[union-attr]
|
||||
found = self.data.rfind(PROLOGUE, start, end)
|
||||
if found < 0:
|
||||
raise ValueError(f"no prologue before 0x{address:x}")
|
||||
return self.file_to_virtual(found)
|
||||
|
||||
def locate_targets(self) -> dict[str, int]:
|
||||
launch_strings = self.string_addresses(b"launchApp(%s)", True)
|
||||
launch_xrefs = self.rip_xrefs(launch_strings)
|
||||
if len(launch_xrefs) != 1:
|
||||
raise ValueError(f"launchApp xrefs: {launch_xrefs!r}")
|
||||
launch = self.preceding_prologue(launch_xrefs[0])
|
||||
|
||||
app_exit_strings = self.string_addresses(
|
||||
b"[SceLncService] onAppExit() appId={0x%08x}", False
|
||||
)
|
||||
app_exit_groups = {
|
||||
self.preceding_prologue(xref)
|
||||
for xref in self.rip_xrefs(app_exit_strings, slop=8)
|
||||
}
|
||||
if len(app_exit_groups) != 1:
|
||||
raise ValueError(f"onAppExit candidates: {app_exit_groups!r}")
|
||||
app_exit = next(iter(app_exit_groups))
|
||||
|
||||
# Validate the candidate with an independent source-expression log.
|
||||
# This prevents a changed format string from silently selecting an
|
||||
# unrelated function that also logs an app id.
|
||||
app_remove_strings = self.string_addresses(
|
||||
b"m_app_list.remove(app);", False
|
||||
)
|
||||
app_remove_groups = {
|
||||
self.preceding_prologue(xref)
|
||||
for xref in self.rip_xrefs(app_remove_strings, slop=8)
|
||||
}
|
||||
if app_exit not in app_remove_groups:
|
||||
raise ValueError(
|
||||
"onAppExit candidate does not remove the application: "
|
||||
f"0x{app_exit:x}, remove candidates {app_remove_groups!r}"
|
||||
)
|
||||
|
||||
install_strings = self.string_addresses(b"AppInstallTitleDirMain", False)
|
||||
install_groups: dict[int, int] = {}
|
||||
for xref in self.rip_xrefs(install_strings, slop=8):
|
||||
prologue = self.preceding_prologue(xref)
|
||||
install_groups[prologue] = install_groups.get(prologue, 0) + 1
|
||||
if not install_groups:
|
||||
raise ValueError("AppInstallTitleDirMain xrefs not found")
|
||||
best_count = max(install_groups.values())
|
||||
best = [address for address, count in install_groups.items() if count == best_count]
|
||||
if len(best) != 1:
|
||||
raise ValueError(f"ambiguous AppInstallTitleDirMain: {install_groups!r}")
|
||||
|
||||
app_install_all_strings = self.string_addresses(b"AppInstallAll()", True)
|
||||
app_install_all_xrefs = self.rip_xrefs(app_install_all_strings)
|
||||
call_targets: dict[int, int] = {}
|
||||
for xref in app_install_all_xrefs:
|
||||
xref_file = self.virtual_to_file(xref)
|
||||
for call_file in range(xref_file - 32, xref_file):
|
||||
if self.data[call_file] != 0xE8:
|
||||
continue
|
||||
call_address = self.file_to_virtual(call_file)
|
||||
displacement = struct.unpack_from("<i", self.data, call_file + 1)[0]
|
||||
target = call_address + 5 + displacement
|
||||
call_targets[target] = call_targets.get(target, 0) + 1
|
||||
if not call_targets:
|
||||
raise ValueError("AppInstallAll call target not found")
|
||||
app_install_all = max(call_targets, key=call_targets.get)
|
||||
|
||||
return {
|
||||
"launch_app": launch,
|
||||
"app_exit": app_exit,
|
||||
"install_title_dir": best[0],
|
||||
"install_all": app_install_all,
|
||||
}
|
||||
|
||||
def patch_size(self, address: int, minimum: int = 12) -> int:
|
||||
offset = self.virtual_to_file(address)
|
||||
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||
size = 0
|
||||
for instruction in decoder.disasm(self.data[offset : offset + 64], address):
|
||||
size += instruction.size
|
||||
if size >= minimum:
|
||||
return size
|
||||
raise ValueError(f"cannot determine patch size at 0x{address:x}")
|
||||
|
||||
def firmware_key(name: str) -> int:
|
||||
major, minor = (int(part) for part in name.split(".", 1))
|
||||
if major > 99 or minor > 99:
|
||||
raise ValueError(f"firmware version is not BCD-compatible: {name}")
|
||||
major_bcd = ((major // 10) << 4) | (major % 10)
|
||||
minor_bcd = ((minor // 10) << 4) | (minor % 10)
|
||||
return (major_bcd << 8) | minor_bcd
|
||||
|
||||
|
||||
def generate(root: Path) -> str:
|
||||
firmware_dirs = sorted(
|
||||
(path for path in root.iterdir() if (path / "system/vsh/SceShellCore.elf").is_file()),
|
||||
key=lambda path: tuple(int(part) for part in path.name.split(".")),
|
||||
)
|
||||
records: list[str] = []
|
||||
for firmware_dir in firmware_dirs:
|
||||
shellcore = ShellCore(firmware_dir / "system/vsh/SceShellCore.elf")
|
||||
targets = shellcore.locate_targets()
|
||||
records.append(
|
||||
" {\n"
|
||||
f" .firmware = 0x{firmware_key(firmware_dir.name):04x}u,\n"
|
||||
f" .name = \"{firmware_dir.name}\",\n"
|
||||
" .targets = {"
|
||||
+ ", ".join(
|
||||
"{.offset = "
|
||||
f"0x{targets[name]:x}u, .patch_size = "
|
||||
f"{shellcore.patch_size(targets[name])}u}}"
|
||||
for name in TARGET_NAMES
|
||||
)
|
||||
+ "},\n }"
|
||||
)
|
||||
return "/* Generated by tools/generate_shellcore_offsets.py. */\n" + ",\n".join(records) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("root", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
output = generate(args.root)
|
||||
if args.output:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(output)
|
||||
else:
|
||||
print(output, end="")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate SceShellCore lifecycle offsets from a flat ELF directory.
|
||||
|
||||
Input files must be named ``SceShellCoreXXX.elf`` or ``SceShellCoreXXXX.elf``.
|
||||
The last two digits are the minor firmware version; the preceding digits are
|
||||
the major version. For example, ``SceShellCore1140.elf`` is firmware 11.40.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from generate_shellcore_offsets import ShellCore, TARGET_NAMES, firmware_key
|
||||
|
||||
|
||||
SHELLCORE_NAME = re.compile(r"^SceShellCore(\d{3,4})\.elf$")
|
||||
|
||||
|
||||
def firmware_files(root: Path) -> list[tuple[int, int, Path]]:
|
||||
result: list[tuple[int, int, Path]] = []
|
||||
for path in root.iterdir():
|
||||
if not path.is_file():
|
||||
continue
|
||||
match = SHELLCORE_NAME.fullmatch(path.name)
|
||||
if match is None:
|
||||
continue
|
||||
version = match.group(1)
|
||||
result.append((int(version[:-2]), int(version[-2:]), path))
|
||||
return sorted(result, key=lambda item: (item[0], item[1]))
|
||||
|
||||
|
||||
def generate(root: Path) -> str:
|
||||
records: list[str] = []
|
||||
for major, minor, path in firmware_files(root):
|
||||
firmware_name = f"{major}.{minor:02d}"
|
||||
shellcore = ShellCore(path)
|
||||
targets = shellcore.locate_targets()
|
||||
records.append(
|
||||
" {\n"
|
||||
f" .firmware = 0x{firmware_key(firmware_name):04x}u,\n"
|
||||
f" .name = \"{firmware_name}\",\n"
|
||||
" .targets = {"
|
||||
+ ", ".join(
|
||||
"{.offset = "
|
||||
f"0x{targets[name]:x}u, .patch_size = "
|
||||
f"{shellcore.patch_size(targets[name])}u}}"
|
||||
for name in TARGET_NAMES
|
||||
)
|
||||
+ "},\n }"
|
||||
)
|
||||
return (
|
||||
"/* Generated by tools/generate_shellcore_offsets_from_files.py. */\n"
|
||||
+ ",\n".join(records)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate offsets from SceShellCoreXXXX.elf files"
|
||||
)
|
||||
parser.add_argument("root", type=Path, help="directory containing the ELF files")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
output = generate(args.root)
|
||||
if args.output:
|
||||
with args.output.open("w", encoding="utf-8", newline="\n") as file:
|
||||
file.write(output)
|
||||
else:
|
||||
print(output, end="")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify that generated ShellCore hook prologues are safe to copy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from capstone import CS_ARCH_X86, CS_GRP_CALL, CS_GRP_JUMP, CS_MODE_64, Cs
|
||||
from capstone.x86 import X86_OP_MEM, X86_REG_RIP
|
||||
|
||||
from generate_shellcore_offsets import ShellCore, firmware_key
|
||||
from generate_shellcore_offsets_from_files import firmware_files
|
||||
|
||||
|
||||
def verify_firmware(path: Path, firmware: int) -> None:
|
||||
shellcore = ShellCore(path)
|
||||
targets = shellcore.locate_targets()
|
||||
decoder = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||
decoder.detail = True
|
||||
|
||||
names = ["launch_app", "app_exit"]
|
||||
# AppInstallAll is hooked after the public TitleDir RPC disappeared.
|
||||
if firmware >= 0x1200:
|
||||
names.append("install_all")
|
||||
|
||||
for name in names:
|
||||
address = targets[name]
|
||||
patch_size = shellcore.patch_size(address)
|
||||
offset = shellcore.virtual_to_file(address)
|
||||
code = shellcore.data[offset : offset + patch_size]
|
||||
instructions = list(decoder.disasm(code, address))
|
||||
if sum(instruction.size for instruction in instructions) != patch_size:
|
||||
raise ValueError(f"{path}: {name}: split instruction")
|
||||
for instruction in instructions:
|
||||
if instruction.group(CS_GRP_CALL) or instruction.group(CS_GRP_JUMP):
|
||||
raise ValueError(
|
||||
f"{path}: {name}: control flow in copied prologue at "
|
||||
f"0x{instruction.address:x}"
|
||||
)
|
||||
for operand in instruction.operands:
|
||||
if operand.type == X86_OP_MEM and operand.mem.base == X86_REG_RIP:
|
||||
raise ValueError(
|
||||
f"{path}: {name}: RIP-relative operand at "
|
||||
f"0x{instruction.address:x}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("root", type=Path)
|
||||
args = parser.parse_args()
|
||||
firmware_inputs: list[tuple[int, Path]] = []
|
||||
firmware_dirs = sorted(
|
||||
path
|
||||
for path in args.root.iterdir()
|
||||
if (path / "system/vsh/SceShellCore.elf").is_file()
|
||||
)
|
||||
for firmware_dir in firmware_dirs:
|
||||
firmware_inputs.append(
|
||||
(
|
||||
firmware_key(firmware_dir.name),
|
||||
firmware_dir / "system/vsh/SceShellCore.elf",
|
||||
)
|
||||
)
|
||||
for major, minor, path in firmware_files(args.root):
|
||||
firmware_inputs.append((firmware_key(f"{major}.{minor:02d}"), path))
|
||||
if not firmware_inputs:
|
||||
raise ValueError(f"no SceShellCore firmware files under {args.root}")
|
||||
|
||||
for firmware, path in firmware_inputs:
|
||||
verify_firmware(path, firmware)
|
||||
print(f"verified {len(firmware_inputs)} firmware trampoline layouts")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user