mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 21:59:57 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4102fa7aa5 | ||
|
|
d71bacfbc0 | ||
|
|
072c17b050 | ||
|
|
7bf0a6efaf | ||
|
|
2424c98ed4 | ||
|
|
c08e207755 | ||
|
|
3d55f1c4c7 | ||
|
|
18d0f141e7 | ||
|
|
86c67639f6 | ||
|
|
aa69be7892 | ||
|
|
87fbe72b46 | ||
|
|
6b7dc2ced5 | ||
|
|
7b3c0bd9ba | ||
|
|
6488632c73 | ||
|
|
5d3ebd8caa | ||
|
|
a57696a512 | ||
|
|
0d7544a5d6 | ||
|
|
ad0ca65f9a | ||
|
|
297b9003f2 | ||
|
|
dc363e69f1 | ||
|
|
5d4f48cedd | ||
|
|
5b7ab6ef8c | ||
|
|
f81eb60e94 | ||
|
|
9e6b7165a0 | ||
|
|
960204baa0 | ||
|
|
f560898b2c | ||
|
|
3a167431a3 | ||
|
|
ff0bc74468 | ||
|
|
5b9816dd08 | ||
|
|
f3df8f040d | ||
|
|
b564c74611 | ||
|
|
7671106a32 | ||
|
|
b1a0dce78d | ||
|
|
01ea403b88 | ||
|
|
06576fabe3 | ||
|
|
01f5cf7182 | ||
|
|
640d8bdd59 | ||
|
|
bc34a3bb26 | ||
|
|
3638cdaae5 | ||
|
|
3190100de8 |
No files matched your search
@@ -10,6 +10,7 @@ mkexfat_macos.sh
|
||||
mkufs2.sh
|
||||
New-OsfExfatImage.ps1
|
||||
README.md
|
||||
docs/socket-api.md
|
||||
docs/api.md
|
||||
docs/openapi.yaml
|
||||
tools/api_test.py
|
||||
web/index.html
|
||||
@@ -80,6 +80,24 @@ jobs:
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 zlib package
|
||||
working-directory: pacbrew-repo/zlib
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 libpng package
|
||||
working-directory: pacbrew-repo/libpng
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 libmicrohttpd package
|
||||
working-directory: pacbrew-repo/libmicrohttpd
|
||||
run: |
|
||||
makepkg -c -f
|
||||
sudo pacman --noconfirm -U ./ps5-payload-*.pkg.tar.gz
|
||||
|
||||
- name: Build PS5 sqlite package
|
||||
working-directory: pacbrew-repo/sqlite
|
||||
run: |
|
||||
@@ -89,7 +107,7 @@ jobs:
|
||||
- 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'
|
||||
python3 -c 'import pathlib, yaml; yaml.safe_load(pathlib.Path("docs/openapi.yaml").read_text(encoding="utf-8"))'
|
||||
|
||||
- name: Build payload
|
||||
run: |
|
||||
|
||||
@@ -5,8 +5,11 @@ VERSION_TAG := $(shell git describe --abbrev=6 --dirty --always --tags 2>/dev/nu
|
||||
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Compiler and dependency flags
|
||||
JSON_C_ROOT := $(PS5_PAYLOAD_SDK)/target/user/homebrew
|
||||
JSON_CFLAGS := -I$(JSON_C_ROOT)/include
|
||||
HOMEBREW_ROOT := $(PS5_PAYLOAD_SDK)/target/user/homebrew
|
||||
HOMEBREW_CFLAGS := -I$(HOMEBREW_ROOT)/include
|
||||
MHD_LIB := $(HOMEBREW_ROOT)/lib/libmicrohttpd.a
|
||||
PNG_LIB := $(HOMEBREW_ROOT)/lib/libpng16.a
|
||||
ZLIB_LIB := $(HOMEBREW_ROOT)/lib/libz.a
|
||||
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)\"
|
||||
|
||||
@@ -14,12 +17,13 @@ CFLAGS += -DSHADOWMOUNT_VERSION=\"$(VERSION_TAG)\"
|
||||
LDFLAGS := -flto=thin -Wl,--gc-sections
|
||||
|
||||
# Libraries
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lSceNet -lSceSsl -lSceHttp2 -lsqlite3 $(JSON_C_ROOT)/lib/libjson-c.a -lm
|
||||
LIBS := -lSceNotification -lSceSystemService -lSceUserService -lSceAppInstUtil -lSceNet -lSceSsl -lSceHttp2 -lsqlite3 $(HOMEBREW_ROOT)/lib/libjson-c.a $(MHD_LIB) $(PNG_LIB) $(ZLIB_LIB) -lpthread -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
|
||||
ASSET_SRCS := src/notify_icon_asset.c src/config_ini_example_asset.c src/web_index_asset.c
|
||||
ASSET_SRCS += src/shell_icon_param_asset.c
|
||||
SRCS := src/main.c $(wildcard src/sm_*.c) $(ASSET_SRCS)
|
||||
ASM_SRCS := src/sm_shellcore_bridge.S
|
||||
OBJS := $(SRCS:.c=.o) $(ASM_SRCS:.S=.o)
|
||||
@@ -37,7 +41,7 @@ usb-info.elf: tools/usb_info.c
|
||||
shadowmountplus.elf: $(OBJS) $(KERNEL_SYS_STUB_SO)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(KERNEL_SYS_STUB_SO) $(LIBS)
|
||||
$(PS5_PAYLOAD_SDK)/bin/prospero-strip --strip-all $@
|
||||
rm -f src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
rm -f $(ASSET_SRCS)
|
||||
|
||||
$(KERNEL_SYS_STUB_SO): $(KERNEL_SYS_STUB_SRCS)
|
||||
test -f "$(PS5_SCE_STUBS_DIR)/libkernel_sys.c"
|
||||
@@ -49,12 +53,19 @@ 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/web_index_asset.c: web/index.html
|
||||
xxd -i $< > $@
|
||||
|
||||
src/shell_icon_param_asset.c: assets/shell_icon_param.json
|
||||
xxd -i $< > $@
|
||||
|
||||
src/sm_api_service.o src/sm_icon_thumb.o: CFLAGS += $(HOMEBREW_CFLAGS)
|
||||
src/main.o src/sm_image_index.o: CFLAGS += -DSHADOWMOUNT_BUILD_TIME=\"$(BUILD_TIME)\"
|
||||
src/main.o src/sm_image_index.o: FORCE
|
||||
|
||||
src/sm_l10n.o: $(L10N_CATALOGS)
|
||||
src/sm_ampr_updater.o: src/sm_ampr_ca.inc
|
||||
src/sm_shellcore_offsets.o: src/sm_shellcore_offsets.inc
|
||||
|
||||
.PHONY: FORCE
|
||||
FORCE:
|
||||
@@ -66,4 +77,4 @@ src/%.o: src/%.S
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f shadowmountplus.elf usb-info.elf api-test.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) src/notify_icon_asset.c src/config_ini_example_asset.c
|
||||
rm -f shadowmountplus.elf usb-info.elf api-test.elf kill.elf src/*.o $(KERNEL_SYS_STUB_SO) $(ASSET_SRCS)
|
||||
@@ -11,7 +11,7 @@
|
||||
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
|
||||
|
||||
|
||||
**Compatibility:** Supports all Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**.
|
||||
**Compatibility:** Supports Jailbroken PS5 firmwares running **[Kstuff-lite v1.07+](https://github.com/EchoStretch/kstuff-lite)**, including firmware 2.00.
|
||||
|
||||
|
||||
## 💜 Support Development
|
||||
@@ -73,17 +73,13 @@ Supported keys (all optional):
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
- `exfat_backend=lvd|md` (default: `lvd`)
|
||||
- `ufs_backend=lvd|md` (default: `lvd`)
|
||||
- `legacy_mount_ufs=1|0` (`.ffpkg`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_exfat=1|0` (`.exfat`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_pfs=1|0` (`.ffpfs` and nested `pfs_image.dat`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `legacy_mount_pfsc=1|0` (outer `.ffpfsc`; `1` selects the version 1.6 mount profile; default: `1`)
|
||||
- `nested_pfs_index_cache=1|0` (cache the containing PFS compressed-file index before attaching a nested image in legacy mode; default: `0`; optimized mode always requests it; the old `legacy_gddr5_cache` name remains accepted)
|
||||
- `nested_pfs_index_cache=1|0` (request the containing PFS compressed-file index cache before attaching a nested image; default: `0`)
|
||||
- `backport_fakelib=1|0` (`1` mounts sandbox `fakelib` overlays for running games; default: `1`)
|
||||
- `update_emulators=1|0` (`1` updates all emulators with matching files in a game's own fakelib; default: `1`)
|
||||
- `update_emulators=1|0` (`1` updates all emulators with matching files in a game's own fakelib; `fakelib2` is excluded; default: `1`)
|
||||
- `emulators_path=<absolute_path>` (folder containing emulator update files; default: `/data/shadowmount/emus`)
|
||||
- `auto_update_ampr=1|0` (check for a new `libSceAmpr.sprx` 30 seconds after startup and every four hours; default: `0`)
|
||||
- `ampr_update_url=<http_or_https_url>` (AMPR emulator download URL; default: `https://github.com/drakmor/ampr_emu/releases/latest/download/libSceAmpr.sprx`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; default: `1`)
|
||||
- `global_fakelib=1|0` (`1` enables the global fakelib overlay when the folder exists; `fakelib2` remains exclusive; default: `1`)
|
||||
- `global_fakelib_path=<absolute_path>` (global fakelib folder; default: `/data/shadowmount/fakelib`)
|
||||
- `global_fakelib_priority=game|global` (overlay priority when both global and game fakelib exist; default: `game`)
|
||||
- `global_fakelib_exclude=<TITLE_ID>` (repeatable; disables the global fakelib overlay for matching titles)
|
||||
@@ -103,7 +99,7 @@ Supported keys (all optional):
|
||||
- `lvd_ufs_sector_size=<value>` (default: `4096`)
|
||||
- `lvd_pfs_sector_size=<value>` (default: `4096`; the optimized profile uses a `65536`-byte LVD mapping unit)
|
||||
- `md_exfat_sector_size=<value>` (default: `512`)
|
||||
- `md_ufs_sector_size=<value>` (default: `512` with the legacy UFS profile, `4096` with the optimized profile)
|
||||
- `md_ufs_sector_size=<value>` (default: `4096`)
|
||||
|
||||
Supported notification languages:
|
||||
|
||||
@@ -138,6 +134,7 @@ Supported notification languages:
|
||||
| Swedish | `sv-SE` |
|
||||
| Thai | `th-TH` |
|
||||
| Turkish | `tr-TR` |
|
||||
| Ukrainian | `uk-UA` |
|
||||
| Vietnamese | `vi-VN` |
|
||||
|
||||
The public API routes and JSON schemas are documented in [docs/socket-api.md](docs/socket-api.md)
|
||||
@@ -186,12 +183,13 @@ Backport overlay behavior:
|
||||
- `<scanpath>/backports/<TITLE_ID>/`
|
||||
- The `backports` folder is ignored during normal game scanning.
|
||||
- A backport is applied automatically to the matching mounted game from any configured scan path.
|
||||
- For an installed PKG, the full backport directory is not overlaid on package `app0`, and external `backports/<TITLE_ID>` sources are ignored. Only the package's own `app0/fakelib2` or `app0/fakelib` is mounted into `common/lib` through the pre-spawn hook.
|
||||
- If multiple scan paths provide the same title backport, the game's own scan path wins; otherwise scan path order is used.
|
||||
- ShadowMount+ checks the selected backport for `fakelib2` and then `fakelib`; if neither exists, it uses only `fakelib` from the original game source. The selected directory is mounted into the running game's sandbox `common/lib`.
|
||||
- If `update_emulators=1`, matching files from `emulators_path` replace files in the selected game fakelib. The cache is refreshed when its sources change and expires after seven days without a game launch.
|
||||
- ShadowMount+ checks the selected backport for `fakelib2` and then `fakelib`; if neither exists, it checks the original game source in the same order. The selected directory is mounted into the running game's sandbox `common/lib`. A selected `fakelib2` is always mounted directly and exclusively: neither emulator updates nor the global fakelib can replace or supplement it.
|
||||
- If `update_emulators=1`, matching files from `emulators_path` replace files in the selected game fakelib, except when `fakelib2` is selected. The cache is refreshed when its sources change and expires after seven days without a game launch.
|
||||
- With `auto_update_ampr=1`, ShadowMount+ checks for AMPR updates 30 seconds after startup and every four hours. It downloads a missing or newer emulator and displays a notification after a successful update.
|
||||
- The backport notification adds `Emulators updated` when emulator files are updated for the launched game.
|
||||
- If both global and per-game fakelib exist, they are combined in the game cache according to `global_fakelib_priority`. Without a per-game fakelib, the global folder is mounted directly.
|
||||
- If both global and per-game fakelib exist, they are combined in the game cache according to `global_fakelib_priority`, unless the selected backport contains `fakelib2`. Without a per-game fakelib, the global folder is mounted directly.
|
||||
- Use repeatable `global_fakelib_exclude=<TITLE_ID>` entries to skip the global fakelib for specific games without disabling per-game fakelib.
|
||||
- `backport_fakelib=0` disables the sandbox `fakelib` watcher, including global fakelib and emulator updates.
|
||||
- For `backport_fakelib` to work correctly, the standalone `BackPork` payload must be disabled. Running both at the same time will conflict.
|
||||
@@ -239,7 +237,7 @@ PFSC container layout requirement (`.ffpfsc`):
|
||||
- Do not place game files directly in the container root.
|
||||
- Place supported nested image files inside the container; ShadowMountPlus mounts those nested images and scans them for the game.
|
||||
- A nested `pfs_image.dat` file inside a PFSC container is treated as a PFS image.
|
||||
- `.ffpfsc` uses the nested outer PFS profile (`img_type=0x02`); `.ffpfs` and `pfs_image.dat` files mounted from inside it use the nested inner profile (`img_type=0x82`). Signature verification remains disabled for unsigned images; nested inner images request the PFSC compressed-offset cache before LVD attach.
|
||||
- `.ffpfsc` always uses the optimized nested outer PFS profile (`img_type=0x02`). A standalone `.ffpfs` source uses the optimized profile only under `/data/...` or `/user/...`; other standalone sources use the version 1.6 parameters. Nested `.ffpfs`/`pfs_image.dat` images always use the optimized inner profile with `img_type=0x82`. Signature verification remains disabled for unsigned images; set `nested_pfs_index_cache=1` to request the PFSC compressed-offset cache before attaching nested images.
|
||||
|
||||
## Compressed PFS containers (`.ffpfsc`)
|
||||
|
||||
@@ -397,9 +395,9 @@ Recommended only for titles that need external-drive-style compatibility. For ge
|
||||
|
||||
### LVD type-5 / BFS fast-path requirements
|
||||
|
||||
With `legacy_mount_exfat=0`, ShadowMount attaches `.exfat` through LVD as
|
||||
`img_type=5 (Sv)`, with a 512-byte
|
||||
logical sector and a 64 KiB `secondary_unit`. This lets the LVD worker collect
|
||||
For direct source paths under `/data/...` or `/user/...`, ShadowMount attaches
|
||||
`.exfat` through LVD as `img_type=5 (Sv)`, with a 512-byte logical sector and a
|
||||
64 KiB `secondary_unit`. This lets the LVD worker collect
|
||||
up to 31 queued, 64-KiB-aligned requests of exactly 64 KiB and
|
||||
submit them through `bfs_iosession_rw_sdimg`/`BfsSdimg`. Non-matching requests
|
||||
continue through the normal vnode path.
|
||||
@@ -408,8 +406,8 @@ All of the following are required for the kernel to consider that fast path:
|
||||
|
||||
- Use `exfat_backend=lvd` (the default). `/dev/mdctl` has no LVD image type or
|
||||
BFS sdimg batching.
|
||||
- Set `legacy_mount_exfat=0`; the default version 1.6 profile uses
|
||||
`img_type=0` and does not request this fast path.
|
||||
- A `.exfat` source outside `/data/...` and `/user/...` uses the `img_type=0`
|
||||
profile directly. No second mount attempt is made after failure.
|
||||
- Store the final `.exfat` file directly on the internal BFS, normally below
|
||||
`/data`. An image on USB/UFS/exFAT/PFS, or an exFAT image nested in compressed
|
||||
PFS, uses the normal path.
|
||||
@@ -427,7 +425,7 @@ All of the following are required for the kernel to consider that fast path:
|
||||
|
||||
The 64 KiB cluster improves eligibility but cannot force every access through
|
||||
the batch path: exFAT metadata and small or unaligned application I/O still use
|
||||
the safe fallback. In the ShadowMount debug log, an eligible profile request is
|
||||
the normal vnode path. In the ShadowMount debug log, an eligible profile request is
|
||||
shown by `img=5 sec=512 sec2=65536`; the kernel still makes the final BFS and
|
||||
platform checks internally.
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# ShadowMountPlus 1.7
|
||||
|
||||
Version `1.7` is a major ShadowMountPlus update compared with `1.6`.
|
||||
It focuses on the game lifecycle, performance with large libraries, safe
|
||||
release of mount resources, and new management features.
|
||||
|
||||
> [!WARNING]
|
||||
> Mounting images can still cause shutdown problems and data corruption on
|
||||
> internal drives, especially on older firmware versions. Back up important
|
||||
> data and configuration before updating.
|
||||
|
||||
## Highlights
|
||||
|
||||
### New SceShellCore based runtime
|
||||
|
||||
- Game registration and runtime mount preparation now use the SceShellCore bridge.
|
||||
- By default, images are mounted on demand before a game starts and released
|
||||
after the application exits.
|
||||
- Games can be switched without leaving stale mount layers behind.
|
||||
- Improved handling of `ExitSpawn`/`LoadExec`, PID changes, game exit,
|
||||
suspend/resume, and payload shutdown.
|
||||
- On FW 12.00+, individual games are registered through the TitleDir bridge;
|
||||
forcing `app_install_all` is no longer required.
|
||||
- Firmware versions up to 13.60 are supported.
|
||||
|
||||
### Large libraries and faster scanning
|
||||
|
||||
- A reusable image index supports up to **8,192 games across 2,048 images**.
|
||||
- Faster metadata reading, staging, and game registration.
|
||||
- Fewer file descriptors are open at the same time.
|
||||
- Faster USB library recovery after rest mode.
|
||||
- The new `persistent_image_mounts=1` mode keeps discovered images mounted
|
||||
between game launches. It is disabled by default.
|
||||
- Optional removal of unavailable games from the system library includes a
|
||||
delay and cancels pending removal if the source returns.
|
||||
- Games with installed DLC are protected from automatic removal unless
|
||||
`auto_remove_games_with_dlc=1` is explicitly enabled.
|
||||
|
||||
### Web interface and HTTP/JSON API
|
||||
|
||||
- The built-in library interface offers search, filters, game cards, source
|
||||
management, scanning, settings, and log viewing.
|
||||
- A Shadow Mount+ launcher is installed in the PS5 menu and opens the local
|
||||
web interface.
|
||||
- HTTP/JSON API v1 provides access to images, games, and storage devices, as
|
||||
well as game mounting, source management, settings, and scanning.
|
||||
- Background jobs can copy, move, unpack an image, or delete a source. Their
|
||||
progress is available, and they can be cancelled before an irreversible stage.
|
||||
- The default address is `127.0.0.1:10101`, so the API is accessible only to
|
||||
applications running on the PS5.
|
||||
- Configure `api_bind_address` and `api_port` to allow access from the local
|
||||
network.
|
||||
- The release includes the [API documentation](docs/api.md),
|
||||
[OpenAPI schema](docs/openapi.yaml), [web page](web/index.html), and
|
||||
`tools/api_test.py` test client.
|
||||
|
||||
> [!CAUTION]
|
||||
> The API has no authentication. Do not use `api_bind_address=0.0.0.0` on an
|
||||
> untrusted network.
|
||||
|
||||
### Mount profiles and image performance
|
||||
|
||||
- For `.exfat`, `.ffpkg`, and standalone `.ffpfs`, the optimized profile is used
|
||||
only for direct paths under `/data/...` or `/user/...`. Other direct paths use
|
||||
the 1.6 profile. Outer `.ffpfsc` images and nested
|
||||
`.ffpfs`/`pfs_image.dat` images always use the new profile.
|
||||
- The optimized exFAT profile supports LVD `img_type=5` and the BFS fast path
|
||||
for images on internal storage.
|
||||
- The fast path requires the `lvd` backend, a 512-byte logical sector, and
|
||||
64 KiB exFAT clusters.
|
||||
- The Linux, macOS, and Windows exFAT image creation scripts now use a fixed
|
||||
64 KiB cluster size.
|
||||
- For nested PFS images, `nested_pfs_index_cache` requests the compressed
|
||||
offset index cache before attachment.
|
||||
- Improved handling of asynchronous LVD/MD detach, deferred unmounts for busy
|
||||
resources, and cleanup of incomplete attachments after mount failures.
|
||||
- Forced detach of MD devices was removed to avoid disrupting mounts that
|
||||
are still in use.
|
||||
|
||||
### fakelib and emulator updates
|
||||
|
||||
- Global and per-game `fakelib` are now combined in a per-title cache with
|
||||
configurable `game` or `global` priority.
|
||||
- Matching game emulator files can be replaced with files from
|
||||
`emulators_path` without changing the original game folder.
|
||||
- Unused fakelib caches are automatically removed seven days after the last
|
||||
game launch.
|
||||
- Optional automatic checks and downloads for `libSceAmpr.sprx` updates.
|
||||
- `fakelib2` always has the highest priority: it is mounted directly and is
|
||||
not combined with global `fakelib` or emulator updates.
|
||||
- Fixed fakelib remounting across `ExitSpawn`/`LoadExec` transitions.
|
||||
- For installed PKGs, their own `fakelib` is mounted before the game process
|
||||
starts.
|
||||
|
||||
### Localization, cooling, and diagnostics
|
||||
|
||||
- System notifications are localized into 31 languages.
|
||||
- `language=auto` selects the console's system language; a language can also
|
||||
be selected explicitly.
|
||||
- Added `fan_target_temperature=system|50..91`. The default `system` value
|
||||
leaves fan control to the console.
|
||||
- Startup notifications show the active temperature and the result of
|
||||
backport and emulator updates more clearly.
|
||||
- Expanded diagnostics for busy mounts, fatal game signals, and the reasons
|
||||
behind runtime state transitions.
|
||||
- Improved the reliability of automatic Kstuff pause/resume and recovery
|
||||
after a game exits early.
|
||||
|
||||
## New configuration options
|
||||
|
||||
Key options added in 1.7:
|
||||
|
||||
```ini
|
||||
# Notification language
|
||||
language=auto
|
||||
|
||||
# HTTP/JSON API
|
||||
api_enabled=1
|
||||
api_bind_address=127.0.0.1
|
||||
api_port=10101
|
||||
|
||||
# Runtime and library maintenance
|
||||
persistent_image_mounts=0
|
||||
auto_remove_missing_games=0
|
||||
auto_remove_games_with_dlc=0
|
||||
auto_remove_missing_delay_seconds=300
|
||||
|
||||
# fakelib and emulators
|
||||
update_emulators=1
|
||||
emulators_path=/data/shadowmount/emus
|
||||
auto_update_ampr=0
|
||||
|
||||
# Cooling
|
||||
fan_target_temperature=system
|
||||
```
|
||||
|
||||
See `config.ini.example` for the full list of options and valid values.
|
||||
|
||||
## Upgrading from 1.6beta16
|
||||
|
||||
- An existing `/data/shadowmount/config.ini` remains compatible and does not
|
||||
have to be recreated.
|
||||
- New options are not added to an existing configuration file automatically.
|
||||
If needed, copy the relevant lines from the new `config.ini.example`.
|
||||
- Back up `config.ini` and `autotune.ini` before updating.
|
||||
- Mount profiles are selected by source path. The old `legacy_mount_*` options
|
||||
are no longer supported and should be removed from the configuration.
|
||||
`nested_pfs_index_cache` remains available; `legacy_gddr5_cache` is accepted
|
||||
as its old name.
|
||||
- The default `lvd_pfs_sector_size` changed from `32768` to `4096`.
|
||||
- The default `md_ufs_sector_size` is `4096`.
|
||||
- For the exFAT fast path, do not set `image_sector=...:65536`: 64 KiB is the
|
||||
cluster/secondary-unit size, while the logical sector must remain 512 bytes.
|
||||
- `auto_remove_missing_games`, `persistent_image_mounts`, `auto_update_ampr`,
|
||||
and network access to the API are disabled by default.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Supports jailbroken PS5 systems with **Kstuff-lite v1.07 or newer**.
|
||||
- UFS (`.ffpkg`) is the recommended format for normal use.
|
||||
- PFS (`.ffpfs`/`.ffpfsc`) remains experimental.
|
||||
- Running ShadowMountPlus alongside the separate BackPork payload is still
|
||||
unsupported because their fakelib mounts conflict.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"applicationCategoryType": 66048,
|
||||
"titleId": "FAKE10101",
|
||||
"deeplinkUri": "http://127.0.0.1:10101/",
|
||||
"localizedParameters": {
|
||||
"defaultLanguage": "en-US",
|
||||
"en-US": { "titleName": "Shadow Mount+" }
|
||||
}
|
||||
}
|
||||
+12
-23
@@ -31,6 +31,7 @@
|
||||
# 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_enabled=1
|
||||
# api_bind_address=127.0.0.1
|
||||
# api_port=10101
|
||||
|
||||
@@ -106,36 +107,24 @@
|
||||
# exfat_backend=lvd
|
||||
# ufs_backend=lvd
|
||||
|
||||
# Mount profile per image type:
|
||||
# 1/true/yes/on -> use the version 1.6 attach/nmount profile (default)
|
||||
# 0/false/no/off -> use the optimized version 1.7 profile
|
||||
# Changes apply the next time an image is mounted; active mounts are not reset.
|
||||
# ufs = .ffpkg
|
||||
# exfat = .exfat
|
||||
# pfs = .ffpfs and nested pfs_image.dat
|
||||
# pfsc = outer .ffpfsc container
|
||||
# legacy_mount_ufs=1
|
||||
# legacy_mount_exfat=1
|
||||
# legacy_mount_pfs=1
|
||||
# legacy_mount_pfsc=1
|
||||
|
||||
# PFS compressed-file index cache for nested images in legacy mode:
|
||||
# PFS compressed-file index cache for nested images:
|
||||
# 1/true/yes/on -> cache the containing PFS compressed-file index before
|
||||
# attaching a nested image
|
||||
# 0/false/no/off -> keep the version 1.6 behavior (default)
|
||||
# The optimized profile always requests this cache when supported.
|
||||
# 0/false/no/off -> skip this optional request (default)
|
||||
# The old legacy_gddr5_cache key remains accepted for compatibility.
|
||||
# nested_pfs_index_cache=0
|
||||
|
||||
# Sandbox fakelib backport watcher:
|
||||
# 1/true/yes/on -> mount app0/fakelib2 when present, otherwise app0/fakelib, into common/lib
|
||||
# 0/false/no/off -> disable fakelib watcher
|
||||
# Sandbox fakelib support:
|
||||
# 1/true/yes/on -> mount fakelib2 when present, otherwise fakelib, into common/lib
|
||||
# Installed PKGs use only their own app0/fakelib2 or app0/fakelib; external
|
||||
# backport directories are not overlaid on app0 and are not used as sources.
|
||||
# 0/false/no/off -> disable fakelib overlays
|
||||
# Default: 1
|
||||
# backport_fakelib=1
|
||||
|
||||
# Updates for multiple emulators in the game fakelib:
|
||||
# 1/true/yes/on -> cache the game's fakelib before launch and replace matching
|
||||
# cached files from emulators_path
|
||||
# cached files from emulators_path; fakelib2 is never changed
|
||||
# 0/false/no/off -> disable emulator updates
|
||||
# Default: 1
|
||||
# update_emulators=1
|
||||
@@ -159,7 +148,8 @@
|
||||
# ampr_update_url=https://github.com/drakmor/ampr_emu/releases/latest/download/libSceAmpr.sprx
|
||||
|
||||
# Global fakelib overlay:
|
||||
# 1/true/yes/on -> use global fakelib for every tracked game when present
|
||||
# 1/true/yes/on -> use global fakelib for every tracked game when present;
|
||||
# fakelib2 always remains exclusive when present
|
||||
# 0/false/no/off -> do not use the global fakelib folder
|
||||
# Default: 1
|
||||
# global_fakelib=1
|
||||
@@ -204,8 +194,7 @@
|
||||
# kstuff_pause_delay_direct_seconds=15
|
||||
|
||||
# Backend-specific sector size overrides:
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=4096, md_exfat=512
|
||||
# md_ufs defaults to 512 in legacy mode and 4096 in optimized mode.
|
||||
# Defaults: lvd_exfat=512, lvd_ufs=4096, lvd_pfs=4096, md_exfat=512,md_ufs=4096.
|
||||
# Keep lvd_exfat at 512 for the BFS fast path. 65536 is the exFAT cluster and
|
||||
# LVD secondary unit in the optimized profile, not the logical sector size.
|
||||
# lvd_exfat_sector_size=512
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
# 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.
|
||||
These settings are applied on runtime config reload. `api_enabled` defaults to
|
||||
`1`; setting it to `0` stops the listener and joins the libmicrohttpd service
|
||||
and worker threads. Re-enabling it restarts the HTTP service without restarting
|
||||
the payload. An already-running storage job is allowed to finish, but no new
|
||||
jobs can be submitted while the API is disabled.
|
||||
|
||||
`/data/shadowmount/index.html` overrides the web interface bundled into the
|
||||
payload. If the file is absent, the same `web/index.html` built into the ELF is
|
||||
served automatically, so enabling the API always provides a landing page.
|
||||
|
||||
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 uses libmicrohttpd with eight fixed polling workers. It accepts
|
||||
up to 32 concurrent connections, uses a listen backlog of 32 and closes idle
|
||||
connections after 15 seconds. Slow clients therefore do not serialize unrelated
|
||||
requests or create an unbounded number of threads. If daemon startup fails, the
|
||||
service retries with an interruptible 250 ms to 5 s backoff. Suspend, shutdown
|
||||
and config reload stop the daemon cleanly; resume recreates it.
|
||||
|
||||
Every JSON API operation uses `POST`, includes `Content-Type: application/json`
|
||||
and contains one JSON object. `GET /` returns the optional web interface and the
|
||||
icon endpoint returns PNG bytes. Connections are closed after one response;
|
||||
chunked request bodies are not supported. The maximum request body is 4096 bytes.
|
||||
The parsed request-header limit remains 8192 bytes.
|
||||
|
||||
All responses include `Access-Control-Allow-Origin: *`. Browser preflight
|
||||
requests using `OPTIONS` are supported and allow `GET`, `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 |
|
||||
| --- | --- | --- |
|
||||
| `/` | GET | Serve `/data/shadowmount/index.html`, or the embedded page when it is absent |
|
||||
| `/api/v1/version` | `{}` | API and ShadowMount versions plus capabilities |
|
||||
| `/api/v1/storage` | `{}` | Mounted storage filesystems with total, free and available space |
|
||||
| `/api/v1/images` | `{}` | Complete image snapshot |
|
||||
| `/api/v1/scan` | `{"reset_attempts":false}` | Queue an immediate full rescan; optionally reset title and image retry counters |
|
||||
| `/api/v1/manual/list` | `{}` | List direct paths to individual game folders or images from `manual.lst` |
|
||||
| `/api/v1/manual/add` | `{"path":"/mnt/usb0/games/PPSA12345"}` | Idempotently add one game source to `manual.lst` |
|
||||
| `/api/v1/manual/remove` | `{"path":"/mnt/usb0/games/PPSA12345"}` | Remove matching source lines from `manual.lst` |
|
||||
| `/api/v1/settings` | `{}` | Read web-managed runtime settings and scan paths |
|
||||
| `/api/v1/settings/update` | `{"debug":true,"quiet_mode":false,"update_emulators":true,"auto_update_ampr":false,"auto_remove_missing_games":false,"auto_remove_missing_delay_seconds":300,"allow_lan_access":true,"fan_target_temperature":0,"scan_paths":["/mnt/usb0/games"]}` | Atomically update the web-managed part of `config.ini` |
|
||||
| `/api/v1/debug-log` | `{"max_bytes":131072}` | Read a bounded tail of `debug.log` for the web log dialog |
|
||||
| `/api/v1/kernel-log` | `{"max_bytes":131072}` | Read a bounded tail of the SDK kernel-log stream used by crash detection |
|
||||
| `/api/v1/games` | `{"include_size":false}` | Detailed game snapshot; optional physical source-size calculation |
|
||||
| `/api/v1/games/info` | `{"title_id":"PPSA12345"}` | Detailed app.db and source information for one game; size is always calculated |
|
||||
| `/api/v1/games/icon?title_id=PPSA12345[&size=thumb]` | GET | Stream the full PNG or a cached 128x128 thumbnail |
|
||||
| `/api/v1/games/mount` | `{"title_id":"PPSA12345","mode":"ro"}` | Mount a managed game, optionally overriding its image mode with `ro`/`rw` |
|
||||
| `/api/v1/games/unmount` | `{"title_id":"PPSA12345"}` | Unmount a managed game |
|
||||
| `/api/v1/games/uninstall` | `{"title_id":"PPSA12345"}` | Request uninstallation through AppInstUtil |
|
||||
| `/api/v1/games/move` | `{"title_id":"PPSA12345","destination_dir":"/mnt/usb1/games"}` | Start an asynchronous move job |
|
||||
| `/api/v1/games/copy` | `{"title_id":"PPSA12345","destination_dir":"/mnt/usb1/games"}` | Start an asynchronous copy job |
|
||||
| `/api/v1/games/unpack` | `{"title_id":"PPSA12345","destination_dir":"/mnt/usb1/games","delete_source":false}` | Mount an image read-only and asynchronously copy the game into a folder |
|
||||
| `/api/v1/games/storage/status` | `{"job_id":1}` | Get current or last storage job status; `job_id` is optional |
|
||||
| `/api/v1/games/storage/cancel` | `{"job_id":1}` | Request cancellation while the active job is still cancellable |
|
||||
| `/api/v1/games/delete` | `{"title_id":"PPSA12345","confirm":true}` | Start asynchronous permanent deletion of the physical source |
|
||||
|
||||
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 '{}'
|
||||
|
||||
curl -sS http://127.0.0.1:10101/api/v1/scan \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}'
|
||||
|
||||
curl -sS http://127.0.0.1:10101/api/v1/games/mount \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"title_id":"PPSA12345","mode":"rw"}'
|
||||
```
|
||||
|
||||
An autonomous example library UI is provided as `web/index.html`. Copy it to
|
||||
`/data/shadowmount/index.html` on the PS5 and open the configured API listener
|
||||
in a browser, for example `http://192.168.1.50:10101/`. The file is read for
|
||||
each request, so it can be replaced without restarting ShadowMount. If it is
|
||||
missing, `GET /` returns HTTP 404 with the normal JSON error response.
|
||||
The bundled interface covers all 31 console locales, selects the closest match
|
||||
from the browser locale, and keeps a manually selected locale in local storage.
|
||||
|
||||
## 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": ["web_ui", "storage_space", "list_images", "list_games", "game_info", "game_icon", "mount_game", "unmount_game", "uninstall_game", "move_game_source", "copy_game_source", "delete_game_source", "unpack_game_image", "storage_job_status", "storage_job_cancel", "list_manual_sources", "add_manual_source", "remove_manual_source", "manage_settings", "read_debug_log", "read_kernel_log", "rescan"]
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
combine ShadowMount's game cache with `tbl_contentinfo` from app.db. They expose
|
||||
the physical `path`, `runtime_path`, `source_type` (`folder` or `image`), image
|
||||
filesystem type, PS4/PS5 platform, title/content IDs, name, last-launch and
|
||||
install timestamps, relative `icon_url`, app.db size, and runtime state.
|
||||
The timestamp values from `AppInfoJson` (`#_last_access_time` and
|
||||
`#_install_time`) take precedence over the stale top-level columns when they
|
||||
are present.
|
||||
|
||||
`POST /api/v1/games` does not walk game directories by default. Set
|
||||
`include_size=true` to add `size_status` and, on success, `size_bytes` to every
|
||||
item. `POST /api/v1/games/info` always performs this calculation. An image uses
|
||||
its file size; a folder uses the sum of its regular-file sizes.
|
||||
|
||||
`POST /api/v1/storage` returns capacity-bearing mounted filesystems and omits
|
||||
virtual mounts such as `devfs`, `nullfs` and `tmpfs`. Each item contains the
|
||||
device `source`, `mount_point`, `filesystem`, `total_bytes`, `free_bytes`,
|
||||
`available_bytes`, `used_bytes` and `read_only`. `available_bytes` is the space
|
||||
available for new files; `used_bytes` is calculated against that value.
|
||||
|
||||
`copy` and `move` preserve the source basename and require an existing
|
||||
destination under a configured non-runtime scan root. They and `delete` return
|
||||
HTTP 202 with a `job_id` and continue in one background worker;
|
||||
only one of these jobs can be active. A second start returns HTTP 409/`EBUSY`.
|
||||
`delete` still requires `confirm=true`. The status response contains the phase,
|
||||
byte-based percentage, processed/total bytes,
|
||||
processed/total files, average processed-byte rate, elapsed time and final
|
||||
errno-style result. The initial `measuring` phase discovers the totals and has
|
||||
zero percent until they are known. Only the active or most recently finished
|
||||
job is retained; an older explicit `job_id` returns HTTP 404.
|
||||
|
||||
Cancellation is cooperative during `preparing`, `measuring` and `transferring`.
|
||||
A partial copy destination is removed. A delete job becomes non-cancellable
|
||||
when it enters `deleting`, before its first physical removal. Likewise, once a
|
||||
cross-filesystem move enters
|
||||
`finalizing` and starts deleting the complete source, cancellation is rejected
|
||||
to avoid leaving the only source tree partially removed. The scanner and
|
||||
ShellCore mutation gates remain held for the whole job and a rescan is queued
|
||||
after success, cancellation or failure. `move` uses `rename()` on one
|
||||
filesystem and exact copy-plus-delete across filesystems. A backing image
|
||||
shared by several titles is handled as one physical source.
|
||||
`unpack` uses the same worker and progress endpoint. It mounts the requested
|
||||
image read-only, copies the mounted game tree to `<destination_dir>/<TITLE_ID>`,
|
||||
and always releases the runtime mount afterward. `delete_source=true` removes
|
||||
the image only after a complete copy and successful unmount; this irreversible
|
||||
final phase is non-cancellable. Source deletion is rejected when one image is
|
||||
shared by several titles.
|
||||
Changing the API bind address or port restarts only the HTTP listener; an
|
||||
active storage job continues and remains available through the new listener.
|
||||
|
||||
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.
|
||||
The optional mount `mode` is request-scoped and does not change `config.ini`.
|
||||
Accepted values are `ro`, `rw`, `r/o` and `r/w`; responses normalize them to
|
||||
`ro` or `rw`. It applies to every image layer needed for that title. A direct
|
||||
directory source or an image profile that is inherently read-only rejects an
|
||||
explicit mode it cannot support with HTTP 501 and `ENOTSUP`. An already mounted
|
||||
image in the other mode returns HTTP 409 and `EBUSY`.
|
||||
With `persistent_image_mounts=1`, unmounting a game releases its per-title
|
||||
runtime layers but keeps the backing image mounted.
|
||||
|
||||
The scan command queues the existing full-scan path and wakes the scanner. If a
|
||||
game runtime mount is active, the request remains pending until the lifecycle
|
||||
watcher reports that scanning is safe; it does not create a parallel scan.
|
||||
By default retry counters are preserved. With `reset_attempts=true`, immediately
|
||||
before the requested full scan starts it clears all title registration and
|
||||
install/remount counters plus all path-owned image-mount counters. If several
|
||||
scan requests merge while one is pending, one `true` request keeps the reset
|
||||
enabled for that scan.
|
||||
|
||||
Manual entries are direct paths to individual game directories or image files;
|
||||
they are not recursive scan roots. Updates atomically replace `manual.lst` and preserve comments and
|
||||
unrelated lines. Adding an existing normalized path and removing a missing path
|
||||
both succeed with `changed: false`. A real change queues a scan. Removing a
|
||||
manual source does not uninstall its game; use the uninstall route separately
|
||||
when both operations are desired.
|
||||
The list route returns `{status, count, paths}`. Paths use the same trimming and
|
||||
trailing-slash normalization as the scanner; comments and empty lines are not
|
||||
included. A missing `manual.lst` is reported as an empty list.
|
||||
|
||||
The settings update owns only `debug`, `quiet_mode`, `update_emulators`,
|
||||
`auto_update_ampr`, `auto_remove_missing_games`,
|
||||
`auto_remove_missing_delay_seconds`,
|
||||
`api_bind_address`, `fan_target_temperature`, and repeated `scanpath` keys. The
|
||||
`allow_lan_access` field maps to `0.0.0.0` when enabled and `127.0.0.1` when
|
||||
disabled. It atomically rewrites those keys while preserving every unrelated
|
||||
`config.ini` line and comment, then wakes the scanner so its existing
|
||||
runtime-reload path applies the values and rebuilds scan watches. `api_enabled`
|
||||
is deliberately config-only, so the web interface cannot disable the service it
|
||||
needs to restore access. A fan target of `0` means system control; explicit
|
||||
values must be 50 through 91 degrees Celsius. Unlike `manual.lst`, `scan_paths`
|
||||
are recursive library roots. The settings response contains only explicit
|
||||
custom roots, never compile-time defaults or internal image-mount roots. Saving
|
||||
an empty array removes the custom override and restores compile-time defaults.
|
||||
The debug-log route returns at most 256 KiB and never modifies or rotates the log.
|
||||
The kernel-log route reads the same `sceKernelDebugGetSdkLogText` snapshot used
|
||||
by `kstuff_crash_detection`.
|
||||
|
||||
Uninstall returns success after `sceAppInstUtilAppUnInstall` accepts the
|
||||
request; the on-disk removal itself is asynchronous. It returns `EBUSY` while a
|
||||
game is active, another title owns a prepared runtime mount, or batch install
|
||||
work is pending. An unchanged discoverable source may be installed again by a
|
||||
later scan, so remove its manual entry or source first when that is not wanted.
|
||||
|
||||
## 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. It also runs 16 parallel version requests while one client holds an
|
||||
incomplete request open, 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.
|
||||
+1187
-10
File diff suppressed because it is too large.
Load diff
@@ -1,97 +0,0 @@
|
||||
# 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.
|
||||
With `persistent_image_mounts=1`, unmounting a game releases its per-title
|
||||
runtime layers but keeps the backing image mounted.
|
||||
|
||||
## 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.
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_ar_sa[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "الانتقال إلى إعدادات التصحيح",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor والمجتمع",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor والمجتمع",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor والمجتمع\nالتحكم: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "شادوماونت+: لم يعد الفحص التكراري مدعوماً، وسيتم استخدام عمق الفحص ٢.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_ar_sa[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: تعذر تثبيت خطافات ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "درجة الحرارة المستهدفة %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "تم توصيل محرك الأقراص الخارجي %s.\nالمتاح %llu.%llu من %llu.%llu غيغابايت (حجم الكتلة %llu كيلوبايت)\nالألعاب على القرص: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nللحصول على أقصى أداء، استخدم حجم عنقود لا يزيد عن 64 كيلوبايت",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "تم فصل محرك الأقراص الخارجي %s",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_cs_cz[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Přejít do nastavení ladění",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor a komunita",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor a komunita",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor a komunita\nOvládání: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 je zastaralé, používá se scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_cs_cz[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: instalace hooků ShellCore se nezdařila",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Cílová teplota %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externí disk %s připojen.\nVolné místo: %llu.%llu z %llu.%llu GB (blok %llu KB)\nHer na disku: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPro maximální výkon použijte velikost clusteru nejvýše 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externí disk %s odpojen",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_da_dk[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå til fejlfindingsindstillinger",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor og fællesskabet",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor og fællesskabet",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor og fællesskabet\nStyring: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 er forældet, bruger scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_da_dk[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-hooks kunne ikke installeres",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Eksternt drev %s tilsluttet.\nLedig plads: %llu.%llu af %llu.%llu GB (blok %llu KB)\nSpil på drevet: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nBrug en klyngestørrelse på højst 64 KB for maksimal ydeevne",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Eksternt drev %s frakoblet",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_de_de[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Zu den Debug-Einstellungen",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor und die Community",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor und die Community",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor und die Community\nSteuerung: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 ist veraltet, scan_depth=2 wird verwendet.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_de_de[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-Hooks konnten nicht installiert werden",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Zieltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externes Laufwerk %s verbunden.\nFrei: %llu.%llu von %llu.%llu GB (Blockgröße %llu KB)\nSpiele auf dem Laufwerk: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nVerwenden Sie für maximale Leistung eine Clustergröße von höchstens 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externes Laufwerk %s getrennt",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_el_gr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Μετάβαση στις ρυθμίσεις αποσφαλμάτωσης",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor και η κοινότητα",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor και η κοινότητα",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor και η κοινότητα\nΈλεγχος: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: το recursive_scan=1 είναι παρωχημένο, χρησιμοποιείται scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_el_gr[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: απέτυχε η εγκατάσταση των hooks του ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Θερμοκρασία-στόχος %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ο εξωτερικός δίσκος %s συνδέθηκε.\nΕλεύθερα: %llu.%llu από %llu.%llu GB (μέγεθος μπλοκ %llu KB)\nΠαιχνίδια στον δίσκο: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nΓια μέγιστη απόδοση, χρησιμοποιήστε μέγεθος συμπλέγματος έως 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ο εξωτερικός δίσκος %s αποσυνδέθηκε",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_en_gb[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Go to Debug Settings",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor and Community",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor and Community",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor and Community\nControl: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_en_gb[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: failed to install ShellCore hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Target temperature %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nFor maximum performance, use a cluster size of no more than 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "External drive %s disconnected",
|
||||
};
|
||||
+10
-1
@@ -1,6 +1,8 @@
|
||||
static const char *const g_en_us[SM_L10N_COUNT] = {
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Go to Debug Settings",
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor and Community",
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor and Community",
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor and Community\nControl: http://%s:%u",
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] =
|
||||
"ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2.",
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: scan depth %u enabled.",
|
||||
@@ -76,4 +78,11 @@ static const char *const g_en_us[SM_L10N_COUNT] = {
|
||||
"ShadowMount+: failed to install ShellCore hooks",
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] =
|
||||
"Target temperature %u °C → %u °C",
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] =
|
||||
"External drive %s connected\n"
|
||||
"Free %llu.%llu GB of %llu.%llu (Block size %llu KB)\n"
|
||||
"Games on drive: %d%s",
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] =
|
||||
"\nFor maximum performance, use a cluster size of no more than 64 KB",
|
||||
[SM_L10N_USB_DISCONNECTED] = "External drive %s disconnected",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_es_es[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir a ajustes de depuración",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor y la comunidad",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor y la comunidad",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor y la comunidad\nControl: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, se usará scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_es_es[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: no se pudieron instalar los hooks de ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura objetivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidad externa %s conectada.\nLibres: %llu.%llu de %llu.%llu GB (bloque %llu KB)\nJuegos en la unidad: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPara obtener el máximo rendimiento, use un tamaño de clúster no superior a 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidad externa %s desconectada",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_es_mx[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir a configuración de depuración",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor y la comunidad",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor y la comunidad",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor y la comunidad\nControl: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, se usará scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_es_mx[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: no se pudieron instalar los hooks de ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura objetivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidad externa %s conectada.\nLibres: %llu.%llu de %llu.%llu GB (bloque %llu KB)\nJuegos en la unidad: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPara obtener el máximo rendimiento, use un tamaño de clúster no superior a 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidad externa %s desconectada",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_fi_fi[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Siirry virheenkorjausasetuksiin",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor ja yhteisö",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor ja yhteisö",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor ja yhteisö\nHallinta: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 on vanhentunut, käytetään scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_fi_fi[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore-hookkien asennus epäonnistui",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Tavoitelämpötila %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ulkoinen asema %s yhdistetty.\nVapaana: %llu.%llu / %llu.%llu Gt (lohko %llu kt)\nPelejä asemalla: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nSaat parhaan suorituskyvyn käyttämällä enintään 64 kt:n klusterikokoa",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ulkoinen asema %s irrotettu",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_fr_ca[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Accéder aux paramètres de débogage",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor et la communauté",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor et la communauté",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor et la communauté\nContrôle : http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+ : recursive_scan=1 est obsolète, scan_depth=2 sera utilisé.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_fr_ca[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+ : échec de l’installation des hooks ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Température cible %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Lecteur externe %s connecté.\nLibre : %llu.%llu sur %llu.%llu Go (bloc %llu Ko)\nJeux sur le lecteur : %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPour des performances maximales, utilisez une taille de cluster ne dépassant pas 64 Ko",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Lecteur externe %s déconnecté",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_fr_fr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Accéder aux paramètres de débogage",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor et la communauté",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor et la communauté",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor et la communauté\nContrôle : http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+ : recursive_scan=1 est obsolète, scan_depth=2 sera utilisé.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_fr_fr[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+ : échec de l’installation des hooks ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Température cible %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Lecteur externe %s connecté.\nLibre : %llu.%llu sur %llu.%llu Go (bloc %llu Ko)\nJeux sur le lecteur : %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPour des performances maximales, utilisez une taille de cluster ne dépassant pas 64 Ko",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Lecteur externe %s déconnecté",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_hu_hu[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ugrás a hibakeresési beállításokhoz",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor és a közösség",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor és a közösség",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor és a közösség\nVezérlés: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: a recursive_scan=1 elavult, scan_depth=2 lesz használva.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_hu_hu[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: a ShellCore hookok telepítése sikertelen",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Célhőmérséklet %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Külső meghajtó csatlakoztatva: %s.\nSzabad: %llu.%llu / %llu.%llu GB (blokkméret %llu KB)\nJátékok a meghajtón: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nA maximális teljesítményhez használjon legfeljebb 64 KB-os klaszterméretet",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Külső meghajtó leválasztva: %s",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_id_id[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Buka pengaturan debug",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor dan Komunitas",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor dan Komunitas",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor dan Komunitas\nKontrol: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 sudah usang, memakai scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_id_id[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: gagal memasang hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Suhu target %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Drive eksternal %s tersambung.\nKosong: %llu.%llu dari %llu.%llu GB (ukuran blok %llu KB)\nGame di drive: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nUntuk performa maksimal, gunakan ukuran klaster tidak lebih dari 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Drive eksternal %s terputus",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_it_it[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Vai alle impostazioni di debug",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor e la community",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e la community",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor e la community\nControllo: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 è deprecato; verrà usato scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_it_it[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: impossibile installare gli hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura obiettivo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unità esterna %s collegata.\nLiberi: %llu.%llu di %llu.%llu GB (blocco %llu KB)\nGiochi sull'unità: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPer prestazioni ottimali, usa una dimensione cluster non superiore a 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unità esterna %s scollegata",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_ja_jp[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "デバッグ設定へ移動",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor とコミュニティ",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor とコミュニティ",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor とコミュニティ\n管理: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 は非推奨です。scan_depth=2 を使用します。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_ja_jp[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCoreフックのインストールに失敗しました",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目標温度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外付けドライブ %s が接続されました。\n空き容量: %llu.%llu / %llu.%llu GB(ブロックサイズ: %llu KB)\nドライブ上のゲーム: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\n最大限のパフォーマンスを得るには、クラスターサイズを 64 KB 以下にしてください",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外付けドライブ %s が切断されました",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_ko_kr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "디버그 설정으로 이동",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor 및 커뮤니티",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 및 커뮤니티",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor 및 커뮤니티\n관리: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1은 더 이상 권장되지 않으며 scan_depth=2를 사용합니다.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_ko_kr[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore 후크 설치에 실패했습니다",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "목표 온도 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "외장 드라이브 %s 연결됨.\n여유 공간: %llu.%llu / %llu.%llu GB (블록 크기: %llu KB)\n드라이브의 게임: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\n최대 성능을 위해 64 KB 이하의 클러스터 크기를 사용하세요",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "외장 드라이브 %s 연결 해제됨",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_nl_nl[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ga naar debug-instellingen",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor en de community",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor en de community",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor en de community\nBeheer: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 is verouderd, scan_depth=2 wordt gebruikt.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_nl_nl[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: installeren van ShellCore-hooks mislukt",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Doeltemperatuur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Externe schijf %s aangesloten.\nVrij: %llu.%llu van %llu.%llu GB (blokgrootte %llu KB)\nGames op de schijf: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nGebruik voor maximale prestaties een clustergrootte van maximaal 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Externe schijf %s losgekoppeld",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_no_no[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå til feilsøkingsinnstillinger",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor og fellesskapet",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor og fellesskapet",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor og fellesskapet\nKontroll: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 er utdatert, bruker scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_no_no[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: kunne ikke installere ShellCore-hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Ekstern disk %s tilkoblet.\nLedig: %llu.%llu av %llu.%llu GB (blokkstørrelse %llu KB)\nSpill på disken: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nBruk en klyngestørrelse på maksimalt 64 KB for maksimal ytelse",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Ekstern disk %s frakoblet",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_pl_pl[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Przejdź do ustawień debugowania",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor i społeczność",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor i społeczność",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor i społeczność\nSterowanie: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 jest przestarzałe, zastosowano scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_pl_pl[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: nie udało się zainstalować hooków ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura docelowa %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Dysk zewnętrzny %s podłączony.\nWolne: %llu.%llu z %llu.%llu GB (rozmiar bloku %llu KB)\nGry na dysku: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nAby uzyskać maksymalną wydajność, użyj rozmiaru klastra nie większego niż 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Dysk zewnętrzny %s odłączony",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_pt_br[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir para as Configurações de Depuração",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor e a comunidade",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e a comunidade",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor e a comunidade\nControle: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, usando scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_pt_br[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: falha ao instalar hooks do ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura-alvo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidade externa %s conectada.\nLivre: %llu.%llu de %llu.%llu GB (bloco %llu KB)\nJogos na unidade: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPara obter o máximo desempenho, use clusters de no máximo 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidade externa %s desconectada",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_pt_pt[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Ir para Definições de depuração",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor e a comunidade",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor e a comunidade",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor e a comunidade\nControlo: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 está obsoleto, será usado scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_pt_pt[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: não foi possível instalar os hooks do ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura-alvo %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unidade externa %s ligada.\nLivre: %llu.%llu de %llu.%llu GB (bloco %llu KB)\nJogos na unidade: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPara obter o máximo desempenho, utilize clusters de no máximo 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unidade externa %s desligada",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_ro_ro[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Mergi la setările de depanare",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor și comunitatea",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor și comunitatea",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor și comunitatea\nControl: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 este depășit, se folosește scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_ro_ro[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: instalarea hook-urilor ShellCore a eșuat",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Temperatura țintă %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Unitatea externă %s a fost conectată.\nLiber: %llu.%llu din %llu.%llu GB (dimensiunea blocului %llu KB)\nJocuri pe unitate: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nPentru performanță maximă, utilizați o dimensiune a clusterului de cel mult 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Unitatea externă %s a fost deconectată",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_ru_ru[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Перейти к настройкам отладки",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor и Сообщество",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor и Сообщество",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor и Сообщество\nУправление: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: параметр recursive_scan=1 устарел; используется scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_ru_ru[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: не удалось установить хуки ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Целевая температура %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Внешний диск %s подключён.\nСвободно %llu.%llu из %llu.%llu ГБ (блок %llu КБ)\nИгр на диске: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nДля максимальной производительности используйте кластер не более 64 КБ",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Внешний диск %s отключён.",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_sv_se[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Gå till felsökningsinställningar",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor och gemenskapen",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor och gemenskapen",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor och gemenskapen\nStyrning: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 är föråldrat, använder scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_sv_se[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: det gick inte att installera ShellCore-hooks",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Måltemperatur %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Extern enhet %s ansluten.\nLedigt: %llu.%llu av %llu.%llu GB (blockstorlek %llu KB)\nSpel på enheten: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nAnvänd en klusterstorlek på högst 64 KB för maximal prestanda",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Extern enhet %s frånkopplad",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_th_th[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "ไปที่การตั้งค่าดีบัก",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor และชุมชน",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor และชุมชน",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor และชุมชน\nควบคุม: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 เลิกใช้แล้ว จะใช้ scan_depth=2",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_th_th[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ไม่สามารถติดตั้งฮุก ShellCore ได้",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "อุณหภูมิเป้าหมาย %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "เชื่อมต่อไดรฟ์ภายนอก %s แล้ว\nว่าง: %llu.%llu จาก %llu.%llu GB (ขนาดบล็อก %llu KB)\nเกมในไดรฟ์: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nเพื่อประสิทธิภาพสูงสุด ให้ใช้ขนาดคลัสเตอร์ไม่เกิน 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "ตัดการเชื่อมต่อไดรฟ์ภายนอก %s แล้ว",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_tr_tr[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Hata ayıklama ayarlarına git",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor ve Topluluk",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor ve Topluluk",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor ve Topluluk\nKontrol: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 kullanımdan kaldırıldı; scan_depth=2 kullanılacak.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_tr_tr[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: ShellCore hook'ları yüklenemedi",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Hedef sıcaklık %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Harici sürücü %s bağlandı.\nBoş: %llu.%llu / %llu.%llu GB (blok boyutu %llu KB)\nSürücüdeki oyunlar: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nMaksimum performans için en fazla 64 KB küme boyutu kullanın",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Harici sürücü %s çıkarıldı",
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
static const char *const g_uk_ua[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Перейти до налаштувань налагодження",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor і спільнота",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor і спільнота\nКерування: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: параметр recursive_scan=1 застарів; використовується scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: увімкнено глибину сканування %u.",
|
||||
/* English: Backport overlay failed: %s\n%s\n0x%08X */
|
||||
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "Не вдалося змонтувати оверлей бекпорту: %s\n%s\n0x%08X",
|
||||
/* English: Backport applied to game: %s */
|
||||
[SM_L10N_GAME_BACKPORTED] = "До гри %s застосовано бекпорт",
|
||||
/* English: Backport applied to game: %s\nEmulators updated */
|
||||
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "До гри %s застосовано бекпорт\nЕмулятори оновлено",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "Монтування образу відхилено:\n%s\nРозмір кластера замалий (%llu < %u).\nЗбережено перевизначення image_sector: %u.\nСпробуйте змонтувати знову.",
|
||||
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
|
||||
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "Монтування образу відхилено:\n%s\nРозмір кластера замалий (%llu < %u).\nЗбільште розмір кластера в образі або зменште розмір сектора в конфігурації.",
|
||||
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
|
||||
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "Не вдалося зареєструвати: %s (%s)\nAppInstallTitleDir недоступний",
|
||||
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
|
||||
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "Не вдалося зареєструвати: %s (%s)\nМіст TitleDir недоступний",
|
||||
/* English: Register failed: %s (%s)\ncode=0x%08X */
|
||||
[SM_L10N_REGISTER_FAILED_CODE] = "Не вдалося зареєструвати: %s (%s)\nкод=0x%08X",
|
||||
/* English: Installing: %s (%s)... */
|
||||
[SM_L10N_INSTALLING] = "Встановлення: %s (%s)...",
|
||||
/* English: KStuff active again */
|
||||
[SM_L10N_KSTUFF_ACTIVE] = "KStuff знову активний",
|
||||
/* English: KStuff paused while the game is running */
|
||||
[SM_L10N_KSTUFF_PAUSED] = "KStuff призупинено на час роботи гри",
|
||||
/* English: Installed: %s */
|
||||
[SM_L10N_INSTALLED] = "Встановлено: %s",
|
||||
/* English: Unknown */
|
||||
[SM_L10N_UNKNOWN] = "Невідомо",
|
||||
/* English: Image mount failed: 0x%08X (%s)\n%s */
|
||||
[SM_L10N_IMAGE_MOUNT_FAILED] = "Не вдалося змонтувати образ: 0x%08X (%s)\n%s",
|
||||
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
|
||||
[SM_L10N_FS_CLUSTER_SMALL] = "Розмір кластера файлової системи (%llu) менший за розмір сектора пристрою (%u) для %s",
|
||||
/* English: Mounted image is unreadable or damaged */
|
||||
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "Змонтований образ не читається або пошкоджений",
|
||||
/* English: Image cache full (%u entries), cannot track mount %s */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "Кеш образів заповнений (%u записів), неможливо відстежити монтування %s",
|
||||
/* English: Image cache full (%u entries), rolling back mount */
|
||||
[SM_L10N_IMAGE_CACHE_FULL_ROLLBACK] = "Кеш образів заповнений (%u записів), монтування скасовується",
|
||||
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
|
||||
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "Не вдалося встановити: %s (%s)\nЧас очікування вичерпано через 5 хвилин",
|
||||
/* English: Batch install queued (%d): */
|
||||
[SM_L10N_BATCH_INSTALL_QUEUED] = "Пакетне встановлення додано до черги (%d):",
|
||||
/* English: \n... and %d more */
|
||||
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... і ще %d",
|
||||
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
|
||||
[SM_L10N_BATCH_INSTALL_FAILED] = "Не вдалося пакетно встановити %d застосунків.\nкод=0x%08X",
|
||||
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF] = "Застосунок %s аварійно завершився до призупинення KStuff. Причина не в KStuff.",
|
||||
/* English: %s crashed before KStuff pause:\n%s */
|
||||
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "Застосунок %s аварійно завершився до призупинення KStuff:\n%s",
|
||||
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "Застосунок %s аварійно завершився після призупинення KStuff. Автоналаштування пропущено.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
|
||||
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nАвтоналаштування пропущено.",
|
||||
/* English: Can't load module after KStuff pause */
|
||||
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "Не вдалося завантажити модуль після призупинення KStuff",
|
||||
/* English: can't load module %.*s after KStuff pause */
|
||||
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "не вдалося завантажити модуль %.*s після призупинення KStuff",
|
||||
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. Затримку збільшено до %u с.\nЗапустіть гру знову.",
|
||||
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
|
||||
[SM_L10N_CRASH_DELAY_INCREASED] = "Виявлено збій після призупинення KStuff: затримку для %s збільшено до %u с. Запустіть гру знову.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nЗатримку збільшено до %u с.\nЗапустіть гру знову.",
|
||||
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
|
||||
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "Виявлено збій після призупинення KStuff: %s. Не вдалося оновити затримку автоналаштування.",
|
||||
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
|
||||
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "Застосунок %s аварійно завершився після призупинення KStuff:\n%s\nНе вдалося оновити затримку автоналаштування.",
|
||||
/* English: Missing/invalid param.json:\n%s */
|
||||
[SM_L10N_MISSING_PARAM] = "Файл param.json відсутній або некоректний:\n%s",
|
||||
/* English: Found %d new games. Processing... */
|
||||
[SM_L10N_FOUND_NEW_GAMES] = "Знайдено %d нових ігор. Обробка...",
|
||||
/* English: Library synchronized.\nFound %d games. */
|
||||
[SM_L10N_LIBRARY_SYNCED] = "Бібліотеку синхронізовано.\nЗнайдено %d ігор.",
|
||||
/* English: ShadowMount+: config reloaded. */
|
||||
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: конфігурацію перезавантажено.",
|
||||
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
|
||||
[SM_L10N_DUPLICATE_IGNORED] = "Дублікат %s проігноровано:\n%s\nнаявний: %s",
|
||||
/* English: AMPR emulator updated */
|
||||
[SM_L10N_AMPR_EMULATOR_UPDATED] = "Емулятор AMPR оновлено",
|
||||
/* English: ShadowMount+: failed to install ShellCore hooks */
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: не вдалося встановити хуки ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Цільова температура %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Зовнішній диск %s підключено.\nВільно %llu.%llu із %llu.%llu ГБ (блок %llu КБ)\nІгор на диску: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nДля максимальної продуктивності використовуйте кластер не більше 64 КБ",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Зовнішній диск %s відключено",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_vi_vn[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "Đi tới cài đặt gỡ lỗi",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor và Cộng đồng",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor và Cộng đồng",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor và Cộng đồng\nĐiều khiển: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 đã lỗi thời; sẽ sử dụng scan_depth=2.",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_vi_vn[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+: không thể cài đặt hook ShellCore",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "Nhiệt độ mục tiêu %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "Đã kết nối ổ đĩa ngoài %s.\nTrống: %llu.%llu / %llu.%llu GB (kích thước khối %llu KB)\nTrò chơi trên ổ đĩa: %d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nĐể đạt hiệu suất tối đa, hãy sử dụng kích thước cụm không quá 64 KB",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "Đã ngắt kết nối ổ đĩa ngoài %s",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_zh_cn[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "前往调试设置",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor 与社区",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 与社区",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor 与社区\n管理: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 已弃用,将使用 scan_depth=2。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_zh_cn[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+:无法安装 ShellCore 钩子",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目标温度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外部驱动器 %s 已连接。\n可用:%llu.%llu / %llu.%llu GB(块大小 %llu KB)\n驱动器上的游戏:%d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\n为获得最佳性能,请使用不超过 64 KB 的簇大小",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外部驱动器 %s 已断开",
|
||||
};
|
||||
+11
-2
@@ -1,8 +1,11 @@
|
||||
static const char *const g_zh_tw[SM_L10N_COUNT] = {
|
||||
/* English: Go to Debug Settings */
|
||||
[SM_L10N_ACTION_DEBUG_SETTINGS] = "前往除錯設定",
|
||||
/* English: ShadowMount+ v%s © Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s © Drakmor 與社群",
|
||||
/* English: ShadowMount+ v%s\n© Drakmor and Community */
|
||||
[SM_L10N_STARTUP] = "ShadowMount+ v%s\n© Drakmor 與社群",
|
||||
/* English: ShadowMount+ startup with web interface */
|
||||
[SM_L10N_STARTUP_WEB] =
|
||||
"ShadowMount+ v%s\n© Drakmor 與社群\n管理: http://%s:%u",
|
||||
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
|
||||
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: recursive_scan=1 已淘汰,將使用 scan_depth=2。",
|
||||
/* English: ShadowMount+: scan depth %u enabled. */
|
||||
@@ -89,4 +92,10 @@ static const char *const g_zh_tw[SM_L10N_COUNT] = {
|
||||
[SM_L10N_SHELLCORE_HOOKS_FAILED] = "ShadowMount+:無法安裝 ShellCore 掛鉤",
|
||||
/* English: Target temperature %u °C → %u °C */
|
||||
[SM_L10N_TEMPERATURE_TRANSITION] = "目標溫度 %u °C → %u °C",
|
||||
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d */
|
||||
[SM_L10N_USB_CONNECTED_SCANNING] = "外接磁碟機 %s 已連接。\n可用:%llu.%llu / %llu.%llu GB(區塊大小 %llu KB)\n磁碟機上的遊戲:%d%s",
|
||||
/* English: For maximum performance, use a cluster size of no more than 64 KB */
|
||||
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\n為獲得最佳效能,請使用不超過 64 KB 的叢集大小",
|
||||
/* English: External drive %s disconnected */
|
||||
[SM_L10N_USB_DISCONNECTED] = "外接磁碟機 %s 已中斷連接",
|
||||
};
|
||||
@@ -9,6 +9,8 @@
|
||||
bool sm_ampr_updater_start(void);
|
||||
// Interrupt any active request and join the update worker.
|
||||
void sm_ampr_updater_stop(void);
|
||||
// Cancel network work on sleep entry and wake the dormant worker on resume.
|
||||
void sm_ampr_updater_on_sleep_change(bool active);
|
||||
// Wake the worker when its enabled state, URL, or destination changes.
|
||||
void sm_ampr_updater_on_config_reload(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg);
|
||||
|
||||
@@ -7,10 +7,29 @@
|
||||
#define SM_API_MAX_HTTP_HEADER_SIZE 8192u
|
||||
#define SM_API_MAX_JSON_BODY_SIZE 4096u
|
||||
|
||||
#define SM_API_ROUTE_INDEX "/"
|
||||
#define SM_API_ROUTE_VERSION "/api/v1/version"
|
||||
#define SM_API_ROUTE_STORAGE "/api/v1/storage"
|
||||
#define SM_API_ROUTE_IMAGES "/api/v1/images"
|
||||
#define SM_API_ROUTE_GAMES "/api/v1/games"
|
||||
#define SM_API_ROUTE_GAME_INFO "/api/v1/games/info"
|
||||
#define SM_API_ROUTE_GAME_ICON "/api/v1/games/icon"
|
||||
#define SM_API_ROUTE_MOUNT "/api/v1/games/mount"
|
||||
#define SM_API_ROUTE_UNMOUNT "/api/v1/games/unmount"
|
||||
#define SM_API_ROUTE_UNINSTALL "/api/v1/games/uninstall"
|
||||
#define SM_API_ROUTE_GAME_MOVE "/api/v1/games/move"
|
||||
#define SM_API_ROUTE_GAME_COPY "/api/v1/games/copy"
|
||||
#define SM_API_ROUTE_GAME_DELETE "/api/v1/games/delete"
|
||||
#define SM_API_ROUTE_GAME_UNPACK "/api/v1/games/unpack"
|
||||
#define SM_API_ROUTE_GAME_STORAGE_STATUS "/api/v1/games/storage/status"
|
||||
#define SM_API_ROUTE_GAME_STORAGE_CANCEL "/api/v1/games/storage/cancel"
|
||||
#define SM_API_ROUTE_MANUAL_LIST "/api/v1/manual/list"
|
||||
#define SM_API_ROUTE_MANUAL_ADD "/api/v1/manual/add"
|
||||
#define SM_API_ROUTE_MANUAL_REMOVE "/api/v1/manual/remove"
|
||||
#define SM_API_ROUTE_SETTINGS "/api/v1/settings"
|
||||
#define SM_API_ROUTE_SETTINGS_UPDATE "/api/v1/settings/update"
|
||||
#define SM_API_ROUTE_DEBUG_LOG "/api/v1/debug-log"
|
||||
#define SM_API_ROUTE_KERNEL_LOG "/api/v1/kernel-log"
|
||||
#define SM_API_ROUTE_SCAN "/api/v1/scan"
|
||||
|
||||
#endif
|
||||
@@ -6,5 +6,7 @@
|
||||
bool sm_api_service_start(void);
|
||||
void sm_api_service_stop(void);
|
||||
bool sm_api_service_reconfigure(void);
|
||||
// Stop the HTTP daemon on sleep entry and recreate it after resume.
|
||||
void sm_api_service_on_sleep_change(bool active);
|
||||
|
||||
#endif
|
||||
@@ -2,9 +2,24 @@
|
||||
#define SM_APPDB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
typedef struct {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char content_id[MAX_CONTENT_ID];
|
||||
char title_name[MAX_TITLE_NAME];
|
||||
char last_access_time[MAX_APP_DB_TIMESTAMP];
|
||||
char install_time[MAX_APP_DB_TIMESTAMP];
|
||||
char icon_path[MAX_PATH];
|
||||
int platform;
|
||||
uint64_t installed_size;
|
||||
} sm_app_db_game_info_t;
|
||||
|
||||
// Close the cached app.db handle and release related resources.
|
||||
void shutdown_app_db(void);
|
||||
// Normalize existing snd0info rows during process startup.
|
||||
@@ -26,5 +41,11 @@ bool get_app_db_title_list_cached(struct AppDbTitleList *list_out);
|
||||
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);
|
||||
// Allocate a read-only snapshot of game metadata from tbl_contentinfo.
|
||||
bool app_db_game_info_snapshot(sm_app_db_game_info_t **entries_out,
|
||||
size_t *count_out);
|
||||
// Find one title in a snapshot sorted by title_id.
|
||||
const sm_app_db_game_info_t *app_db_find_game_info(
|
||||
const sm_app_db_game_info_t *entries, size_t count, const char *title_id);
|
||||
|
||||
#endif
|
||||
@@ -2,9 +2,10 @@
|
||||
#define SM_CONFIG_MOUNT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct runtime_config runtime_config_t;
|
||||
#include "sm_types.h"
|
||||
|
||||
// Ensure runtime configuration is loaded before use.
|
||||
void ensure_runtime_config_ready(void);
|
||||
@@ -12,12 +13,26 @@ void ensure_runtime_config_ready(void);
|
||||
bool load_runtime_config(void);
|
||||
// Reload runtime configuration from disk when config.ini changed.
|
||||
bool reload_runtime_config_if_changed(bool *reloaded_out);
|
||||
// Return the current runtime configuration.
|
||||
const runtime_config_t *runtime_config(void);
|
||||
// Atomically persist the settings exposed by the HTTP API while preserving
|
||||
// unrelated config.ini keys and comments. Runtime reload remains scanner-owned.
|
||||
bool sm_config_write_web_settings(bool debug_enabled, bool quiet_mode,
|
||||
bool update_emulators_enabled,
|
||||
bool auto_update_ampr_enabled,
|
||||
bool auto_remove_missing_games,
|
||||
uint32_t auto_remove_missing_delay_seconds,
|
||||
bool allow_lan_access,
|
||||
uint32_t fan_target_temperature_c,
|
||||
const char *const *scan_paths,
|
||||
size_t scan_path_count);
|
||||
// Return a coherent caller-owned copy of the current runtime configuration.
|
||||
runtime_config_t runtime_config(void);
|
||||
// Return the number of configured scan roots.
|
||||
int get_scan_path_count(void);
|
||||
// Return a scan root by index, or NULL if out of range.
|
||||
const char *get_scan_path(int index);
|
||||
// Copy a scan root; return false and an empty path if the index is invalid.
|
||||
bool get_scan_path(int index, char path_out[MAX_PATH]);
|
||||
// Return only scan roots explicitly configured through scanpath entries.
|
||||
int get_custom_scan_path_count(void);
|
||||
bool get_custom_scan_path(int index, char path_out[MAX_PATH]);
|
||||
// Return scan depth for a root, including managed container-root expansion.
|
||||
uint32_t get_scan_depth_for_root(const char *scan_path);
|
||||
// Resolve a per-image read-only override from the file name.
|
||||
|
||||
@@ -12,7 +12,12 @@ void sm_fakelib_prepare_title_cache(const char *title_id,
|
||||
const char *game_path);
|
||||
// Remove invalid and unused fakelib caches during the scanner's rare cycle.
|
||||
void sm_fakelib_cleanup_caches(void);
|
||||
// Track a supported game launch and mount its fakelib overlay immediately.
|
||||
// Mount a prepared fakelib overlay after ShellCore creates the sandbox but
|
||||
// before it spawns the application process.
|
||||
bool sm_fakelib_game_on_sandbox_ready(const char *title_id);
|
||||
// Roll back an unbound pre-spawn overlay when ShellCore rejects the launch.
|
||||
void sm_fakelib_game_on_launch_failed(const char *title_id);
|
||||
// Bind a pre-spawn overlay to the process, or mount it as a NOTE_EXEC fallback.
|
||||
// notify_user is false for an internal process handoff of the same application.
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
bool notify_user);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef SM_ICON_THUMB_H
|
||||
#define SM_ICON_THUMB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
#define SM_ICON_THUMB_SIZE 128u
|
||||
|
||||
// Return a cached thumbnail path, creating it atomically when necessary.
|
||||
bool sm_icon_thumbnail_path(const char *title_id, const char *source_path,
|
||||
const struct stat *source_st,
|
||||
char out[MAX_PATH]);
|
||||
|
||||
#endif
|
||||
+20
-3
@@ -5,15 +5,27 @@
|
||||
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct unmount_result {
|
||||
bool filesystem_released;
|
||||
bool device_released;
|
||||
bool directory_removed;
|
||||
int error;
|
||||
} unmount_result_t;
|
||||
|
||||
// Log filesystem statistics for a mounted path.
|
||||
void log_fs_stats(const char *tag, const char *path, const char *type_hint);
|
||||
// Attach and mount an image file to its runtime mount point.
|
||||
bool mount_image(const char *file_path, image_fs_type_t fs_type);
|
||||
// Attach and mount with a request-scoped mode override. A NULL override uses
|
||||
// the configured per-image/global mode.
|
||||
bool mount_image_with_mode(const char *file_path, image_fs_type_t fs_type,
|
||||
const bool *mount_read_only_override);
|
||||
// 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_result_t 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);
|
||||
unmount_result_t 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.
|
||||
@@ -31,6 +43,9 @@ 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 *display_name,
|
||||
bool *unstable_out);
|
||||
bool maybe_mount_image_file_with_mode(
|
||||
const char *full_path, const char *display_name, bool *unstable_out,
|
||||
const bool *mount_read_only_override);
|
||||
// Build the deterministic runtime mount point for an image source path.
|
||||
void get_image_mount_point_for_source(const char *file_path,
|
||||
char mount_point[MAX_PATH]);
|
||||
@@ -38,5 +53,7 @@ void get_image_mount_point_for_source(const char *file_path,
|
||||
bool is_supported_image_file_name(const char *name);
|
||||
// Return true when the source path is a supported image, including nested names.
|
||||
bool is_supported_image_file_path(const char *full_path, const char *name);
|
||||
// Return the public filesystem type name for a supported image path.
|
||||
const char *image_fs_type_name_for_path(const char *path);
|
||||
|
||||
#endif
|
||||
@@ -6,30 +6,54 @@
|
||||
#include "sm_types.h"
|
||||
|
||||
typedef struct {
|
||||
uint64_t generation;
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
attached_device_state_t state;
|
||||
attached_unit_detach_state_t detach_state;
|
||||
} image_cache_entry_t;
|
||||
|
||||
// Reserve a cache record before issuing an attach ioctl.
|
||||
bool begin_image_attachment(const char *path, const char *mount_point,
|
||||
uint64_t *generation_out);
|
||||
// Cancel an unpublished attachment reservation of the same generation.
|
||||
bool cancel_image_attachment(const char *path, uint64_t generation);
|
||||
// Publish the kernel unit immediately after a successful attach ioctl.
|
||||
bool publish_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Mark a published device as a verified filesystem mount.
|
||||
bool complete_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache a successful image mount and its attached device.
|
||||
// Returns false when no free cache slot is available.
|
||||
// Returns false when no slot is available or either key has another owner.
|
||||
bool cache_image_mount(const char *path, const char *mount_point,
|
||||
int unit_id, attach_backend_t backend);
|
||||
// Cache an image source mapping without attach metadata.
|
||||
// Cache an image source mapping without attach metadata. Conflicting active
|
||||
// source-path and mount-point owners are never retargeted.
|
||||
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);
|
||||
// Publish an accepted asynchronous detach for the same cached device.
|
||||
bool update_image_cache_detach_state(
|
||||
const char *path, int unit_id, attach_backend_t backend,
|
||||
// Keep a device in the detach-retry queue until its node is released.
|
||||
bool mark_image_cache_detach_requested(const char *path, uint64_t generation,
|
||||
int unit_id,
|
||||
attach_backend_t backend);
|
||||
// Claim one detach attempt and return a stable snapshot of its record.
|
||||
bool claim_image_cache_detach(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend,
|
||||
image_cache_entry_t *entry_out);
|
||||
// Publish the result of a claimed detach attempt and release its claim.
|
||||
bool finish_image_cache_detach_attempt(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend,
|
||||
const attached_unit_detach_state_t *detach_state);
|
||||
// Mark a cached image mount entry as invalid.
|
||||
void invalidate_image_cache_entry(int index);
|
||||
// Invalidate only when the slot still owns the complete expected attachment.
|
||||
bool invalidate_image_cache_entry(int index,
|
||||
const image_cache_entry_t *expected);
|
||||
// Resolve a device mapping from the in-memory mount cache.
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
typedef void (*sm_image_index_title_visitor_t)(const char *title_id);
|
||||
typedef void (*sm_image_index_title_visitor_t)(const char *title_id,
|
||||
const void *ctx);
|
||||
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
@@ -25,7 +26,7 @@ typedef struct {
|
||||
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);
|
||||
sm_image_index_title_visitor_t visitor, const void *visitor_ctx);
|
||||
// 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.
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
#ifndef SM_INSTALL_H
|
||||
#define SM_INSTALL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct scan_candidate scan_candidate_t;
|
||||
|
||||
// Register one already-staged title directory. Returns false when no usable
|
||||
// TitleDir registration path is available; the API result is returned intact.
|
||||
bool sm_install_register_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out);
|
||||
|
||||
// Install or remount the collected scan candidates.
|
||||
void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count);
|
||||
|
||||
@@ -5,10 +5,18 @@
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#define SM_KSTUFF_KEKCALL_CHECK UINT64_C(0xffffffff00000027)
|
||||
#define SM_KSTUFF_KEKCALL_REMOTE_SYSCALL UINT64_C(0x500000027)
|
||||
|
||||
// Resolve firmware-specific kstuff sysentvec addresses and initialize state.
|
||||
void sm_kstuff_init(void);
|
||||
// Shut down runtime kstuff control state.
|
||||
void sm_kstuff_shutdown(void);
|
||||
// Return true when kstuff answers its own KEKCALL_CHECK readiness probe.
|
||||
bool sm_kstuff_is_loaded(void);
|
||||
// Run mprotect for another process through kstuff's remote-syscall service.
|
||||
bool sm_kstuff_remote_mprotect(pid_t pid, uintptr_t address, size_t size,
|
||||
int protection);
|
||||
// Return true when firmware-specific kstuff control is available.
|
||||
bool sm_kstuff_is_supported(void);
|
||||
// Return true when both tracked kstuff sysentvecs are enabled.
|
||||
|
||||
@@ -51,6 +51,10 @@ typedef enum {
|
||||
SM_L10N_AMPR_EMULATOR_UPDATED,
|
||||
SM_L10N_SHELLCORE_HOOKS_FAILED,
|
||||
SM_L10N_TEMPERATURE_TRANSITION,
|
||||
SM_L10N_USB_CONNECTED_SCANNING,
|
||||
SM_L10N_USB_CLUSTER_RECOMMENDATION,
|
||||
SM_L10N_USB_DISCONNECTED,
|
||||
SM_L10N_STARTUP_WEB,
|
||||
SM_L10N_COUNT
|
||||
} sm_l10n_key_t;
|
||||
|
||||
|
||||
@@ -42,8 +42,11 @@
|
||||
#define GAME_SLEEP_EXIT_TIMEOUT_US 5000000u
|
||||
#define RUNTIME_RESUME_GRACE_US 10000000u
|
||||
#define RUNTIME_RESUME_POLL_US 100000u
|
||||
#define SHELLCORE_MPROTECT_WAIT_MAX_US 30000000u
|
||||
#define SHELLCORE_MPROTECT_WAIT_POLL_US 100000u
|
||||
#define KSTUFF_RESTORE_RETRY_US 1000000u
|
||||
#define SHELLCORE_FLAG_POLL_INTERVAL_US 100000u
|
||||
#define SHELLCORE_FLAG_SLEEP_POLL_INTERVAL_US 500000u
|
||||
#define MIN_SCAN_DEPTH 1u
|
||||
#define MAX_SCAN_DEPTH 2u
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
@@ -62,6 +65,8 @@
|
||||
#define MAX_PATH 1024
|
||||
#define MAX_TITLE_ID 32
|
||||
#define MAX_TITLE_NAME 256
|
||||
#define MAX_CONTENT_ID 128
|
||||
#define MAX_APP_DB_TIMESTAMP 64
|
||||
#define MAX_API_BIND_ADDRESS 16
|
||||
#define MAX_PARAM_JSON_SIZE (1024u * 1024u)
|
||||
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct AppDbTitleList;
|
||||
|
||||
// Normalize one absolute manual source path using manual.lst rules.
|
||||
bool sm_manual_normalize_path(const char *path, char out[MAX_PATH]);
|
||||
// Visit each manual source path from manual.lst.
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx);
|
||||
// Idempotently add or remove one normalized absolute source path. The list is
|
||||
// replaced atomically and changed_out reports whether its contents changed.
|
||||
bool sm_manual_add_path(const char *path, bool *changed_out);
|
||||
bool sm_manual_remove_path(const char *path, bool *changed_out);
|
||||
// Mark previously installed manual titles as deleted when they disappear from
|
||||
// app.db, and remove their source lines from manual.lst.
|
||||
bool sm_manual_reconcile_deleted_titles(const struct AppDbTitleList *app_db_titles,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define SM_MDBG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
@@ -22,5 +23,9 @@ void sm_mdbg_game_shutdown(void);
|
||||
uint64_t sm_mdbg_next_wake_us(uint64_t now_us);
|
||||
// Poll the tracked process for mdbg crash-candidate state.
|
||||
void sm_mdbg_poll(void);
|
||||
// Return a caller-owned tail of the same SDK log stream used by crash
|
||||
// detection. The total byte count describes the current complete snapshot.
|
||||
int sm_mdbg_get_log_tail(size_t max_bytes, char **text_out,
|
||||
size_t *text_length_out, size_t *total_length_out);
|
||||
|
||||
#endif
|
||||
+11
-10
@@ -40,12 +40,17 @@
|
||||
* optional OTBL metadata. Optimized ShadowMount profiles are:
|
||||
*
|
||||
* .exfat type 5 Sv, direct + BFS sdimg batching
|
||||
* fallback: type 0 Dfl
|
||||
* .ffpkg (UFS) type 5 Sv, direct + BFS sdimg batching
|
||||
* standalone .ffpfs type 5 Sv, direct
|
||||
* outer .ffpfsc type 9 AcO, layered package outer
|
||||
* inner .ffpfs/pfs_image.dat type 8 AcI, layered package inner
|
||||
*
|
||||
* exFAT, UFS/.ffpkg and standalone PFS/.ffpfs use the optimized profiles above
|
||||
* only when their direct source path is under /data or /user. Other direct
|
||||
* sources use the version 1.6 profiles: type 0 for exFAT, type 7 for UFS and
|
||||
* the sector-size mapping unit for standalone PFS. The outer .ffpfsc container
|
||||
* and its nested .ffpfs/pfs_image.dat images always use optimized profiles.
|
||||
*
|
||||
* ShadowMount deliberately uses type 5 with single/save attach flags for UFS
|
||||
* images so a standalone .ffpkg on BFS can use sdimg batching. Type 7 with
|
||||
* Download Data instead favors HDD request isolation. UFS nmount has
|
||||
@@ -150,9 +155,8 @@
|
||||
* exists.
|
||||
*
|
||||
* 2. PFS_GDDR5_CACHE_IOCTL can be issued on a nested image's backing vnode
|
||||
* before LVD attach. The optimized profile requests it automatically; the
|
||||
* legacy profile requires nested_pfs_index_cache=1. It requests the
|
||||
* containing PFS/PPR read path to cache the
|
||||
* before LVD attach when nested_pfs_index_cache=1.
|
||||
* The ioctl asks the containing PFS/PPR read path to cache the
|
||||
* compressed-offset table used to reach that vnode's data; it does not
|
||||
* inspect or impose a format on the bytes stored inside the file. Thus the
|
||||
* nested file may contain PFS, UFS, exFAT or arbitrary block data and need
|
||||
@@ -203,7 +207,7 @@
|
||||
* -------------------------
|
||||
* LVD is the default for every format. MD is a compatibility backend for
|
||||
* UFS/exFAT; it uses AUTOUNIT|ASYNC and configurable logical sectors (modern
|
||||
* UFS default 4096, legacy UFS default 512, exFAT default 512). Read-only state
|
||||
* UFS default 4096, exFAT default 512). Read-only state
|
||||
* is applied to the backing device and nmount. The shared nmount profiles are:
|
||||
*
|
||||
* UFS ufs + budgetid + async + noatime + automounted
|
||||
@@ -219,10 +223,8 @@
|
||||
* zone-aware modes are separate filesystem features and are not DD flags.
|
||||
*
|
||||
* force is appended only for explicit recovery. Unsigned PFS keeps signature
|
||||
* verification disabled. The optimized profile primes every nested image on
|
||||
* PFS before LVD attach; the legacy profile does so only when explicitly
|
||||
* enabled.
|
||||
*/
|
||||
* verification disabled. The source path selects one profile before attach;
|
||||
*/
|
||||
|
||||
#define LVD_CTRL_PATH "/dev/lvdctl"
|
||||
#define MD_CTRL_PATH "/dev/mdctl"
|
||||
@@ -266,7 +268,6 @@
|
||||
#define LVD_SECONDARY_UNIT_IMAGE_IO 0x10000u
|
||||
#define MD_SECTOR_SIZE_EXFAT 512u
|
||||
#define MD_SECTOR_SIZE_UFS_OPTIMIZED 4096u
|
||||
#define MD_SECTOR_SIZE_UFS_LEGACY 512u
|
||||
_Static_assert(LVD_SECONDARY_UNIT_IMAGE_IO % LVD_SECTOR_SIZE_EXFAT == 0,
|
||||
"exFAT LVD geometry must be aligned");
|
||||
_Static_assert(LVD_SECONDARY_UNIT_IMAGE_IO % LVD_SECTOR_SIZE_UFS == 0,
|
||||
|
||||
@@ -19,8 +19,8 @@ bool resolve_device_from_mount(const char *mount_point,
|
||||
bool is_active_image_mount_point(const char *path);
|
||||
// Wait until /dev/lvd2 is no longer mounted during startup.
|
||||
bool wait_for_lvd_release(void);
|
||||
// Detach a previously attached MD or LVD unit. A non-NULL state makes an
|
||||
// accepted asynchronous detach idempotent across later cleanup attempts.
|
||||
// Detach a previously attached MD or LVD unit. State and node identity are
|
||||
// required so an accepted asynchronous detach remains bound to one device.
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id,
|
||||
attached_unit_detach_state_t *state);
|
||||
// Wait for an accepted detach to remove its node, or detect unit reuse.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define SM_PATH_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -28,12 +29,18 @@ bool is_image_mount_limited(const char *path);
|
||||
uint8_t bump_image_mount_attempts(const char *path);
|
||||
// Clear image mount retry state for a path.
|
||||
void clear_image_mount_attempts(const char *path);
|
||||
// Clear image-mount retry counters for every tracked source path.
|
||||
size_t reset_all_image_mount_attempts(void);
|
||||
// Return whether backport overlay remounts are suppressed for a path.
|
||||
bool is_backport_mount_blocked(const char *path);
|
||||
// Record the first failed backport overlay mount for a path.
|
||||
void note_backport_mount_failure(const char *path);
|
||||
// Clear failed backport overlay mount state for a path.
|
||||
void clear_backport_mount_failure(const char *path);
|
||||
// Return true once while permission repair for a backport root is incomplete.
|
||||
bool note_backport_permissions_incomplete_once(const char *path);
|
||||
// Allow a recovered backport root to report a future permission failure.
|
||||
void clear_backport_permissions_incomplete(const char *path);
|
||||
// Return true once for a missing manual source until the path appears again.
|
||||
bool note_manual_missing_source_once(const char *path);
|
||||
// Allow a manual source to be reported missing again after it becomes visible.
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"https://github.com/drakmor/ampr_emu/releases/latest/download/" \
|
||||
"libSceAmpr.sprx"
|
||||
#define FAKELIB_CACHE_PATH "/data/shadowmount/cache"
|
||||
#define ICON_CACHE_DIR "/data/shadowmount/icon-cache"
|
||||
#define LOG_DIR "/data/shadowmount"
|
||||
#define LOG_FILE "/data/shadowmount/debug.log"
|
||||
#define LOG_FILE_PREV "/data/shadowmount/debug.log.1"
|
||||
#define WEB_INDEX_FILE "/data/shadowmount/index.html"
|
||||
#define CONFIG_FILE "/data/shadowmount/config.ini"
|
||||
#define AUTOTUNE_FILE "/data/shadowmount/autotune.ini"
|
||||
#define MANUAL_LIST_FILE "/data/shadowmount/manual.lst"
|
||||
|
||||
@@ -25,8 +25,12 @@ void runtime_mount_state_lock(void);
|
||||
void runtime_mount_state_unlock(void);
|
||||
// Request an immediate scan cycle with a descriptive source string.
|
||||
void request_scan_now(const char *reason);
|
||||
// Consume a pending immediate scan request and copy its reason into caller storage.
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size);
|
||||
// Request an immediate scan and optionally reset mount/install retry counters
|
||||
// immediately before that scan starts.
|
||||
void request_scan_now_with_options(const char *reason, bool reset_attempts);
|
||||
// Consume a pending immediate scan request and its merged options.
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size,
|
||||
bool *reset_attempts_out);
|
||||
// Sleep in chunks and stop early if shutdown was requested.
|
||||
bool sleep_with_stop_check(unsigned int total_us);
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ bool sm_scanner_init(void);
|
||||
void sm_scanner_wake(void);
|
||||
// Return true after scanner-owned USB directory descriptors are closed.
|
||||
bool sm_scanner_usb_watches_suspended(void);
|
||||
// Exclude an external mutation from full/targeted scan and install work.
|
||||
// Returns false immediately when scanner-owned work is already active.
|
||||
bool sm_scanner_try_begin_external_mutation(void);
|
||||
void sm_scanner_end_external_mutation(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,7 @@
|
||||
#ifndef SM_SHELL_ICON_H
|
||||
#define SM_SHELL_ICON_H
|
||||
|
||||
// Install the Shell UI launcher once. Existing installations are untouched.
|
||||
void sm_shell_icon_install_if_missing(void);
|
||||
|
||||
#endif
|
||||
@@ -1,10 +1,12 @@
|
||||
#ifndef SM_SHELLCORE_OFFSETS_H
|
||||
#define SM_SHELLCORE_OFFSETS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
SM_SHELLCORE_TARGET_LAUNCH_APP = 0,
|
||||
SM_SHELLCORE_TARGET_SANDBOX_READY,
|
||||
SM_SHELLCORE_TARGET_INSTALL_TITLE_DIR,
|
||||
SM_SHELLCORE_TARGET_INSTALL_ALL,
|
||||
SM_SHELLCORE_TARGET_COUNT
|
||||
@@ -18,6 +20,9 @@ typedef struct {
|
||||
typedef struct {
|
||||
uint16_t firmware;
|
||||
const char *name;
|
||||
uintptr_t bridge_cave_offset;
|
||||
size_t bridge_cave_size;
|
||||
uintptr_t sandbox_call_target_offset;
|
||||
sm_shellcore_target_offset_t targets[SM_SHELLCORE_TARGET_COUNT];
|
||||
} sm_shellcore_firmware_offsets_t;
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
typedef enum {
|
||||
SM_SHELLCORE_REQUEST_LAUNCH = SM_SHELLCORE_REQUEST_LAUNCH_VALUE,
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED =
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED_VALUE
|
||||
SM_SHELLCORE_REQUEST_LAUNCH_FAILED_VALUE,
|
||||
SM_SHELLCORE_REQUEST_SANDBOX_READY =
|
||||
SM_SHELLCORE_REQUEST_SANDBOX_READY_VALUE
|
||||
} sm_shellcore_request_op_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -7,9 +7,24 @@
|
||||
#define SM_SHELLCORE_PROTOCOL_VERSION 1
|
||||
#define SM_SHELLCORE_REQUEST_LAUNCH_VALUE 1
|
||||
#define SM_SHELLCORE_REQUEST_LAUNCH_FAILED_VALUE 2
|
||||
#define SM_SHELLCORE_REQUEST_SANDBOX_READY_VALUE 3
|
||||
#define SM_SHELLCORE_REQUEST_TITLE_ID_SIZE 32
|
||||
#define SM_SHELLCORE_REQUEST_SIZE 40
|
||||
#define SM_SHELLCORE_RESPONSE_SIZE 4
|
||||
#define SM_SHELLCORE_BRIDGE_INSTALL_DIR_SIZE 1024
|
||||
#define SM_SHELLCORE_BRIDGE_INSTALL_STRING_SIZE 16
|
||||
#define SM_SHELLCORE_BRIDGE_MAX_PROLOGUE_SIZE 16
|
||||
#define SM_SHELLCORE_BRIDGE_ABSOLUTE_JUMP_SIZE 12
|
||||
#define SM_SHELLCORE_BRIDGE_RELATIVE_JUMP_SIZE 5
|
||||
#define SM_SHELLCORE_BRIDGE_MAX_BLOB_SIZE 2048
|
||||
#define SM_SHELLCORE_BRIDGE_IO_TIMEOUT_SEC 15
|
||||
#define SM_SHELLCORE_BRIDGE_SANDBOX_TIMEOUT_SEC 3
|
||||
#define SM_SHELLCORE_BRIDGE_SOCKADDR_SIZE 31
|
||||
#define SM_SHELLCORE_BRIDGE_SOL_SOCKET 0xffff
|
||||
#define SM_SHELLCORE_BRIDGE_SO_SNDTIMEO 0x1005
|
||||
#define SM_SHELLCORE_BRIDGE_SO_RCVTIMEO 0x1006
|
||||
#define SM_SHELLCORE_BRIDGE_TIMEVAL_SIZE 16
|
||||
#define SM_SHELLCORE_BRIDGE_TRAMPOLINE_SIZE \
|
||||
(SM_SHELLCORE_BRIDGE_MAX_PROLOGUE_SIZE + \
|
||||
SM_SHELLCORE_BRIDGE_RELATIVE_JUMP_SIZE)
|
||||
|
||||
#endif
|
||||
@@ -17,12 +17,9 @@ typedef struct {
|
||||
|
||||
// Resolve and validate every lifecycle target in a running SceShellCore.
|
||||
bool sm_shellcore_remote_resolve(pid_t pid, sm_shellcore_remote_t *remote_out);
|
||||
// Stop/continue a remote process for register-safe syscall injection.
|
||||
// Stop/continue a remote process while executable bytes are inspected/patched.
|
||||
bool sm_remote_process_attach(pid_t pid);
|
||||
bool sm_remote_process_detach(pid_t pid);
|
||||
// Manage anonymous memory while the remote process is attached.
|
||||
uintptr_t sm_remote_process_map(pid_t pid, size_t size);
|
||||
bool sm_remote_process_unmap(pid_t pid, uintptr_t address, size_t size);
|
||||
// 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);
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
// Start/stop the local Unix-socket owner used by the SceShellCore bridge.
|
||||
bool sm_shellcore_service_start(void);
|
||||
void sm_shellcore_service_stop(void);
|
||||
// Close the bridge socket on sleep entry and recreate it after resume.
|
||||
void sm_shellcore_service_on_sleep_change(bool active);
|
||||
|
||||
// Release transient title/image mounts after launch failure or explicit API use.
|
||||
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);
|
||||
// Public API entry points. Return 0 or a positive errno value. A NULL mount
|
||||
// mode uses the configured per-image/global mode.
|
||||
int sm_shellcore_mount_title_runtime(const char *title_id,
|
||||
const bool *mount_read_only_override);
|
||||
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,
|
||||
@@ -37,5 +41,8 @@ bool sm_shellcore_ensure_title_runtime(const char *title_id);
|
||||
bool sm_shellcore_service_title_is_prepared(const char *title_id);
|
||||
// Return true while ShellCore owns any title mount prepared for launch or exit.
|
||||
bool sm_shellcore_service_has_prepared_mount(void);
|
||||
// Exclude new launch preparation while an external AppInst mutation runs.
|
||||
bool sm_shellcore_try_begin_external_mutation(void);
|
||||
void sm_shellcore_end_external_mutation(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef SM_STORAGE_H
|
||||
#define SM_STORAGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef bool (*sm_storage_cancel_fn)(void *ctx);
|
||||
typedef void (*sm_storage_progress_fn)(uint64_t bytes_delta,
|
||||
uint64_t files_delta, void *ctx);
|
||||
typedef bool (*sm_storage_finalize_fn)(void *ctx);
|
||||
|
||||
// Sum regular-file sizes under a file or directory without following links.
|
||||
int sm_storage_measure_path(const char *path, uint64_t *size_out);
|
||||
// Measure a source and count regular files, with optional cooperative cancel.
|
||||
int sm_storage_measure_path_progress(const char *path, uint64_t *size_out,
|
||||
uint64_t *file_count_out,
|
||||
sm_storage_cancel_fn cancel, void *ctx);
|
||||
// Measure every non-directory entry for deletion; only regular files add bytes.
|
||||
int sm_storage_measure_delete_path_progress(
|
||||
const char *path, uint64_t *size_out, uint64_t *file_count_out,
|
||||
sm_storage_cancel_fn cancel, void *ctx);
|
||||
// Copy a regular file or directory tree exactly. Destination must not exist.
|
||||
int sm_storage_copy_path(const char *source, const char *destination);
|
||||
// Copy while reporting written-byte and completed-file deltas.
|
||||
int sm_storage_copy_path_progress(const char *source, const char *destination,
|
||||
sm_storage_progress_fn progress,
|
||||
sm_storage_cancel_fn cancel, void *ctx);
|
||||
// Move a file or directory, falling back to copy+delete across filesystems.
|
||||
int sm_storage_move_path(const char *source, const char *destination);
|
||||
// Move while reporting progress for a cross-filesystem copy. begin_finalize
|
||||
// atomically closes cancellation before source deletion; returning false keeps
|
||||
// the source and removes the destination copy. renamed_out is true when an
|
||||
// atomic same-filesystem rename completed without copying.
|
||||
int sm_storage_move_path_progress(const char *source, const char *destination,
|
||||
sm_storage_progress_fn progress,
|
||||
sm_storage_cancel_fn cancel,
|
||||
sm_storage_finalize_fn begin_finalize,
|
||||
void *ctx,
|
||||
bool *renamed_out);
|
||||
// Recursively remove a regular file or directory without following links.
|
||||
int sm_storage_delete_path(const char *path);
|
||||
// Delete while reporting removed-byte and removed-file deltas. Cancellation
|
||||
// must be decided before calling because recursive deletion is irreversible.
|
||||
int sm_storage_delete_path_progress(const char *path,
|
||||
sm_storage_progress_fn progress,
|
||||
void *ctx);
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
#define SM_TITLE_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Return whether registration was already attempted for a title.
|
||||
@@ -23,5 +24,13 @@ uint8_t get_failed_mount_attempts(const char *title_id);
|
||||
void clear_failed_mount_attempts(const char *title_id);
|
||||
// Increment and return failed install/remount attempts for a title.
|
||||
uint8_t bump_failed_mount_attempts(const char *title_id);
|
||||
// Atomically return and clear both title-level registration and
|
||||
// install/remount attempt counters.
|
||||
void reset_title_attempts(const char *title_id,
|
||||
uint8_t *register_attempts_out,
|
||||
uint8_t *mount_install_attempts_out);
|
||||
// Clear registration and install/remount attempt counters for every title and
|
||||
// return the number of title entries that changed.
|
||||
size_t reset_all_title_attempts(void);
|
||||
|
||||
#endif
|
||||
+8
-4
@@ -103,6 +103,13 @@ typedef enum {
|
||||
ATTACH_BACKEND_MD,
|
||||
} attach_backend_t;
|
||||
|
||||
typedef enum {
|
||||
ATTACHED_DEVICE_RELEASED = 0,
|
||||
ATTACHED_DEVICE_ATTACHING,
|
||||
ATTACHED_DEVICE_MOUNTED,
|
||||
ATTACHED_DEVICE_DETACH_REQUESTED,
|
||||
} attached_device_state_t;
|
||||
|
||||
typedef struct {
|
||||
// A detach ioctl was accepted for this specific device instance. Once set,
|
||||
// the same unit number must not receive another detach request because the
|
||||
@@ -115,6 +122,7 @@ typedef struct {
|
||||
} attached_unit_detach_state_t;
|
||||
|
||||
typedef struct runtime_config {
|
||||
bool api_enabled;
|
||||
bool debug_enabled;
|
||||
bool quiet_mode;
|
||||
bool mount_read_only;
|
||||
@@ -148,10 +156,6 @@ typedef struct runtime_config {
|
||||
int32_t language_id;
|
||||
attach_backend_t exfat_backend;
|
||||
attach_backend_t ufs_backend;
|
||||
bool legacy_mount_ufs;
|
||||
bool legacy_mount_exfat;
|
||||
bool legacy_mount_pfs;
|
||||
bool legacy_mount_pfsc;
|
||||
bool nested_pfs_index_cache_enabled;
|
||||
uint32_t lvd_sector_exfat;
|
||||
uint32_t lvd_sector_ufs;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 101 KiB |
+71
-8
@@ -1,7 +1,11 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include "sm_runtime.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
#include "sm_shellcore_flags.h"
|
||||
#include "sm_shellcore_hooks.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_shell_icon.h"
|
||||
#include "sm_config_mount.h"
|
||||
#include "sm_game_lifecycle.h"
|
||||
#include "sm_kstuff.h"
|
||||
@@ -60,6 +65,7 @@ static pthread_mutex_t g_runtime_mount_state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
typedef struct {
|
||||
pthread_mutex_t reason_mutex;
|
||||
char reason[128];
|
||||
bool reset_attempts;
|
||||
} immediate_scan_request_t;
|
||||
|
||||
static immediate_scan_request_t g_scan_now = {
|
||||
@@ -70,6 +76,35 @@ static immediate_scan_request_t g_scan_now = {
|
||||
extern unsigned char config_ini_example[];
|
||||
extern unsigned int config_ini_example_len;
|
||||
|
||||
static void resolve_web_interface_address(const char *bind_address,
|
||||
char *address,
|
||||
size_t address_size) {
|
||||
(void)strlcpy(address, bind_address, address_size);
|
||||
if (strcmp(bind_address, "0.0.0.0") != 0)
|
||||
return;
|
||||
|
||||
struct ifaddrs *ifaddr = NULL;
|
||||
if (getifaddrs(&ifaddr) != 0)
|
||||
return;
|
||||
|
||||
for (const struct ifaddrs *ifa = ifaddr; ifa != NULL;
|
||||
ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET ||
|
||||
strncmp(ifa->ifa_name, "lo", 2) == 0)
|
||||
continue;
|
||||
|
||||
const struct sockaddr_in *in =
|
||||
(const struct sockaddr_in *)ifa->ifa_addr;
|
||||
if (inet_ntop(AF_INET, &in->sin_addr, address, address_size) != NULL &&
|
||||
strncmp(address, "0.", 2) != 0)
|
||||
break;
|
||||
|
||||
(void)strlcpy(address, bind_address, address_size);
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr);
|
||||
}
|
||||
|
||||
static void on_signal(int sig) {
|
||||
(void)sig;
|
||||
g_stop_requested = 1;
|
||||
@@ -106,7 +141,7 @@ bool should_stop_requested(void) {
|
||||
}
|
||||
|
||||
if (remove(KILL_FILE) == 0) {
|
||||
g_stop_requested = 1;
|
||||
request_shutdown_stop("STOP file " KILL_FILE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -147,6 +182,7 @@ bool runtime_resume_grace_active(void) {
|
||||
static void clear_scan_now_request(void) {
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
g_scan_now.reason[0] = '\0';
|
||||
g_scan_now.reset_attempts = false;
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
}
|
||||
|
||||
@@ -179,6 +215,9 @@ bool request_runtime_sleep_mode(bool active, const char *reason) {
|
||||
(reason && reason[0] != '\0') ? reason : "unknown sleep source";
|
||||
log_debug("[SLEEP] %s by %s", active ? "entered" : "left",
|
||||
resolved_reason);
|
||||
sm_api_service_on_sleep_change(active);
|
||||
sm_shellcore_service_on_sleep_change(active);
|
||||
sm_ampr_updater_on_sleep_change(active);
|
||||
sm_scanner_wake();
|
||||
wake_game_lifecycle_watcher();
|
||||
return true;
|
||||
@@ -193,6 +232,10 @@ void runtime_mount_state_unlock(void) {
|
||||
}
|
||||
|
||||
void request_scan_now(const char *reason) {
|
||||
request_scan_now_with_options(reason, false);
|
||||
}
|
||||
|
||||
void request_scan_now_with_options(const char *reason, bool reset_attempts) {
|
||||
const char *resolved_reason =
|
||||
(reason && reason[0] != '\0') ? reason : "unknown scan source";
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -207,6 +250,8 @@ void request_scan_now(const char *reason) {
|
||||
(void)strlcpy(log_reason, g_scan_now.reason, sizeof(log_reason));
|
||||
should_log = true;
|
||||
}
|
||||
if (reset_attempts)
|
||||
g_scan_now.reset_attempts = true;
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
|
||||
if (should_log)
|
||||
@@ -214,9 +259,12 @@ void request_scan_now(const char *reason) {
|
||||
sm_scanner_wake();
|
||||
}
|
||||
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size) {
|
||||
bool consume_scan_now_request(char *reason_out, size_t reason_out_size,
|
||||
bool *reset_attempts_out) {
|
||||
if (reason_out && reason_out_size > 0)
|
||||
reason_out[0] = '\0';
|
||||
if (reset_attempts_out)
|
||||
*reset_attempts_out = false;
|
||||
pthread_mutex_lock(&g_scan_now.reason_mutex);
|
||||
if (g_scan_now.reason[0] == '\0') {
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
@@ -224,7 +272,10 @@ bool consume_scan_now_request(char *reason_out, size_t reason_out_size) {
|
||||
}
|
||||
if (reason_out && reason_out_size > 0)
|
||||
(void)strlcpy(reason_out, g_scan_now.reason, reason_out_size);
|
||||
if (reset_attempts_out)
|
||||
*reset_attempts_out = g_scan_now.reset_attempts;
|
||||
g_scan_now.reason[0] = '\0';
|
||||
g_scan_now.reset_attempts = false;
|
||||
pthread_mutex_unlock(&g_scan_now.reason_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -340,7 +391,9 @@ static bool wait_for_existing_instance_exit(pid_t target_pid) {
|
||||
|
||||
static void log_non_empty_scan_paths(void) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
DIR *d = opendir(scan_path);
|
||||
if (!d)
|
||||
continue;
|
||||
@@ -447,7 +500,7 @@ static void cleanup_kstuff_noautomount_file(void) {
|
||||
}
|
||||
|
||||
static void stop_conflicting_backpork(void) {
|
||||
if (!runtime_config()->backport_fakelib_enabled)
|
||||
if (!runtime_config().backport_fakelib_enabled)
|
||||
return;
|
||||
|
||||
const char *names[] = {BACKPORK_PROCESS_NAME, BACKPORK_PROCESS_NAME_ALT};
|
||||
@@ -543,6 +596,7 @@ int main(void) {
|
||||
log_debug(" [SHELLCORE] lifecycle hooks unavailable; stock behavior kept");
|
||||
notify_system_l10n(SM_L10N_SHELLCORE_HOOKS_FAILED);
|
||||
}
|
||||
sm_shell_icon_install_if_missing();
|
||||
if (!refresh_game_lifecycle_watcher())
|
||||
log_debug(" [GAME] lifecycle watcher unavailable");
|
||||
// Publish the initial AppFocus only after its lifecycle/kstuff consumers.
|
||||
@@ -558,14 +612,23 @@ int main(void) {
|
||||
log_debug(" [MOUNT] remount_system_ex failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
notify_system_l10n(SM_L10N_STARTUP, SHADOWMOUNT_VERSION);
|
||||
const runtime_config_t startup_cfg = runtime_config();
|
||||
if (startup_cfg.api_enabled) {
|
||||
char web_address[MAX_API_BIND_ADDRESS];
|
||||
resolve_web_interface_address(startup_cfg.api_bind_address, web_address,
|
||||
sizeof(web_address));
|
||||
notify_system_l10n(SM_L10N_STARTUP_WEB, SHADOWMOUNT_VERSION,
|
||||
web_address, startup_cfg.api_port);
|
||||
} else {
|
||||
notify_system_l10n(SM_L10N_STARTUP, SHADOWMOUNT_VERSION);
|
||||
}
|
||||
log_non_empty_scan_paths();
|
||||
|
||||
if (runtime_config()->legacy_recursive_scan_forced) {
|
||||
if (runtime_config().legacy_recursive_scan_forced) {
|
||||
notify_system_info_l10n(SM_L10N_RECURSIVE_SCAN_DEPRECATED);
|
||||
} else if (runtime_config()->scan_depth > 1u) {
|
||||
} else if (runtime_config().scan_depth > 1u) {
|
||||
notify_system_info_l10n(SM_L10N_SCAN_DEPTH_ENABLED,
|
||||
runtime_config()->scan_depth);
|
||||
runtime_config().scan_depth);
|
||||
}
|
||||
|
||||
cleanup_mount_dirs();
|
||||
|
||||
+68
-13
@@ -103,22 +103,22 @@ static bool ampr_update_busy(void) {
|
||||
}
|
||||
|
||||
static void snapshot_update_config(ampr_update_config_t *snapshot) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
snapshot->enabled = cfg->auto_update_ampr_enabled;
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
snapshot->enabled = cfg.auto_update_ampr_enabled;
|
||||
snapshot->generation = g_ampr_updater.config_generation;
|
||||
(void)strlcpy(snapshot->url, cfg->ampr_update_url, sizeof(snapshot->url));
|
||||
(void)strlcpy(snapshot->emulators_path, cfg->emulators_path,
|
||||
(void)strlcpy(snapshot->url, cfg.ampr_update_url, sizeof(snapshot->url));
|
||||
(void)strlcpy(snapshot->emulators_path, cfg.emulators_path,
|
||||
sizeof(snapshot->emulators_path));
|
||||
}
|
||||
|
||||
static bool
|
||||
update_snapshot_is_current_locked(const ampr_update_config_t *snapshot) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
return !g_ampr_updater.stop_requested &&
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
return !g_ampr_updater.stop_requested && !runtime_sleep_mode_active() &&
|
||||
snapshot->generation == g_ampr_updater.config_generation &&
|
||||
cfg->auto_update_ampr_enabled &&
|
||||
strcmp(snapshot->url, cfg->ampr_update_url) == 0 &&
|
||||
strcmp(snapshot->emulators_path, cfg->emulators_path) == 0;
|
||||
cfg.auto_update_ampr_enabled &&
|
||||
strcmp(snapshot->url, cfg.ampr_update_url) == 0 &&
|
||||
strcmp(snapshot->emulators_path, cfg.emulators_path) == 0;
|
||||
}
|
||||
|
||||
static bool update_snapshot_is_current(const ampr_update_config_t *snapshot) {
|
||||
@@ -139,6 +139,8 @@ static bool wait_for_update_event_locked(uint32_t seconds,
|
||||
|
||||
bool wait_failed = false;
|
||||
while (!g_ampr_updater.stop_requested) {
|
||||
if (runtime_sleep_mode_active())
|
||||
break;
|
||||
if (wake_early && g_ampr_updater.wake_requested)
|
||||
break;
|
||||
g_ampr_updater.wake_requested = false;
|
||||
@@ -860,6 +862,28 @@ static void *ampr_updater_thread_main(void *unused) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (runtime_sleep_mode_active()) {
|
||||
while (!g_ampr_updater.stop_requested &&
|
||||
runtime_sleep_mode_active()) {
|
||||
g_ampr_updater.wake_requested = false;
|
||||
int rc =
|
||||
pthread_cond_wait(&g_ampr_updater.cond, &g_ampr_updater.mutex);
|
||||
if (rc != 0) {
|
||||
log_debug(" [AMPR] sleep wait failed: %s", strerror(rc));
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (g_ampr_updater.stop_requested)
|
||||
break;
|
||||
g_ampr_updater.wake_requested = false;
|
||||
if (!wait_for_update_event_locked(AMPR_INITIAL_CHECK_DELAY_SECONDS,
|
||||
true)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ampr_update_busy()) {
|
||||
if (!wait_for_update_event_locked(AMPR_BUSY_RETRY_SECONDS, true))
|
||||
break;
|
||||
@@ -922,11 +946,13 @@ void sm_ampr_updater_stop(void) {
|
||||
g_ampr_updater.stop_requested = true;
|
||||
g_ampr_updater.wake_requested = true;
|
||||
g_ampr_updater.config_generation++;
|
||||
if (g_ampr_updater.active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(g_ampr_updater.active_request_id);
|
||||
int active_request_id = g_ampr_updater.active_request_id;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_cond_broadcast(&g_ampr_updater.cond);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
|
||||
if (active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(active_request_id);
|
||||
(void)pthread_join(g_ampr_updater.thread, NULL);
|
||||
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
@@ -937,6 +963,32 @@ void sm_ampr_updater_stop(void) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
}
|
||||
|
||||
void sm_ampr_updater_on_sleep_change(bool active) {
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
if (!g_ampr_updater.started) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
bool update_enabled = runtime_config().auto_update_ampr_enabled;
|
||||
if (!update_enabled && g_ampr_updater.active_request_id < 0) {
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
int active_request_id = -1;
|
||||
if (active) {
|
||||
g_ampr_updater.config_generation++;
|
||||
active_request_id = g_ampr_updater.active_request_id;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
}
|
||||
pthread_cond_broadcast(&g_ampr_updater.cond);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
|
||||
if (active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(active_request_id);
|
||||
}
|
||||
|
||||
void sm_ampr_updater_on_config_reload(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg) {
|
||||
if (!old_cfg || !new_cfg ||
|
||||
@@ -950,8 +1002,11 @@ void sm_ampr_updater_on_config_reload(const runtime_config_t *old_cfg,
|
||||
pthread_mutex_lock(&g_ampr_updater.mutex);
|
||||
g_ampr_updater.config_generation++;
|
||||
g_ampr_updater.wake_requested = true;
|
||||
if (g_ampr_updater.active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(g_ampr_updater.active_request_id);
|
||||
int active_request_id = g_ampr_updater.active_request_id;
|
||||
g_ampr_updater.active_request_id = -1;
|
||||
pthread_cond_broadcast(&g_ampr_updater.cond);
|
||||
pthread_mutex_unlock(&g_ampr_updater.mutex);
|
||||
|
||||
if (active_request_id >= 0)
|
||||
(void)sceHttp2AbortRequest(active_request_id);
|
||||
}
|
||||
+2628
-425
File diff suppressed because it is too large.
Load diff
+362
-216
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <json-c/json.h>
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
@@ -11,35 +12,41 @@
|
||||
#include "sm_paths.h"
|
||||
|
||||
static sqlite3 *g_app_db;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0;
|
||||
static sqlite3_stmt *g_app_db_stmt_normalize_snd0;
|
||||
static struct AppDbTitleList g_app_db_title_cache;
|
||||
static struct AppDbTitleList g_app_db_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;
|
||||
|
||||
#define APP_DB_API_BUSY_TIMEOUT_MS 250
|
||||
|
||||
struct AppDbTitleCache {
|
||||
struct AppDbTitleList list;
|
||||
uint64_t revision;
|
||||
time_t mtime;
|
||||
bool ready;
|
||||
};
|
||||
|
||||
static struct AppDbTitleCache g_app_db_title_cache;
|
||||
static struct AppDbTitleCache g_app_db_auto_remove_cache;
|
||||
static struct AppDbTitleCache g_app_db_blocked_uninstall_ppsa_cache;
|
||||
static pthread_mutex_t g_app_db_write_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_app_db_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
#define APP_DB_STARTUP_MAINTENANCE_RETRIES 3
|
||||
#define APP_DB_STARTUP_MAINTENANCE_BUSY_TIMEOUT_MS 250
|
||||
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_normalize_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_normalize_snd0);
|
||||
g_app_db_stmt_normalize_snd0 = NULL;
|
||||
}
|
||||
if (g_app_db) {
|
||||
sqlite3_close(g_app_db);
|
||||
g_app_db = NULL;
|
||||
static bool close_sqlite_handle(sqlite3 **db, const char *label) {
|
||||
if (!*db)
|
||||
return true;
|
||||
|
||||
int rc = sqlite3_close_v2(*db);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_debug(" [DB] close failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(*db));
|
||||
return false;
|
||||
}
|
||||
*db = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool close_app_db(void) {
|
||||
return close_sqlite_handle(&g_app_db, "write connection");
|
||||
}
|
||||
|
||||
void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
@@ -49,19 +56,27 @@ void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
list->capacity = 0;
|
||||
}
|
||||
|
||||
static void invalidate_app_db_title_cache_entry(
|
||||
struct AppDbTitleCache *cache) {
|
||||
cache->ready = false;
|
||||
cache->mtime = 0;
|
||||
++cache->revision;
|
||||
}
|
||||
|
||||
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);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
(void)close_app_db();
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
free_app_db_title_list(&g_app_db_title_cache.list);
|
||||
free_app_db_title_list(&g_app_db_auto_remove_cache.list);
|
||||
free_app_db_title_list(&g_app_db_blocked_uninstall_ppsa_cache.list);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_title_cache);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_auto_remove_cache);
|
||||
invalidate_app_db_title_cache_entry(
|
||||
&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
}
|
||||
|
||||
static bool ensure_app_db_open(void) {
|
||||
@@ -181,7 +196,7 @@ static int app_db_query_int(const char *sql, const char *label) {
|
||||
|
||||
bool app_db_run_startup_maintenance(void) {
|
||||
bool ok = false;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
|
||||
if (!ensure_app_db_open())
|
||||
goto out;
|
||||
@@ -221,110 +236,72 @@ bool app_db_run_startup_maintenance(void) {
|
||||
ok = true;
|
||||
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
static int app_db_update_title(const char *title_id, const char *sql,
|
||||
const char *label) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(
|
||||
sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES, label);
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
|
||||
if (!g_app_db_stmt_update_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_update_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info update");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_update_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_update_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_update_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info update: %s",
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; ++attempt) {
|
||||
(void)sqlite3_reset(stmt);
|
||||
(void)sqlite3_clear_bindings(stmt);
|
||||
if (sqlite3_bind_text(stmt, 1, title_id, -1, SQLITE_TRANSIENT) !=
|
||||
SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for %s: %s", label,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_update_snd0);
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
result = sqlite3_changes(g_app_db);
|
||||
break;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info update: rc=%d err=%s", rc,
|
||||
log_debug(" [DB] step failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
(void)sqlite3_finalize(stmt);
|
||||
(void)close_app_db();
|
||||
return result;
|
||||
}
|
||||
|
||||
int update_snd0info(const char *title_id) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0info = '/user/appmeta/' || ?1 || '/snd0.at9' "
|
||||
"WHERE titleId = ?1;";
|
||||
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
int result = app_db_update_title(title_id, sql, "snd0info update");
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
int normalize_snd0info_for_title(const char *title_id) {
|
||||
int result = -1;
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
|
||||
if (!g_app_db_stmt_normalize_snd0) {
|
||||
const char *sql =
|
||||
"UPDATE tbl_contentinfo "
|
||||
"SET snd0Info = replace(replace(snd0Info, '/user/app/', "
|
||||
"'/user/appmeta/'), '/sce_sys', '') "
|
||||
"WHERE titleId = ?1 "
|
||||
"AND (instr(snd0Info, '/user/app/') > 0 "
|
||||
"OR instr(snd0Info, '/sce_sys') > 0);";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_normalize_snd0,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"snd0info normalize");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
goto out;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_normalize_snd0);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_normalize_snd0);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_normalize_snd0, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for snd0info normalize: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_normalize_snd0);
|
||||
if (rc == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
close_app_db();
|
||||
result = changes;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
log_debug(" [DB] step failed for snd0info normalize: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
goto out;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
out:
|
||||
pthread_mutex_unlock(&g_app_db_mutex);
|
||||
pthread_mutex_lock(&g_app_db_write_mutex);
|
||||
int result = app_db_update_title(title_id, sql, "snd0info normalize");
|
||||
pthread_mutex_unlock(&g_app_db_write_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -360,86 +337,100 @@ bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
compare_title_id_str) != NULL;
|
||||
}
|
||||
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list,
|
||||
sqlite3_stmt *stmt,
|
||||
const char *label) {
|
||||
static bool step_app_db_title_query(struct AppDbTitleList *list, sqlite3 *db,
|
||||
sqlite3_stmt *stmt, const char *label) {
|
||||
int busy_attempts = 0;
|
||||
while (!should_stop_requested()) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
if (append_app_db_title(list, title_id))
|
||||
continue;
|
||||
log_debug(" [DB] %s allocation failed", label);
|
||||
return false;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
if (rc == SQLITE_DONE)
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
busy_attempts++;
|
||||
++busy_attempts;
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] %s query failed: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
sqlite3_errmsg(db));
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
static bool load_app_db_title_query(struct AppDbTitleList *list,
|
||||
const char *sql, const char *label) {
|
||||
free_app_db_title_list(list);
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
int rc = sqlite3_open_v2(APP_DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_debug(" [DB] open failed for %s: rc=%d err=%s", label, rc,
|
||||
db ? sqlite3_errmsg(db) : APP_DB_PATH);
|
||||
if (db)
|
||||
(void)close_sqlite_handle(&db, label);
|
||||
return false;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(db, APP_DB_BUSY_TIMEOUT_MS);
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
for (int attempt = 0; attempt < APP_DB_PREPARE_BUSY_RETRIES; ++attempt) {
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK)
|
||||
break;
|
||||
if ((rc != SQLITE_BUSY && rc != SQLITE_LOCKED) ||
|
||||
attempt + 1 >= APP_DB_PREPARE_BUSY_RETRIES ||
|
||||
should_stop_requested()) {
|
||||
log_debug(" [DB] prepare failed for %s: rc=%d err=%s", label, rc,
|
||||
sqlite3_errmsg(db));
|
||||
break;
|
||||
}
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
}
|
||||
|
||||
bool loaded = rc == SQLITE_OK &&
|
||||
step_app_db_title_query(list, db, stmt, label);
|
||||
|
||||
if (stmt)
|
||||
(void)sqlite3_finalize(stmt);
|
||||
if (!close_sqlite_handle(&db, label))
|
||||
loaded = false;
|
||||
if (!loaded)
|
||||
free_app_db_title_list(list);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "title list");
|
||||
bool loaded = load_app_db_title_query(list, sql, "title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
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");
|
||||
bool loaded =
|
||||
load_app_db_title_query(list, sql, "auto-remove title list");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded app.db auto-remove title list: %d entries",
|
||||
list->count);
|
||||
@@ -447,22 +438,14 @@ static bool load_app_db_auto_remove_title_list(struct AppDbTitleList *list) {
|
||||
}
|
||||
|
||||
static bool load_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list) {
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId LIKE 'PPSA%' "
|
||||
"AND uninstallable = 0 "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc =
|
||||
app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"blocked PPSA uninstallable query");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool loaded = step_app_db_title_query(list, stmt, "blocked PPSA uninstall");
|
||||
bool loaded =
|
||||
load_app_db_title_query(list, sql, "blocked PPSA uninstall");
|
||||
if (loaded)
|
||||
log_debug(" [DB] loaded blocked PPSA uninstall list: %d entries",
|
||||
list->count);
|
||||
@@ -470,14 +453,12 @@ 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);
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_title_cache);
|
||||
invalidate_app_db_title_cache_entry(&g_app_db_auto_remove_cache);
|
||||
invalidate_app_db_title_cache_entry(
|
||||
&g_app_db_blocked_uninstall_ppsa_cache);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
}
|
||||
|
||||
static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
@@ -503,71 +484,236 @@ static bool copy_app_db_title_list(struct AppDbTitleList *dst,
|
||||
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) {
|
||||
struct AppDbTitleList *list_out, struct AppDbTitleCache *cache,
|
||||
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);
|
||||
|
||||
// ShellCore can keep app.db busy for several five-second timeout windows
|
||||
// immediately after WORKING. The pre-suspend snapshot is sufficient for
|
||||
// mounting already registered games and keeps resume scans non-blocking.
|
||||
if (*cache_ready && runtime_resume_grace_active())
|
||||
return copy_app_db_title_list(list_out, cache);
|
||||
|
||||
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;
|
||||
}
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
bool cache_is_current =
|
||||
cache->ready &&
|
||||
(runtime_resume_grace_active() || app_db_stat_rc != 0 ||
|
||||
cache->mtime == st.st_mtime);
|
||||
if (cache_is_current) {
|
||||
bool copied = copy_app_db_title_list(list_out, &cache->list);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
return copied;
|
||||
}
|
||||
uint64_t revision = cache->revision;
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
|
||||
return copy_app_db_title_list(list_out, cache);
|
||||
struct AppDbTitleList fresh = {0};
|
||||
bool loaded = app_db_stat_rc == 0 && load_fn(&fresh);
|
||||
struct stat verified_st;
|
||||
bool source_unchanged =
|
||||
loaded && stat(APP_DB_PATH, &verified_st) == 0 &&
|
||||
verified_st.st_mtime == st.st_mtime;
|
||||
|
||||
pthread_mutex_lock(&g_app_db_cache_mutex);
|
||||
if (source_unchanged && cache->revision == revision) {
|
||||
free_app_db_title_list(&cache->list);
|
||||
cache->list = fresh;
|
||||
memset(&fresh, 0, sizeof(fresh));
|
||||
cache->mtime = st.st_mtime;
|
||||
cache->ready = true;
|
||||
++cache->revision;
|
||||
}
|
||||
bool copied = cache->ready &&
|
||||
copy_app_db_title_list(list_out, &cache->list);
|
||||
pthread_mutex_unlock(&g_app_db_cache_mutex);
|
||||
free_app_db_title_list(&fresh);
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool get_app_db_title_list_cached(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
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;
|
||||
return get_cached_app_db_title_list(list_out, &g_app_db_title_cache,
|
||||
load_app_db_title_list);
|
||||
}
|
||||
|
||||
bool get_app_db_auto_remove_title_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
bool copied = get_cached_app_db_title_list(
|
||||
return 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;
|
||||
}
|
||||
|
||||
bool get_app_db_blocked_uninstall_ppsa_list(struct AppDbTitleList *list_out) {
|
||||
if (!list_out)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_app_db_mutex);
|
||||
bool loaded = get_cached_app_db_title_list(
|
||||
return 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;
|
||||
}
|
||||
|
||||
static void copy_sqlite_text(sqlite3_stmt *stmt, int column, char *out,
|
||||
size_t out_size) {
|
||||
const unsigned char *value = sqlite3_column_text(stmt, column);
|
||||
(void)strlcpy(out, value ? (const char *)value : "", out_size);
|
||||
}
|
||||
|
||||
static void copy_json_string(struct json_object *object, const char *key,
|
||||
char *out, size_t out_size) {
|
||||
struct json_object *value = NULL;
|
||||
if (!json_object_object_get_ex(object, key, &value) ||
|
||||
!json_object_is_type(value, json_type_string)) {
|
||||
return;
|
||||
}
|
||||
const char *text = json_object_get_string(value);
|
||||
if (text && text[0] != '\0')
|
||||
(void)strlcpy(out, text, out_size);
|
||||
}
|
||||
|
||||
static void apply_app_info_timestamps(sqlite3_stmt *stmt, int column,
|
||||
sm_app_db_game_info_t *entry) {
|
||||
// ShellCore updates these JSON values while the top-level timestamp columns
|
||||
// can remain at their initial install values.
|
||||
const unsigned char *text = sqlite3_column_text(stmt, column);
|
||||
if (!text || text[0] == '\0')
|
||||
return;
|
||||
|
||||
struct json_object *app_info =
|
||||
json_tokener_parse((const char *)text);
|
||||
if (!app_info)
|
||||
return;
|
||||
copy_json_string(app_info, "#_last_access_time", entry->last_access_time,
|
||||
sizeof(entry->last_access_time));
|
||||
copy_json_string(app_info, "#_install_time", entry->install_time,
|
||||
sizeof(entry->install_time));
|
||||
json_object_put(app_info);
|
||||
}
|
||||
|
||||
bool app_db_game_info_snapshot(sm_app_db_game_info_t **entries_out,
|
||||
size_t *count_out) {
|
||||
if (!entries_out || !count_out)
|
||||
return false;
|
||||
*entries_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
int rc = sqlite3_open_v2(APP_DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
if (db)
|
||||
(void)close_sqlite_handle(&db, "game metadata snapshot");
|
||||
errno = EIO;
|
||||
return false;
|
||||
}
|
||||
(void)sqlite3_busy_timeout(db, APP_DB_API_BUSY_TIMEOUT_MS);
|
||||
|
||||
static const char sql[] =
|
||||
"SELECT titleId, contentId, titleName, lastAccessTime, installTime, "
|
||||
"icon0Info, platform, size, AppInfoJson FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' ORDER BY titleId COLLATE BINARY;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_debug(" [DB] prepare failed for game metadata snapshot: rc=%d err=%s",
|
||||
rc, sqlite3_errmsg(db));
|
||||
(void)close_sqlite_handle(&db, "game metadata snapshot");
|
||||
errno = EIO;
|
||||
return false;
|
||||
}
|
||||
|
||||
sm_app_db_game_info_t *entries = NULL;
|
||||
size_t count = 0;
|
||||
size_t capacity = 0;
|
||||
int busy_attempts = 0;
|
||||
bool loaded = false;
|
||||
while (!should_stop_requested() && !runtime_sleep_mode_active()) {
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
busy_attempts = 0;
|
||||
if (count == capacity) {
|
||||
size_t new_capacity = capacity > 0 ? capacity * 2u : 128u;
|
||||
if (new_capacity < capacity ||
|
||||
new_capacity > SIZE_MAX / sizeof(*entries)) {
|
||||
errno = ENOMEM;
|
||||
break;
|
||||
}
|
||||
sm_app_db_game_info_t *new_entries =
|
||||
realloc(entries, new_capacity * sizeof(*new_entries));
|
||||
if (!new_entries) {
|
||||
errno = ENOMEM;
|
||||
break;
|
||||
}
|
||||
entries = new_entries;
|
||||
capacity = new_capacity;
|
||||
}
|
||||
|
||||
sm_app_db_game_info_t *entry = &entries[count++];
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
copy_sqlite_text(stmt, 0, entry->title_id, sizeof(entry->title_id));
|
||||
copy_sqlite_text(stmt, 1, entry->content_id, sizeof(entry->content_id));
|
||||
copy_sqlite_text(stmt, 2, entry->title_name, sizeof(entry->title_name));
|
||||
copy_sqlite_text(stmt, 3, entry->last_access_time,
|
||||
sizeof(entry->last_access_time));
|
||||
copy_sqlite_text(stmt, 4, entry->install_time,
|
||||
sizeof(entry->install_time));
|
||||
copy_sqlite_text(stmt, 5, entry->icon_path, sizeof(entry->icon_path));
|
||||
entry->platform = sqlite3_column_type(stmt, 6) == SQLITE_NULL
|
||||
? -1
|
||||
: sqlite3_column_int(stmt, 6);
|
||||
sqlite3_int64 installed_size = sqlite3_column_int64(stmt, 7);
|
||||
entry->installed_size =
|
||||
installed_size > 0 ? (uint64_t)installed_size : 0;
|
||||
apply_app_info_timestamps(stmt, 8, entry);
|
||||
continue;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
loaded = true;
|
||||
break;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
++busy_attempts < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
log_debug(" [DB] game metadata snapshot failed: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(db));
|
||||
errno = EIO;
|
||||
break;
|
||||
}
|
||||
|
||||
(void)sqlite3_finalize(stmt);
|
||||
if (!close_sqlite_handle(&db, "game metadata snapshot"))
|
||||
loaded = false;
|
||||
if (!loaded) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
errno = ECANCELED;
|
||||
else if (errno == 0)
|
||||
errno = EIO;
|
||||
free(entries);
|
||||
return false;
|
||||
}
|
||||
|
||||
*entries_out = entries;
|
||||
*count_out = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
const sm_app_db_game_info_t *app_db_find_game_info(
|
||||
const sm_app_db_game_info_t *entries, size_t count, const char *title_id) {
|
||||
size_t low = 0;
|
||||
size_t high = count;
|
||||
while (low < high) {
|
||||
size_t mid = low + (high - low) / 2u;
|
||||
int comparison = strcmp(entries[mid].title_id, title_id);
|
||||
if (comparison < 0)
|
||||
low = mid + 1u;
|
||||
else
|
||||
high = mid;
|
||||
}
|
||||
return low < count && strcmp(entries[low].title_id, title_id) == 0
|
||||
? &entries[low]
|
||||
: NULL;
|
||||
}
|
||||
+342
-114
@@ -1,6 +1,7 @@
|
||||
#include "sm_platform.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sm_api_protocol.h"
|
||||
@@ -14,8 +15,6 @@
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
static const char *const k_default_scan_paths[] = SM_DEFAULT_SCAN_PATHS_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
@@ -37,6 +36,7 @@ typedef struct {
|
||||
runtime_config_t cfg;
|
||||
char scan_path_storage[MAX_SCAN_PATHS][MAX_PATH];
|
||||
int scan_path_count;
|
||||
int custom_scan_path_count;
|
||||
image_mode_rule_t image_mode_rules[MAX_IMAGE_MODE_RULES];
|
||||
char kstuff_no_pause_title_ids[MAX_KSTUFF_TITLE_RULES][MAX_TITLE_ID];
|
||||
int kstuff_no_pause_title_count;
|
||||
@@ -59,14 +59,15 @@ typedef enum {
|
||||
CONFIG_LOAD_ERROR,
|
||||
} config_load_status_t;
|
||||
|
||||
#define RUNTIME_CONFIG_ACTIVE_SLOT_COUNT 2
|
||||
#define RUNTIME_CONFIG_PARSE_SLOT RUNTIME_CONFIG_ACTIVE_SLOT_COUNT
|
||||
|
||||
static runtime_config_state_t
|
||||
g_runtime_state_slots[RUNTIME_CONFIG_ACTIVE_SLOT_COUNT + 1];
|
||||
static _Atomic int g_runtime_state_active_index = 0;
|
||||
static atomic_bool g_runtime_cfg_ready = false;
|
||||
static runtime_config_state_t g_runtime_state;
|
||||
static runtime_config_state_t g_runtime_parse_state;
|
||||
static pthread_once_t g_runtime_init_once = PTHREAD_ONCE_INIT;
|
||||
// Never hold the state mutex across I/O or logging: those can read config.
|
||||
static pthread_mutex_t g_runtime_state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_runtime_load_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static config_file_stamp_t g_config_file_stamp;
|
||||
static pthread_mutex_t g_autotune_file_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_config_file_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static char *trim_ascii(char *s);
|
||||
static bool parse_ini_line(char *line, char **key_out, char **value_out);
|
||||
@@ -108,6 +109,24 @@ static bool upsert_kstuff_delay_override_in_file(const char *path,
|
||||
const char *title_id,
|
||||
uint32_t delay_seconds);
|
||||
|
||||
static bool web_managed_config_key(const char *key) {
|
||||
return strcasecmp(key, "debug") == 0 ||
|
||||
strcasecmp(key, "quiet_mode") == 0 ||
|
||||
strcasecmp(key, "update_emulators") == 0 ||
|
||||
strcasecmp(key, "auto_update_ampr") == 0 ||
|
||||
strcasecmp(key, "auto_remove_missing_games") == 0 ||
|
||||
strcasecmp(key, "auto_remove_missing_delay_seconds") == 0 ||
|
||||
strcasecmp(key, "auto_remove_missing_delay_sec") == 0 ||
|
||||
strcasecmp(key, "fan_target_temperature") == 0 ||
|
||||
strcasecmp(key, "api_bind_address") == 0 ||
|
||||
strcasecmp(key, "api_bind_adress") == 0 ||
|
||||
strcasecmp(key, "scanpath") == 0;
|
||||
}
|
||||
|
||||
static int config_io_error(void) {
|
||||
return errno != 0 ? errno : EIO;
|
||||
}
|
||||
|
||||
static char *trim_ascii(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
s++;
|
||||
@@ -160,11 +179,6 @@ static bool parse_ini_line(char *line, char **key_out, char **value_out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
|
||||
static attach_backend_t default_exfat_backend(void) {
|
||||
#if EXFAT_ATTACH_USE_MDCTL
|
||||
return ATTACH_BACKEND_MD;
|
||||
@@ -183,6 +197,7 @@ static attach_backend_t default_ufs_backend(void) {
|
||||
|
||||
static void clear_runtime_scan_paths(runtime_config_state_t *state) {
|
||||
state->scan_path_count = 0;
|
||||
state->custom_scan_path_count = 0;
|
||||
memset(state->scan_path_storage, 0, sizeof(state->scan_path_storage));
|
||||
}
|
||||
|
||||
@@ -240,6 +255,7 @@ static void clear_kstuff_title_rules(runtime_config_state_t *state) {
|
||||
|
||||
static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->cfg.api_enabled = true;
|
||||
state->cfg.debug_enabled = true;
|
||||
state->cfg.quiet_mode = false;
|
||||
state->cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
@@ -278,16 +294,12 @@ static void init_runtime_config_defaults(runtime_config_state_t *state) {
|
||||
state->cfg.language_id = SM_LANGUAGE_AUTO;
|
||||
state->cfg.exfat_backend = default_exfat_backend();
|
||||
state->cfg.ufs_backend = default_ufs_backend();
|
||||
state->cfg.legacy_mount_ufs = true;
|
||||
state->cfg.legacy_mount_exfat = true;
|
||||
state->cfg.legacy_mount_pfs = true;
|
||||
state->cfg.legacy_mount_pfsc = true;
|
||||
state->cfg.nested_pfs_index_cache_enabled = false;
|
||||
state->cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT;
|
||||
state->cfg.lvd_sector_ufs = LVD_SECTOR_SIZE_UFS;
|
||||
state->cfg.lvd_sector_pfs = LVD_SECTOR_SIZE_PFS;
|
||||
state->cfg.md_sector_exfat = MD_SECTOR_SIZE_EXFAT;
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS_LEGACY;
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS_OPTIMIZED;
|
||||
memset(state->image_mode_rules, 0, sizeof(state->image_mode_rules));
|
||||
clear_kstuff_title_rules(state);
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
@@ -319,61 +331,68 @@ static bool config_file_stamp_equals(const config_file_stamp_t *a,
|
||||
a->ctime_nsec == b->ctime_nsec;
|
||||
}
|
||||
|
||||
static void apply_reloadable_runtime_fields(runtime_config_state_t *dst,
|
||||
const runtime_config_state_t *src) {
|
||||
dst->cfg = src->cfg;
|
||||
dst->scan_path_count = src->scan_path_count;
|
||||
memcpy(dst->scan_path_storage, src->scan_path_storage,
|
||||
sizeof(dst->scan_path_storage));
|
||||
memcpy(dst->image_mode_rules, src->image_mode_rules,
|
||||
sizeof(dst->image_mode_rules));
|
||||
memcpy(dst->kstuff_no_pause_title_ids, src->kstuff_no_pause_title_ids,
|
||||
sizeof(dst->kstuff_no_pause_title_ids));
|
||||
dst->kstuff_no_pause_title_count = src->kstuff_no_pause_title_count;
|
||||
memcpy(dst->kstuff_delay_rules, src->kstuff_delay_rules,
|
||||
sizeof(dst->kstuff_delay_rules));
|
||||
}
|
||||
|
||||
static bool runtime_config_states_equal(const runtime_config_state_t *a,
|
||||
const runtime_config_state_t *b) {
|
||||
return memcmp(a, b, sizeof(*a)) == 0;
|
||||
}
|
||||
|
||||
static void activate_runtime_config_state(int slot_index) {
|
||||
atomic_store_explicit(&g_runtime_state_active_index, slot_index,
|
||||
memory_order_release);
|
||||
atomic_store_explicit(&g_runtime_cfg_ready, true, memory_order_release);
|
||||
}
|
||||
|
||||
void ensure_runtime_config_ready(void) {
|
||||
if (atomic_load_explicit(&g_runtime_cfg_ready, memory_order_acquire))
|
||||
return;
|
||||
|
||||
init_runtime_config_defaults(&g_runtime_state_slots[0]);
|
||||
activate_runtime_config_state(0);
|
||||
static void init_runtime_config(void) {
|
||||
init_runtime_config_defaults(&g_runtime_state);
|
||||
g_config_file_stamp = read_config_file_stamp();
|
||||
}
|
||||
|
||||
const runtime_config_t *runtime_config(void) {
|
||||
void ensure_runtime_config_ready(void) {
|
||||
(void)pthread_once(&g_runtime_init_once, init_runtime_config);
|
||||
}
|
||||
|
||||
runtime_config_t runtime_config(void) {
|
||||
ensure_runtime_config_ready();
|
||||
return &active_runtime_state()->cfg;
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
runtime_config_t cfg = g_runtime_state.cfg;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
int get_scan_path_count(void) {
|
||||
ensure_runtime_config_ready();
|
||||
return active_runtime_state()->scan_path_count;
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
int count = g_runtime_state.scan_path_count;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
const char *get_scan_path(int index) {
|
||||
bool get_scan_path(int index, char path_out[MAX_PATH]) {
|
||||
ensure_runtime_config_ready();
|
||||
const runtime_config_state_t *state = active_runtime_state();
|
||||
if (index < 0 || index >= state->scan_path_count)
|
||||
return NULL;
|
||||
return state->scan_path_storage[index];
|
||||
path_out[0] = '\0';
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
bool valid = index >= 0 && index < g_runtime_state.scan_path_count;
|
||||
if (valid)
|
||||
(void)strlcpy(path_out, g_runtime_state.scan_path_storage[index], MAX_PATH);
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return valid;
|
||||
}
|
||||
|
||||
int get_custom_scan_path_count(void) {
|
||||
ensure_runtime_config_ready();
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
int count = g_runtime_state.custom_scan_path_count;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool get_custom_scan_path(int index, char path_out[MAX_PATH]) {
|
||||
ensure_runtime_config_ready();
|
||||
path_out[0] = '\0';
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
bool valid = index >= 0 && index < g_runtime_state.custom_scan_path_count;
|
||||
if (valid)
|
||||
(void)strlcpy(path_out, g_runtime_state.scan_path_storage[index], MAX_PATH);
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return valid;
|
||||
}
|
||||
|
||||
uint32_t get_scan_depth_for_root(const char *scan_path) {
|
||||
uint32_t scan_depth = runtime_config()->scan_depth;
|
||||
uint32_t scan_depth = runtime_config().scan_depth;
|
||||
if (scan_depth < MIN_SCAN_DEPTH)
|
||||
scan_depth = MIN_SCAN_DEPTH;
|
||||
if (is_pfsc_image_mount_base_or_child(scan_path))
|
||||
@@ -386,11 +405,12 @@ bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
|
||||
if (!filename || !mount_read_only_out)
|
||||
return false;
|
||||
|
||||
const runtime_config_state_t *state = active_runtime_state();
|
||||
char normalized[MAX_PATH];
|
||||
if (!normalize_image_filename_value(filename, normalized))
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
const runtime_config_state_t *state = &g_runtime_state;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (!state->image_mode_rules[k].valid)
|
||||
continue;
|
||||
@@ -399,9 +419,11 @@ bool get_image_mode_override(const char *filename, bool *mount_read_only_out) {
|
||||
if (strcasecmp(state->image_mode_rules[k].filename, normalized) != 0)
|
||||
continue;
|
||||
*mount_read_only_out = state->image_mode_rules[k].mount_read_only;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -411,16 +433,20 @@ bool get_image_sector_size_override(const char *filename,
|
||||
if (!filename || !sector_size_out)
|
||||
return false;
|
||||
|
||||
if (lookup_image_sector_override_in_file(AUTOTUNE_FILE, filename,
|
||||
sector_size_out)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool autotuned = lookup_image_sector_override_in_file(
|
||||
AUTOTUNE_FILE, filename, sector_size_out);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (autotuned) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const runtime_config_state_t *state = active_runtime_state();
|
||||
char normalized[MAX_PATH];
|
||||
if (!normalize_image_filename_value(filename, normalized))
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
const runtime_config_state_t *state = &g_runtime_state;
|
||||
for (int k = 0; k < MAX_IMAGE_MODE_RULES; k++) {
|
||||
if (!state->image_mode_rules[k].valid)
|
||||
continue;
|
||||
@@ -429,25 +455,31 @@ bool get_image_sector_size_override(const char *filename,
|
||||
if (strcasecmp(state->image_mode_rules[k].filename, normalized) != 0)
|
||||
continue;
|
||||
*sector_size_out = state->image_mode_rules[k].sector_size;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_kstuff_pause_disabled_for_title(const char *title_id) {
|
||||
ensure_runtime_config_ready();
|
||||
const runtime_config_state_t *state = active_runtime_state();
|
||||
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
const runtime_config_state_t *state = &g_runtime_state;
|
||||
for (int i = 0; i < state->kstuff_no_pause_title_count; ++i) {
|
||||
if (strcmp(state->kstuff_no_pause_title_ids[i], normalized) == 0)
|
||||
if (strcmp(state->kstuff_no_pause_title_ids[i], normalized) == 0) {
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -458,12 +490,16 @@ bool is_global_fakelib_excluded_for_title(const char *title_id) {
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
const runtime_config_t *cfg = &g_runtime_state.cfg;
|
||||
for (uint32_t i = 0; i < cfg->global_fakelib_exclude_title_count; ++i) {
|
||||
if (strcmp(cfg->global_fakelib_exclude_title_ids[i], normalized) == 0)
|
||||
if (strcmp(cfg->global_fakelib_exclude_title_ids[i], normalized) == 0) {
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -473,27 +509,33 @@ bool get_kstuff_pause_delay_override_for_title(const char *title_id,
|
||||
if (!delay_seconds_out)
|
||||
return false;
|
||||
|
||||
const runtime_config_state_t *state = active_runtime_state();
|
||||
char normalized[MAX_TITLE_ID];
|
||||
if (!normalize_title_id_value(title_id, normalized))
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
const runtime_config_state_t *state = &g_runtime_state;
|
||||
for (int i = 0; i < MAX_KSTUFF_TITLE_RULES; ++i) {
|
||||
if (!state->kstuff_delay_rules[i].valid)
|
||||
continue;
|
||||
if (strcmp(state->kstuff_delay_rules[i].title_id, normalized) != 0)
|
||||
continue;
|
||||
*delay_seconds_out = state->kstuff_delay_rules[i].delay_seconds;
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get_kstuff_autotune_pause_delay_for_title(const char *title_id,
|
||||
uint32_t *delay_seconds_out) {
|
||||
return lookup_kstuff_delay_override_in_file(AUTOTUNE_FILE, title_id,
|
||||
delay_seconds_out);
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool found = lookup_kstuff_delay_override_in_file(
|
||||
AUTOTUNE_FILE, title_id, delay_seconds_out);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
@@ -511,8 +553,11 @@ bool upsert_kstuff_autotune_pause_delay(const char *title_id,
|
||||
tuned_delay_seconds = 1;
|
||||
if (tuned_delay_seconds > MAX_KSTUFF_PAUSE_DELAY_SECONDS)
|
||||
tuned_delay_seconds = MAX_KSTUFF_PAUSE_DELAY_SECONDS;
|
||||
if (!upsert_kstuff_delay_override_in_file(AUTOTUNE_FILE, normalized,
|
||||
(uint32_t)tuned_delay_seconds)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool updated = upsert_kstuff_delay_override_in_file(
|
||||
AUTOTUNE_FILE, normalized, (uint32_t)tuned_delay_seconds);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
if (delay_seconds_out)
|
||||
@@ -532,8 +577,11 @@ bool upsert_image_sector_size_autotune(const char *filename,
|
||||
if (!normalize_image_filename_value(filename, normalized_filename))
|
||||
return false;
|
||||
|
||||
if (!upsert_image_sector_override_in_file(AUTOTUNE_FILE, normalized_filename,
|
||||
sector_size)) {
|
||||
pthread_mutex_lock(&g_autotune_file_mutex);
|
||||
bool updated = upsert_image_sector_override_in_file(
|
||||
AUTOTUNE_FILE, normalized_filename, sector_size);
|
||||
pthread_mutex_unlock(&g_autotune_file_mutex);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -597,6 +645,10 @@ static bool normalize_absolute_path_value(const char *value,
|
||||
size_t len = strlen(trimmed);
|
||||
if (len == 0 || len >= MAX_PATH || trimmed[0] != '/')
|
||||
return false;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (iscntrl((unsigned char)trimmed[i]) || trimmed[i] == '#')
|
||||
return false;
|
||||
}
|
||||
|
||||
while (len > 1 && trimmed[len - 1] == '/') {
|
||||
trimmed[len - 1] = '\0';
|
||||
@@ -1205,7 +1257,6 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
int line_no = 0;
|
||||
bool has_custom_scanpaths = false;
|
||||
bool legacy_recursive_scan_requested = false;
|
||||
bool md_ufs_sector_configured = false;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
line_no++;
|
||||
char *key = NULL;
|
||||
@@ -1255,7 +1306,18 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_bind_address") == 0) {
|
||||
if (strcasecmp(key, "api_enabled") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
state->cfg.api_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "api_bind_address") == 0 ||
|
||||
strcasecmp(key, "api_bind_adress") == 0) {
|
||||
struct in_addr address;
|
||||
if (strlen(value) >= sizeof(state->cfg.api_bind_address) ||
|
||||
inet_pton(AF_INET, value, &address) != 1) {
|
||||
@@ -1263,6 +1325,9 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
line_no, key, value);
|
||||
continue;
|
||||
}
|
||||
if (strcasecmp(key, "api_bind_adress") == 0)
|
||||
log_debug(" [CFG] legacy api_bind_adress at line %d; use "
|
||||
"api_bind_address", line_no);
|
||||
(void)strlcpy(state->cfg.api_bind_address, value,
|
||||
sizeof(state->cfg.api_bind_address));
|
||||
continue;
|
||||
@@ -1611,26 +1676,14 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
continue;
|
||||
}
|
||||
|
||||
bool *bool_option = NULL;
|
||||
if (strcasecmp(key, "legacy_mount_ufs") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_ufs;
|
||||
} else if (strcasecmp(key, "legacy_mount_exfat") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_exfat;
|
||||
} else if (strcasecmp(key, "legacy_mount_pfs") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_pfs;
|
||||
} else if (strcasecmp(key, "legacy_mount_pfsc") == 0) {
|
||||
bool_option = &state->cfg.legacy_mount_pfsc;
|
||||
} else if (strcasecmp(key, "nested_pfs_index_cache") == 0 ||
|
||||
strcasecmp(key, "legacy_gddr5_cache") == 0) {
|
||||
bool_option = &state->cfg.nested_pfs_index_cache_enabled;
|
||||
}
|
||||
if (bool_option) {
|
||||
if (strcasecmp(key, "nested_pfs_index_cache") == 0 ||
|
||||
strcasecmp(key, "legacy_gddr5_cache") == 0) {
|
||||
if (!parse_bool_ini(value, &bval)) {
|
||||
log_debug(" [CFG] invalid bool at line %d: %s=%s", line_no, key,
|
||||
value);
|
||||
continue;
|
||||
}
|
||||
*bool_option = bval;
|
||||
state->cfg.nested_pfs_index_cache_enabled = bval;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1674,7 +1727,6 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.md_sector_exfat = u32;
|
||||
} else if (strcasecmp(key, "md_ufs_sector_size") == 0) {
|
||||
state->cfg.md_sector_ufs = u32;
|
||||
md_ufs_sector_configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1683,12 +1735,11 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
if (has_custom_scanpaths && state->scan_path_count == 0) {
|
||||
log_debug(" [CFG] no valid scanpath entries, using defaults");
|
||||
init_runtime_scan_paths_defaults(state);
|
||||
} else if (has_custom_scanpaths) {
|
||||
state->custom_scan_path_count = state->scan_path_count;
|
||||
}
|
||||
add_runtime_managed_scan_paths(state);
|
||||
|
||||
if (!md_ufs_sector_configured && !state->cfg.legacy_mount_ufs)
|
||||
state->cfg.md_sector_ufs = MD_SECTOR_SIZE_UFS_OPTIMIZED;
|
||||
|
||||
if (legacy_recursive_scan_requested) {
|
||||
state->cfg.scan_depth = 2u;
|
||||
state->cfg.legacy_recursive_scan_forced = true;
|
||||
@@ -1708,7 +1759,8 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
kstuff_delay_rule_count++;
|
||||
}
|
||||
|
||||
log_debug(" [CFG] loaded: debug=%d quiet=%d language=%s ro=%d force=%d "
|
||||
log_debug(" [CFG] loaded: api_enabled=%d debug=%d quiet=%d language=%s "
|
||||
"ro=%d force=%d "
|
||||
"persistent_image_mounts=%d app_install_all=%d "
|
||||
"auto_remove_missing_games=%d "
|
||||
"auto_remove_games_with_dlc=%d auto_remove_missing_delay_s=%u "
|
||||
@@ -1722,11 +1774,11 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
"fan_target_temperature=%u (0=system) "
|
||||
"kstuff_pause_delay_image_s=%u kstuff_pause_delay_direct_s=%u "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
"legacy_mount(ufs=%d exfat=%d pfs=%d pfsc=%d) "
|
||||
"nested_pfs_index_cache=%d "
|
||||
"lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) "
|
||||
"scan_interval_s=%u stability_wait_s=%u scan_paths=%d image_rules=%d "
|
||||
"kstuff_no_pause=%d kstuff_delay_rules=%d",
|
||||
state->cfg.api_enabled ? 1 : 0,
|
||||
state->cfg.debug_enabled ? 1 : 0, state->cfg.quiet_mode ? 1 : 0,
|
||||
sm_l10n_language_name(state->cfg.language_id),
|
||||
state->cfg.mount_read_only ? 1 : 0,
|
||||
@@ -1755,10 +1807,6 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
state->cfg.kstuff_pause_delay_direct_seconds,
|
||||
attach_backend_name(state->cfg.exfat_backend),
|
||||
attach_backend_name(state->cfg.ufs_backend),
|
||||
state->cfg.legacy_mount_ufs ? 1 : 0,
|
||||
state->cfg.legacy_mount_exfat ? 1 : 0,
|
||||
state->cfg.legacy_mount_pfs ? 1 : 0,
|
||||
state->cfg.legacy_mount_pfsc ? 1 : 0,
|
||||
state->cfg.nested_pfs_index_cache_enabled ? 1 : 0,
|
||||
state->cfg.lvd_sector_exfat, state->cfg.lvd_sector_ufs,
|
||||
state->cfg.lvd_sector_pfs, state->cfg.md_sector_exfat,
|
||||
@@ -1771,10 +1819,15 @@ static config_load_status_t load_runtime_config_state(runtime_config_state_t *st
|
||||
}
|
||||
|
||||
bool load_runtime_config(void) {
|
||||
ensure_runtime_config_ready();
|
||||
pthread_mutex_lock(&g_runtime_load_mutex);
|
||||
bool loaded =
|
||||
load_runtime_config_state(&g_runtime_state_slots[0]) == CONFIG_LOAD_OK;
|
||||
activate_runtime_config_state(0);
|
||||
load_runtime_config_state(&g_runtime_parse_state) == CONFIG_LOAD_OK;
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
memcpy(&g_runtime_state, &g_runtime_parse_state, sizeof(g_runtime_state));
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
g_config_file_stamp = read_config_file_stamp();
|
||||
pthread_mutex_unlock(&g_runtime_load_mutex);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -1783,29 +1836,204 @@ bool reload_runtime_config_if_changed(bool *reloaded_out) {
|
||||
if (reloaded_out)
|
||||
*reloaded_out = false;
|
||||
|
||||
pthread_mutex_lock(&g_runtime_load_mutex);
|
||||
config_file_stamp_t new_stamp = read_config_file_stamp();
|
||||
if (config_file_stamp_equals(&new_stamp, &g_config_file_stamp))
|
||||
if (config_file_stamp_equals(&new_stamp, &g_config_file_stamp)) {
|
||||
pthread_mutex_unlock(&g_runtime_load_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
const runtime_config_state_t *current = active_runtime_state();
|
||||
runtime_config_state_t *parsed = &g_runtime_state_slots[RUNTIME_CONFIG_PARSE_SLOT];
|
||||
runtime_config_state_t *parsed = &g_runtime_parse_state;
|
||||
config_load_status_t status = load_runtime_config_state(parsed);
|
||||
if (status == CONFIG_LOAD_ERROR)
|
||||
if (status == CONFIG_LOAD_ERROR) {
|
||||
pthread_mutex_unlock(&g_runtime_load_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int current_slot = atomic_load_explicit(&g_runtime_state_active_index,
|
||||
memory_order_acquire);
|
||||
int candidate_slot = (current_slot == 0) ? 1 : 0;
|
||||
runtime_config_state_t *candidate = &g_runtime_state_slots[candidate_slot];
|
||||
memcpy(candidate, current, sizeof(*candidate));
|
||||
apply_reloadable_runtime_fields(candidate, parsed);
|
||||
pthread_mutex_lock(&g_runtime_state_mutex);
|
||||
bool changed = !runtime_config_states_equal(&g_runtime_state, parsed);
|
||||
if (changed)
|
||||
memcpy(&g_runtime_state, parsed, sizeof(g_runtime_state));
|
||||
pthread_mutex_unlock(&g_runtime_state_mutex);
|
||||
|
||||
g_config_file_stamp = new_stamp;
|
||||
if (runtime_config_states_equal(candidate, current))
|
||||
return true;
|
||||
|
||||
activate_runtime_config_state(candidate_slot);
|
||||
pthread_mutex_unlock(&g_runtime_load_mutex);
|
||||
if (reloaded_out)
|
||||
*reloaded_out = true;
|
||||
*reloaded_out = changed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_config_write_web_settings(bool debug_enabled, bool quiet_mode,
|
||||
bool update_emulators_enabled,
|
||||
bool auto_update_ampr_enabled,
|
||||
bool auto_remove_missing_games,
|
||||
uint32_t auto_remove_missing_delay_seconds,
|
||||
bool allow_lan_access,
|
||||
uint32_t fan_target_temperature_c,
|
||||
const char *const *scan_paths,
|
||||
size_t scan_path_count) {
|
||||
if (auto_remove_missing_delay_seconds <
|
||||
MIN_AUTO_REMOVE_MISSING_DELAY_SECONDS ||
|
||||
auto_remove_missing_delay_seconds >
|
||||
MAX_AUTO_REMOVE_MISSING_DELAY_SECONDS ||
|
||||
(fan_target_temperature_c != FAN_TARGET_TEMPERATURE_SYSTEM &&
|
||||
(fan_target_temperature_c < MIN_FAN_TARGET_TEMPERATURE_C ||
|
||||
fan_target_temperature_c > MAX_FAN_TARGET_TEMPERATURE_C)) ||
|
||||
scan_path_count > MAX_SCAN_PATHS ||
|
||||
(scan_path_count > 0 && !scan_paths)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
char(*normalized_paths)[MAX_PATH] =
|
||||
scan_path_count > 0 ? calloc(scan_path_count, MAX_PATH) : NULL;
|
||||
if (scan_path_count > 0 && !normalized_paths) {
|
||||
errno = ENOMEM;
|
||||
return false;
|
||||
}
|
||||
size_t normalized_count = 0;
|
||||
for (size_t i = 0; i < scan_path_count; ++i) {
|
||||
char path[MAX_PATH];
|
||||
if (!normalize_absolute_path_value(scan_paths[i], path) ||
|
||||
strcmp(path, "/") == 0 ||
|
||||
is_under_image_mount_base(path)) {
|
||||
free(normalized_paths);
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
bool duplicate = false;
|
||||
for (size_t j = 0; j < normalized_count; ++j) {
|
||||
if (strcmp(normalized_paths[j], path) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicate) {
|
||||
(void)strlcpy(normalized_paths[normalized_count], path, MAX_PATH);
|
||||
normalized_count++;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_config_file_mutex);
|
||||
(void)mkdir(LOG_DIR, 0777);
|
||||
FILE *in = fopen(CONFIG_FILE, "r");
|
||||
if (!in && errno != ENOENT) {
|
||||
int saved_errno = config_io_error();
|
||||
pthread_mutex_unlock(&g_config_file_mutex);
|
||||
free(normalized_paths);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
char temp_path[MAX_PATH];
|
||||
int temp_written = snprintf(temp_path, sizeof(temp_path), "%s.web.tmp",
|
||||
CONFIG_FILE);
|
||||
if (temp_written < 0 || (size_t)temp_written >= sizeof(temp_path)) {
|
||||
if (in)
|
||||
(void)fclose(in);
|
||||
pthread_mutex_unlock(&g_config_file_mutex);
|
||||
free(normalized_paths);
|
||||
errno = ENAMETOOLONG;
|
||||
return false;
|
||||
}
|
||||
(void)unlink(temp_path);
|
||||
FILE *out = fopen(temp_path, "w");
|
||||
if (!out) {
|
||||
int saved_errno = config_io_error();
|
||||
if (in)
|
||||
(void)fclose(in);
|
||||
pthread_mutex_unlock(&g_config_file_mutex);
|
||||
free(normalized_paths);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
int saved_errno = 0;
|
||||
int last_written = '\n';
|
||||
char line[MAX_PATH + 256u];
|
||||
while (in && fgets(line, sizeof(line), in)) {
|
||||
bool truncated = strchr(line, '\n') == NULL && !feof(in);
|
||||
char parsed[sizeof(line)];
|
||||
(void)strlcpy(parsed, line, sizeof(parsed));
|
||||
char *key = NULL;
|
||||
char *value = NULL;
|
||||
char *trimmed = trim_ascii(parsed);
|
||||
bool skip = strcmp(trimmed,
|
||||
"# Managed by the ShadowMount web interface.") == 0;
|
||||
if (!skip)
|
||||
skip = parse_ini_line(parsed, &key, &value) &&
|
||||
web_managed_config_key(key);
|
||||
if (!skip && fputs(line, out) == EOF) {
|
||||
saved_errno = config_io_error();
|
||||
break;
|
||||
}
|
||||
if (!skip && line[0] != '\0')
|
||||
last_written = (unsigned char)line[strlen(line) - 1u];
|
||||
if (!truncated)
|
||||
continue;
|
||||
int ch;
|
||||
while ((ch = fgetc(in)) != EOF) {
|
||||
if (!skip && fputc(ch, out) == EOF && saved_errno == 0)
|
||||
saved_errno = config_io_error();
|
||||
if (!skip)
|
||||
last_written = ch;
|
||||
if (ch == '\n')
|
||||
break;
|
||||
}
|
||||
if (saved_errno != 0)
|
||||
break;
|
||||
}
|
||||
if (in && ferror(in) && saved_errno == 0)
|
||||
saved_errno = config_io_error();
|
||||
if (saved_errno == 0 && last_written != '\n' && fputc('\n', out) == EOF)
|
||||
saved_errno = config_io_error();
|
||||
if (saved_errno == 0 &&
|
||||
fprintf(out,
|
||||
"\n# Managed by the ShadowMount web interface.\n"
|
||||
"debug=%u\nquiet_mode=%u\nupdate_emulators=%u\n"
|
||||
"auto_update_ampr=%u\n"
|
||||
"auto_remove_missing_games=%u\n"
|
||||
"auto_remove_missing_delay_seconds=%u\n"
|
||||
"api_bind_address=%s\n",
|
||||
debug_enabled ? 1u : 0u, quiet_mode ? 1u : 0u,
|
||||
update_emulators_enabled ? 1u : 0u,
|
||||
auto_update_ampr_enabled ? 1u : 0u,
|
||||
auto_remove_missing_games ? 1u : 0u,
|
||||
auto_remove_missing_delay_seconds,
|
||||
allow_lan_access ? "0.0.0.0" : "127.0.0.1") < 0) {
|
||||
saved_errno = config_io_error();
|
||||
}
|
||||
if (saved_errno == 0) {
|
||||
if (fan_target_temperature_c == FAN_TARGET_TEMPERATURE_SYSTEM) {
|
||||
if (fputs("fan_target_temperature=system\n", out) == EOF)
|
||||
saved_errno = config_io_error();
|
||||
} else if (fprintf(out, "fan_target_temperature=%u\n",
|
||||
fan_target_temperature_c) < 0) {
|
||||
saved_errno = config_io_error();
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; saved_errno == 0 && i < normalized_count; ++i) {
|
||||
if (fprintf(out, "scanpath=%s\n", normalized_paths[i]) < 0)
|
||||
saved_errno = config_io_error();
|
||||
}
|
||||
if (fflush(out) != 0 && saved_errno == 0)
|
||||
saved_errno = config_io_error();
|
||||
if (saved_errno == 0 && fsync(fileno(out)) != 0)
|
||||
saved_errno = config_io_error();
|
||||
if (fclose(out) != 0 && saved_errno == 0)
|
||||
saved_errno = config_io_error();
|
||||
if (in && fclose(in) != 0 && saved_errno == 0)
|
||||
saved_errno = config_io_error();
|
||||
if (saved_errno == 0 && rename(temp_path, CONFIG_FILE) != 0)
|
||||
saved_errno = config_io_error();
|
||||
if (saved_errno != 0)
|
||||
(void)unlink(temp_path);
|
||||
pthread_mutex_unlock(&g_config_file_mutex);
|
||||
free(normalized_paths);
|
||||
if (saved_errno != 0) {
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+232
-83
@@ -31,9 +31,13 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
pid_t pid;
|
||||
char title_id[MAX_TITLE_ID];
|
||||
char sandbox_app0_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
fakelib_layer_t layers[1];
|
||||
size_t layer_count;
|
||||
size_t emulator_file_count;
|
||||
bool notify_pending;
|
||||
} fakelib_session_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -67,6 +71,12 @@ typedef struct {
|
||||
char emulators_path[MAX_PATH];
|
||||
} fakelib_cache_context_t;
|
||||
|
||||
typedef enum {
|
||||
FAKELIB_SOURCE_NONE = 0,
|
||||
FAKELIB_SOURCE_COMPOSABLE,
|
||||
FAKELIB_SOURCE_FAKELIB2,
|
||||
} fakelib_source_kind_t;
|
||||
|
||||
static fakelib_session_t g_fakelib_mount;
|
||||
static pthread_mutex_t g_fakelib_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t g_fakelib_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
@@ -76,7 +86,7 @@ static bool fakelib_session_active(void) {
|
||||
}
|
||||
|
||||
bool sm_fakelib_game_feature_enabled(void) {
|
||||
return runtime_config()->backport_fakelib_enabled;
|
||||
return runtime_config().backport_fakelib_enabled;
|
||||
}
|
||||
|
||||
static bool mount_fakelib_overlay(const char *title_id,
|
||||
@@ -124,18 +134,26 @@ static bool track_fakelib_overlay(const char *title_id,
|
||||
const char *source_path,
|
||||
const char *mount_path,
|
||||
const char *label) {
|
||||
if (g_fakelib_mount.layer_count >=
|
||||
sizeof(g_fakelib_mount.layers) / sizeof(g_fakelib_mount.layers[0])) {
|
||||
errno = ENOSPC;
|
||||
return false;
|
||||
}
|
||||
if (!mount_fakelib_overlay(title_id, source_path, mount_path, label))
|
||||
return false;
|
||||
|
||||
fakelib_layer_t *layer = &g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
|
||||
fakelib_layer_t *layer =
|
||||
&g_fakelib_mount.layers[g_fakelib_mount.layer_count++];
|
||||
layer->label = label;
|
||||
(void)strlcpy(layer->source_path, source_path, sizeof(layer->source_path));
|
||||
(void)strlcpy(layer->mount_path, mount_path, sizeof(layer->mount_path));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_sandbox_mount_path(const char *title_id,
|
||||
char mount_path[MAX_PATH]) {
|
||||
static bool resolve_sandbox_paths(const char *title_id,
|
||||
char app0_path[MAX_PATH],
|
||||
char mount_path[MAX_PATH]) {
|
||||
app0_path[0] = '\0';
|
||||
mount_path[0] = '\0';
|
||||
|
||||
char sandbox_id[MAX_TITLE_ID];
|
||||
@@ -182,7 +200,15 @@ static bool resolve_sandbox_mount_path(const char *title_id,
|
||||
|
||||
struct stat st;
|
||||
char sandbox_root[MAX_PATH];
|
||||
snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s", sandbox_id);
|
||||
int written = snprintf(sandbox_root, sizeof(sandbox_root), "/mnt/sandbox/%s",
|
||||
sandbox_id);
|
||||
if (written < 0 || (size_t)written >= sizeof(sandbox_root))
|
||||
return false;
|
||||
written = snprintf(app0_path, MAX_PATH, "%s/app0", sandbox_root);
|
||||
if (written < 0 || (size_t)written >= MAX_PATH ||
|
||||
stat(app0_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
app0_path[0] = '\0';
|
||||
}
|
||||
d = opendir(sandbox_root);
|
||||
if (!d)
|
||||
return false;
|
||||
@@ -194,8 +220,10 @@ static bool resolve_sandbox_mount_path(const char *title_id,
|
||||
if (strcmp(entry->d_name, "app0") == 0)
|
||||
continue;
|
||||
|
||||
snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
|
||||
entry->d_name);
|
||||
written = snprintf(mount_path, MAX_PATH, "%s/%s/common/lib", sandbox_root,
|
||||
entry->d_name);
|
||||
if (written < 0 || (size_t)written >= MAX_PATH)
|
||||
continue;
|
||||
if (stat(mount_path, &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
continue;
|
||||
|
||||
@@ -204,25 +232,31 @@ static bool resolve_sandbox_mount_path(const char *title_id,
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
return found;
|
||||
if (!found)
|
||||
mount_path[0] = '\0';
|
||||
return app0_path[0] != '\0' || found;
|
||||
}
|
||||
|
||||
static bool resolve_game_fakelib_source_for_path(
|
||||
const char *title_id, const char *game_path,
|
||||
static fakelib_source_kind_t resolve_game_fakelib_source_for_path(
|
||||
const char *title_id, const char *game_path, bool include_backport,
|
||||
char source_path[MAX_PATH]) {
|
||||
source_path[0] = '\0';
|
||||
|
||||
char backport_path[MAX_PATH] = {0};
|
||||
bool has_backport =
|
||||
bool has_backport = include_backport &&
|
||||
resolve_backport_path_for_title(title_id, NULL, backport_path);
|
||||
bool has_game = game_path && game_path[0] != '\0';
|
||||
const struct {
|
||||
const char *root;
|
||||
const char *directory;
|
||||
fakelib_source_kind_t kind;
|
||||
} candidates[] = {
|
||||
{has_backport ? backport_path : NULL, "fakelib2"},
|
||||
{has_backport ? backport_path : NULL, "fakelib"},
|
||||
{has_game ? game_path : NULL, "fakelib"},
|
||||
{has_backport ? backport_path : NULL, "fakelib2",
|
||||
FAKELIB_SOURCE_FAKELIB2},
|
||||
{has_backport ? backport_path : NULL, "fakelib",
|
||||
FAKELIB_SOURCE_COMPOSABLE},
|
||||
{has_game ? game_path : NULL, "fakelib2", FAKELIB_SOURCE_FAKELIB2},
|
||||
{has_game ? game_path : NULL, "fakelib", FAKELIB_SOURCE_COMPOSABLE},
|
||||
};
|
||||
struct stat st;
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) {
|
||||
@@ -232,21 +266,12 @@ static bool resolve_game_fakelib_source_for_path(
|
||||
candidates[i].root, candidates[i].directory);
|
||||
if (written > 0 && (size_t)written < MAX_PATH &&
|
||||
stat(source_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
return true;
|
||||
return candidates[i].kind;
|
||||
}
|
||||
}
|
||||
|
||||
source_path[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool resolve_game_fakelib_source(const char *title_id,
|
||||
char source_path[MAX_PATH]) {
|
||||
char game_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, game_path, sizeof(game_path)))
|
||||
game_path[0] = '\0';
|
||||
return resolve_game_fakelib_source_for_path(title_id, game_path,
|
||||
source_path);
|
||||
return FAKELIB_SOURCE_NONE;
|
||||
}
|
||||
|
||||
static bool path_overlaps_fakelib_cache(const char *path) {
|
||||
@@ -258,31 +283,31 @@ static bool resolve_global_fakelib_source(const char *title_id,
|
||||
char source_path[MAX_PATH]) {
|
||||
source_path[0] = '\0';
|
||||
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->global_fakelib_enabled)
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
if (!cfg.global_fakelib_enabled)
|
||||
return false;
|
||||
if (is_global_fakelib_excluded_for_title(title_id))
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (stat(cfg->global_fakelib_path, &st) != 0) {
|
||||
if (stat(cfg.global_fakelib_path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
log_debug(" [FAKELIB] global path unavailable for %s: %s (%s)",
|
||||
title_id, cfg->global_fakelib_path, strerror(errno));
|
||||
title_id, cfg.global_fakelib_path, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode)) {
|
||||
log_debug(" [FAKELIB] global path is not a directory for %s: %s",
|
||||
title_id, cfg->global_fakelib_path);
|
||||
title_id, cfg.global_fakelib_path);
|
||||
return false;
|
||||
}
|
||||
if (path_overlaps_fakelib_cache(cfg->global_fakelib_path)) {
|
||||
if (path_overlaps_fakelib_cache(cfg.global_fakelib_path)) {
|
||||
log_debug(" [FAKELIB] global path overlaps cache root for %s: %s",
|
||||
title_id, cfg->global_fakelib_path);
|
||||
title_id, cfg.global_fakelib_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)strlcpy(source_path, cfg->global_fakelib_path, MAX_PATH);
|
||||
(void)strlcpy(source_path, cfg.global_fakelib_path, MAX_PATH);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -838,16 +863,16 @@ static void init_cache_context(const char *title_id, const char *game_path,
|
||||
memset(context, 0, sizeof(*context));
|
||||
(void)strlcpy(context->game_path, game_path, sizeof(context->game_path));
|
||||
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (cfg->update_emulators_enabled) {
|
||||
(void)strlcpy(context->emulators_path, cfg->emulators_path,
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
if (cfg.update_emulators_enabled) {
|
||||
(void)strlcpy(context->emulators_path, cfg.emulators_path,
|
||||
sizeof(context->emulators_path));
|
||||
}
|
||||
|
||||
if (resolve_global_fakelib_source(title_id, context->global_path) &&
|
||||
strcmp(context->global_path, game_path) != 0) {
|
||||
context->flags |= FAKELIB_CACHE_HAS_GLOBAL;
|
||||
if (!cfg->global_fakelib_game_priority)
|
||||
if (!cfg.global_fakelib_game_priority)
|
||||
context->flags |= FAKELIB_CACHE_GLOBAL_PRIORITY;
|
||||
} else {
|
||||
context->global_path[0] = '\0';
|
||||
@@ -986,16 +1011,18 @@ static bool rebuild_fakelib_cache(
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prepare_title_cache(const char *title_id, const char *game_path) {
|
||||
static void prepare_title_cache(const char *title_id, const char *game_path,
|
||||
bool include_backport) {
|
||||
if (!is_supported_game_title_id(title_id))
|
||||
return;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->backport_fakelib_enabled)
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
if (!cfg.backport_fakelib_enabled)
|
||||
return;
|
||||
|
||||
char game_source_path[MAX_PATH];
|
||||
if (!resolve_game_fakelib_source_for_path(title_id, game_path,
|
||||
game_source_path)) {
|
||||
fakelib_source_kind_t source_kind = resolve_game_fakelib_source_for_path(
|
||||
title_id, game_path, include_backport, game_source_path);
|
||||
if (source_kind != FAKELIB_SOURCE_COMPOSABLE) {
|
||||
remove_title_cache(title_id);
|
||||
return;
|
||||
}
|
||||
@@ -1051,7 +1078,7 @@ static void prepare_title_cache(const char *title_id, const char *game_path) {
|
||||
void sm_fakelib_prepare_title_cache(const char *title_id,
|
||||
const char *game_path) {
|
||||
pthread_mutex_lock(&g_fakelib_cache_mutex);
|
||||
prepare_title_cache(title_id, game_path);
|
||||
prepare_title_cache(title_id, game_path, true);
|
||||
pthread_mutex_unlock(&g_fakelib_cache_mutex);
|
||||
}
|
||||
|
||||
@@ -1125,50 +1152,79 @@ static bool cleanup_fakelib_mount(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
bool notify_user) {
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
static bool mount_fakelib_for_game_locked(pid_t pid, const char *title_id,
|
||||
bool notify_user) {
|
||||
if (!title_id || !is_supported_game_title_id(title_id)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fakelib_session_active() && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
char managed_game_path[MAX_PATH] = {0};
|
||||
bool managed_title =
|
||||
read_mount_link(title_id, managed_game_path, sizeof(managed_game_path));
|
||||
char sandbox_app0_path[MAX_PATH] = {0};
|
||||
char mount_path[MAX_PATH] = {0};
|
||||
bool sandbox_resolved = false;
|
||||
|
||||
if (!managed_title) {
|
||||
sandbox_resolved =
|
||||
resolve_sandbox_paths(title_id, sandbox_app0_path, mount_path);
|
||||
}
|
||||
|
||||
if (fakelib_session_active() && g_fakelib_mount.pid != pid) {
|
||||
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
|
||||
(long)g_fakelib_mount.pid, (long)pid, title_id);
|
||||
if (!cleanup_fakelib_mount()) {
|
||||
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
|
||||
(long)g_fakelib_mount.pid, title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
// Sandbox-ready mounts the overlay before the game process exists. The
|
||||
// subsequent NOTE_EXEC only needs to attach the PID to that session; avoid
|
||||
// fingerprinting and rebuilding the package cache a second time.
|
||||
if (pid > 0 && fakelib_session_active() && g_fakelib_mount.pid <= 0 &&
|
||||
strcmp(g_fakelib_mount.title_id, title_id) == 0) {
|
||||
if (!sandbox_resolved) {
|
||||
sandbox_resolved =
|
||||
resolve_sandbox_paths(title_id, sandbox_app0_path, mount_path);
|
||||
}
|
||||
if (sandbox_resolved && mount_path[0] != '\0' &&
|
||||
strcmp(g_fakelib_mount.sandbox_app0_path, sandbox_app0_path) == 0 &&
|
||||
strcmp(g_fakelib_mount.mount_path, mount_path) == 0) {
|
||||
g_fakelib_mount.pid = pid;
|
||||
bool notify = notify_user && g_fakelib_mount.notify_pending;
|
||||
g_fakelib_mount.notify_pending = false;
|
||||
if (notify) {
|
||||
notify_system_info_l10n(
|
||||
g_fakelib_mount.emulator_file_count > 0
|
||||
? SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED
|
||||
: SM_L10N_GAME_BACKPORTED,
|
||||
title_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
char game_source_path[MAX_PATH];
|
||||
char global_source_path[MAX_PATH];
|
||||
char mount_path[MAX_PATH];
|
||||
bool has_game = resolve_game_fakelib_source(title_id, game_source_path);
|
||||
bool has_global =
|
||||
resolve_global_fakelib_source(title_id, global_source_path);
|
||||
if (!has_global && !has_game) {
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
if (!managed_title && sandbox_resolved) {
|
||||
// A package's own fakelib is not visible until ShellCore has mounted
|
||||
// app0. Build the same composable cache used by folder/image games now,
|
||||
// while the process is still blocked at the sandbox-ready hook.
|
||||
pthread_mutex_lock(&g_fakelib_cache_mutex);
|
||||
prepare_title_cache(title_id, sandbox_app0_path, false);
|
||||
pthread_mutex_unlock(&g_fakelib_cache_mutex);
|
||||
}
|
||||
|
||||
char game_source_path[MAX_PATH] = {0};
|
||||
char global_source_path[MAX_PATH] = {0};
|
||||
const char *game_path = managed_title ? managed_game_path
|
||||
: sandbox_app0_path;
|
||||
fakelib_source_kind_t source_kind = resolve_game_fakelib_source_for_path(
|
||||
title_id, game_path, managed_title, game_source_path);
|
||||
bool has_game = source_kind != FAKELIB_SOURCE_NONE;
|
||||
bool allows_composition = source_kind != FAKELIB_SOURCE_FAKELIB2;
|
||||
bool has_global = allows_composition &&
|
||||
resolve_global_fakelib_source(title_id,
|
||||
global_source_path);
|
||||
|
||||
bool needs_combined_cache =
|
||||
has_global && has_game &&
|
||||
strcmp(global_source_path, game_source_path) != 0;
|
||||
size_t emulator_file_count = 0;
|
||||
bool cache_resolved = false;
|
||||
bool cache_includes_global = false;
|
||||
if (has_game) {
|
||||
if (source_kind == FAKELIB_SOURCE_COMPOSABLE) {
|
||||
char cache_path[MAX_PATH];
|
||||
cache_resolved = resolve_cached_fakelib(
|
||||
title_id, game_source_path, cache_path, &emulator_file_count,
|
||||
@@ -1186,23 +1242,66 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
title_id);
|
||||
}
|
||||
|
||||
bool has_source = has_game || has_global;
|
||||
if (has_source && !sandbox_resolved) {
|
||||
sandbox_resolved =
|
||||
resolve_sandbox_paths(title_id, sandbox_app0_path, mount_path);
|
||||
}
|
||||
if (has_source && (!sandbox_resolved || mount_path[0] == '\0')) {
|
||||
errno = ENOENT;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fakelib_session_active() &&
|
||||
strcmp(g_fakelib_mount.title_id, title_id) == 0 && has_source &&
|
||||
strcmp(g_fakelib_mount.sandbox_app0_path, sandbox_app0_path) == 0 &&
|
||||
strcmp(g_fakelib_mount.mount_path, mount_path) == 0) {
|
||||
if (pid > 0) {
|
||||
g_fakelib_mount.pid = pid;
|
||||
bool notify = notify_user && g_fakelib_mount.notify_pending;
|
||||
g_fakelib_mount.notify_pending = false;
|
||||
if (notify) {
|
||||
notify_system_info_l10n(
|
||||
g_fakelib_mount.emulator_file_count > 0
|
||||
? SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED
|
||||
: SM_L10N_GAME_BACKPORTED,
|
||||
title_id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fakelib_session_active()) {
|
||||
log_debug(" [FAKELIB] handoff active mount pid=%ld -> pid=%ld (%s)",
|
||||
(long)g_fakelib_mount.pid, (long)pid, title_id);
|
||||
if (!cleanup_fakelib_mount()) {
|
||||
log_debug(" [FAKELIB] handoff cleanup failed for pid=%ld, skipping %s",
|
||||
(long)g_fakelib_mount.pid, title_id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_source)
|
||||
return true;
|
||||
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
g_fakelib_mount.pid = pid;
|
||||
|
||||
if (!resolve_sandbox_mount_path(title_id, mount_path)) {
|
||||
memset(&g_fakelib_mount, 0, sizeof(g_fakelib_mount));
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
(void)strlcpy(g_fakelib_mount.title_id, title_id,
|
||||
sizeof(g_fakelib_mount.title_id));
|
||||
(void)strlcpy(g_fakelib_mount.sandbox_app0_path, sandbox_app0_path,
|
||||
sizeof(g_fakelib_mount.sandbox_app0_path));
|
||||
(void)strlcpy(g_fakelib_mount.mount_path, mount_path,
|
||||
sizeof(g_fakelib_mount.mount_path));
|
||||
g_fakelib_mount.emulator_file_count = emulator_file_count;
|
||||
g_fakelib_mount.notify_pending = has_game && pid <= 0;
|
||||
|
||||
const char *source_path = has_game ? game_source_path : global_source_path;
|
||||
const char *label = has_game ? "game" : "global";
|
||||
const char *label = !allows_composition
|
||||
? "fakelib2"
|
||||
: (has_game ? "game" : "global");
|
||||
if (!track_fakelib_overlay(title_id, source_path, mount_path, label)) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (has_game && notify_user) {
|
||||
@@ -1211,6 +1310,55 @@ void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
: SM_L10N_GAME_BACKPORTED,
|
||||
title_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_fakelib_game_on_sandbox_ready(const char *title_id) {
|
||||
if (!title_id || !is_supported_game_title_id(title_id))
|
||||
return true;
|
||||
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
bool ready = !sm_fakelib_game_feature_enabled() ||
|
||||
mount_fakelib_for_game_locked(0, title_id, false);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_launch_failed(const char *title_id) {
|
||||
if (!title_id)
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (fakelib_session_active() && g_fakelib_mount.pid <= 0 &&
|
||||
strcmp(g_fakelib_mount.title_id, title_id) == 0) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
}
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
void sm_fakelib_game_on_exec(pid_t pid, const char *title_id,
|
||||
bool notify_user) {
|
||||
if (pid <= 0 || !title_id || !is_supported_game_title_id(title_id))
|
||||
return;
|
||||
|
||||
pthread_mutex_lock(&g_fakelib_mutex);
|
||||
if (!sm_fakelib_game_feature_enabled()) {
|
||||
if (fakelib_session_active() && g_fakelib_mount.pid <= 0 && title_id &&
|
||||
strcmp(g_fakelib_mount.title_id, title_id) == 0) {
|
||||
(void)cleanup_fakelib_mount();
|
||||
}
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fakelib_session_active() && g_fakelib_mount.pid == pid) {
|
||||
log_debug(" [FAKELIB] already tracking pid=%ld for %s", (long)pid,
|
||||
title_id);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)mount_fakelib_for_game_locked(pid, title_id, notify_user);
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
@@ -1221,8 +1369,9 @@ void sm_fakelib_game_on_exit(pid_t pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld mount=%s", (long)pid,
|
||||
g_fakelib_mount.mount_path);
|
||||
log_debug(" [FAKELIB] game stopped: pid=%ld title=%s layers=%u",
|
||||
(long)pid, g_fakelib_mount.title_id,
|
||||
(unsigned)g_fakelib_mount.layer_count);
|
||||
cleanup_fakelib_mount();
|
||||
pthread_mutex_unlock(&g_fakelib_mutex);
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ static int apply_fan_target(uint8_t target_c,
|
||||
void sm_fan_apply_game_target(const char *title_id) {
|
||||
static bool target_notification_sent = false;
|
||||
|
||||
uint32_t target_c = runtime_config()->fan_target_temperature_c;
|
||||
uint32_t target_c = runtime_config().fan_target_temperature_c;
|
||||
if (target_c == FAN_TARGET_TEMPERATURE_SYSTEM)
|
||||
return;
|
||||
|
||||
|
||||
+3
-1
@@ -467,7 +467,9 @@ static bool path_is_managed_backport_for_title(const char *title_id,
|
||||
char root[MAX_PATH];
|
||||
int scan_path_count = get_scan_path_count();
|
||||
for (int i = 0; i < scan_path_count; i++) {
|
||||
if (!build_backports_root_path(get_scan_path(i), root))
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path) ||
|
||||
!build_backports_root_path(scan_path, root))
|
||||
continue;
|
||||
size_t root_len = strlen(root);
|
||||
if (strncmp(path, root, root_len) != 0)
|
||||
|
||||
+35
-16
@@ -4,6 +4,7 @@
|
||||
#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"
|
||||
@@ -26,7 +27,14 @@ 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);
|
||||
if (entry->path[0] != '\0' && path_exists(entry->path))
|
||||
return true;
|
||||
|
||||
char image_path[MAX_PATH];
|
||||
return entry->title_id[0] != '\0' &&
|
||||
read_mount_image_link(entry->title_id, image_path,
|
||||
sizeof(image_path)) &&
|
||||
path_exists(image_path);
|
||||
}
|
||||
|
||||
static bool title_is_protected_by_dlc(const char *title_id,
|
||||
@@ -48,20 +56,28 @@ static bool title_is_protected_by_dlc(const char *title_id,
|
||||
}
|
||||
|
||||
static bool resolve_game_cache_owning_scan_root(const char *path,
|
||||
const char *title_id,
|
||||
char owning_scan_root[MAX_PATH]) {
|
||||
char resolved_source_path[MAX_PATH];
|
||||
const char *match_path = path;
|
||||
owning_scan_root[0] = '\0';
|
||||
|
||||
if (is_under_image_mount_base(path) &&
|
||||
resolve_image_source_from_mount_cache(path, resolved_source_path,
|
||||
sizeof(resolved_source_path))) {
|
||||
match_path = resolved_source_path;
|
||||
if (is_under_image_mount_base(path)) {
|
||||
bool resolved = resolve_image_source_from_mount_cache(
|
||||
path, resolved_source_path, sizeof(resolved_source_path));
|
||||
if (!resolved && title_id && title_id[0] != '\0') {
|
||||
resolved = read_mount_image_link(title_id, resolved_source_path,
|
||||
sizeof(resolved_source_path));
|
||||
}
|
||||
if (resolved)
|
||||
match_path = resolved_source_path;
|
||||
}
|
||||
|
||||
size_t best_match_len = 0;
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (!path_matches_root_or_child(match_path, scan_path))
|
||||
continue;
|
||||
|
||||
@@ -93,7 +109,8 @@ static bool ensure_game_cache_owning_scan_root(struct GameCache *entry) {
|
||||
if (entry->owning_scan_root[0] != '\0')
|
||||
return true;
|
||||
|
||||
return resolve_game_cache_owning_scan_root(entry->path, entry->owning_scan_root);
|
||||
return resolve_game_cache_owning_scan_root(
|
||||
entry->path, entry->title_id, entry->owning_scan_root);
|
||||
}
|
||||
|
||||
static void start_missing_timer(struct GameCache *entry, uint64_t now_us) {
|
||||
@@ -138,7 +155,8 @@ static void clear_game_cache_slot(int index, const char *reason) {
|
||||
void cache_game_entry(const char *path, const char *title_id,
|
||||
const char *title_name) {
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, owning_scan_root);
|
||||
(void)resolve_game_cache_owning_scan_root(path, title_id,
|
||||
owning_scan_root);
|
||||
|
||||
pthread_mutex_lock(&g_game_cache_mutex);
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
@@ -166,7 +184,7 @@ void cache_game_entry(const char *path, const char *title_id,
|
||||
}
|
||||
|
||||
static void prune_game_cache_entries(const char *root) {
|
||||
const bool auto_remove = runtime_config()->auto_remove_missing_games;
|
||||
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++) {
|
||||
@@ -213,6 +231,9 @@ void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
if (!path || !title_id || title_id[0] == '\0')
|
||||
return;
|
||||
|
||||
char owning_scan_root[MAX_PATH];
|
||||
(void)resolve_game_cache_owning_scan_root(path, title_id,
|
||||
owning_scan_root);
|
||||
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)
|
||||
@@ -220,8 +241,6 @@ void note_game_cache_source_seen(const char *path, const char *title_id,
|
||||
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)",
|
||||
@@ -303,8 +322,8 @@ static void reconcile_missing_game_cache(
|
||||
}
|
||||
|
||||
void reconcile_missing_app_db_games(void) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
if (!cfg->auto_remove_missing_games)
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
if (!cfg.auto_remove_missing_games)
|
||||
return;
|
||||
|
||||
struct AppDbTitleList auto_remove_titles = {0};
|
||||
@@ -315,9 +334,9 @@ void reconcile_missing_app_db_games(void) {
|
||||
|
||||
const uint64_t now_us = monotonic_time_us();
|
||||
const uint64_t delay_us =
|
||||
(uint64_t)cfg->auto_remove_missing_delay_seconds * 1000000ull;
|
||||
(uint64_t)cfg.auto_remove_missing_delay_seconds * 1000000ull;
|
||||
reconcile_missing_game_cache(&auto_remove_titles, now_us,
|
||||
cfg->auto_remove_games_with_dlc);
|
||||
cfg.auto_remove_games_with_dlc);
|
||||
|
||||
for (;;) {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
@@ -341,7 +360,7 @@ void reconcile_missing_app_db_games(void) {
|
||||
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))
|
||||
cfg.auto_remove_games_with_dlc))
|
||||
continue;
|
||||
|
||||
int res = sceAppInstUtilAppUnInstall(title_id);
|
||||
|
||||
@@ -300,11 +300,10 @@ static bool try_game_process_handoff(int kq, pid_t pid, const char *title_id,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep the ShellCore runtime across ExitSpawn/LoadExec, but deliberately
|
||||
// restart the per-process fakelib overlay. Holding the old nullfs overlay
|
||||
// can keep the old title sandbox busy and prevent ShellCore from removing
|
||||
// it. sm_fakelib_game_on_exec() also handles the NOTE_EXEC-before-NOTE_EXIT
|
||||
// race by cleaning any mount still owned by old_pid before mounting for pid.
|
||||
// Keep the ShellCore runtime across ExitSpawn/LoadExec. If NOTE_EXEC arrives
|
||||
// first, the replacement process can reuse the fakelib overlay in the same
|
||||
// sandbox; if NOTE_EXIT arrived first, handle_game_exit() already removed it
|
||||
// and sm_fakelib_game_on_exec() mounts it again for the replacement process.
|
||||
uint32_t effective_app_id = app_id != 0 ? app_id : old_app_id;
|
||||
sm_shellcore_service_bind_prepared_app(title_id, effective_app_id, pid);
|
||||
sm_fakelib_game_on_exec(pid, title_id, false);
|
||||
@@ -761,8 +760,8 @@ static void handle_game_exit(pid_t pid) {
|
||||
publish_active_game_pid(0);
|
||||
|
||||
// Match the 1.6 fakelib lifetime: release the overlay as soon as this PID
|
||||
// exits. The nullfs mount otherwise holds common/lib inside the old sandbox
|
||||
// busy and can prevent that sandbox from being removed during ExitSpawn.
|
||||
// exits. Keeping common/lib covered can leave the old sandbox busy and
|
||||
// prevent it from being removed during ExitSpawn.
|
||||
// ShellCore/KStuff ownership remains pending and may still be handed to a
|
||||
// replacement PID of the same title/app.
|
||||
sm_fakelib_game_on_exit(pid);
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@ 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) {
|
||||
strncmp(title_id, "CUSA", 4u) != 0 &&
|
||||
strncmp(title_id, "FAKE", 4u) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 4u; i < 9u; ++i) {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <png.h>
|
||||
|
||||
#include "sm_icon_thumb.h"
|
||||
#include "sm_paths.h"
|
||||
|
||||
#define SM_ICON_SOURCE_MAX_SIZE (4u * 1024u * 1024u)
|
||||
#define SM_ICON_SOURCE_MAX_DIMENSION 2048u
|
||||
#define SM_ICON_TEMP_MAX_AGE_SECONDS 300
|
||||
|
||||
static unsigned int g_icon_temp_serial;
|
||||
|
||||
static bool ensure_icon_cache_directory(void) {
|
||||
if (mkdir(LOG_DIR, 0777) != 0 && errno != EEXIST)
|
||||
return false;
|
||||
return mkdir(ICON_CACHE_DIR, 0777) == 0 || errno == EEXIST;
|
||||
}
|
||||
|
||||
static void resize_rgba(const unsigned char *source, uint32_t source_width,
|
||||
uint32_t source_height, unsigned char *target) {
|
||||
const uint32_t target_size = SM_ICON_THUMB_SIZE;
|
||||
for (uint32_t y = 0; y < target_size; ++y) {
|
||||
uint64_t source_y = target_size > 1u
|
||||
? ((uint64_t)y * (source_height - 1u) << 16u) /
|
||||
(target_size - 1u)
|
||||
: 0;
|
||||
uint32_t y0 = (uint32_t)(source_y >> 16u);
|
||||
uint32_t y1 = y0 + 1u < source_height ? y0 + 1u : y0;
|
||||
uint32_t wy = (uint32_t)(source_y & 0xffffu);
|
||||
for (uint32_t x = 0; x < target_size; ++x) {
|
||||
uint64_t source_x = target_size > 1u
|
||||
? ((uint64_t)x * (source_width - 1u) << 16u) /
|
||||
(target_size - 1u)
|
||||
: 0;
|
||||
uint32_t x0 = (uint32_t)(source_x >> 16u);
|
||||
uint32_t x1 = x0 + 1u < source_width ? x0 + 1u : x0;
|
||||
uint32_t wx = (uint32_t)(source_x & 0xffffu);
|
||||
const unsigned char *p00 = source + ((size_t)y0 * source_width + x0) * 4u;
|
||||
const unsigned char *p10 = source + ((size_t)y0 * source_width + x1) * 4u;
|
||||
const unsigned char *p01 = source + ((size_t)y1 * source_width + x0) * 4u;
|
||||
const unsigned char *p11 = source + ((size_t)y1 * source_width + x1) * 4u;
|
||||
unsigned char *pixel = target + ((size_t)y * target_size + x) * 4u;
|
||||
for (size_t channel = 0; channel < 4u; ++channel) {
|
||||
uint32_t top = (p00[channel] * (65536u - wx) +
|
||||
p10[channel] * wx + 32768u) >>
|
||||
16u;
|
||||
uint32_t bottom = (p01[channel] * (65536u - wx) +
|
||||
p11[channel] * wx + 32768u) >>
|
||||
16u;
|
||||
pixel[channel] = (unsigned char)((top * (65536u - wy) +
|
||||
bottom * wy + 32768u) >>
|
||||
16u);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void cleanup_stale_temp_files(const char *title_id) {
|
||||
DIR *directory = opendir(ICON_CACHE_DIR);
|
||||
if (!directory)
|
||||
return;
|
||||
|
||||
char prefix[MAX_TITLE_ID + 32u];
|
||||
int written = snprintf(prefix, sizeof(prefix), "%s-%u-", title_id,
|
||||
SM_ICON_THUMB_SIZE);
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix)) {
|
||||
(void)closedir(directory);
|
||||
return;
|
||||
}
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(directory)) != NULL) {
|
||||
if (strncmp(entry->d_name, prefix, (size_t)written) != 0)
|
||||
continue;
|
||||
char path[MAX_PATH];
|
||||
int path_written = snprintf(path, sizeof(path), "%s/%s", ICON_CACHE_DIR,
|
||||
entry->d_name);
|
||||
if (path_written < 0 || (size_t)path_written >= sizeof(path) ||
|
||||
!strstr(entry->d_name, ".tmp.")) {
|
||||
continue;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || now < st.st_mtime ||
|
||||
now - st.st_mtime < SM_ICON_TEMP_MAX_AGE_SECONDS) {
|
||||
continue;
|
||||
}
|
||||
(void)unlink(path);
|
||||
}
|
||||
(void)closedir(directory);
|
||||
}
|
||||
|
||||
static bool create_thumbnail(const char *source_path, const char *cache_path) {
|
||||
png_image image;
|
||||
memset(&image, 0, sizeof(image));
|
||||
image.version = PNG_IMAGE_VERSION;
|
||||
if (!png_image_begin_read_from_file(&image, source_path))
|
||||
return false;
|
||||
if (image.width == 0 || image.height == 0 ||
|
||||
image.width > SM_ICON_SOURCE_MAX_DIMENSION ||
|
||||
image.height > SM_ICON_SOURCE_MAX_DIMENSION) {
|
||||
png_image_free(&image);
|
||||
return false;
|
||||
}
|
||||
|
||||
image.format = PNG_FORMAT_RGBA;
|
||||
size_t source_size = PNG_IMAGE_SIZE(image);
|
||||
unsigned char *source = malloc(source_size);
|
||||
if (!source) {
|
||||
png_image_free(&image);
|
||||
return false;
|
||||
}
|
||||
if (!png_image_finish_read(&image, NULL, source, 0, NULL)) {
|
||||
free(source);
|
||||
png_image_free(&image);
|
||||
return false;
|
||||
}
|
||||
uint32_t source_width = image.width;
|
||||
uint32_t source_height = image.height;
|
||||
png_image_free(&image);
|
||||
|
||||
size_t target_size = (size_t)SM_ICON_THUMB_SIZE * SM_ICON_THUMB_SIZE * 4u;
|
||||
unsigned char *target = malloc(target_size);
|
||||
if (!target) {
|
||||
free(source);
|
||||
return false;
|
||||
}
|
||||
resize_rgba(source, source_width, source_height, target);
|
||||
free(source);
|
||||
|
||||
unsigned int serial = __sync_add_and_fetch(&g_icon_temp_serial, 1u);
|
||||
char temp_path[MAX_PATH];
|
||||
int written = snprintf(temp_path, sizeof(temp_path), "%s.tmp.%ld.%u",
|
||||
cache_path, (long)getpid(), serial);
|
||||
if (written < 0 || (size_t)written >= sizeof(temp_path)) {
|
||||
free(target);
|
||||
return false;
|
||||
}
|
||||
|
||||
png_image output;
|
||||
memset(&output, 0, sizeof(output));
|
||||
output.version = PNG_IMAGE_VERSION;
|
||||
output.width = SM_ICON_THUMB_SIZE;
|
||||
output.height = SM_ICON_THUMB_SIZE;
|
||||
output.format = PNG_FORMAT_RGBA;
|
||||
(void)unlink(temp_path);
|
||||
bool encoded = png_image_write_to_file(&output, temp_path, 0, target, 0,
|
||||
NULL) != 0;
|
||||
free(target);
|
||||
png_image_free(&output);
|
||||
if (!encoded) {
|
||||
(void)unlink(temp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
int fd = open(temp_path, O_RDONLY);
|
||||
bool synced = fd >= 0 && fsync(fd) == 0;
|
||||
if (fd >= 0)
|
||||
(void)close(fd);
|
||||
if (!synced || rename(temp_path, cache_path) != 0) {
|
||||
(void)unlink(temp_path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_icon_thumbnail_path(const char *title_id, const char *source_path,
|
||||
const struct stat *source_st,
|
||||
char out[MAX_PATH]) {
|
||||
if (!title_id || title_id[0] == '\0' || strpbrk(title_id, "/\\") ||
|
||||
!source_path || !source_st || !S_ISREG(source_st->st_mode) ||
|
||||
source_st->st_size <= 0 ||
|
||||
(uint64_t)source_st->st_size > SM_ICON_SOURCE_MAX_SIZE ||
|
||||
!ensure_icon_cache_directory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int written = snprintf(out, MAX_PATH, "%s/%s-%u-%lld-%lld.png",
|
||||
ICON_CACHE_DIR, title_id, SM_ICON_THUMB_SIZE,
|
||||
(long long)source_st->st_mtime,
|
||||
(long long)source_st->st_size);
|
||||
if (written < 0 || written >= MAX_PATH)
|
||||
return false;
|
||||
|
||||
struct stat cache_st;
|
||||
if (stat(out, &cache_st) == 0 && S_ISREG(cache_st.st_mode) &&
|
||||
cache_st.st_size > 0) {
|
||||
return true;
|
||||
}
|
||||
if (!create_thumbnail(source_path, out))
|
||||
return false;
|
||||
cleanup_stale_temp_files(title_id);
|
||||
return true;
|
||||
}
|
||||
+470
-325
File diff suppressed because it is too large.
Load diff
+376
-45
@@ -5,31 +5,51 @@
|
||||
#include "sm_limits.h"
|
||||
|
||||
struct ImageCache {
|
||||
uint64_t generation;
|
||||
char path[MAX_PATH];
|
||||
char mount_point[MAX_PATH];
|
||||
int unit_id;
|
||||
attach_backend_t backend;
|
||||
attached_device_state_t state;
|
||||
attached_unit_detach_state_t detach_state;
|
||||
bool detach_attempt_active;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
static struct ImageCache g_image_cache[MAX_IMAGE_MOUNTS];
|
||||
static pthread_mutex_t g_image_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static uint64_t g_image_cache_generation;
|
||||
|
||||
enum {
|
||||
IMAGE_CACHE_INDEX_CONFLICT = -2,
|
||||
};
|
||||
|
||||
static uint64_t next_cache_generation(void) {
|
||||
++g_image_cache_generation;
|
||||
if (g_image_cache_generation == 0)
|
||||
++g_image_cache_generation;
|
||||
return g_image_cache_generation;
|
||||
}
|
||||
|
||||
static bool attach_backend_is_valid(attach_backend_t backend) {
|
||||
return backend == ATTACH_BACKEND_MD || backend == ATTACH_BACKEND_LVD;
|
||||
}
|
||||
|
||||
static bool device_identity_matches(
|
||||
const attached_unit_detach_state_t *left,
|
||||
const attached_unit_detach_state_t *right) {
|
||||
if (!left->node_identity_valid || !right->node_identity_valid)
|
||||
return true;
|
||||
return false;
|
||||
return left->node_device == right->node_device &&
|
||||
left->node_inode == right->node_inode &&
|
||||
left->node_rdev == right->node_rdev;
|
||||
}
|
||||
|
||||
static void capture_attached_unit_identity(struct ImageCache *entry) {
|
||||
memset(&entry->detach_state, 0, sizeof(entry->detach_state));
|
||||
if (entry->backend == ATTACH_BACKEND_NONE || entry->unit_id < 0)
|
||||
return;
|
||||
static bool refresh_attached_unit_identity(struct ImageCache *entry) {
|
||||
if (!attach_backend_is_valid(entry->backend) || entry->unit_id < 0)
|
||||
return false;
|
||||
if (entry->detach_state.node_identity_valid)
|
||||
return true;
|
||||
|
||||
const char *prefix =
|
||||
entry->backend == ATTACH_BACKEND_MD ? "/dev/md" : "/dev/lvd";
|
||||
@@ -38,45 +58,163 @@ static void capture_attached_unit_identity(struct ImageCache *entry) {
|
||||
|
||||
struct stat st;
|
||||
if (stat(devname, &st) != 0)
|
||||
return;
|
||||
return false;
|
||||
|
||||
entry->detach_state.node_identity_valid = true;
|
||||
entry->detach_state.node_device = (uint64_t)st.st_dev;
|
||||
entry->detach_state.node_inode = (uint64_t)st.st_ino;
|
||||
entry->detach_state.node_rdev = (uint64_t)st.st_rdev;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void reset_attached_unit_identity(struct ImageCache *entry) {
|
||||
memset(&entry->detach_state, 0, sizeof(entry->detach_state));
|
||||
(void)refresh_attached_unit_identity(entry);
|
||||
}
|
||||
|
||||
static void copy_cache_entry(int index, image_cache_entry_t *entry_out) {
|
||||
memset(entry_out, 0, sizeof(*entry_out));
|
||||
entry_out->generation = g_image_cache[index].generation;
|
||||
(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;
|
||||
entry_out->state = g_image_cache[index].state;
|
||||
entry_out->detach_state = g_image_cache[index].detach_state;
|
||||
}
|
||||
|
||||
static int find_cache_index(const char *path, const char *mount_point) {
|
||||
static int find_source_path_index(const char *path) {
|
||||
if (!path)
|
||||
return -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (mount_point && strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
return k;
|
||||
}
|
||||
if (path && strcmp(g_image_cache[k].path, path) == 0)
|
||||
if (strcmp(g_image_cache[k].path, path) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path, const char *mount_point) {
|
||||
int entry_index = find_cache_index(path, mount_point);
|
||||
static int find_mount_point_index(const char *mount_point) {
|
||||
if (!mount_point)
|
||||
return -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; k++) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_image_cache[k].mount_point, mount_point) == 0)
|
||||
return k;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool cache_entry_is_metadata_only(const struct ImageCache *entry) {
|
||||
return entry->state == ATTACHED_DEVICE_RELEASED && entry->unit_id < 0 &&
|
||||
entry->backend == ATTACH_BACKEND_NONE &&
|
||||
!entry->detach_attempt_active;
|
||||
}
|
||||
|
||||
static bool find_mapping_indices(const char *path, const char *mount_point,
|
||||
int *path_index_out,
|
||||
int *mount_index_out) {
|
||||
*path_index_out = -1;
|
||||
*mount_index_out = -1;
|
||||
|
||||
for (int k = 0; k < MAX_IMAGE_MOUNTS; ++k) {
|
||||
if (!g_image_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(g_image_cache[k].path, path) == 0) {
|
||||
if (*path_index_out >= 0) {
|
||||
errno = EEXIST;
|
||||
return false;
|
||||
}
|
||||
*path_index_out = k;
|
||||
}
|
||||
if (strcmp(g_image_cache[k].mount_point, mount_point) == 0) {
|
||||
if (*mount_index_out >= 0) {
|
||||
errno = EEXIST;
|
||||
return false;
|
||||
}
|
||||
*mount_index_out = k;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int resolve_or_merge_mapping_index(const char *path,
|
||||
const char *mount_point,
|
||||
bool *merged_out) {
|
||||
int path_index = -1;
|
||||
int mount_index = -1;
|
||||
*merged_out = false;
|
||||
if (!find_mapping_indices(path, mount_point, &path_index, &mount_index))
|
||||
return IMAGE_CACHE_INDEX_CONFLICT;
|
||||
|
||||
if (path_index < 0 || mount_index < 0 || path_index == mount_index)
|
||||
return path_index >= 0 ? path_index : mount_index;
|
||||
|
||||
struct ImageCache *path_entry = &g_image_cache[path_index];
|
||||
struct ImageCache *mount_entry = &g_image_cache[mount_index];
|
||||
if (!cache_entry_is_metadata_only(path_entry) ||
|
||||
!cache_entry_is_metadata_only(mount_entry)) {
|
||||
errno = EBUSY;
|
||||
return IMAGE_CACHE_INDEX_CONFLICT;
|
||||
}
|
||||
|
||||
// Both IDs are metadata-only. The requested pair supersedes their stale
|
||||
// half-mappings, so merge them into the source-path owner.
|
||||
memset(mount_entry, 0, sizeof(*mount_entry));
|
||||
*merged_out = true;
|
||||
return path_index;
|
||||
}
|
||||
|
||||
static struct ImageCache *find_attachment_record(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].unit_id != unit_id ||
|
||||
g_image_cache[index].backend != backend) {
|
||||
return NULL;
|
||||
}
|
||||
return &g_image_cache[index];
|
||||
}
|
||||
|
||||
static int upsert_image_source_mapping(const char *path,
|
||||
const char *mount_point) {
|
||||
if (!path || path[0] == '\0' || !mount_point || mount_point[0] == '\0') {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (strnlen(path, MAX_PATH) >= MAX_PATH ||
|
||||
strnlen(mount_point, MAX_PATH) >= MAX_PATH) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool merged = false;
|
||||
int entry_index =
|
||||
resolve_or_merge_mapping_index(path, mount_point, &merged);
|
||||
if (entry_index == IMAGE_CACHE_INDEX_CONFLICT)
|
||||
return -1;
|
||||
if (entry_index >= 0) {
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
bool mapping_changed = merged || strcmp(entry->path, path) != 0 ||
|
||||
strcmp(entry->mount_point, mount_point) != 0;
|
||||
if (!cache_entry_is_metadata_only(entry) && mapping_changed) {
|
||||
errno = EBUSY;
|
||||
return -1;
|
||||
}
|
||||
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
||||
(void)strlcpy(entry->mount_point, mount_point, sizeof(entry->mount_point));
|
||||
if (mapping_changed)
|
||||
entry->generation = next_cache_generation();
|
||||
return entry_index;
|
||||
}
|
||||
|
||||
@@ -87,35 +225,132 @@ static int upsert_image_source_mapping(const char *path, const char *mount_point
|
||||
sizeof(g_image_cache[k].mount_point));
|
||||
g_image_cache[k].unit_id = -1;
|
||||
g_image_cache[k].backend = ATTACH_BACKEND_NONE;
|
||||
g_image_cache[k].state = ATTACHED_DEVICE_RELEASED;
|
||||
g_image_cache[k].generation = next_cache_generation();
|
||||
g_image_cache[k].valid = true;
|
||||
return k;
|
||||
}
|
||||
}
|
||||
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool begin_image_attachment(const char *path, const char *mount_point,
|
||||
uint64_t *generation_out) {
|
||||
if (!path || !mount_point || !generation_out) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
if (!cache_entry_is_metadata_only(&g_image_cache[entry_index])) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EBUSY;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->generation = next_cache_generation();
|
||||
entry->unit_id = -1;
|
||||
entry->backend = ATTACH_BACKEND_NONE;
|
||||
entry->state = ATTACHED_DEVICE_ATTACHING;
|
||||
memset(&entry->detach_state, 0, sizeof(entry->detach_state));
|
||||
entry->detach_attempt_active = false;
|
||||
*generation_out = entry->generation;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cancel_image_attachment(const char *path, uint64_t generation) {
|
||||
if (!path || generation == 0)
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING ||
|
||||
g_image_cache[index].unit_id >= 0 ||
|
||||
g_image_cache[index].backend != ATTACH_BACKEND_NONE) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool publish_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING ||
|
||||
g_image_cache[index].unit_id >= 0 ||
|
||||
g_image_cache[index].backend != ATTACH_BACKEND_NONE) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[index];
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
(void)refresh_attached_unit_identity(entry);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool complete_image_attachment(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0 || g_image_cache[index].generation != generation ||
|
||||
g_image_cache[index].unit_id != unit_id ||
|
||||
g_image_cache[index].backend != backend ||
|
||||
g_image_cache[index].state != ATTACHED_DEVICE_ATTACHING) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_image_cache[index].state = ATTACHED_DEVICE_MOUNTED;
|
||||
(void)refresh_attached_unit_identity(&g_image_cache[index]);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
if (!path || !mount_point || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = upsert_image_source_mapping(path, mount_point);
|
||||
if (entry_index < 0) {
|
||||
@@ -124,9 +359,12 @@ bool cache_image_mount(const char *path, const char *mount_point, int unit_id,
|
||||
}
|
||||
|
||||
struct ImageCache *entry = &g_image_cache[entry_index];
|
||||
entry->generation = next_cache_generation();
|
||||
entry->unit_id = unit_id;
|
||||
entry->backend = backend;
|
||||
capture_attached_unit_identity(entry);
|
||||
entry->state = ATTACHED_DEVICE_MOUNTED;
|
||||
entry->detach_attempt_active = false;
|
||||
reset_attached_unit_identity(entry);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -149,7 +387,7 @@ bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
return false;
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_cache_index(path, NULL);
|
||||
int index = find_source_path_index(path);
|
||||
if (index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
@@ -161,43 +399,136 @@ bool find_image_cache_entry(const char *path, image_cache_entry_t *entry_out,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update_image_cache_detach_state(
|
||||
const char *path, int unit_id, attach_backend_t backend,
|
||||
const attached_unit_detach_state_t *detach_state) {
|
||||
if (!path || !detach_state)
|
||||
bool mark_image_cache_detach_requested(const char *path, uint64_t generation,
|
||||
int unit_id,
|
||||
attach_backend_t backend) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int index = find_cache_index(path, NULL);
|
||||
if (index < 0 ||
|
||||
g_image_cache[index].unit_id != unit_id ||
|
||||
g_image_cache[index].backend != backend ||
|
||||
!device_identity_matches(&g_image_cache[index].detach_state,
|
||||
detach_state)) {
|
||||
struct ImageCache *entry = find_attachment_record(
|
||||
path, generation, unit_id, backend);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
if (!refresh_attached_unit_identity(entry)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_image_cache[index].detach_state = *detach_state;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void invalidate_image_cache_entry(int index) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return;
|
||||
bool claim_image_cache_detach(const char *path, uint64_t generation,
|
||||
int unit_id, attach_backend_t backend,
|
||||
image_cache_entry_t *entry_out) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend) || !entry_out) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
struct ImageCache *entry =
|
||||
find_attachment_record(path, generation, unit_id, backend);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
if (entry->detach_attempt_active) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
if (!refresh_attached_unit_identity(entry)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
entry->detach_attempt_active = true;
|
||||
copy_cache_entry((int)(entry - g_image_cache), entry_out);
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool finish_image_cache_detach_attempt(
|
||||
const char *path, uint64_t generation, int unit_id,
|
||||
attach_backend_t backend,
|
||||
const attached_unit_detach_state_t *detach_state) {
|
||||
if (!path || generation == 0 || unit_id < 0 ||
|
||||
!attach_backend_is_valid(backend) || !detach_state ||
|
||||
!detach_state->node_identity_valid) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
struct ImageCache *entry = find_attachment_record(
|
||||
path, generation, unit_id, backend);
|
||||
if (!entry || !entry->detach_attempt_active ||
|
||||
!device_identity_matches(&entry->detach_state, detach_state)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->detach_state = *detach_state;
|
||||
entry->state = ATTACHED_DEVICE_DETACH_REQUESTED;
|
||||
entry->detach_attempt_active = false;
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool invalidate_image_cache_entry(int index,
|
||||
const image_cache_entry_t *expected) {
|
||||
if (!expected) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
if (index < 0 || index >= MAX_IMAGE_MOUNTS ||
|
||||
!g_image_cache[index].valid) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
const struct ImageCache *entry = &g_image_cache[index];
|
||||
if (entry->generation != expected->generation ||
|
||||
strcmp(entry->path, expected->path) != 0 ||
|
||||
strcmp(entry->mount_point, expected->mount_point) != 0 ||
|
||||
entry->unit_id != expected->unit_id ||
|
||||
entry->backend != expected->backend ||
|
||||
entry->state != expected->state || entry->detach_attempt_active ||
|
||||
entry->detach_state.requested != expected->detach_state.requested ||
|
||||
!device_identity_matches(&entry->detach_state,
|
||||
&expected->detach_state)) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
errno = ESTALE;
|
||||
return false;
|
||||
}
|
||||
g_image_cache[index].state = ATTACHED_DEVICE_RELEASED;
|
||||
memset(&g_image_cache[index], 0, sizeof(g_image_cache[index]));
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool resolve_device_from_mount_cache(const char *mount_point,
|
||||
attach_backend_t *backend_out,
|
||||
int *unit_out) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
int entry_index = find_mount_point_index(mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
@@ -217,7 +548,7 @@ bool resolve_image_source_from_mount_cache(const char *mount_point,
|
||||
char *path_out,
|
||||
size_t path_out_size) {
|
||||
pthread_mutex_lock(&g_image_cache_mutex);
|
||||
int entry_index = find_cache_index(NULL, mount_point);
|
||||
int entry_index = find_mount_point_index(mount_point);
|
||||
if (entry_index < 0) {
|
||||
pthread_mutex_unlock(&g_image_cache_mutex);
|
||||
return false;
|
||||
|
||||
+26
-2
@@ -355,10 +355,12 @@ static bool cached_titles_ready(int entry_index,
|
||||
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) {
|
||||
sm_image_index_title_visitor_t visitor, const void *visitor_ctx) {
|
||||
if (!path || !st || !visitor)
|
||||
return false;
|
||||
|
||||
char (*titles)[MAX_TITLE_ID] = NULL;
|
||||
size_t title_count = 0;
|
||||
pthread_mutex_lock(&g_image_index_mutex);
|
||||
load_index();
|
||||
int entry_index = find_entry(path);
|
||||
@@ -370,10 +372,32 @@ bool sm_image_index_visit_ready_titles(
|
||||
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);
|
||||
title_count++;
|
||||
}
|
||||
if (title_count > 0) {
|
||||
titles = calloc(title_count, sizeof(*titles));
|
||||
if (!titles) {
|
||||
ready = false;
|
||||
} else {
|
||||
size_t copied = 0;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
(void)strlcpy(titles[copied++], title->title_id, MAX_TITLE_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_image_index_mutex);
|
||||
|
||||
if (ready) {
|
||||
for (size_t i = 0; i < title_count; ++i)
|
||||
visitor(titles[i], visitor_ctx);
|
||||
}
|
||||
free(titles);
|
||||
return ready;
|
||||
}
|
||||
|
||||
|
||||
+25
-13
@@ -62,6 +62,24 @@ static app_install_title_dir_fn_t resolve_app_install_title_dir(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sm_install_register_title_dir(const char *title_id,
|
||||
const char *install_dir,
|
||||
int *result_out) {
|
||||
if (!title_id || !install_dir || !result_out)
|
||||
return false;
|
||||
|
||||
if (sm_shellcore_install_bridge_enabled())
|
||||
return sm_shellcore_install_title_dir(title_id, install_dir, result_out);
|
||||
|
||||
app_install_title_dir_fn_t install_title_dir =
|
||||
resolve_app_install_title_dir();
|
||||
if (!install_title_dir)
|
||||
return false;
|
||||
|
||||
*result_out = install_title_dir(title_id, install_dir, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_appmeta_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
@@ -332,26 +350,20 @@ static bool mount_and_install(const char *src_path, const char *title_id,
|
||||
|
||||
// REGISTER
|
||||
int res = 0;
|
||||
if (sm_shellcore_install_bridge_enabled()) {
|
||||
if (!sm_shellcore_install_title_dir(title_id, APP_BASE "/", &res)) {
|
||||
bool bridge_enabled = sm_shellcore_install_bridge_enabled();
|
||||
if (!sm_install_register_title_dir(title_id, APP_BASE "/", &res)) {
|
||||
if (bridge_enabled) {
|
||||
log_debug(" [REG] internal AppInstallTitleDir bridge unavailable");
|
||||
notify_system_l10n(SM_L10N_REGISTER_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) {
|
||||
} else {
|
||||
log_debug(" [REG] sceAppInstUtilAppInstallTitleDir unavailable");
|
||||
notify_system_l10n(SM_L10N_REGISTER_FAILED_UNAVAILABLE, title_name,
|
||||
title_id);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
res = app_install_title_dir_fn(title_id, APP_BASE "/", NULL);
|
||||
return false;
|
||||
}
|
||||
mark_register_attempted(title_id);
|
||||
if (res == 0) {
|
||||
invalidate_app_db_title_cache();
|
||||
clear_register_attempts(title_id);
|
||||
@@ -391,7 +403,7 @@ void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
const scan_candidate_t *c = &candidates[i];
|
||||
bool has_src_snd0 = false;
|
||||
bool use_app_install_all =
|
||||
runtime_config()->app_install_all_enabled && !c->in_app_db;
|
||||
runtime_config().app_install_all_enabled && !c->in_app_db;
|
||||
if (c->installed) {
|
||||
log_debug(" [ACTION] Remounting: %s", c->title_name);
|
||||
} else {
|
||||
|
||||
+22
-9
@@ -1,5 +1,7 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
|
||||
#include "sm_install_queue.h"
|
||||
#include "sm_types.h"
|
||||
#include "sm_appdb.h"
|
||||
@@ -37,7 +39,7 @@ typedef struct {
|
||||
static pending_install_entry_t g_pending_installs[MAX_PENDING];
|
||||
static uint64_t g_pending_install_poll_due_us = 0;
|
||||
static uint64_t g_queued_install_submit_due_us = 0;
|
||||
static int g_tracked_install_count = 0;
|
||||
static atomic_int g_tracked_install_count = 0;
|
||||
static int g_submitted_install_count = 0;
|
||||
static bool g_queued_install_batch_announced = false;
|
||||
static bool g_queued_install_submit_failure_notified = false;
|
||||
@@ -47,11 +49,14 @@ static uint8_t note_pending_install_failure(const pending_install_entry_t *entry
|
||||
static void drop_queued_install_entry(pending_install_entry_t *entry);
|
||||
|
||||
static bool install_queue_active(void) {
|
||||
return runtime_config()->app_install_all_enabled || g_tracked_install_count > 0;
|
||||
return runtime_config().app_install_all_enabled ||
|
||||
atomic_load_explicit(&g_tracked_install_count,
|
||||
memory_order_acquire) > 0;
|
||||
}
|
||||
|
||||
bool sm_install_has_pending_work(void) {
|
||||
return g_tracked_install_count > 0;
|
||||
return atomic_load_explicit(&g_tracked_install_count,
|
||||
memory_order_acquire) > 0;
|
||||
}
|
||||
|
||||
static pending_install_entry_t *find_pending_install_entry(
|
||||
@@ -135,12 +140,17 @@ static void clear_pending_install_entry(pending_install_entry_t *entry) {
|
||||
return;
|
||||
if (entry->state == INSTALL_TRACK_SUBMITTED && g_submitted_install_count > 0)
|
||||
g_submitted_install_count--;
|
||||
if (entry->state != INSTALL_TRACK_NONE && g_tracked_install_count > 0)
|
||||
g_tracked_install_count--;
|
||||
if (entry->state != INSTALL_TRACK_NONE &&
|
||||
atomic_load_explicit(&g_tracked_install_count,
|
||||
memory_order_relaxed) > 0) {
|
||||
atomic_fetch_sub_explicit(&g_tracked_install_count, 1,
|
||||
memory_order_release);
|
||||
}
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
if (g_submitted_install_count <= 0)
|
||||
g_pending_install_poll_due_us = 0;
|
||||
if (g_tracked_install_count <= 0) {
|
||||
if (atomic_load_explicit(&g_tracked_install_count,
|
||||
memory_order_acquire) <= 0) {
|
||||
g_queued_install_submit_due_us = 0;
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
@@ -182,7 +192,7 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!runtime_config()->app_install_all_enabled)
|
||||
if (!runtime_config().app_install_all_enabled)
|
||||
return false;
|
||||
|
||||
bool was_queue_empty = (count_queued_installs() <= 0);
|
||||
@@ -209,7 +219,8 @@ bool sm_install_queue_candidate(const scan_candidate_t *candidate,
|
||||
entry->has_src_snd0 = has_src_snd0;
|
||||
entry->state = INSTALL_TRACK_QUEUED;
|
||||
if (previous_state == INSTALL_TRACK_NONE)
|
||||
g_tracked_install_count++;
|
||||
atomic_fetch_add_explicit(&g_tracked_install_count, 1,
|
||||
memory_order_release);
|
||||
if (was_queue_empty) {
|
||||
g_queued_install_batch_announced = false;
|
||||
g_queued_install_submit_failure_notified = false;
|
||||
@@ -347,7 +358,9 @@ static void log_batch_submit_retry_limit(const pending_install_entry_t *entry) {
|
||||
}
|
||||
|
||||
static int count_queued_installs(void) {
|
||||
int queued_count = g_tracked_install_count - g_submitted_install_count;
|
||||
int queued_count =
|
||||
atomic_load_explicit(&g_tracked_install_count, memory_order_acquire) -
|
||||
g_submitted_install_count;
|
||||
return queued_count > 0 ? queued_count : 0;
|
||||
}
|
||||
|
||||
|
||||
+82
-12
@@ -34,6 +34,7 @@ typedef struct {
|
||||
intptr_t sysentvec_ps5;
|
||||
intptr_t sysentvec_ps4;
|
||||
bool probe_available;
|
||||
bool loaded_observed;
|
||||
bool supported;
|
||||
bool restore_on_empty;
|
||||
bool sleep_pause_restore;
|
||||
@@ -56,6 +57,44 @@ static uint32_t get_pause_delay_seconds_for_title(const char *title_id,
|
||||
bool *autopause_delay_valid_out);
|
||||
static void restore_kstuff_if_needed(const char *reason);
|
||||
|
||||
bool sm_kstuff_is_loaded(void) {
|
||||
register uint64_t r10 __asm__("r10") = 0;
|
||||
register uint64_t r8 __asm__("r8") = 0;
|
||||
register uint64_t r9 __asm__("r9") = 0;
|
||||
uint64_t result;
|
||||
unsigned char is_error;
|
||||
|
||||
/* Keep this probe identical to ps5-kstuff's KEKCALL_CHECK contract. */
|
||||
__asm__ __volatile__("syscall"
|
||||
: "=a"(result), "=@ccc"(is_error), "+r"(r10),
|
||||
"+r"(r8), "+r"(r9)
|
||||
: "a"(SM_KSTUFF_KEKCALL_CHECK), "D"(UINT64_C(0)),
|
||||
"S"(UINT64_C(0)), "d"(UINT64_C(0))
|
||||
: "rcx", "r11", "memory");
|
||||
|
||||
return !is_error && result == 0;
|
||||
}
|
||||
|
||||
bool sm_kstuff_remote_mprotect(pid_t pid, uintptr_t address, size_t size,
|
||||
int protection) {
|
||||
uint64_t args[6] = {address, size, (uint64_t)protection, 0, 0, 0};
|
||||
register uint64_t r10 __asm__("r10") = 0;
|
||||
register uint64_t r8 __asm__("r8") = 0;
|
||||
register uint64_t r9 __asm__("r9") = 0;
|
||||
uint64_t result;
|
||||
unsigned char is_error;
|
||||
|
||||
__asm__ __volatile__("syscall"
|
||||
: "=a"(result), "=@ccc"(is_error), "+r"(r10),
|
||||
"+r"(r8), "+r"(r9)
|
||||
: "a"(SM_KSTUFF_KEKCALL_REMOTE_SYSCALL),
|
||||
"D"((uint64_t)(uint32_t)pid),
|
||||
"S"((uint64_t)SYS_mprotect), "d"((uint64_t)args)
|
||||
: "rcx", "r11", "memory");
|
||||
|
||||
return !is_error && result == 0;
|
||||
}
|
||||
|
||||
static char *trim_ascii_inplace(char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
|
||||
s++;
|
||||
@@ -190,10 +229,10 @@ static bool resolve_title_pause_source(const char *title_id,
|
||||
static uint32_t get_configured_pause_delay_seconds(const char *title_id,
|
||||
bool image_backed,
|
||||
bool *override_applied_out) {
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
uint32_t delay_seconds =
|
||||
image_backed ? cfg->kstuff_pause_delay_image_seconds
|
||||
: cfg->kstuff_pause_delay_direct_seconds;
|
||||
image_backed ? cfg.kstuff_pause_delay_image_seconds
|
||||
: cfg.kstuff_pause_delay_direct_seconds;
|
||||
uint32_t override_delay_seconds = 0;
|
||||
if (get_kstuff_pause_delay_override_for_title(title_id,
|
||||
&override_delay_seconds)) {
|
||||
@@ -220,17 +259,34 @@ static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out)
|
||||
case 0x1100000:
|
||||
case 0x1110000:
|
||||
case 0x1120000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0x1c2a5c8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0x1c2a720;
|
||||
return true;
|
||||
|
||||
case 0x1130000:
|
||||
case 0x1140000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0x1c2a628;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0x1c2a780;
|
||||
return true;
|
||||
|
||||
case 0x2000000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0x1c50298;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0x1c50410;
|
||||
return true;
|
||||
|
||||
case 0x2200000:
|
||||
case 0x2250000:
|
||||
case 0x2260000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0x1c502d8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0x1c50450;
|
||||
return true;
|
||||
|
||||
case 0x2300000:
|
||||
case 0x2500000:
|
||||
case 0x2700000:
|
||||
// Older jailbreak chains usually rely on Byepervisor instead of kstuff.
|
||||
return false;
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0x1c50458;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0x1c505d0;
|
||||
return true;
|
||||
|
||||
case 0x3000000:
|
||||
case 0x3100000:
|
||||
@@ -328,8 +384,15 @@ static bool resolve_kstuff_sysentvec_addrs(intptr_t *ps5_out, intptr_t *ps4_out)
|
||||
|
||||
case 0x13000000:
|
||||
case 0x13200000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8a8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda20;
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8b8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda30;
|
||||
return true;
|
||||
|
||||
case 0x13400000:
|
||||
case 0x13420000:
|
||||
case 0x13600000:
|
||||
*ps5_out = KERNEL_ADDRESS_DATA_BASE + 0xddd8f8;
|
||||
*ps4_out = KERNEL_ADDRESS_DATA_BASE + 0xddda70;
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -376,6 +439,13 @@ static bool refresh_kstuff_support_state(void) {
|
||||
g_kstuff.supported = false;
|
||||
return false;
|
||||
}
|
||||
if (!g_kstuff.loaded_observed) {
|
||||
if (!sm_kstuff_is_loaded()) {
|
||||
g_kstuff.supported = false;
|
||||
return false;
|
||||
}
|
||||
g_kstuff.loaded_observed = true;
|
||||
}
|
||||
|
||||
uint16_t ps5_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps5);
|
||||
uint16_t ps4_toggle = read_kstuff_sysentvec_toggle(g_kstuff.sysentvec_ps4);
|
||||
@@ -410,10 +480,10 @@ static void finish_tracked_game_clear(const char *reason) {
|
||||
}
|
||||
|
||||
static void apply_kstuff_config_reload(void) {
|
||||
if (!runtime_config()->kstuff_crash_detection_enabled)
|
||||
if (!runtime_config().kstuff_crash_detection_enabled)
|
||||
sm_mdbg_game_shutdown();
|
||||
|
||||
if (!runtime_config()->kstuff_game_auto_toggle) {
|
||||
if (!runtime_config().kstuff_game_auto_toggle) {
|
||||
sm_kstuff_game_shutdown();
|
||||
return;
|
||||
}
|
||||
@@ -725,7 +795,7 @@ bool sm_kstuff_set_enabled(bool enabled, bool notify_user) {
|
||||
}
|
||||
|
||||
bool sm_kstuff_game_feature_enabled(void) {
|
||||
return runtime_config()->kstuff_game_auto_toggle && g_kstuff.probe_available;
|
||||
return runtime_config().kstuff_game_auto_toggle && g_kstuff.probe_available;
|
||||
}
|
||||
|
||||
void sm_kstuff_game_on_exec(pid_t pid, const char *title_id, uint32_t app_id,
|
||||
@@ -922,8 +992,8 @@ void sm_kstuff_init(void) {
|
||||
|
||||
log_debug(" [KSTUFF] runtime control ready (image_delay=%u "
|
||||
"direct_delay=%u)",
|
||||
runtime_config()->kstuff_pause_delay_image_seconds,
|
||||
runtime_config()->kstuff_pause_delay_direct_seconds);
|
||||
runtime_config().kstuff_pause_delay_image_seconds,
|
||||
runtime_config().kstuff_pause_delay_direct_seconds);
|
||||
}
|
||||
|
||||
void sm_kstuff_shutdown(void) {
|
||||
|
||||
+12
-4
@@ -8,6 +8,7 @@
|
||||
|
||||
#define SCE_SYSTEM_SERVICE_PARAM_ID_LANG 1
|
||||
#define SM_LANGUAGE_EN_US 1
|
||||
#define SM_SYSTEM_LANGUAGE_COUNT 31
|
||||
#define SM_LANGUAGE_UNINITIALIZED (-2)
|
||||
|
||||
#include "lang/ar_sa.inc"
|
||||
@@ -37,6 +38,7 @@
|
||||
#include "lang/sv_se.inc"
|
||||
#include "lang/th_th.inc"
|
||||
#include "lang/tr_tr.inc"
|
||||
#include "lang/uk_ua.inc"
|
||||
#include "lang/vi_vn.inc"
|
||||
#include "lang/zh_cn.inc"
|
||||
#include "lang/zh_tw.inc"
|
||||
@@ -64,11 +66,13 @@ static const sm_region_t g_regions[] = {
|
||||
{"hu", "hu", "hu-HU", g_hu_hu}, {"gr", "el", "el-GR", g_el_gr},
|
||||
{"ro", "ro", "ro-RO", g_ro_ro}, {"th", "th", "th-TH", g_th_th},
|
||||
{"vn", "vi", "vi-VN", g_vi_vn}, {"id", "id", "id-ID", g_id_id},
|
||||
{"ua", "uk", "uk-UA", g_uk_ua},
|
||||
};
|
||||
|
||||
#define SM_REGION_COUNT (sizeof(g_regions) / sizeof(g_regions[0]))
|
||||
|
||||
_Static_assert(SM_REGION_COUNT == 30, "unexpected SCE language count");
|
||||
_Static_assert(SM_REGION_COUNT == SM_SYSTEM_LANGUAGE_COUNT,
|
||||
"unexpected language count");
|
||||
|
||||
static atomic_int g_active_lang = ATOMIC_VAR_INIT(SM_LANGUAGE_UNINITIALIZED);
|
||||
|
||||
@@ -76,6 +80,10 @@ static bool is_valid_language_id(int32_t language_id) {
|
||||
return language_id >= 0 && language_id < (int32_t)SM_REGION_COUNT;
|
||||
}
|
||||
|
||||
static bool is_valid_system_language_id(int32_t language_id) {
|
||||
return language_id >= 0 && language_id < SM_SYSTEM_LANGUAGE_COUNT;
|
||||
}
|
||||
|
||||
static bool language_code_matches(const char *value, const char *code) {
|
||||
char normalized[16];
|
||||
size_t len = strlen(code);
|
||||
@@ -124,14 +132,14 @@ const char *sm_l10n_language_name(int32_t language_id) {
|
||||
|
||||
void sm_l10n_init(void) {
|
||||
int32_t sys_lang = -1;
|
||||
const runtime_config_t *cfg = runtime_config();
|
||||
int32_t active_lang = cfg->language_id;
|
||||
const runtime_config_t cfg = runtime_config();
|
||||
int32_t active_lang = cfg.language_id;
|
||||
bool auto_language = (active_lang == SM_LANGUAGE_AUTO);
|
||||
|
||||
if (auto_language &&
|
||||
sceSystemServiceParamGetInt(SCE_SYSTEM_SERVICE_PARAM_ID_LANG,
|
||||
&sys_lang) == 0 &&
|
||||
is_valid_language_id(sys_lang)) {
|
||||
is_valid_system_language_id(sys_lang)) {
|
||||
active_lang = sys_lang;
|
||||
} else if (auto_language) {
|
||||
active_lang = SM_LANGUAGE_EN_US;
|
||||
|
||||
+4
-4
@@ -291,7 +291,7 @@ static void log_to_file_locked(const char *fmt, va_list args) {
|
||||
}
|
||||
|
||||
void log_debug(const char *fmt, ...) {
|
||||
if (runtime_config()->debug_enabled == false)
|
||||
if (runtime_config().debug_enabled == false)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
@@ -323,7 +323,7 @@ void notify_system_rich_l10n(bool allow_in_quiet_mode, sm_l10n_key_t key, ...) {
|
||||
char message[3075];
|
||||
va_list args;
|
||||
|
||||
if (runtime_config()->quiet_mode && !allow_in_quiet_mode)
|
||||
if (runtime_config().quiet_mode && !allow_in_quiet_mode)
|
||||
return;
|
||||
|
||||
va_start(args, key);
|
||||
@@ -351,7 +351,7 @@ void notify_game_installed_rich(const char *title_id) {
|
||||
}
|
||||
|
||||
void notify_system_info(const char *fmt, ...) {
|
||||
if (runtime_config()->quiet_mode)
|
||||
if (runtime_config().quiet_mode)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
@@ -361,7 +361,7 @@ void notify_system_info(const char *fmt, ...) {
|
||||
}
|
||||
|
||||
void notify_system_info_l10n(sm_l10n_key_t key, ...) {
|
||||
if (runtime_config()->quiet_mode)
|
||||
if (runtime_config().quiet_mode)
|
||||
return;
|
||||
|
||||
va_list args;
|
||||
|
||||
+152
-1
@@ -1,5 +1,7 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "sm_manual.h"
|
||||
|
||||
#include "sm_appdb.h"
|
||||
@@ -26,6 +28,7 @@ typedef struct {
|
||||
} manual_status_entry_t;
|
||||
|
||||
static manual_status_entry_t g_manual_status_entries[MANUAL_STATUS_CAPACITY];
|
||||
static pthread_mutex_t g_manual_list_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void trim_manual_line(char *line) {
|
||||
if (!line)
|
||||
@@ -65,6 +68,150 @@ static void copy_manual_file_line_remainder(FILE *in, FILE *out,
|
||||
}
|
||||
}
|
||||
|
||||
bool sm_manual_normalize_path(const char *path, char out[MAX_PATH]) {
|
||||
if (!path || !out || strlcpy(out, path, MAX_PATH) >= MAX_PATH)
|
||||
return false;
|
||||
if (strchr(out, '\r') || strchr(out, '\n'))
|
||||
return false;
|
||||
trim_manual_line(out);
|
||||
return out[0] == '/' && out[1] != '\0';
|
||||
}
|
||||
|
||||
static bool update_manual_list(const char *path, bool add,
|
||||
bool *changed_out) {
|
||||
if (changed_out)
|
||||
*changed_out = false;
|
||||
|
||||
char normalized_path[MAX_PATH];
|
||||
if (!sm_manual_normalize_path(path, normalized_path)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_manual_list_mutex);
|
||||
mkdir(LOG_DIR, 0777);
|
||||
|
||||
FILE *in = fopen(MANUAL_LIST_FILE, "r");
|
||||
if (!in && errno != ENOENT) {
|
||||
int saved_errno = errno;
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needs_separator = false;
|
||||
if (in && fseek(in, 0, SEEK_END) == 0) {
|
||||
long size = ftell(in);
|
||||
if (size > 0 && fseek(in, -1, SEEK_END) == 0)
|
||||
needs_separator = fgetc(in) != '\n';
|
||||
rewind(in);
|
||||
}
|
||||
|
||||
char tmp_path[MAX_PATH];
|
||||
int tmp_len = snprintf(tmp_path, sizeof(tmp_path), "%s.tmp",
|
||||
MANUAL_LIST_FILE);
|
||||
if (tmp_len <= 0 || (size_t)tmp_len >= sizeof(tmp_path)) {
|
||||
if (in)
|
||||
fclose(in);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
errno = ENAMETOOLONG;
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *out = fopen(tmp_path, "w");
|
||||
if (!out) {
|
||||
int saved_errno = errno;
|
||||
if (in)
|
||||
fclose(in);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
bool changed = false;
|
||||
int saved_errno = 0;
|
||||
char line[MAX_PATH + 64];
|
||||
while (in && fgets(line, sizeof(line), in) != NULL) {
|
||||
bool truncated =
|
||||
strchr(line, '\n') == NULL && strlen(line) == sizeof(line) - 1u;
|
||||
char normalized[MAX_PATH + 64];
|
||||
(void)strlcpy(normalized, line, sizeof(normalized));
|
||||
trim_manual_line(normalized);
|
||||
bool matches = !truncated && normalized[0] != '\0' &&
|
||||
normalized[0] != '#' &&
|
||||
strcmp(normalized, normalized_path) == 0;
|
||||
if (matches) {
|
||||
found = true;
|
||||
if (!add) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fputs(line, out) == EOF) {
|
||||
saved_errno = errno;
|
||||
break;
|
||||
}
|
||||
if (truncated)
|
||||
copy_manual_file_line_remainder(in, out, &saved_errno);
|
||||
}
|
||||
if (in && ferror(in) && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
|
||||
if (saved_errno == 0 && add && !found) {
|
||||
if (needs_separator && fputc('\n', out) == EOF)
|
||||
saved_errno = errno;
|
||||
if (saved_errno == 0 &&
|
||||
fprintf(out, "%s\n", normalized_path) < 0) {
|
||||
saved_errno = errno;
|
||||
}
|
||||
if (saved_errno == 0)
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (fflush(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (fclose(out) != 0 && saved_errno == 0)
|
||||
saved_errno = errno;
|
||||
if (in)
|
||||
fclose(in);
|
||||
|
||||
if (saved_errno != 0) {
|
||||
(void)unlink(tmp_path);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
errno = saved_errno;
|
||||
return false;
|
||||
}
|
||||
if (!changed) {
|
||||
(void)unlink(tmp_path);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
return true;
|
||||
}
|
||||
if (rename(tmp_path, MANUAL_LIST_FILE) != 0) {
|
||||
int rename_errno = errno;
|
||||
(void)unlink(tmp_path);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
errno = rename_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
if (changed_out)
|
||||
*changed_out = true;
|
||||
log_debug(" [MANUAL] API %s source: %s", add ? "added" : "removed",
|
||||
normalized_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sm_manual_add_path(const char *path, bool *changed_out) {
|
||||
return update_manual_list(path, true, changed_out);
|
||||
}
|
||||
|
||||
bool sm_manual_remove_path(const char *path, bool *changed_out) {
|
||||
return update_manual_list(path, false, changed_out);
|
||||
}
|
||||
|
||||
bool sm_manual_for_each_path(bool (*visit)(const char *path, void *ctx),
|
||||
void *ctx) {
|
||||
if (!visit)
|
||||
@@ -366,7 +513,11 @@ bool sm_manual_reconcile_deleted_titles(
|
||||
if (!changed)
|
||||
return true;
|
||||
|
||||
if (!rewrite_manual_list_without_deleted_entries(entry_count))
|
||||
pthread_mutex_lock(&g_manual_list_mutex);
|
||||
bool list_rewritten =
|
||||
rewrite_manual_list_without_deleted_entries(entry_count);
|
||||
pthread_mutex_unlock(&g_manual_list_mutex);
|
||||
if (!list_rewritten)
|
||||
return false;
|
||||
return write_manual_status(entry_count);
|
||||
}
|
||||
|
||||
+89
-8
@@ -1,6 +1,7 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sm_config_mount.h"
|
||||
@@ -96,6 +97,7 @@ typedef struct {
|
||||
} sm_mdbg_state_t;
|
||||
|
||||
static sm_mdbg_state_t g_mdbg;
|
||||
static pthread_mutex_t g_mdbg_kernel_log_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static bool sm_mdbg_enabled(void);
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
@@ -135,8 +137,8 @@ static void start_log_monitoring(void);
|
||||
static void handle_crash_candidate(uint64_t flags, uint64_t now_us);
|
||||
|
||||
static bool sm_mdbg_enabled(void) {
|
||||
return runtime_config()->kstuff_crash_detection_enabled &&
|
||||
runtime_config()->kstuff_game_auto_toggle;
|
||||
return runtime_config().kstuff_crash_detection_enabled &&
|
||||
runtime_config().kstuff_game_auto_toggle;
|
||||
}
|
||||
|
||||
#if !MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
@@ -157,8 +159,12 @@ static int elevate_to_coredump(void) {
|
||||
#endif
|
||||
|
||||
static bool ensure_mdbg_privileges(void) {
|
||||
if (g_mdbg.privilege_probe_done)
|
||||
return g_mdbg.privilege_ready;
|
||||
pthread_mutex_lock(&g_mdbg_kernel_log_mutex);
|
||||
if (g_mdbg.privilege_probe_done) {
|
||||
bool ready = g_mdbg.privilege_ready;
|
||||
pthread_mutex_unlock(&g_mdbg_kernel_log_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
g_mdbg.privilege_probe_done = true;
|
||||
#if MDBG_SKIP_PRIVILEGE_ELEVATION
|
||||
@@ -174,7 +180,9 @@ static bool ensure_mdbg_privileges(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
return g_mdbg.privilege_ready;
|
||||
bool ready = g_mdbg.privilege_ready;
|
||||
pthread_mutex_unlock(&g_mdbg_kernel_log_mutex);
|
||||
return ready;
|
||||
}
|
||||
|
||||
static int mdbg_call_raw(int64_t pid, uint64_t subcmd, uint64_t arg,
|
||||
@@ -285,9 +293,10 @@ static int fetch_log_text(const char **text_out, size_t *text_len_out) {
|
||||
*text_out = NULL;
|
||||
*text_len_out = 0;
|
||||
|
||||
int ret =
|
||||
MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size, &raw_text,
|
||||
&raw_len);
|
||||
pthread_mutex_lock(&g_mdbg_kernel_log_mutex);
|
||||
int ret = MDBG_FETCH_LOG_TEXT(g_mdbg.log_storage, g_mdbg.log_buffer_size,
|
||||
&raw_text, &raw_len);
|
||||
pthread_mutex_unlock(&g_mdbg_kernel_log_mutex);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
@@ -302,6 +311,78 @@ static int fetch_log_text(const char **text_out, size_t *text_len_out) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sm_mdbg_get_log_tail(size_t max_bytes, char **text_out,
|
||||
size_t *text_length_out, size_t *total_length_out) {
|
||||
if (!text_out || !text_length_out || !total_length_out || max_bytes == 0)
|
||||
return EINVAL;
|
||||
*text_out = NULL;
|
||||
*text_length_out = 0;
|
||||
*total_length_out = 0;
|
||||
|
||||
if (!ensure_mdbg_privileges())
|
||||
return EACCES;
|
||||
|
||||
uint64_t raw_buffer_size = sceKernelDebugGetLogBufferSize();
|
||||
if (raw_buffer_size == 0 || raw_buffer_size > SIZE_MAX)
|
||||
return EOVERFLOW;
|
||||
size_t buffer_size = (size_t)raw_buffer_size;
|
||||
char *storage = malloc(buffer_size);
|
||||
if (!storage)
|
||||
return ENOMEM;
|
||||
|
||||
char *raw_text = NULL;
|
||||
uint64_t raw_length = 0;
|
||||
pthread_mutex_lock(&g_mdbg_kernel_log_mutex);
|
||||
int ret = MDBG_FETCH_LOG_TEXT(storage, buffer_size, &raw_text, &raw_length);
|
||||
pthread_mutex_unlock(&g_mdbg_kernel_log_mutex);
|
||||
if (ret != 0) {
|
||||
free(storage);
|
||||
return EIO;
|
||||
}
|
||||
if (!raw_text || raw_length == 0) {
|
||||
free(storage);
|
||||
char *empty = calloc(1u, 1u);
|
||||
if (!empty)
|
||||
return ENOMEM;
|
||||
*text_out = empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uintptr_t storage_start = (uintptr_t)storage;
|
||||
uintptr_t text_start = (uintptr_t)raw_text;
|
||||
if (text_start < storage_start || text_start > storage_start + buffer_size) {
|
||||
free(storage);
|
||||
return EIO;
|
||||
}
|
||||
size_t text_offset = (size_t)(text_start - storage_start);
|
||||
if (raw_length > buffer_size - text_offset) {
|
||||
free(storage);
|
||||
return EOVERFLOW;
|
||||
}
|
||||
|
||||
size_t total_length = (size_t)raw_length;
|
||||
size_t start = total_length > max_bytes ? total_length - max_bytes : 0;
|
||||
if (start > 0) {
|
||||
char *newline = memchr(raw_text + start, '\n', total_length - start);
|
||||
if (newline)
|
||||
start = (size_t)(newline - raw_text) + 1u;
|
||||
}
|
||||
size_t text_length = total_length - start;
|
||||
char *result = malloc(text_length + 1u);
|
||||
if (!result) {
|
||||
free(storage);
|
||||
return ENOMEM;
|
||||
}
|
||||
memcpy(result, raw_text + start, text_length);
|
||||
result[text_length] = '\0';
|
||||
free(storage);
|
||||
|
||||
*text_out = result;
|
||||
*text_length_out = text_length;
|
||||
*total_length_out = total_length;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t find_log_overlap(const char *current, size_t current_len) {
|
||||
if (!g_mdbg.log_snapshot || g_mdbg.log_snapshot_length == 0 || !current ||
|
||||
current_len == 0) {
|
||||
|
||||
+46
-35
@@ -212,6 +212,11 @@ static bool device_node_is_mounted(const char *devname,
|
||||
static bool detach_target_is_released_or_reused(
|
||||
attach_backend_t backend, int unit_id, const char *devname,
|
||||
const attached_unit_detach_state_t *state) {
|
||||
if (!state || !state->node_identity_valid) {
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(devname, &st) != 0) {
|
||||
if (errno != ENOENT && errno != ENOTDIR)
|
||||
@@ -221,10 +226,9 @@ static bool detach_target_is_released_or_reused(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state && state->node_identity_valid &&
|
||||
(state->node_device != (uint64_t)st.st_dev ||
|
||||
state->node_inode != (uint64_t)st.st_ino ||
|
||||
state->node_rdev != (uint64_t)st.st_rdev)) {
|
||||
if (state->node_device != (uint64_t)st.st_dev ||
|
||||
state->node_inode != (uint64_t)st.st_ino ||
|
||||
state->node_rdev != (uint64_t)st.st_rdev) {
|
||||
log_debug(" [IMG][%s] detach target released: unit=%d node identity "
|
||||
"changed",
|
||||
attach_backend_name(backend), unit_id);
|
||||
@@ -252,26 +256,39 @@ static attached_unit_detach_prepare_t prepare_attached_unit_detach(
|
||||
const attached_unit_detach_state_t *state, char devname[64],
|
||||
attached_unit_detach_state_t *request_state) {
|
||||
build_attached_unit_devname(backend, unit_id, devname);
|
||||
if (detach_target_is_released_or_reused(backend, unit_id, devname, state))
|
||||
return ATTACHED_UNIT_DETACH_COMPLETE;
|
||||
*request_state = *state;
|
||||
if (!request_state->node_identity_valid &&
|
||||
!read_device_node_identity(devname, request_state)) {
|
||||
log_debug(" [IMG][%s] detach deferred without device identity: unit=%d",
|
||||
attach_backend_name(backend), unit_id);
|
||||
errno = EAGAIN;
|
||||
return ATTACHED_UNIT_DETACH_PENDING;
|
||||
}
|
||||
|
||||
if (state && state->requested) {
|
||||
// A freshly attached unit may not have published its device node yet. An
|
||||
// absent node is only proof of release after the node identity was observed
|
||||
// or a detach request was accepted.
|
||||
if (detach_target_is_released_or_reused(backend, unit_id, devname,
|
||||
request_state)) {
|
||||
return ATTACHED_UNIT_DETACH_COMPLETE;
|
||||
}
|
||||
|
||||
if (request_state->requested) {
|
||||
errno = EINPROGRESS;
|
||||
return ATTACHED_UNIT_DETACH_PENDING;
|
||||
}
|
||||
|
||||
if (state)
|
||||
*request_state = *state;
|
||||
else
|
||||
memset(request_state, 0, sizeof(*request_state));
|
||||
if (!request_state->node_identity_valid)
|
||||
(void)read_device_node_identity(devname, request_state);
|
||||
return ATTACHED_UNIT_DETACH_REQUEST;
|
||||
}
|
||||
|
||||
bool wait_for_attached_unit_release(
|
||||
attach_backend_t backend, int unit_id,
|
||||
const attached_unit_detach_state_t *state) {
|
||||
if (!state || !state->node_identity_valid) {
|
||||
errno = EAGAIN;
|
||||
return false;
|
||||
}
|
||||
|
||||
char devname[64];
|
||||
build_attached_unit_devname(backend, unit_id, devname);
|
||||
if (wait_for_dev_node_state(devname, false))
|
||||
@@ -286,34 +303,30 @@ bool wait_for_attached_unit_release(
|
||||
}
|
||||
|
||||
static bool finish_accepted_detach(
|
||||
attach_backend_t backend, int unit_id,
|
||||
attached_unit_detach_state_t *state,
|
||||
attached_unit_detach_state_t *request_state) {
|
||||
request_state->requested = true;
|
||||
if (state) {
|
||||
*state = *request_state;
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
|
||||
return wait_for_attached_unit_release(backend, unit_id, request_state);
|
||||
*state = *request_state;
|
||||
errno = EINPROGRESS;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool finish_failed_detach(attach_backend_t backend, int unit_id,
|
||||
const char *devname,
|
||||
const attached_unit_detach_state_t *state,
|
||||
int detach_errno) {
|
||||
if (detach_target_is_released_or_reused(backend, unit_id, devname, state))
|
||||
// Before a detach request is accepted, an unobserved node may still belong
|
||||
// to the freshly attached unit. Keep it queued when the ioctl itself fails.
|
||||
if (state->node_identity_valid &&
|
||||
detach_target_is_released_or_reused(backend, unit_id, devname, state)) {
|
||||
return true;
|
||||
}
|
||||
errno = detach_errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool detach_lvd_unit(int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
char devname[64];
|
||||
attached_unit_detach_state_t request_state;
|
||||
attached_unit_detach_prepare_t prepare = prepare_attached_unit_detach(
|
||||
@@ -346,15 +359,11 @@ static bool detach_lvd_unit(int unit_id,
|
||||
}
|
||||
close(fd);
|
||||
|
||||
return finish_accepted_detach(ATTACH_BACKEND_LVD, unit_id, state,
|
||||
&request_state);
|
||||
return finish_accepted_detach(state, &request_state);
|
||||
}
|
||||
|
||||
static bool detach_md_unit(int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
if (unit_id < 0)
|
||||
return true;
|
||||
|
||||
char devname[64];
|
||||
attached_unit_detach_state_t request_state;
|
||||
attached_unit_detach_prepare_t prepare = prepare_attached_unit_detach(
|
||||
@@ -388,15 +397,17 @@ static bool detach_md_unit(int unit_id,
|
||||
}
|
||||
close(fd);
|
||||
|
||||
return finish_accepted_detach(ATTACH_BACKEND_MD, unit_id, state,
|
||||
&request_state);
|
||||
return finish_accepted_detach(state, &request_state);
|
||||
}
|
||||
|
||||
bool detach_attached_unit(attach_backend_t backend, int unit_id,
|
||||
attached_unit_detach_state_t *state) {
|
||||
if (!state || unit_id < 0 ||
|
||||
(backend != ATTACH_BACKEND_MD && backend != ATTACH_BACKEND_LVD)) {
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
}
|
||||
if (backend == ATTACH_BACKEND_MD)
|
||||
return detach_md_unit(unit_id, state);
|
||||
else if (backend == ATTACH_BACKEND_LVD)
|
||||
return detach_lvd_unit(unit_id, state);
|
||||
return true;
|
||||
return detach_lvd_unit(unit_id, state);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ struct PathStateEntry {
|
||||
bool missing_param_limit_logged;
|
||||
bool image_mount_limit_logged;
|
||||
bool backport_mount_blocked;
|
||||
bool backport_permissions_incomplete_logged;
|
||||
bool manual_missing_source_logged;
|
||||
bool game_info_cached;
|
||||
bool game_info_valid;
|
||||
@@ -84,6 +85,7 @@ static struct PathStateEntry *create_path_state(const char *path) {
|
||||
if (g_path_state[k].missing_param_attempts == 0 &&
|
||||
g_path_state[k].image_mount_attempts == 0 &&
|
||||
!g_path_state[k].backport_mount_blocked &&
|
||||
!g_path_state[k].backport_permissions_incomplete_logged &&
|
||||
!g_path_state[k].manual_missing_source_logged &&
|
||||
!g_path_state[k].game_info_cached) {
|
||||
evict_k = k;
|
||||
@@ -262,6 +264,19 @@ void clear_image_mount_attempts(const char *path) {
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
|
||||
size_t reset_all_image_mount_attempts(void) {
|
||||
size_t reset_count = 0;
|
||||
for (int k = 0; k < PATH_STATE_CAPACITY; k++) {
|
||||
struct PathStateEntry *entry = &g_path_state[k];
|
||||
if (!entry->valid || entry->image_mount_attempts == 0)
|
||||
continue;
|
||||
entry->image_mount_attempts = 0;
|
||||
entry->image_mount_limit_logged = false;
|
||||
reset_count++;
|
||||
}
|
||||
return reset_count;
|
||||
}
|
||||
|
||||
bool is_backport_mount_blocked(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
@@ -289,6 +304,25 @@ void clear_backport_mount_failure(const char *path) {
|
||||
entry->backport_mount_blocked = false;
|
||||
}
|
||||
|
||||
bool note_backport_permissions_incomplete_once(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
struct PathStateEntry *entry = get_or_create_path_state(path);
|
||||
if (!entry || entry->backport_permissions_incomplete_logged)
|
||||
return false;
|
||||
entry->backport_permissions_incomplete_logged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear_backport_permissions_incomplete(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return;
|
||||
struct PathStateEntry *entry = find_path_state(path);
|
||||
if (!entry)
|
||||
return;
|
||||
entry->backport_permissions_incomplete_logged = false;
|
||||
}
|
||||
|
||||
bool note_manual_missing_source_once(const char *path) {
|
||||
if (!path || path[0] == '\0')
|
||||
return false;
|
||||
|
||||
+165
-7
@@ -60,6 +60,21 @@ static void note_found_title(const char *title_id) {
|
||||
sizeof(g_scan_workspace.found_titles[slot]));
|
||||
}
|
||||
|
||||
static void note_found_indexed_title(const char *title_id, const void *ctx) {
|
||||
const char *image_path = ctx;
|
||||
note_found_title(title_id);
|
||||
|
||||
char runtime_path[MAX_PATH];
|
||||
char linked_image[MAX_PATH];
|
||||
if (!image_path ||
|
||||
!read_mount_link(title_id, runtime_path, sizeof(runtime_path)) ||
|
||||
!read_mount_image_link(title_id, linked_image, sizeof(linked_image)) ||
|
||||
strcmp(linked_image, image_path) != 0) {
|
||||
return;
|
||||
}
|
||||
cache_game_entry(runtime_path, title_id, "");
|
||||
}
|
||||
|
||||
static bool mount_persistent_indexed_image(const char *image_path,
|
||||
const char *image_name,
|
||||
bool *unstable_found_out) {
|
||||
@@ -537,8 +552,9 @@ static bool collect_candidate_image_visit(const char *image_path,
|
||||
bool index_ready =
|
||||
indexed_image && sm_image_index_visit_ready_titles(
|
||||
image_path, &image_st, ctx->app_db->titles,
|
||||
ctx->app_db->titles_ready, note_found_title);
|
||||
if (index_ready && !runtime_config()->persistent_image_mounts)
|
||||
ctx->app_db->titles_ready,
|
||||
note_found_indexed_title, image_path);
|
||||
if (index_ready && !runtime_config().persistent_image_mounts)
|
||||
return true;
|
||||
|
||||
if (index_ready) {
|
||||
@@ -641,8 +657,8 @@ static void collect_scan_candidates_from_manual_path(
|
||||
is_supported_image_file_path(manual_path, name)) {
|
||||
bool index_ready = sm_image_index_visit_ready_titles(
|
||||
manual_path, &st, app_db->titles, app_db->titles_ready,
|
||||
note_found_title);
|
||||
if (index_ready && !runtime_config()->persistent_image_mounts)
|
||||
note_found_indexed_title, manual_path);
|
||||
if (index_ready && !runtime_config().persistent_image_mounts)
|
||||
return;
|
||||
|
||||
if (index_ready) {
|
||||
@@ -761,7 +777,9 @@ bool resolve_backport_path_for_title(const char *title_id,
|
||||
}
|
||||
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (owning_scan_path && strcmp(scan_path, owning_scan_path) == 0)
|
||||
continue;
|
||||
|
||||
@@ -844,7 +862,7 @@ bool release_scan_runtime_mounts(void) {
|
||||
runtime_mount_state_unlock();
|
||||
return false;
|
||||
}
|
||||
bool released = runtime_config()->persistent_image_mounts ||
|
||||
bool released = runtime_config().persistent_image_mounts ||
|
||||
release_runtime_image_mounts();
|
||||
runtime_mount_state_unlock();
|
||||
return released;
|
||||
@@ -886,6 +904,139 @@ void unmount_usb_sources_for_suspend(void) {
|
||||
log_debug("[SLEEP] USB-backed mount cleanup done");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
size_t fixed;
|
||||
size_t failures;
|
||||
int first_error;
|
||||
char first_failed_path[MAX_PATH];
|
||||
} backport_permission_repair_t;
|
||||
|
||||
static bool backport_filesystem_needs_permission_repair(const char *path) {
|
||||
struct statfs filesystem;
|
||||
if (statfs(path, &filesystem) != 0)
|
||||
return false;
|
||||
|
||||
if (strcmp(filesystem.f_fstypename, "nullfs") == 0) {
|
||||
char source_path[MAX_PATH];
|
||||
if (filesystem.f_mntfromname[0] != '/' ||
|
||||
strlcpy(source_path, filesystem.f_mntfromname,
|
||||
sizeof(source_path)) >= sizeof(source_path) ||
|
||||
statfs(source_path, &filesystem) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return strcmp(filesystem.f_fstypename, "ufs") == 0 ||
|
||||
strcmp(filesystem.f_fstypename, "bfs") == 0;
|
||||
}
|
||||
|
||||
static void note_backport_permission_failure(
|
||||
backport_permission_repair_t *repair, const char *path, int error) {
|
||||
repair->failures++;
|
||||
if (repair->first_failed_path[0] != '\0')
|
||||
return;
|
||||
|
||||
repair->first_error = error;
|
||||
(void)strlcpy(repair->first_failed_path, path,
|
||||
sizeof(repair->first_failed_path));
|
||||
}
|
||||
|
||||
static bool repair_backport_path_permissions(
|
||||
const char *path, backport_permission_repair_t *repair) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return false;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
note_backport_permission_failure(repair, path, errno);
|
||||
return true;
|
||||
}
|
||||
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode))
|
||||
return true;
|
||||
|
||||
if ((st.st_mode & 07777) != 0777) {
|
||||
if (chmod(path, 0777) != 0) {
|
||||
note_backport_permission_failure(repair, path, errno);
|
||||
return true;
|
||||
} else {
|
||||
repair->fixed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!S_ISDIR(st.st_mode))
|
||||
return true;
|
||||
|
||||
DIR *directory = opendir(path);
|
||||
if (!directory) {
|
||||
note_backport_permission_failure(repair, path, errno);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool completed = true;
|
||||
while (true) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active()) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
struct dirent *entry = readdir(directory);
|
||||
if (!entry) {
|
||||
if (errno != 0)
|
||||
note_backport_permission_failure(repair, path, errno);
|
||||
break;
|
||||
}
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
char child_path[MAX_PATH];
|
||||
int written = snprintf(child_path, sizeof(child_path), "%s/%s", path,
|
||||
entry->d_name);
|
||||
if (written < 0 || (size_t)written >= sizeof(child_path)) {
|
||||
note_backport_permission_failure(repair, path, ENAMETOOLONG);
|
||||
continue;
|
||||
}
|
||||
if (!repair_backport_path_permissions(child_path, repair)) {
|
||||
completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (closedir(directory) != 0)
|
||||
note_backport_permission_failure(repair, path, errno);
|
||||
return completed;
|
||||
}
|
||||
|
||||
static void repair_backport_permissions_for_scan_root(const char *scan_path) {
|
||||
char backports_root[MAX_PATH];
|
||||
if (!build_backports_root_path(scan_path, backports_root))
|
||||
return;
|
||||
|
||||
struct stat root_st;
|
||||
if (lstat(backports_root, &root_st) != 0 || !S_ISDIR(root_st.st_mode))
|
||||
return;
|
||||
if (!backport_filesystem_needs_permission_repair(backports_root)) {
|
||||
clear_backport_permissions_incomplete(backports_root);
|
||||
return;
|
||||
}
|
||||
|
||||
backport_permission_repair_t repair = {0};
|
||||
if (!repair_backport_path_permissions(backports_root, &repair))
|
||||
return;
|
||||
|
||||
if (repair.fixed != 0)
|
||||
log_debug(" [BKP] permissions fixed: root=%s entries=%zu", backports_root,
|
||||
repair.fixed);
|
||||
if (repair.failures == 0) {
|
||||
clear_backport_permissions_incomplete(backports_root);
|
||||
} else if (note_backport_permissions_incomplete_once(backports_root)) {
|
||||
log_debug(" [BKP] permission repair incomplete: root=%s failures=%zu "
|
||||
"first=%s (%s)",
|
||||
backports_root, repair.failures, repair.first_failed_path,
|
||||
strerror(repair.first_error));
|
||||
}
|
||||
}
|
||||
|
||||
static void collect_scan_candidates_from_root(
|
||||
const char *scan_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, const scan_app_db_context_t *app_db,
|
||||
@@ -894,6 +1045,10 @@ static void collect_scan_candidates_from_root(
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
repair_backport_permissions_for_scan_root(scan_path);
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
return;
|
||||
|
||||
unsigned int scan_depth = get_scan_depth_for_root(scan_path);
|
||||
|
||||
collect_candidates_walk_ctx_t ctx = {
|
||||
@@ -1026,7 +1181,10 @@ int collect_scan_candidates(scan_candidate_t *candidates, int max_candidates,
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
if (should_stop_requested() || runtime_sleep_mode_active())
|
||||
break;
|
||||
collect_scan_candidates_from_root(get_scan_path(i), candidates,
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
collect_scan_candidates_from_root(scan_path, candidates,
|
||||
max_candidates,
|
||||
&candidate_count, &app_db,
|
||||
g_scan_workspace.discovered_param_roots,
|
||||
|
||||
+3
-1
@@ -32,7 +32,9 @@ static void classify_scan_tree_entry(const char *full_path, unsigned char d_type
|
||||
static bool is_distinct_configured_scan_root(const char *current_scan_root,
|
||||
const char *path) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_path = get_scan_path(i);
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (strcmp(scan_path, path) != 0)
|
||||
continue;
|
||||
if (strcmp(current_scan_root, path) == 0)
|
||||
|
||||
+408
-55
@@ -1,5 +1,6 @@
|
||||
#include "sm_platform.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/event.h>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "sm_limits.h"
|
||||
#include "sm_l10n.h"
|
||||
#include "sm_log.h"
|
||||
#include "sm_path_state.h"
|
||||
#include "sm_path_utils.h"
|
||||
#include "sm_paths.h"
|
||||
#include "sm_runtime.h"
|
||||
@@ -30,6 +32,7 @@
|
||||
#include "sm_scanner.h"
|
||||
#include "sm_shellcore_service.h"
|
||||
#include "sm_time.h"
|
||||
#include "sm_title_state.h"
|
||||
#include "sm_types.h"
|
||||
|
||||
#define SCANNER_EVENT_BATCH 32
|
||||
@@ -39,6 +42,10 @@
|
||||
#define SCANNER_CONFIG_PROBE_INTERVAL_US 10000000ull
|
||||
#define SCANNER_MANUAL_RELOAD_DEBOUNCE_US 250000ull
|
||||
#define SCANNER_MANUAL_PROBE_INTERVAL_US 10000000ull
|
||||
#define SCANNER_USB_SLOT_COUNT 8
|
||||
#define SCANNER_USB_MOUNT_PROBE_DELAY_US 1000000ull
|
||||
#define SCANNER_MAX_RECOMMENDED_CLUSTER_SIZE_BYTES (64ull * 1024ull)
|
||||
#define SCANNER_GIB_BYTES (1024ull * 1024ull * 1024ull)
|
||||
#define SCANNER_FAKELIB_CACHE_CLEANUP_INTERVAL_US \
|
||||
(24ull * 60ull * 60ull * 1000000ull)
|
||||
|
||||
@@ -65,11 +72,13 @@ typedef struct {
|
||||
bool cleanup_pending;
|
||||
bool watch_tree_stale;
|
||||
bool root_present;
|
||||
bool usb_connect_counted;
|
||||
uint8_t watch_tree_rebuild_depth;
|
||||
scanner_watch_kind_t watch_tree_rebuild_kind;
|
||||
uint64_t root_device;
|
||||
uint64_t root_inode;
|
||||
uint64_t ready_after_us;
|
||||
int usb_connect_found_games;
|
||||
char watch_tree_rebuild_path[MAX_PATH];
|
||||
} scanner_root_state_t;
|
||||
|
||||
@@ -79,6 +88,12 @@ typedef struct {
|
||||
scanner_watch_kind_t kind;
|
||||
} scanner_event_subscription_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t available_tenths;
|
||||
uint64_t capacity_tenths;
|
||||
uint64_t block_size_bytes;
|
||||
} scanner_usb_info_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;
|
||||
@@ -97,13 +112,18 @@ static uint64_t g_scanner_config_reload_ready_after_us = 0;
|
||||
static uint64_t g_scanner_config_probe_due_us = 0;
|
||||
static uint64_t g_scanner_manual_scan_due_us = 0;
|
||||
static uint64_t g_scanner_manual_probe_due_us = 0;
|
||||
static uint8_t g_scanner_usb_mounted_mask = 0;
|
||||
static uint8_t g_scanner_usb_scan_result_pending_mask = 0;
|
||||
static scanner_usb_info_t g_scanner_usb_info[SCANNER_USB_SLOT_COUNT];
|
||||
static uint64_t g_scanner_usb_mount_probe_due_us = 0;
|
||||
static pthread_mutex_t g_scanner_cycle_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static uint64_t scanner_stability_wait_us(void) {
|
||||
return (uint64_t)runtime_config()->stability_wait_seconds * 1000000ull;
|
||||
return (uint64_t)runtime_config().stability_wait_seconds * 1000000ull;
|
||||
}
|
||||
|
||||
static uint64_t scanner_full_resync_interval_us(void) {
|
||||
return (uint64_t)runtime_config()->scan_interval_us;
|
||||
return (uint64_t)runtime_config().scan_interval_us;
|
||||
}
|
||||
|
||||
static void schedule_config_reload(uint64_t now_us) {
|
||||
@@ -128,6 +148,134 @@ static void reset_scanner_root_states(void) {
|
||||
memset(g_scanner_root_states, 0, sizeof(g_scanner_root_states));
|
||||
}
|
||||
|
||||
static int scanner_usb_slot_for_path(const char *path) {
|
||||
static const char prefix[] = "/mnt/usb";
|
||||
const size_t prefix_len = sizeof(prefix) - 1u;
|
||||
|
||||
if (!path || strncmp(path, prefix, prefix_len) != 0)
|
||||
return -1;
|
||||
|
||||
char slot = path[prefix_len];
|
||||
char suffix = path[prefix_len + 1u];
|
||||
if (slot < '0' || slot >= '0' + SCANNER_USB_SLOT_COUNT ||
|
||||
(suffix != '\0' && suffix != '/')) {
|
||||
return -1;
|
||||
}
|
||||
return slot - '0';
|
||||
}
|
||||
|
||||
static void build_scanner_usb_root_path(int slot,
|
||||
char usb_root[sizeof("/mnt/usb0")]) {
|
||||
(void)strlcpy(usb_root, "/mnt/usb0", sizeof("/mnt/usb0"));
|
||||
usb_root[8] = (char)('0' + slot);
|
||||
}
|
||||
|
||||
static bool scanner_usb_slot_has_scan_root(int slot) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (scanner_usb_slot_for_path(scan_path) == slot)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void reset_scanner_usb_scan_counts(int slot) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (scanner_usb_slot_for_path(scan_path) != slot)
|
||||
continue;
|
||||
g_scanner_root_states[i].usb_connect_counted = false;
|
||||
g_scanner_root_states[i].usb_connect_found_games = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_scanner_usb_mount_state(void) {
|
||||
g_scanner_usb_mounted_mask = 0;
|
||||
g_scanner_usb_scan_result_pending_mask = 0;
|
||||
memset(g_scanner_usb_info, 0, sizeof(g_scanner_usb_info));
|
||||
g_scanner_usb_mount_probe_due_us = 0;
|
||||
for (int slot = 0; slot < SCANNER_USB_SLOT_COUNT; slot++) {
|
||||
char usb_root[sizeof("/mnt/usb0")];
|
||||
build_scanner_usb_root_path(slot, usb_root);
|
||||
if (usb_storage_root_mounted(usb_root))
|
||||
g_scanner_usb_mounted_mask |= (uint8_t)(1u << slot);
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t bytes_to_gib_tenths(uint64_t bytes) {
|
||||
uint64_t whole = bytes / SCANNER_GIB_BYTES;
|
||||
uint64_t remainder = bytes % SCANNER_GIB_BYTES;
|
||||
return whole * 10ull +
|
||||
(remainder * 10ull + SCANNER_GIB_BYTES / 2ull) /
|
||||
SCANNER_GIB_BYTES;
|
||||
}
|
||||
|
||||
static bool notify_scanner_usb_mount_change(const char *path) {
|
||||
int slot = scanner_usb_slot_for_path(path);
|
||||
if (slot < 0)
|
||||
return false;
|
||||
|
||||
char usb_root[sizeof("/mnt/usb0")];
|
||||
build_scanner_usb_root_path(slot, usb_root);
|
||||
uint8_t slot_mask = (uint8_t)(1u << slot);
|
||||
bool mounted = usb_storage_root_mounted(usb_root);
|
||||
bool was_mounted = (g_scanner_usb_mounted_mask & slot_mask) != 0;
|
||||
if (mounted == was_mounted)
|
||||
return false;
|
||||
|
||||
if (mounted) {
|
||||
struct statfs sfs;
|
||||
if (statfs(usb_root, &sfs) != 0) {
|
||||
log_debug("[SCAN] USB storage info unavailable for %s: %s", usb_root,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t block_size = (uint64_t)sfs.f_bsize;
|
||||
uint64_t capacity_bytes = (uint64_t)sfs.f_blocks * block_size;
|
||||
uint64_t available_bytes = (uint64_t)sfs.f_bavail * block_size;
|
||||
uint64_t capacity_tenths = bytes_to_gib_tenths(capacity_bytes);
|
||||
uint64_t available_tenths = bytes_to_gib_tenths(available_bytes);
|
||||
g_scanner_usb_mounted_mask |= slot_mask;
|
||||
if (scanner_usb_slot_has_scan_root(slot)) {
|
||||
g_scanner_usb_scan_result_pending_mask |= slot_mask;
|
||||
reset_scanner_usb_scan_counts(slot);
|
||||
}
|
||||
g_scanner_usb_info[slot].available_tenths = available_tenths;
|
||||
g_scanner_usb_info[slot].capacity_tenths = capacity_tenths;
|
||||
g_scanner_usb_info[slot].block_size_bytes = block_size;
|
||||
log_debug("[SCAN] USB storage connected; scan scheduled: %s "
|
||||
"capacity=%lluB available=%lluB block_size=%lluB",
|
||||
usb_root, (unsigned long long)capacity_bytes,
|
||||
(unsigned long long)available_bytes,
|
||||
(unsigned long long)block_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
g_scanner_usb_mounted_mask &= (uint8_t)~slot_mask;
|
||||
g_scanner_usb_scan_result_pending_mask &= (uint8_t)~slot_mask;
|
||||
reset_scanner_usb_scan_counts(slot);
|
||||
memset(&g_scanner_usb_info[slot], 0, sizeof(g_scanner_usb_info[slot]));
|
||||
log_debug("[SCAN] USB storage disconnected: %s", usb_root);
|
||||
notify_system_info_l10n(SM_L10N_USB_DISCONNECTED, usb_root);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void schedule_scanner_usb_mount_probe(const char *path,
|
||||
uint64_t now_us) {
|
||||
int slot = scanner_usb_slot_for_path(path);
|
||||
if (slot < 0 ||
|
||||
(g_scanner_usb_mounted_mask & (uint8_t)(1u << slot)) != 0 ||
|
||||
g_scanner_usb_mount_probe_due_us != 0) {
|
||||
return;
|
||||
}
|
||||
g_scanner_usb_mount_probe_due_us = now_us + SCANNER_USB_MOUNT_PROBE_DELAY_US;
|
||||
}
|
||||
|
||||
static void reset_scanner_root_watch_heads(void) {
|
||||
for (int i = 0; i < MAX_SCAN_PATHS; i++)
|
||||
g_scanner_root_watch_heads[i] = SCANNER_WATCH_INDEX_NONE;
|
||||
@@ -230,29 +378,10 @@ static void clear_scanner_manual_scan_state(void) {
|
||||
g_scanner_manual_probe_due_us = 0;
|
||||
}
|
||||
|
||||
static bool fakelib_runtime_config_changed(const runtime_config_t *old_cfg,
|
||||
const runtime_config_t *new_cfg) {
|
||||
return old_cfg->backport_fakelib_enabled !=
|
||||
new_cfg->backport_fakelib_enabled ||
|
||||
old_cfg->global_fakelib_enabled != new_cfg->global_fakelib_enabled ||
|
||||
old_cfg->global_fakelib_game_priority !=
|
||||
new_cfg->global_fakelib_game_priority ||
|
||||
old_cfg->update_emulators_enabled !=
|
||||
new_cfg->update_emulators_enabled ||
|
||||
strcmp(old_cfg->emulators_path, new_cfg->emulators_path) != 0 ||
|
||||
strcmp(old_cfg->global_fakelib_path,
|
||||
new_cfg->global_fakelib_path) != 0 ||
|
||||
old_cfg->global_fakelib_exclude_title_count !=
|
||||
new_cfg->global_fakelib_exclude_title_count ||
|
||||
memcmp(old_cfg->global_fakelib_exclude_title_ids,
|
||||
new_cfg->global_fakelib_exclude_title_ids,
|
||||
sizeof(old_cfg->global_fakelib_exclude_title_ids)) != 0;
|
||||
}
|
||||
|
||||
static uint32_t scanner_config_topology_hash(void) {
|
||||
uint32_t hash = 2166136261u;
|
||||
int scan_path_count = get_scan_path_count();
|
||||
uint32_t values[] = {runtime_config()->scan_depth, (uint32_t)scan_path_count};
|
||||
uint32_t values[] = {runtime_config().scan_depth, (uint32_t)scan_path_count};
|
||||
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (unsigned shift = 0; shift < 32u; shift += 8u) {
|
||||
@@ -261,7 +390,10 @@ static uint32_t scanner_config_topology_hash(void) {
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < scan_path_count; i++) {
|
||||
hash ^= sm_fnv1a32(get_scan_path(i));
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
hash ^= sm_fnv1a32(scan_path);
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
@@ -636,8 +768,8 @@ static bool resolve_watch_tree_rebuild_target(
|
||||
*kind_out = subscription->kind;
|
||||
return true;
|
||||
case SCANNER_WATCH_SCAN_ROOT_PARENT:
|
||||
(void)strlcpy(rebuild_path,
|
||||
get_scan_path(subscription->scan_root_index), MAX_PATH);
|
||||
if (!get_scan_path(subscription->scan_root_index, rebuild_path))
|
||||
return false;
|
||||
*rebuild_depth_out = 0;
|
||||
*kind_out = SCANNER_WATCH_SCAN_ROOT;
|
||||
return true;
|
||||
@@ -690,7 +822,9 @@ static bool register_scan_root_parent_watch(int kq, int scan_root_index,
|
||||
}
|
||||
|
||||
static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
||||
const char *scan_root = get_scan_path(scan_root_index);
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(scan_root_index, scan_root))
|
||||
return false;
|
||||
if (!remove_scan_root_watch_entries(scan_root_index))
|
||||
return false;
|
||||
|
||||
@@ -731,7 +865,9 @@ static bool rebuild_scan_root_watch_subtree(int kq, int scan_root_index,
|
||||
const char *rebuild_path,
|
||||
uint8_t rebuild_depth,
|
||||
scanner_watch_kind_t rebuild_kind) {
|
||||
const char *scan_root = get_scan_path(scan_root_index);
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(scan_root_index, scan_root))
|
||||
return false;
|
||||
if (!rebuild_path || rebuild_path[0] == '\0' ||
|
||||
rebuild_kind == SCANNER_WATCH_SCAN_ROOT ||
|
||||
strcmp(rebuild_path, scan_root) == 0) {
|
||||
@@ -776,7 +912,10 @@ static bool rebuild_all_scan_root_watch_trees(int kq) {
|
||||
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)))
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (!is_usb_storage_path(scan_path))
|
||||
continue;
|
||||
while (g_scanner_root_watch_heads[i] != SCANNER_WATCH_INDEX_NONE) {
|
||||
remove_scanner_watch_entry_at(g_scanner_root_watch_heads[i]);
|
||||
@@ -790,10 +929,104 @@ static bool suspend_usb_scan_root_watch_trees(void) {
|
||||
static void schedule_scan_root_dirty(int scan_root_index, uint64_t now_us,
|
||||
bool immediate);
|
||||
|
||||
static void schedule_scan_roots_for_usb_slot_except(int slot,
|
||||
int excluded_root_index,
|
||||
uint64_t now_us) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (i != excluded_root_index &&
|
||||
scanner_usb_slot_for_path(scan_path) == slot) {
|
||||
schedule_scan_root_dirty(i, now_us, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void schedule_scan_roots_for_usb_slot(int slot, uint64_t now_us) {
|
||||
schedule_scan_roots_for_usb_slot_except(slot, -1, now_us);
|
||||
}
|
||||
|
||||
static bool scanner_usb_slot_scan_incomplete(int slot) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (scanner_usb_slot_for_path(scan_path) != slot)
|
||||
continue;
|
||||
if (g_scanner_root_states[i].dirty ||
|
||||
!g_scanner_root_states[i].usb_connect_counted) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void notify_scanner_usb_scan_complete(int slot) {
|
||||
uint8_t slot_mask = (uint8_t)(1u << slot);
|
||||
if ((g_scanner_usb_scan_result_pending_mask & slot_mask) == 0 ||
|
||||
(g_scanner_usb_mounted_mask & slot_mask) == 0 ||
|
||||
scanner_usb_slot_scan_incomplete(slot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
char usb_root[sizeof("/mnt/usb0")];
|
||||
build_scanner_usb_root_path(slot, usb_root);
|
||||
int game_count = 0;
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
char scan_path[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_path))
|
||||
continue;
|
||||
if (scanner_usb_slot_for_path(scan_path) == slot)
|
||||
game_count += g_scanner_root_states[i].usb_connect_found_games;
|
||||
}
|
||||
|
||||
g_scanner_usb_scan_result_pending_mask &= (uint8_t)~slot_mask;
|
||||
log_debug("[SCAN] USB storage scan complete: %s games=%d", usb_root,
|
||||
game_count);
|
||||
const scanner_usb_info_t *info = &g_scanner_usb_info[slot];
|
||||
const char *cluster_recommendation =
|
||||
info->block_size_bytes <= SCANNER_MAX_RECOMMENDED_CLUSTER_SIZE_BYTES
|
||||
? ""
|
||||
: sm_l10n_get(SM_L10N_USB_CLUSTER_RECOMMENDATION);
|
||||
notify_system_info_l10n(
|
||||
SM_L10N_USB_CONNECTED_SCANNING, usb_root,
|
||||
(unsigned long long)(info->available_tenths / 10ull),
|
||||
(unsigned long long)(info->available_tenths % 10ull),
|
||||
(unsigned long long)(info->capacity_tenths / 10ull),
|
||||
(unsigned long long)(info->capacity_tenths % 10ull),
|
||||
(unsigned long long)((info->block_size_bytes + 1023ull) / 1024ull),
|
||||
game_count, cluster_recommendation);
|
||||
}
|
||||
|
||||
static void schedule_pending_scanner_usb_scans(uint64_t now_us) {
|
||||
for (int slot = 0; slot < SCANNER_USB_SLOT_COUNT; slot++) {
|
||||
if ((g_scanner_usb_scan_result_pending_mask & (uint8_t)(1u << slot)) != 0)
|
||||
schedule_scan_roots_for_usb_slot(slot, now_us);
|
||||
}
|
||||
}
|
||||
|
||||
static void process_due_scanner_usb_mount_probes(uint64_t now_us) {
|
||||
if (g_scanner_usb_mount_probe_due_us == 0 ||
|
||||
g_scanner_usb_mount_probe_due_us > now_us) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_scanner_usb_mount_probe_due_us = 0;
|
||||
for (int slot = 0; slot < SCANNER_USB_SLOT_COUNT; slot++) {
|
||||
char usb_root[sizeof("/mnt/usb0")];
|
||||
build_scanner_usb_root_path(slot, usb_root);
|
||||
if (notify_scanner_usb_mount_change(usb_root))
|
||||
schedule_scan_roots_for_usb_slot(slot, now_us);
|
||||
}
|
||||
}
|
||||
|
||||
static bool resume_usb_scan_root_watch_trees(int kq) {
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
const char *scan_root = get_scan_path(i);
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(i, scan_root))
|
||||
continue;
|
||||
if (!is_usb_storage_path(scan_root))
|
||||
continue;
|
||||
// Rebuild every configured USB root because the same disk can resume
|
||||
@@ -822,6 +1055,15 @@ static void schedule_scan_root_cleanup(int scan_root_index) {
|
||||
static void schedule_scan_root_dirty(int scan_root_index, uint64_t now_us,
|
||||
bool immediate) {
|
||||
scanner_root_state_t *state = &g_scanner_root_states[scan_root_index];
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(scan_root_index, scan_root))
|
||||
return;
|
||||
int usb_slot = scanner_usb_slot_for_path(scan_root);
|
||||
if (usb_slot >= 0 &&
|
||||
(g_scanner_usb_scan_result_pending_mask &
|
||||
(uint8_t)(1u << usb_slot)) != 0) {
|
||||
state->usb_connect_counted = false;
|
||||
}
|
||||
uint64_t ready_after_us =
|
||||
immediate ? now_us : now_us + scanner_stability_wait_us();
|
||||
|
||||
@@ -847,6 +1089,7 @@ static bool scanner_event_requires_consistency_cleanup(uint32_t fflags) {
|
||||
|
||||
static bool scanner_event_requires_watch_tree_refresh(
|
||||
const scanner_event_subscription_t *subscription, uint32_t fflags) {
|
||||
char scan_root[MAX_PATH];
|
||||
uint32_t tree_change_flags =
|
||||
NOTE_WRITE | NOTE_EXTEND | NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE;
|
||||
|
||||
@@ -854,9 +1097,8 @@ static bool scanner_event_requires_watch_tree_refresh(
|
||||
case SCANNER_WATCH_SCAN_ROOT:
|
||||
return (fflags & tree_change_flags) != 0;
|
||||
case SCANNER_WATCH_SCAN_SUBDIR:
|
||||
return subscription->depth <
|
||||
get_scan_depth_for_root(
|
||||
get_scan_path(subscription->scan_root_index)) &&
|
||||
return get_scan_path(subscription->scan_root_index, scan_root) &&
|
||||
subscription->depth < get_scan_depth_for_root(scan_root) &&
|
||||
(fflags & tree_change_flags) != 0;
|
||||
default:
|
||||
return false;
|
||||
@@ -909,9 +1151,8 @@ static void schedule_scan_root_watch_tree_rebuild(
|
||||
state->watch_tree_stale = true;
|
||||
state->watch_tree_rebuild_depth = 0;
|
||||
state->watch_tree_rebuild_kind = SCANNER_WATCH_SCAN_ROOT;
|
||||
(void)strlcpy(state->watch_tree_rebuild_path,
|
||||
get_scan_path(subscription->scan_root_index),
|
||||
sizeof(state->watch_tree_rebuild_path));
|
||||
(void)get_scan_path(subscription->scan_root_index,
|
||||
state->watch_tree_rebuild_path);
|
||||
}
|
||||
|
||||
static void register_config_file_watch(int kq, uint64_t now_us) {
|
||||
@@ -998,7 +1239,8 @@ 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 ||
|
||||
if (old_cfg->api_enabled != new_cfg->api_enabled ||
|
||||
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));
|
||||
@@ -1006,9 +1248,8 @@ static bool apply_runtime_config_reload_effects(int kq,
|
||||
|
||||
sm_ampr_updater_on_config_reload(old_cfg, new_cfg);
|
||||
|
||||
if (old_cfg->backport_fakelib_enabled &&
|
||||
fakelib_runtime_config_changed(old_cfg, new_cfg))
|
||||
sm_fakelib_game_shutdown();
|
||||
// The lifecycle watcher owns the running game's fakelib overlay. Keep it
|
||||
// until exit; the next launch/cache preparation reads the new settings.
|
||||
|
||||
sm_kstuff_on_config_reload();
|
||||
|
||||
@@ -1045,8 +1286,8 @@ static bool should_abort_scan_cycle(void) {
|
||||
return should_stop_requested() || runtime_sleep_mode_active();
|
||||
}
|
||||
|
||||
static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
bool *unstable_found_out) {
|
||||
static bool run_full_scan_cycle_impl(bool startup_sync, const char *reason,
|
||||
bool *unstable_found_out) {
|
||||
scan_candidate_t *candidates = g_scanner_scan_candidates;
|
||||
|
||||
log_immediate_scan_reason(reason);
|
||||
@@ -1054,6 +1295,14 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (!startup_sync) {
|
||||
for (int slot = 0; slot < SCANNER_USB_SLOT_COUNT; slot++) {
|
||||
char usb_root[sizeof("/mnt/usb0")];
|
||||
build_scanner_usb_root_path(slot, usb_root);
|
||||
(void)notify_scanner_usb_mount_change(usb_root);
|
||||
}
|
||||
}
|
||||
|
||||
bool unstable_found = false;
|
||||
cleanup_lost_sources_before_scan();
|
||||
if (should_abort_scan_cycle())
|
||||
@@ -1096,26 +1345,51 @@ static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
return !should_abort_scan_cycle();
|
||||
}
|
||||
|
||||
static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
bool *unstable_found_out) {
|
||||
const char *scan_root = get_scan_path(scan_root_index);
|
||||
scan_candidate_t *candidates = g_scanner_scan_candidates;
|
||||
static bool run_full_scan_cycle(bool startup_sync, const char *reason,
|
||||
bool *unstable_found_out) {
|
||||
pthread_mutex_lock(&g_scanner_cycle_mutex);
|
||||
bool completed =
|
||||
run_full_scan_cycle_impl(startup_sync, reason, unstable_found_out);
|
||||
pthread_mutex_unlock(&g_scanner_cycle_mutex);
|
||||
return completed;
|
||||
}
|
||||
|
||||
log_debug("[SCAN] running targeted scan for %s", scan_root);
|
||||
static bool run_targeted_scan_cycle_impl(int scan_root_index,
|
||||
bool *unstable_found_out) {
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(scan_root_index, scan_root))
|
||||
return false;
|
||||
scan_candidate_t *candidates = g_scanner_scan_candidates;
|
||||
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
int usb_slot = scanner_usb_slot_for_path(scan_root);
|
||||
if (notify_scanner_usb_mount_change(scan_root)) {
|
||||
schedule_scan_roots_for_usb_slot_except(usb_slot, scan_root_index,
|
||||
monotonic_time_us());
|
||||
}
|
||||
log_debug("[SCAN] running targeted scan for %s", scan_root);
|
||||
|
||||
bool unstable_found = false;
|
||||
cleanup_lost_sources_for_scan_root(scan_root);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
int total_found_games = 0;
|
||||
int candidate_count = collect_scan_candidates_for_scan_root(
|
||||
scan_root, candidates, MAX_PENDING, NULL, &unstable_found);
|
||||
scan_root, candidates, MAX_PENDING, &total_found_games, &unstable_found);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
|
||||
if (usb_slot >= 0 && !unstable_found &&
|
||||
(g_scanner_usb_scan_result_pending_mask &
|
||||
(uint8_t)(1u << usb_slot)) != 0) {
|
||||
scanner_root_state_t *state = &g_scanner_root_states[scan_root_index];
|
||||
state->usb_connect_found_games = total_found_games;
|
||||
state->usb_connect_counted = true;
|
||||
}
|
||||
|
||||
process_scan_candidates(candidates, candidate_count);
|
||||
if (should_abort_scan_cycle())
|
||||
return false;
|
||||
@@ -1129,6 +1403,15 @@ static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
return !should_abort_scan_cycle();
|
||||
}
|
||||
|
||||
static bool run_targeted_scan_cycle(int scan_root_index,
|
||||
bool *unstable_found_out) {
|
||||
pthread_mutex_lock(&g_scanner_cycle_mutex);
|
||||
bool completed =
|
||||
run_targeted_scan_cycle_impl(scan_root_index, unstable_found_out);
|
||||
pthread_mutex_unlock(&g_scanner_cycle_mutex);
|
||||
return completed;
|
||||
}
|
||||
|
||||
static int find_pending_cleanup_scan_root(void) {
|
||||
for (int i = 0; i < get_scan_path_count(); i++) {
|
||||
if (g_scanner_root_states[i].cleanup_pending)
|
||||
@@ -1200,6 +1483,12 @@ static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
||||
next_deadline = g_scanner_manual_probe_due_us;
|
||||
}
|
||||
|
||||
if (g_scanner_usb_mount_probe_due_us != 0 &&
|
||||
(next_deadline == 0 ||
|
||||
g_scanner_usb_mount_probe_due_us < next_deadline)) {
|
||||
next_deadline = g_scanner_usb_mount_probe_due_us;
|
||||
}
|
||||
|
||||
if (include_scan_work) {
|
||||
if (cache_cleanup_due_us != 0 &&
|
||||
(next_deadline == 0 || cache_cleanup_due_us < next_deadline)) {
|
||||
@@ -1237,7 +1526,9 @@ static bool handle_scan_root_parent_event(
|
||||
int kq, const scanner_event_subscription_t *subscription,
|
||||
const char *watched_path, uint64_t now_us) {
|
||||
int scan_root_index = subscription->scan_root_index;
|
||||
const char *scan_root = get_scan_path(scan_root_index);
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(scan_root_index, scan_root))
|
||||
return false;
|
||||
bool root_present = false;
|
||||
bool root_changed = update_scan_root_presence_state(scan_root_index, scan_root,
|
||||
&root_present);
|
||||
@@ -1356,10 +1647,25 @@ static bool process_scanner_events(int kq, const struct timespec *timeout,
|
||||
};
|
||||
}
|
||||
|
||||
uint8_t checked_usb_slots = 0;
|
||||
for (size_t subscription_index = 0;
|
||||
subscription_index < subscription_count; subscription_index++) {
|
||||
const scanner_event_subscription_t *subscription =
|
||||
&subscriptions[subscription_index];
|
||||
char scan_root[MAX_PATH];
|
||||
if (!get_scan_path(subscription->scan_root_index, scan_root))
|
||||
continue;
|
||||
int usb_slot = scanner_usb_slot_for_path(scan_root);
|
||||
if (usb_slot >= 0) {
|
||||
uint8_t slot_mask = (uint8_t)(1u << usb_slot);
|
||||
if ((checked_usb_slots & slot_mask) == 0) {
|
||||
checked_usb_slots |= slot_mask;
|
||||
if (notify_scanner_usb_mount_change(scan_root))
|
||||
schedule_scan_roots_for_usb_slot(usb_slot, now_us);
|
||||
}
|
||||
}
|
||||
if (subscription->kind == SCANNER_WATCH_SCAN_ROOT_PARENT)
|
||||
schedule_scanner_usb_mount_probe(scan_root, now_us);
|
||||
|
||||
if (subscription->kind == SCANNER_WATCH_SCAN_ROOT_PARENT) {
|
||||
if (!handle_scan_root_parent_event(kq, subscription, watched_path,
|
||||
@@ -1436,6 +1742,7 @@ bool sm_scanner_init(void) {
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
reset_scanner_usb_mount_state();
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
|
||||
@@ -1481,10 +1788,29 @@ bool sm_scanner_usb_watches_suspended(void) {
|
||||
return atomic_load_explicit(&g_usb_watches_suspended, memory_order_acquire);
|
||||
}
|
||||
|
||||
bool sm_scanner_try_begin_external_mutation(void) {
|
||||
return pthread_mutex_trylock(&g_scanner_cycle_mutex) == 0;
|
||||
}
|
||||
|
||||
void sm_scanner_end_external_mutation(void) {
|
||||
pthread_mutex_unlock(&g_scanner_cycle_mutex);
|
||||
}
|
||||
|
||||
bool sm_scanner_run_startup_sync(void) {
|
||||
while (!should_stop_requested()) {
|
||||
while (runtime_sleep_mode_active() && !should_stop_requested())
|
||||
sceKernelUsleep(200000);
|
||||
while (runtime_sleep_mode_active() && !should_stop_requested()) {
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(g_scanner_wake_pipe[0], &readfds);
|
||||
int rc = select(g_scanner_wake_pipe[0] + 1, &readfds, NULL, NULL, NULL);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
log_debug(" [SCAN] startup sleep wait failed: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
drain_scanner_wake_pipe();
|
||||
}
|
||||
|
||||
if (should_stop_requested())
|
||||
return false;
|
||||
@@ -1561,6 +1887,7 @@ void sm_scanner_run_loop(void) {
|
||||
return;
|
||||
}
|
||||
clear_all_dirty_scan_roots();
|
||||
g_scanner_usb_mount_probe_due_us = 0;
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
wake_game_lifecycle_watcher();
|
||||
@@ -1607,8 +1934,17 @@ void sm_scanner_run_loop(void) {
|
||||
bool game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
char scan_reason[128];
|
||||
bool reset_attempts = false;
|
||||
if (!game_mount_busy &&
|
||||
consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
||||
consume_scan_now_request(scan_reason, sizeof(scan_reason),
|
||||
&reset_attempts)) {
|
||||
if (reset_attempts) {
|
||||
size_t reset_title_count = reset_all_title_attempts();
|
||||
size_t reset_image_count = reset_all_image_mount_attempts();
|
||||
log_debug(" [SCAN] reset retry counters before requested full scan: "
|
||||
"titles=%zu images=%zu",
|
||||
reset_title_count, reset_image_count);
|
||||
}
|
||||
bool unstable_found = false;
|
||||
if (!run_full_scan_cycle(false, scan_reason, &unstable_found)) {
|
||||
if (runtime_sleep_mode_active())
|
||||
@@ -1623,6 +1959,7 @@ void sm_scanner_run_loop(void) {
|
||||
request_scanner_shutdown("scanner watcher refresh failed");
|
||||
return;
|
||||
}
|
||||
schedule_pending_scanner_usb_scans(monotonic_time_us());
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
@@ -1635,6 +1972,7 @@ void sm_scanner_run_loop(void) {
|
||||
}
|
||||
|
||||
uint64_t now_us = monotonic_time_us();
|
||||
process_due_scanner_usb_mount_probes(now_us);
|
||||
game_mount_busy = sm_game_lifecycle_has_active_game() ||
|
||||
sm_shellcore_service_has_prepared_mount();
|
||||
if (game_mount_busy) {
|
||||
@@ -1667,15 +2005,15 @@ void sm_scanner_run_loop(void) {
|
||||
g_scanner_config_reload_ready_after_us = 0;
|
||||
|
||||
uint32_t old_scan_topology_hash = scanner_config_topology_hash();
|
||||
runtime_config_t old_cfg = *runtime_config();
|
||||
runtime_config_t old_cfg = runtime_config();
|
||||
bool reloaded = false;
|
||||
if (!reload_runtime_config_if_changed(&reloaded)) {
|
||||
log_debug(" [CFG] runtime config reload failed");
|
||||
} else if (reloaded) {
|
||||
const runtime_config_t *new_cfg = runtime_config();
|
||||
const runtime_config_t new_cfg = runtime_config();
|
||||
bool scan_topology_changed =
|
||||
old_scan_topology_hash != scanner_config_topology_hash();
|
||||
if (!apply_runtime_config_reload_effects(kq, &old_cfg, new_cfg,
|
||||
if (!apply_runtime_config_reload_effects(kq, &old_cfg, &new_cfg,
|
||||
scan_topology_changed)) {
|
||||
close(kq);
|
||||
clear_scanner_watch_entries();
|
||||
@@ -1718,6 +2056,7 @@ void sm_scanner_run_loop(void) {
|
||||
request_scanner_shutdown("scanner watcher refresh after manual scan failed");
|
||||
return;
|
||||
}
|
||||
schedule_pending_scanner_usb_scans(monotonic_time_us());
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
@@ -1731,7 +2070,9 @@ void sm_scanner_run_loop(void) {
|
||||
|
||||
uint64_t install_wake_us = sm_install_next_wake_us(now_us);
|
||||
if (install_wake_us != 0 && now_us >= install_wake_us) {
|
||||
pthread_mutex_lock(&g_scanner_cycle_mutex);
|
||||
sm_install_service_pending();
|
||||
pthread_mutex_unlock(&g_scanner_cycle_mutex);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1758,6 +2099,7 @@ void sm_scanner_run_loop(void) {
|
||||
request_scanner_shutdown("scanner watcher refresh after full resync failed");
|
||||
return;
|
||||
}
|
||||
schedule_pending_scanner_usb_scans(monotonic_time_us());
|
||||
|
||||
now_us = monotonic_time_us();
|
||||
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
||||
@@ -1773,7 +2115,9 @@ void sm_scanner_run_loop(void) {
|
||||
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));
|
||||
char scan_root[MAX_PATH];
|
||||
if (get_scan_path(cleanup_root_index, scan_root))
|
||||
cleanup_lost_sources_for_scan_root(scan_root);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1838,6 +2182,11 @@ void sm_scanner_run_loop(void) {
|
||||
|
||||
if (unstable_found)
|
||||
schedule_scan_root_dirty(dirty_root_index, monotonic_time_us(), false);
|
||||
char scan_root[MAX_PATH];
|
||||
(void)get_scan_path(dirty_root_index, scan_root);
|
||||
int usb_slot = scanner_usb_slot_for_path(scan_root);
|
||||
if (usb_slot >= 0)
|
||||
notify_scanner_usb_scan_complete(usb_slot);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1875,6 +2224,10 @@ void sm_scanner_shutdown(void) {
|
||||
reset_scanner_root_states();
|
||||
clear_scanner_config_reload_state();
|
||||
clear_scanner_manual_scan_state();
|
||||
g_scanner_usb_mounted_mask = 0;
|
||||
g_scanner_usb_scan_result_pending_mask = 0;
|
||||
memset(g_scanner_usb_info, 0, sizeof(g_scanner_usb_info));
|
||||
g_scanner_usb_mount_probe_due_us = 0;
|
||||
atomic_store_explicit(&g_usb_watches_suspended, true,
|
||||
memory_order_release);
|
||||
}
|
||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user