Compare commits

..
40 Commits
Author SHA1 Message Date
Roman Tarasov 4102fa7aa5 Docs 2026-09-20 23:38:25 +03:00
Roman Tarasov d71bacfbc0 Add Kstuff and ShellCore offsets for PS5 firmware 13.00–13.60 2026-09-20 23:28:12 +03:00
Roman Tarasov 072c17b050 Use full ShellCore bridge cave 2026-09-11 23:49:18 +03:00
Roman Tarasov 7bf0a6efaf Remove kstuff mount hook toggling 2026-09-11 23:41:49 +03:00
Roman Tarasov 2424c98ed4 Mount package fakelib before game spawn 2026-09-11 04:53:03 +03:00
Roman Tarasov c08e207755 Expand ShellCore bridge firmware support 2026-09-11 04:51:52 +03:00
Roman Tarasov 3d55f1c4c7 Resume mount hooks when game process exits 2026-09-11 03:13:26 +03:00
Roman Tarasov 18d0f141e7 Hook ShellCore immediately before game spawn 2026-09-11 02:59:41 +03:00
Roman Tarasov 86c67639f6 Avoid ShellCore execute-only data reads 2026-09-11 01:37:01 +03:00
Roman Tarasov aa69be7892 Mount fakelib before game process spawn 2026-09-11 00:35:24 +03:00
Roman Tarasov 87fbe72b46 Pause mount hooks while games run 2026-09-10 22:15:31 +03:00
Roman Tarasov 6b7dc2ced5 fix: wake runtime waiters when STOP file is detected 2026-09-06 23:08:40 +03:00
Roman Tarasov 7b3c0bd9ba fix: publish runtime config through synchronized snapshots 2026-09-06 23:06:58 +03:00
Roman Tarasov 6488632c73 fix: preserve active fakelib overlays across config reload 2026-09-06 22:57:36 +03:00
Roman Tarasov 5d3ebd8caa Repair backport permissions during scans 2026-09-06 22:14:09 +03:00
Roman Tarasov a57696a512 Wait for successful bridge mprotect before hooking 2026-09-04 06:18:47 +03:00
Roman Tarasov 0d7544a5d6 Add translations for all supported locales 2026-09-03 03:25:43 +03:00
Roman Tarasov ad0ca65f9a Add missing-app cleanup and AMPR update settings 2026-09-03 03:10:18 +03:00
Roman Tarasov 297b9003f2 Add ShadowMount+ Shell UI launcher 2026-09-03 03:00:14 +03:00
Roman Tarasov dc363e69f1 Show web control address in startup notification 2026-09-02 02:22:27 +03:00
Roman Tarasov 5d4f48cedd Keep bridge pages resident and recover stale hooks 2026-09-02 02:12:20 +03:00
Roman Tarasov 5b7ab6ef8c Expand web API and add embedded library interface 2026-09-02 01:54:06 +03:00
Roman Tarasov f81eb60e94 Improve multilingual library controls and game actions (web) 2026-09-02 00:01:42 +03:00
Roman Tarasov 9e6b7165a0 ci/cd 2026-09-01 13:21:55 +03:00
Roman Tarasov 960204baa0 Add resilient async game library management 2026-09-01 13:16:19 +03:00
Roman Tarasov f560898b2c Add HTTP game management API 2026-09-01 02:32:15 +03:00
Roman Tarasov 3a167431a3 Make trampoline execution safe 2026-08-31 19:02:48 +03:00
Roman Tarasov ff0bc74468 Warn about USB clusters larger than 64 KB 2026-08-28 23:25:57 +03:00
BlackBearReloaded 5b9816dd08 lifecycle: support FAKE title IDs 2026-08-28 21:42:11 +03:00
Roman Tarasov f3df8f040d Pause background services during system sleep 2026-08-28 21:39:00 +03:00
Roman Tarasov b564c74611 Use optimized exFAT, UFS/FFPKG and PFS/FFPFS mount profiles only for direct source paths under /data or /user 2026-08-28 19:59:26 +03:00
Roman Tarasov 7671106a32 Add USB connection and scan notifications 2026-08-25 23:51:17 +03:00
Roman Tarasov b1a0dce78d Separate SQLite operations from snapshot cache state 2026-08-25 22:07:42 +03:00
Roman Tarasov 01ea403b88 Prevent detach and cache invalidation reuse races 2026-08-25 15:21:21 +03:00
Roman Tarasov 06576fabe3 Make mount and unmount lifecycle race-safe 2026-08-25 14:53:48 +03:00
Roman Tarasov 01f5cf7182 Enforce one-to-one source and mount mappings 2026-08-25 13:32:37 +03:00
Roman Tarasov 640d8bdd59 Retain attached devices until detach completes 2026-08-25 13:23:35 +03:00
Roman Tarasov bc34a3bb26 Avoid aborting HTTP request under updater mutex 2026-08-25 13:00:23 +03:00
Roman Tarasov 3638cdaae5 Update translations 2026-08-24 22:44:30 +03:00
Roman Tarasov 3190100de8 Keep fakelib2 exclusive from global and emulator overlays 2026-08-24 22:06:20 +03:00
118 changed files with 12717 additions and 2113 deletions

No files matched your search

+2 -1
View File
@@ -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
+19 -1
View File
@@ -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: |
+18 -7
View File
@@ -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)
+17 -19
View File
@@ -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.
+163
View File
@@ -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.
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
-97
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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ı",
};
+101
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 已中斷連接",
};
+2
View File
@@ -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);
+19
View File
@@ -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
+2
View File
@@ -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
+21
View File
@@ -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
+20 -5
View File
@@ -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.
+6 -1
View File
@@ -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);
+16
View File
@@ -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
View File
@@ -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
+31 -7
View File
@@ -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,
+3 -2
View File
@@ -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.
+8
View File
@@ -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);
+8
View File
@@ -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.
+4
View File
@@ -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;
+5
View File
@@ -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)
+8
View File
@@ -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,
+5
View File
@@ -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
View File
@@ -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,
+2 -2
View File
@@ -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.
+7
View File
@@ -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.
+2
View File
@@ -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"
+6 -2
View File
@@ -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);
+4
View File
@@ -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.
+7
View File
@@ -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
+5
View File
@@ -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;
+3 -1
View File
@@ -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 {
+16 -1
View File
@@ -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
+1 -4
View File
@@ -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);
+9 -2
View File
@@ -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
+48
View File
@@ -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
+9
View File
@@ -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
View File
@@ -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;
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+71 -8
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+362 -216
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+6 -7
View File
@@ -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
View File
@@ -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) {
+195
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+376 -45
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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