mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-20 21:59:57 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe6bbb92b | ||
|
|
896e5af0fd | ||
|
|
15776edb8b | ||
|
|
1bc6477ea7 | ||
|
|
eb2a4c1036 |
No files matched your search
+16
-35
@@ -3,7 +3,7 @@
|
||||
PURPOSE
|
||||
-------
|
||||
Create a RAW image file, mount it via OSFMount as a logical volume,
|
||||
auto-select FAT32/exFAT by image size, and either:
|
||||
format it as exFAT, and either:
|
||||
- format + copy + dismount (default), or
|
||||
- create + mount only for manual steps.
|
||||
|
||||
@@ -45,11 +45,10 @@
|
||||
NOTES
|
||||
-----
|
||||
- This script does NOT auto-elevate. Start PowerShell as Administrator.
|
||||
- FS selection is automatic by image size:
|
||||
* < 4 GB -> FAT32, cluster 65536
|
||||
* >= 4 GB -> exFAT, cluster auto-selected:
|
||||
- large-file sets: 65536
|
||||
- small/mixed-file sets: 32768
|
||||
- Filesystem is always exFAT.
|
||||
- Cluster size is auto-selected:
|
||||
- large-file sets: 65536
|
||||
- small/mixed-file sets: 32768
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
@@ -89,9 +88,9 @@ function Find-OSFMountCom {
|
||||
if ($cmd) { return $cmd.Source }
|
||||
|
||||
$candidates = @(
|
||||
"$env:ProgramFiles\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles}\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\OSFMount\osfmount.com",
|
||||
"$env:ProgramFiles\PassMark\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles}\PassMark\OSFMount\osfmount.com",
|
||||
"${env:ProgramFiles(x86)}\PassMark\OSFMount\osfmount.com"
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
|
||||
@@ -245,11 +244,9 @@ function Dismount-OsfVolume([string]$osfPath, [string]$mountPoint, [int]$maxAtte
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-FormatVolume([string]$driveLetter, [string]$fileSystem, [int]$clusterSize, [string]$label) {
|
||||
function Invoke-FormatVolume([string]$driveLetter, [int]$clusterSize, [string]$label) {
|
||||
$target = "${driveLetter}:"
|
||||
if ($fileSystem -notin @("FAT32", "exFAT")) {
|
||||
throw "Unsupported file system '$fileSystem'. Expected FAT32 or exFAT."
|
||||
}
|
||||
$fileSystem = "exFAT"
|
||||
$clusterArg = Format-AllocationUnitArg -clusterSize $clusterSize
|
||||
$attempts = @(
|
||||
@{ Name = "$fileSystem quick with requested allocation unit"; Args = @($target, "/FS:$fileSystem", "/A:$clusterArg", "/Q", "/V:$label", "/X", "/Y") }
|
||||
@@ -311,40 +308,23 @@ if (Test-Path $ImagePath) {
|
||||
|
||||
[Int64]$expectedBytes = 0
|
||||
[string]$osfSizeArg = $null
|
||||
[Int64]$FsSwitchThresholdBytes = 4GB
|
||||
[int]$Fat32ClusterSize = 65536
|
||||
[int]$ExfatClusterSize = 65536
|
||||
[int]$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
[bool]$sizeProvided = -not [string]::IsNullOrWhiteSpace($Size)
|
||||
[string]$TargetFs = ""
|
||||
[int]$TargetClusterSize = 0
|
||||
[int]$TargetClusterSize = $ExfatClusterSize
|
||||
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Size not provided. Computing an optimal image size from '$SourceDir'..."
|
||||
$fat32Candidate = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $Fat32ClusterSize
|
||||
if ($fat32Candidate -lt $FsSwitchThresholdBytes) {
|
||||
$expectedBytes = $fat32Candidate
|
||||
} else {
|
||||
$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $ExfatClusterSize
|
||||
}
|
||||
$expectedBytes = Get-OptimalImageSizeBytes -dir $SourceDir -clusterBytes $TargetClusterSize
|
||||
$osfSizeArg = "$expectedBytes"
|
||||
} else {
|
||||
$expectedBytes = Parse-SizeToBytes $Size
|
||||
$osfSizeArg = $Size
|
||||
}
|
||||
|
||||
if ($expectedBytes -lt $FsSwitchThresholdBytes) {
|
||||
$TargetFs = "FAT32"
|
||||
$TargetClusterSize = $Fat32ClusterSize
|
||||
} else {
|
||||
$TargetFs = "exFAT"
|
||||
$ExfatClusterSize = Get-OptimalExfatClusterSize -dir $SourceDir
|
||||
$TargetClusterSize = $ExfatClusterSize
|
||||
}
|
||||
if (-not $sizeProvided) {
|
||||
Write-Host "[Info] Computed image size: $(Format-Bytes $expectedBytes) ($expectedBytes bytes)."
|
||||
}
|
||||
Write-Host "[Info] Selected filesystem: $TargetFs (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
|
||||
Write-Host "[Info] Selected filesystem: exFAT (cluster=$TargetClusterSize) for image size $(Format-Bytes $expectedBytes)."
|
||||
|
||||
$osf = Find-OSFMountCom
|
||||
|
||||
@@ -380,8 +360,8 @@ try {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[2/4] Formatting $dest as $TargetFs (cluster=$TargetClusterSize, label='$Label') via format.com ..."
|
||||
Invoke-FormatVolume -driveLetter $DriveLetter -fileSystem $TargetFs -clusterSize $TargetClusterSize -label $Label
|
||||
Write-Host "[2/4] Formatting $dest as exFAT (cluster=$TargetClusterSize, label='$Label') via format.com ..."
|
||||
Invoke-FormatVolume -driveLetter $DriveLetter -clusterSize $TargetClusterSize -label $Label
|
||||
|
||||
if (-not (Test-Path $dest)) { throw "Drive $dest is not accessible after formatting." }
|
||||
|
||||
@@ -390,6 +370,7 @@ try {
|
||||
$SourceDir, $dest,
|
||||
"/E", "/COPY:DAT", "/DCOPY:DAT",
|
||||
"/R:1", "/W:1",
|
||||
"/NFL", "/NDL",
|
||||
"/ETA"
|
||||
)
|
||||
& robocopy.exe @roboArgs
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# ShadowMountPlus (PS5)
|
||||
|
||||
**Version:** `1.5beta3`
|
||||
**Version:** `1.5beta4`
|
||||
|
||||
**Repository:** https://github.com/drakmor/shadowMountPlus
|
||||
|
||||
**Warning! Mounting images can cause shutdown problems and data corruption on internal drives! This depends on many factors, but is more common with older firmware versions. Please take this into account when testing.**
|
||||
|
||||
|
||||
**ShadowMountPlus** is a fully automated, background "Auto-Mounter" payload for Jailbroken PlayStation 5 consoles. It streamlines the game mounting process by eliminating the need for manual configuration or external tools (such as DumpRunner or Itemzflow). ShadowMountPlus automatically detects, mounts, and installs game dumps from both **internal and external storage**.
|
||||
|
||||
@@ -17,19 +19,19 @@
|
||||
|
||||
| Extension | Mounted FS | Attach backend | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Stable |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | High performance |
|
||||
| `.exfat` | `exfatfs` | `LVD` or `MD` (configurable) | Optimal |
|
||||
| `.ffpkg` | `ufs` | `LVD` or `MD` (configurable) | Legacy |
|
||||
| `.ffpfs` | `pfs` | `LVD` | Experimental |
|
||||
|
||||
Notes:
|
||||
- Backend, read-only mode, and sector size can be configured via `/data/shadowmount/config.ini`.
|
||||
- Debug logging is enabled by default (`debug=1`) and writes to console plus `/data/shadowmount/debug.log` (set `debug=0` to disable).
|
||||
- **On 4.xx firmware, shutting down the console may not work correctly when using image files.**
|
||||
- **exFAT is the preferred image filesystem, including on 4.xx firmware, with no known reboot/shutdown issues in typical use.**
|
||||
|
||||
## Recommended FS choice
|
||||
|
||||
- Prefer **UFS (`.ffpkg`)** for games that work correctly on UFS: it is generally more performant (~1.5-2.5 x exFAT).
|
||||
- Use **exFAT (`.exfat`)** for games that only work from external-disk style layouts, because exFAT is case-insensitive.
|
||||
- Prefer **exFAT (`.exfat`)** in most cases: it is generally more performant, case-insensitive, and does not have reboot/shutdown issues in typical use.
|
||||
- Use **UFS (`.ffpkg`)** only when specifically needed for compatibility with your game/setup.
|
||||
|
||||
## Runtime config (`/data/shadowmount/config.ini`)
|
||||
|
||||
@@ -39,6 +41,8 @@ Supported keys (all optional):
|
||||
- `debug=1|0` (`1` enables `log_debug` output to console + `/data/shadowmount/debug.log`; default is `1`)
|
||||
- `mount_read_only=1|0` (default: `1`)
|
||||
- `recursive_scan=1|0` (`0` = scan only first-level subfolders, `1` = recursive scan without depth limit; default: `0`)
|
||||
- `scan_interval_seconds=<1..3600>` (full scan loop interval; default: `10`)
|
||||
- `stability_wait_seconds=<0..3600>` (minimum source age before processing; default: `10`)
|
||||
- `exfat_backend=lvd|md` (default: `lvd`)
|
||||
- `ufs_backend=lvd|md` (default: `lvd`)
|
||||
- `scanpath=<absolute_path>` (can be repeated on multiple lines; default: built-in scan path list below)
|
||||
@@ -53,7 +57,8 @@ Scan path behavior:
|
||||
- `/data/ufsmnt` is always added automatically, even with custom paths.
|
||||
- With `recursive_scan=0` (default), only first-level subfolders are checked.
|
||||
- With `recursive_scan=1`, subfolders are scanned recursively.
|
||||
- Full scan loop runs every 10 seconds.
|
||||
- Full scan loop runs every `scan_interval_seconds` (default: `10`).
|
||||
- Sources newer than `stability_wait_seconds` are deferred until stable (default: `10`).
|
||||
|
||||
Validation:
|
||||
- See `config.ini.example` for a ready-to-use template.
|
||||
@@ -148,7 +153,9 @@ FreeBSD:
|
||||
|
||||
Windows:
|
||||
- You can create UFS2 images with **UFS2Tool** https://github.com/SvenGDK/UFS2Tool.
|
||||
- Recommended output format for this project is `.ffpkg`.
|
||||
- Example:
|
||||
- `UFS2Tool.exe newfs -O 2 -b 32768 -f 4096 -m 0 -i 262144 -D ./APPXXXX ./PPSA12345.ffpkg`
|
||||
|
||||
|
||||
## Installation and usage
|
||||
|
||||
@@ -187,6 +194,7 @@ If a game is not mounted:
|
||||
- Verify scan depth:
|
||||
- `recursive_scan=0` scans only first-level subfolders;
|
||||
- `recursive_scan=1` scans recursively.
|
||||
- If logs show `source not stable yet`, adjust `stability_wait_seconds` (or wait for source copy/write to finish).
|
||||
- Verify game structure:
|
||||
- folder game: `<GAME_DIR>/sce_sys/param.json`;
|
||||
- image game (`.ffpkg` / `.exfat` / `.ffpfs`): `sce_sys/param.json` must be at image root (no extra top-level folder).
|
||||
|
||||
@@ -19,6 +19,13 @@ mount_read_only=1
|
||||
# 0/false/no/off -> scan only first-level subdirectories (default)
|
||||
recursive_scan=0
|
||||
|
||||
# Full scan loop interval (seconds), range: 1..3600
|
||||
scan_interval_seconds=10
|
||||
|
||||
# Source stabilization wait window (seconds), range: 0..3600
|
||||
# If source mtime is newer than this window, it will be deferred to next scan.
|
||||
stability_wait_seconds=10
|
||||
|
||||
# Backend selection per filesystem:
|
||||
# lvd -> /dev/lvdctl -> /dev/lvdN
|
||||
# md -> /dev/mdctl -> /dev/mdN
|
||||
|
||||
+213
-371
@@ -27,10 +27,10 @@
|
||||
#include <sqlite3.h>
|
||||
|
||||
// --- Configuration ---
|
||||
#define SCAN_INTERVAL_US 10000000
|
||||
#define DEFAULT_SCAN_INTERVAL_US 10000000u
|
||||
#define DEFAULT_STABILITY_WAIT_SECONDS 10u
|
||||
#define MAX_PENDING 512
|
||||
#define MAX_UFS_MOUNTS 64
|
||||
#define MAX_NULLFS_MOUNTS MAX_PENDING
|
||||
#define PATH_STATE_CAPACITY MAX_PENDING
|
||||
#define TITLE_STATE_CAPACITY MAX_PENDING
|
||||
#define STATE_HASH_SIZE 1024u
|
||||
@@ -39,6 +39,9 @@
|
||||
#define MAX_MISSING_PARAM_SCAN_ATTEMPTS 3
|
||||
#define MAX_IMAGE_MOUNT_ATTEMPTS 3
|
||||
#define IMAGE_MOUNT_READ_ONLY 1
|
||||
#define MIN_SCAN_INTERVAL_SECONDS 1u
|
||||
#define MAX_SCAN_INTERVAL_SECONDS 3600u
|
||||
#define MAX_STABILITY_WAIT_SECONDS 3600u
|
||||
#define APP_DB_QUERY_BUSY_RETRIES 3
|
||||
#define APP_DB_UPDATE_BUSY_RETRIES 25
|
||||
#define APP_DB_PREPARE_BUSY_RETRIES 25
|
||||
@@ -78,10 +81,10 @@
|
||||
#define SCE_LVD_IOC_ATTACH 0xC0286D00
|
||||
#define SCE_LVD_IOC_DETACH 0xC0286D01
|
||||
#define LVD_ATTACH_IO_VERSION 1
|
||||
#define LVD_ATTACH_OPTION_FLAGS_DEFAULT 0x8
|
||||
#define LVD_ATTACH_OPTION_FLAGS_RW 0x9
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RO 0x16
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RW 0x1E
|
||||
#define LVD_ATTACH_OPTION_FLAGS_DEFAULT 0x9
|
||||
#define LVD_ATTACH_OPTION_FLAGS_RW 0x8
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RO 0x1E
|
||||
#define LVD_ATTACH_OPTION_NORM_DD_RW 0x16
|
||||
#define LVD_SECTOR_SIZE_EXFAT 512u
|
||||
#define LVD_SECTOR_SIZE_UFS 4096u
|
||||
#define LVD_SECTOR_SIZE_PFS 32768u
|
||||
@@ -224,7 +227,7 @@ void notify_system(const char *fmt, ...);
|
||||
void log_debug(const char *fmt, ...);
|
||||
static void close_app_db(void);
|
||||
static void shutdown_ufs_mounts(void);
|
||||
static void shutdown_nullfs_mounts(void);
|
||||
static void ensure_runtime_config_ready(void);
|
||||
|
||||
// Standard Notification
|
||||
typedef struct notify_request {
|
||||
@@ -300,10 +303,10 @@ struct TitleStateEntry {
|
||||
struct TitleStateEntry g_title_state[TITLE_STATE_CAPACITY];
|
||||
static uint16_t g_title_state_hash[STATE_HASH_SIZE];
|
||||
|
||||
struct AppDbLookupCache {
|
||||
char title_id[MAX_TITLE_ID];
|
||||
bool in_app_db;
|
||||
bool valid;
|
||||
struct AppDbTitleList {
|
||||
char(*ids)[MAX_TITLE_ID];
|
||||
int count;
|
||||
int capacity;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -338,6 +341,8 @@ typedef struct {
|
||||
bool debug_enabled;
|
||||
bool mount_read_only;
|
||||
bool recursive_scan;
|
||||
uint32_t scan_interval_us;
|
||||
uint32_t stability_wait_seconds;
|
||||
attach_backend_t exfat_backend;
|
||||
attach_backend_t ufs_backend;
|
||||
uint32_t lvd_sector_exfat;
|
||||
@@ -350,10 +355,8 @@ typedef struct {
|
||||
static runtime_config_t g_runtime_cfg;
|
||||
static bool g_runtime_cfg_ready = false;
|
||||
static sqlite3 *g_app_db = NULL;
|
||||
static sqlite3_stmt *g_app_db_stmt_has_title = NULL;
|
||||
static sqlite3_stmt *g_app_db_stmt_update_snd0 = NULL;
|
||||
static scan_candidate_t g_scan_candidates[MAX_PENDING];
|
||||
static struct AppDbLookupCache g_scan_db_lookup_cache[MAX_PENDING];
|
||||
static char g_scan_discovered_param_roots[MAX_PENDING][MAX_PATH];
|
||||
|
||||
struct UfsCache {
|
||||
@@ -368,14 +371,6 @@ struct UfsCache {
|
||||
};
|
||||
struct UfsCache ufs_cache[MAX_UFS_MOUNTS];
|
||||
|
||||
struct NullfsCache {
|
||||
// Mounted target path in /system_ex/app/...
|
||||
char mount_point[MAX_PATH];
|
||||
// Slot occupancy flag.
|
||||
bool valid;
|
||||
};
|
||||
struct NullfsCache nullfs_cache[MAX_NULLFS_MOUNTS];
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested = 0;
|
||||
|
||||
typedef enum {
|
||||
@@ -790,7 +785,7 @@ static void clear_image_mount_attempts(const char *path) {
|
||||
entry->image_mount_limit_logged = false;
|
||||
}
|
||||
|
||||
// --- Active Mount and NullFS Session Caches ---
|
||||
// --- Active Mount Session Caches ---
|
||||
static void cache_ufs_mount(const char *path, int unit_id,
|
||||
attach_backend_t backend) {
|
||||
for (int k = 0; k < MAX_UFS_MOUNTS; k++) {
|
||||
@@ -812,35 +807,6 @@ static void cache_ufs_mount(const char *path, int unit_id,
|
||||
}
|
||||
}
|
||||
|
||||
static void cache_nullfs_mount(const char *mount_point) {
|
||||
for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) {
|
||||
if (nullfs_cache[k].valid &&
|
||||
strcmp(nullfs_cache[k].mount_point, mount_point) == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) {
|
||||
if (!nullfs_cache[k].valid) {
|
||||
(void)strlcpy(nullfs_cache[k].mount_point, mount_point,
|
||||
sizeof(nullfs_cache[k].mount_point));
|
||||
nullfs_cache[k].valid = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void invalidate_nullfs_mount(const char *mount_point) {
|
||||
for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) {
|
||||
if (!nullfs_cache[k].valid)
|
||||
continue;
|
||||
if (strcmp(nullfs_cache[k].mount_point, mount_point) == 0) {
|
||||
nullfs_cache[k].valid = false;
|
||||
nullfs_cache[k].mount_point[0] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Shutdown and Stop Signal Handling ---
|
||||
static void on_signal(int sig) {
|
||||
(void)sig;
|
||||
@@ -977,28 +943,19 @@ static bool read_mount_link(const char *title_id, char *out, size_t out_size) {
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
static void build_system_ex_app_path(const char *title_id, char *out,
|
||||
size_t out_size) {
|
||||
if (out_size == 0)
|
||||
return;
|
||||
snprintf(out, out_size, "/system_ex/app/%s", title_id);
|
||||
}
|
||||
static bool mount_link_matches_source(const char *title_id,
|
||||
const char *source_path) {
|
||||
if (!source_path || source_path[0] == '\0')
|
||||
return false;
|
||||
|
||||
static bool mount_link_matches_system_ex(const char *title_id) {
|
||||
char tracked_path[MAX_PATH];
|
||||
char expected_path[MAX_PATH];
|
||||
if (!read_mount_link(title_id, tracked_path, sizeof(tracked_path)))
|
||||
return false;
|
||||
build_system_ex_app_path(title_id, expected_path, sizeof(expected_path));
|
||||
return strcmp(tracked_path, expected_path) == 0;
|
||||
return strcmp(tracked_path, source_path) == 0;
|
||||
}
|
||||
|
||||
// --- app.db Access Layer ---
|
||||
static void close_app_db(void) {
|
||||
if (g_app_db_stmt_has_title) {
|
||||
sqlite3_finalize(g_app_db_stmt_has_title);
|
||||
g_app_db_stmt_has_title = NULL;
|
||||
}
|
||||
if (g_app_db_stmt_update_snd0) {
|
||||
sqlite3_finalize(g_app_db_stmt_update_snd0);
|
||||
g_app_db_stmt_update_snd0 = NULL;
|
||||
@@ -1057,147 +1014,6 @@ static int app_db_prepare_with_retry(const char *sql, sqlite3_stmt **stmt_out,
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool init_app_triggers(void) {
|
||||
static const char *find_sql =
|
||||
"SELECT substr(name, 14) FROM sqlite_master "
|
||||
"WHERE type='table' "
|
||||
"AND name GLOB 'tbl_iconinfo_[0-9]*' "
|
||||
"LIMIT 1;";
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_UPDATE_BUSY_RETRIES; attempt++) {
|
||||
if (!ensure_app_db_open())
|
||||
return false;
|
||||
|
||||
char suffix[21];
|
||||
suffix[0] = '\0';
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(g_app_db, find_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
int step_rc = sqlite3_step(stmt);
|
||||
if (step_rc == SQLITE_ROW) {
|
||||
const char *val = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (val)
|
||||
(void)strlcpy(suffix, val, sizeof(suffix));
|
||||
rc = SQLITE_OK;
|
||||
} else {
|
||||
rc = step_rc;
|
||||
}
|
||||
}
|
||||
if (stmt)
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_OK && rc != SQLITE_DONE) {
|
||||
log_debug(" [DB] query tbl_iconinfo failed: rc=%d err=%s", rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, true))
|
||||
continue;
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (suffix[0] == '\0') {
|
||||
log_debug(" [DB] no tbl_iconinfo table found, skipping triggers");
|
||||
close_app_db();
|
||||
return true;
|
||||
}
|
||||
|
||||
char table_name[34];
|
||||
snprintf(table_name, sizeof(table_name), "tbl_iconinfo_%s", suffix);
|
||||
|
||||
char sql[2048];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"CREATE TRIGGER IF NOT EXISTS trig_update_drm_tbl_iconinfo_%s "
|
||||
"AFTER UPDATE OF appDrmType ON %s "
|
||||
"WHEN new.appDrmType = 1 "
|
||||
"BEGIN "
|
||||
"UPDATE %s SET appDrmType = 5 WHERE titleId = old.titleId; "
|
||||
"END;"
|
||||
"CREATE TRIGGER IF NOT EXISTS trig_insert_drm_tbl_iconinfo_%s "
|
||||
"AFTER INSERT ON %s "
|
||||
"WHEN new.appDrmType = 1 "
|
||||
"BEGIN "
|
||||
"UPDATE %s SET appDrmType = 5 WHERE titleId = new.titleId; "
|
||||
"END;"
|
||||
"UPDATE %s SET appDrmType=5 WHERE appDrmType=1;",
|
||||
suffix, table_name, table_name, suffix, table_name, table_name,
|
||||
table_name);
|
||||
|
||||
char *err = NULL;
|
||||
rc = sqlite3_exec(g_app_db, sql, NULL, NULL, &err);
|
||||
if (rc == SQLITE_OK) {
|
||||
int changes = sqlite3_changes(g_app_db);
|
||||
log_debug(" [DB] triggers updated for %s, fixed rows=%d",
|
||||
table_name, changes);
|
||||
close_app_db();
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [DB] update triggers failed: rc=%d err=%s", rc,
|
||||
err ? err : (g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
if (err)
|
||||
sqlite3_free(err);
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_UPDATE_BUSY_RETRIES, true))
|
||||
continue;
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
|
||||
static int query_title_registered_in_app_db(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return 0;
|
||||
|
||||
if (!g_app_db_stmt_has_title) {
|
||||
const char *sql =
|
||||
"SELECT 1 FROM tbl_contentinfo WHERE titleId = ?1 LIMIT 1;";
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &g_app_db_stmt_has_title,
|
||||
APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"titleId check");
|
||||
if (prep_rc != SQLITE_OK)
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < APP_DB_QUERY_BUSY_RETRIES; attempt++) {
|
||||
sqlite3_reset(g_app_db_stmt_has_title);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_has_title);
|
||||
if (sqlite3_bind_text(g_app_db_stmt_has_title, 1, title_id, -1,
|
||||
SQLITE_TRANSIENT) != SQLITE_OK) {
|
||||
log_debug(" [DB] bind failed for titleId check: %s",
|
||||
sqlite3_errmsg(g_app_db));
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(g_app_db_stmt_has_title);
|
||||
if (rc == SQLITE_ROW || rc == SQLITE_DONE) {
|
||||
int ret = (rc == SQLITE_ROW) ? 1 : 0;
|
||||
// Always reset after stepping, to avoid keeping a cursor/lock open.
|
||||
sqlite3_reset(g_app_db_stmt_has_title);
|
||||
sqlite3_clear_bindings(g_app_db_stmt_has_title);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (app_db_wait_retry(rc, attempt, APP_DB_QUERY_BUSY_RETRIES, false))
|
||||
continue;
|
||||
|
||||
if (g_app_db) {
|
||||
log_debug(" [DB] step failed for titleId check: rc=%d err=%s", rc,
|
||||
sqlite3_errmsg(g_app_db));
|
||||
}
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
close_app_db();
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int update_snd0info(const char *title_id) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return -1;
|
||||
@@ -1249,61 +1065,143 @@ static int update_snd0info(const char *title_id) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int lookup_title_in_app_db_cached(const char *title_id,
|
||||
struct AppDbLookupCache *lookup_cache,
|
||||
int *cache_count) {
|
||||
if (!title_id || title_id[0] == '\0')
|
||||
return 0;
|
||||
static void free_app_db_title_list(struct AppDbTitleList *list) {
|
||||
if (!list)
|
||||
return;
|
||||
free(list->ids);
|
||||
list->ids = NULL;
|
||||
list->count = 0;
|
||||
list->capacity = 0;
|
||||
}
|
||||
|
||||
int count = (cache_count ? *cache_count : 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (lookup_cache[i].valid && strcmp(lookup_cache[i].title_id, title_id) == 0)
|
||||
return lookup_cache[i].in_app_db ? 1 : 0;
|
||||
static bool append_app_db_title(struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\0')
|
||||
return true;
|
||||
|
||||
if (list->count >= list->capacity) {
|
||||
int new_capacity = (list->capacity > 0) ? (list->capacity * 2) : 1024;
|
||||
char(*new_ids)[MAX_TITLE_ID] =
|
||||
realloc(list->ids, (size_t)new_capacity * sizeof(*list->ids));
|
||||
if (!new_ids)
|
||||
return false;
|
||||
list->ids = new_ids;
|
||||
list->capacity = new_capacity;
|
||||
}
|
||||
|
||||
int q = query_title_registered_in_app_db(title_id);
|
||||
if (q < 0)
|
||||
return -1;
|
||||
(void)strlcpy(list->ids[list->count], title_id, MAX_TITLE_ID);
|
||||
list->count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool in_app_db = (q != 0);
|
||||
if (cache_count && count < MAX_PENDING) {
|
||||
(void)strlcpy(lookup_cache[count].title_id, title_id,
|
||||
sizeof(lookup_cache[count].title_id));
|
||||
lookup_cache[count].in_app_db = in_app_db;
|
||||
lookup_cache[count].valid = true;
|
||||
*cache_count = count + 1;
|
||||
static int compare_title_id_str(const void *a, const void *b) {
|
||||
return strcmp((const char *)a, (const char *)b);
|
||||
}
|
||||
|
||||
static bool app_db_title_list_contains(const struct AppDbTitleList *list,
|
||||
const char *title_id) {
|
||||
if (!list || !title_id || title_id[0] == '\0')
|
||||
return false;
|
||||
return bsearch(title_id, list->ids, (size_t)list->count, sizeof(*list->ids),
|
||||
compare_title_id_str) != NULL;
|
||||
}
|
||||
|
||||
static bool load_app_db_title_list(struct AppDbTitleList *list) {
|
||||
if (!list)
|
||||
return false;
|
||||
free_app_db_title_list(list);
|
||||
|
||||
const char *sql =
|
||||
"SELECT DISTINCT titleId "
|
||||
"FROM tbl_contentinfo "
|
||||
"WHERE titleId != '' "
|
||||
"ORDER BY titleId;";
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int prep_rc = app_db_prepare_with_retry(sql, &stmt, APP_DB_PREPARE_BUSY_RETRIES,
|
||||
"title list query");
|
||||
if (prep_rc != SQLITE_OK) {
|
||||
close_app_db();
|
||||
return false;
|
||||
}
|
||||
return in_app_db ? 1 : 0;
|
||||
|
||||
int busy_attempts = 0;
|
||||
while (!should_stop_requested()) {
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const char *title_id = (const char *)sqlite3_column_text(stmt, 0);
|
||||
if (title_id && title_id[0] != '\0') {
|
||||
if (!append_app_db_title(list, title_id)) {
|
||||
log_debug(" [DB] title list allocation failed");
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rc == SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
log_debug(" [DB] loaded app.db title list: %d entries", list->count);
|
||||
return true;
|
||||
}
|
||||
if ((rc == SQLITE_BUSY || rc == SQLITE_LOCKED) &&
|
||||
busy_attempts + 1 < APP_DB_QUERY_BUSY_RETRIES) {
|
||||
busy_attempts++;
|
||||
sceKernelUsleep(APP_DB_BUSY_RETRY_SLEEP_US);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug(" [DB] title list query failed: rc=%d err=%s", rc,
|
||||
(g_app_db ? sqlite3_errmsg(g_app_db) : "unknown"));
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
close_app_db();
|
||||
free_app_db_title_list(list);
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- FAST STABILITY CHECK ---
|
||||
static bool is_path_stable_now(const char *path, double *root_diff_out) {
|
||||
static bool is_path_stable_now(const char *path, double *root_diff_out,
|
||||
int *stat_errno_out) {
|
||||
struct stat st;
|
||||
time_t now = time(NULL);
|
||||
if (stat(path, &st) != 0)
|
||||
ensure_runtime_config_ready();
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = 0;
|
||||
if (stat(path, &st) != 0) {
|
||||
if (root_diff_out)
|
||||
*root_diff_out = -1.0;
|
||||
if (stat_errno_out)
|
||||
*stat_errno_out = errno;
|
||||
return false;
|
||||
}
|
||||
|
||||
double root_diff = difftime(now, st.st_mtime);
|
||||
if (root_diff_out)
|
||||
*root_diff_out = root_diff;
|
||||
if (root_diff <= 10.0)
|
||||
return false;
|
||||
|
||||
char sys_path[MAX_PATH];
|
||||
snprintf(sys_path, sizeof(sys_path), "%s/sce_sys", path);
|
||||
if (stat(sys_path, &st) == 0)
|
||||
return difftime(now, st.st_mtime) > 10.0;
|
||||
|
||||
// No sce_sys? Trust root timestamp.
|
||||
return true;
|
||||
if (root_diff < 0.0)
|
||||
return true;
|
||||
return root_diff > (double)g_runtime_cfg.stability_wait_seconds;
|
||||
}
|
||||
|
||||
bool wait_for_stability_fast(const char *path, const char *name) {
|
||||
double diff = 0.0;
|
||||
if (is_path_stable_now(path, &diff))
|
||||
int st_err = 0;
|
||||
if (is_path_stable_now(path, &diff, &st_err))
|
||||
return true;
|
||||
|
||||
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
|
||||
if (st_err != 0) {
|
||||
log_debug(" [WAIT] %s stat failed for %s: %s", name, path, strerror(st_err));
|
||||
} else {
|
||||
log_debug(" [WAIT] %s modified %.0fs ago. Waiting...", name, diff);
|
||||
}
|
||||
sceKernelUsleep(2000000); // Wait 2s
|
||||
return false; // Force re-scan next cycle
|
||||
}
|
||||
@@ -1311,50 +1209,6 @@ bool wait_for_stability_fast(const char *path, const char *name) {
|
||||
// --- Mount/Copy Helpers for Install Action ---
|
||||
static int copy_file(const char *src, const char *dst);
|
||||
|
||||
static int remount_system_ex(void) {
|
||||
struct iovec iov[] = {
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY("/dev/ssd0.system_ex"),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY("/system_ex"),
|
||||
IOVEC_ENTRY("fstype"), IOVEC_ENTRY("exfatfs"),
|
||||
IOVEC_ENTRY("large"), IOVEC_ENTRY("yes"),
|
||||
IOVEC_ENTRY("timezone"), IOVEC_ENTRY("static"),
|
||||
IOVEC_ENTRY("async"), IOVEC_ENTRY(NULL),
|
||||
IOVEC_ENTRY("ignoreacl"), IOVEC_ENTRY(NULL)};
|
||||
return nmount(iov, IOVEC_SIZE(iov), MNT_UPDATE);
|
||||
}
|
||||
|
||||
static int mount_nullfs(const char *src, const char *dst) {
|
||||
struct iovec iov[] = {IOVEC_ENTRY("fstype"), IOVEC_ENTRY("nullfs"),
|
||||
IOVEC_ENTRY("from"), IOVEC_ENTRY(src),
|
||||
IOVEC_ENTRY("fspath"), IOVEC_ENTRY(dst)};
|
||||
return nmount(iov, IOVEC_SIZE(iov), MNT_RDONLY);
|
||||
}
|
||||
|
||||
static bool unmount_nullfs_mount(const char *mount_point) {
|
||||
if (unmount(mount_point, 0) == 0) {
|
||||
invalidate_nullfs_mount(mount_point);
|
||||
return true;
|
||||
}
|
||||
|
||||
int err = errno;
|
||||
if (err == ENOENT || err == EINVAL) {
|
||||
invalidate_nullfs_mount(mount_point);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [MOUNT] unmount failed for %s: %s, trying force...",
|
||||
mount_point, strerror(err));
|
||||
if (unmount(mount_point, MNT_FORCE) == 0 || errno == ENOENT ||
|
||||
errno == EINVAL) {
|
||||
invalidate_nullfs_mount(mount_point);
|
||||
return true;
|
||||
}
|
||||
|
||||
log_debug(" [MOUNT] force unmount failed for %s: %s", mount_point,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
static int copy_dir(const char *src, const char *dst) {
|
||||
if (mkdir(dst, 0777) != 0 && errno != EEXIST)
|
||||
return -1;
|
||||
@@ -1514,11 +1368,14 @@ static bool wait_for_dev_node_state(const char *devname, bool should_exist) {
|
||||
static bool is_source_stable_for_mount(const char *path, const char *name,
|
||||
const char *tag) {
|
||||
struct stat st;
|
||||
ensure_runtime_config_ready();
|
||||
if (stat(path, &st) != 0)
|
||||
return false;
|
||||
|
||||
double age = difftime(time(NULL), st.st_mtime);
|
||||
if (age < 10.0) {
|
||||
if (age < 0.0)
|
||||
return true;
|
||||
if (age < (double)g_runtime_cfg.stability_wait_seconds) {
|
||||
log_debug(" [%s] %s modified %.0fs ago, waiting...", tag, name, age);
|
||||
return false;
|
||||
}
|
||||
@@ -1530,6 +1387,8 @@ static void init_runtime_config_defaults(void) {
|
||||
g_runtime_cfg.debug_enabled = true;
|
||||
g_runtime_cfg.mount_read_only = (IMAGE_MOUNT_READ_ONLY != 0);
|
||||
g_runtime_cfg.recursive_scan = false;
|
||||
g_runtime_cfg.scan_interval_us = DEFAULT_SCAN_INTERVAL_US;
|
||||
g_runtime_cfg.stability_wait_seconds = DEFAULT_STABILITY_WAIT_SECONDS;
|
||||
g_runtime_cfg.exfat_backend = DEFAULT_EXFAT_BACKEND;
|
||||
g_runtime_cfg.ufs_backend = DEFAULT_UFS_BACKEND;
|
||||
g_runtime_cfg.lvd_sector_exfat = LVD_SECTOR_SIZE_EXFAT;
|
||||
@@ -1688,6 +1547,30 @@ static bool load_runtime_config(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "scan_interval_seconds") == 0 ||
|
||||
strcasecmp(key, "scan_interval_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 < MIN_SCAN_INTERVAL_SECONDS ||
|
||||
u32 > MAX_SCAN_INTERVAL_SECONDS) {
|
||||
log_debug(" [CFG] invalid scan interval at line %d: %s=%s (range: %u..%u)",
|
||||
line_no, key, value, (unsigned)MIN_SCAN_INTERVAL_SECONDS,
|
||||
(unsigned)MAX_SCAN_INTERVAL_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.scan_interval_us = u32 * 1000000u;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "stability_wait_seconds") == 0 ||
|
||||
strcasecmp(key, "stability_wait_sec") == 0) {
|
||||
if (!parse_u32_ini(value, &u32) || u32 > MAX_STABILITY_WAIT_SECONDS) {
|
||||
log_debug(" [CFG] invalid stability wait at line %d: %s=%s (max: %u)",
|
||||
line_no, key, value, (unsigned)MAX_STABILITY_WAIT_SECONDS);
|
||||
continue;
|
||||
}
|
||||
g_runtime_cfg.stability_wait_seconds = u32;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(key, "exfat_backend") == 0) {
|
||||
if (!parse_backend_ini(value, &backend)) {
|
||||
log_debug(" [CFG] invalid backend at line %d: %s=%s", line_no, key,
|
||||
@@ -1763,7 +1646,7 @@ static bool load_runtime_config(void) {
|
||||
log_debug(" [CFG] loaded: debug=%d ro=%d recursive_scan=%d "
|
||||
"exfat_backend=%s ufs_backend=%s "
|
||||
"lvd_sec(exfat=%u ufs=%u pfs=%u) md_sec(exfat=%u ufs=%u) "
|
||||
"scan_paths=%d",
|
||||
"scan_interval_s=%u stability_wait_s=%u scan_paths=%d",
|
||||
g_runtime_cfg.debug_enabled ? 1 : 0,
|
||||
g_runtime_cfg.mount_read_only ? 1 : 0,
|
||||
g_runtime_cfg.recursive_scan ? 1 : 0,
|
||||
@@ -1771,7 +1654,8 @@ static bool load_runtime_config(void) {
|
||||
backend_name(g_runtime_cfg.ufs_backend),
|
||||
g_runtime_cfg.lvd_sector_exfat, g_runtime_cfg.lvd_sector_ufs,
|
||||
g_runtime_cfg.lvd_sector_pfs, g_runtime_cfg.md_sector_exfat,
|
||||
g_runtime_cfg.md_sector_ufs, g_scan_path_count);
|
||||
g_runtime_cfg.md_sector_ufs, g_runtime_cfg.scan_interval_us / 1000000u,
|
||||
g_runtime_cfg.stability_wait_seconds, g_scan_path_count);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1812,13 +1696,13 @@ static uint16_t get_lvd_attach_option(image_fs_type_t fs_type,
|
||||
bool mount_read_only) {
|
||||
if (fs_type == IMAGE_FS_UFS) {
|
||||
// UFS runtime mapping: RO -> 0x1E, RW -> 0x16.
|
||||
return mount_read_only ? LVD_ATTACH_OPTION_NORM_DD_RW
|
||||
: LVD_ATTACH_OPTION_NORM_DD_RO;
|
||||
return mount_read_only ? LVD_ATTACH_OPTION_NORM_DD_RO
|
||||
: LVD_ATTACH_OPTION_NORM_DD_RW;
|
||||
}
|
||||
|
||||
// Generic/PFS runtime mapping: RO -> 0x9, RW -> 0x8.
|
||||
return mount_read_only ? LVD_ATTACH_OPTION_FLAGS_RW
|
||||
: LVD_ATTACH_OPTION_FLAGS_DEFAULT;
|
||||
return mount_read_only ? LVD_ATTACH_OPTION_FLAGS_DEFAULT
|
||||
: LVD_ATTACH_OPTION_FLAGS_RW;
|
||||
}
|
||||
|
||||
static unsigned int get_nmount_flags(image_fs_type_t fs_type,
|
||||
@@ -2527,15 +2411,6 @@ static void shutdown_ufs_mounts(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static void shutdown_nullfs_mounts(void) {
|
||||
for (int k = 0; k < MAX_NULLFS_MOUNTS; k++) {
|
||||
if (!nullfs_cache[k].valid)
|
||||
continue;
|
||||
unmount_nullfs_mount(nullfs_cache[k].mount_point);
|
||||
nullfs_cache[k].valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Game Metadata Parsing (param.json) ---
|
||||
static int extract_json_string(const char *json, const char *key, char *out,
|
||||
size_t out_size) {
|
||||
@@ -2667,14 +2542,11 @@ static void prune_game_cache(void) {
|
||||
if (access(cache[k].path, F_OK) == 0)
|
||||
continue;
|
||||
|
||||
if (cache[k].title_id[0] != '\0') {
|
||||
char mount_point[MAX_PATH];
|
||||
build_system_ex_app_path(cache[k].title_id, mount_point, sizeof(mount_point));
|
||||
if (unmount_nullfs_mount(mount_point)) {
|
||||
log_debug(" [MOUNT] source removed, unmounted: %s (%s)",
|
||||
cache[k].title_id, cache[k].path);
|
||||
}
|
||||
}
|
||||
if (cache[k].title_id[0] != '\0')
|
||||
log_debug(" [CACHE] source removed: %s (%s)", cache[k].title_id,
|
||||
cache[k].path);
|
||||
else
|
||||
log_debug(" [CACHE] source removed: %s", cache[k].path);
|
||||
|
||||
cache[k].valid = false;
|
||||
cache[k].path[0] = '\0';
|
||||
@@ -2730,8 +2602,8 @@ static bool is_under_discovered_param_root(
|
||||
// --- Candidate Discovery ---
|
||||
static bool try_collect_candidate_for_directory(
|
||||
const char *full_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, struct AppDbLookupCache *db_cache,
|
||||
int *db_cache_count, char discovered_param_roots[][MAX_PATH],
|
||||
int *candidate_count, const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count) {
|
||||
struct stat param_st;
|
||||
memset(¶m_st, 0, sizeof(param_st));
|
||||
@@ -2787,14 +2659,12 @@ static bool try_collect_candidate_for_directory(
|
||||
}
|
||||
}
|
||||
|
||||
int in_app_db_q = lookup_title_in_app_db_cached(title_id, db_cache, db_cache_count);
|
||||
if (in_app_db_q < 0) {
|
||||
// app.db is busy/locked right now; defer install/remount decisions to a later scan.
|
||||
if (!app_db_titles_ready) {
|
||||
log_debug(" [SKIP] app.db unavailable (locked/busy), deferring: %s (%s)",
|
||||
title_name, title_id);
|
||||
return true;
|
||||
}
|
||||
bool in_app_db = (in_app_db_q != 0);
|
||||
bool in_app_db = app_db_title_list_contains(app_db_titles, title_id);
|
||||
|
||||
if (in_app_db) {
|
||||
for (int k = 0; k < MAX_PENDING; k++) {
|
||||
@@ -2822,8 +2692,13 @@ static bool try_collect_candidate_for_directory(
|
||||
// Installed status requires both app files and app.db presence.
|
||||
bool installed = is_installed(title_id) && in_app_db;
|
||||
bool mounted = is_data_mounted(title_id);
|
||||
if (installed && mounted && mount_link_matches_system_ex(title_id)) {
|
||||
log_debug(" [SKIP] already installed+mounted: %s (%s)", title_name, title_id);
|
||||
if (installed && mount_link_matches_source(title_id, full_path)) {
|
||||
if (mounted)
|
||||
log_debug(" [SKIP] already installed+mounted+linked: %s (%s)", title_name,
|
||||
title_id);
|
||||
else
|
||||
log_debug(" [SKIP] already installed+linked (waiting kstuff mount): %s (%s)",
|
||||
title_name, title_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2860,8 +2735,8 @@ static bool try_collect_candidate_for_directory(
|
||||
|
||||
static void collect_candidates_recursively(
|
||||
const char *dir_path, scan_candidate_t *candidates, int max_candidates,
|
||||
int *candidate_count, struct AppDbLookupCache *db_cache,
|
||||
int *db_cache_count, char discovered_param_roots[][MAX_PATH],
|
||||
int *candidate_count, const struct AppDbTitleList *app_db_titles,
|
||||
bool app_db_titles_ready, char discovered_param_roots[][MAX_PATH],
|
||||
int *discovered_param_root_count) {
|
||||
if (should_stop_requested() || !dir_path || dir_path[0] == '\0')
|
||||
return;
|
||||
@@ -2869,8 +2744,9 @@ static void collect_candidates_recursively(
|
||||
// Once a directory has sce_sys/param.json (valid or not),
|
||||
// it is treated as a terminal game root and descendants are skipped.
|
||||
if (try_collect_candidate_for_directory(
|
||||
dir_path, candidates, max_candidates, candidate_count, db_cache,
|
||||
db_cache_count, discovered_param_roots, discovered_param_root_count)) {
|
||||
dir_path, candidates, max_candidates, candidate_count, app_db_titles,
|
||||
app_db_titles_ready, discovered_param_roots,
|
||||
discovered_param_root_count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2901,8 +2777,8 @@ static void collect_candidates_recursively(
|
||||
continue;
|
||||
|
||||
collect_candidates_recursively(
|
||||
full_path, candidates, max_candidates, candidate_count, db_cache,
|
||||
db_cache_count, discovered_param_roots, discovered_param_root_count);
|
||||
full_path, candidates, max_candidates, candidate_count, app_db_titles,
|
||||
app_db_titles_ready, discovered_param_roots, discovered_param_root_count);
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
@@ -2910,9 +2786,9 @@ static void collect_candidates_recursively(
|
||||
|
||||
// --- Unified Scan Pass (images + game candidates) ---
|
||||
static void cleanup_lost_sources_before_scan(void) {
|
||||
// 1) Remove stale nullfs mounts for deleted game sources.
|
||||
// 1) Drop stale game cache entries for deleted sources.
|
||||
prune_game_cache();
|
||||
// 2) After nullfs cleanup, stale image unmounts are less likely to be busy.
|
||||
// 2) Unmount stale image mounts for deleted image files.
|
||||
cleanup_stale_image_mounts();
|
||||
// 3) Drop stale path-state entries.
|
||||
prune_path_state();
|
||||
@@ -2921,13 +2797,16 @@ static void cleanup_lost_sources_before_scan(void) {
|
||||
static int collect_scan_candidates(scan_candidate_t *candidates,
|
||||
int max_candidates) {
|
||||
int candidate_count = 0;
|
||||
struct AppDbLookupCache *db_cache = g_scan_db_lookup_cache;
|
||||
int db_cache_count = 0;
|
||||
struct AppDbTitleList app_db_titles = {0};
|
||||
bool app_db_titles_ready = load_app_db_title_list(&app_db_titles);
|
||||
char (*discovered_param_roots)[MAX_PATH] = g_scan_discovered_param_roots;
|
||||
int discovered_param_root_count = 0;
|
||||
memset(db_cache, 0, sizeof(g_scan_db_lookup_cache));
|
||||
memset(discovered_param_roots, 0, sizeof(g_scan_discovered_param_roots));
|
||||
|
||||
if (!app_db_titles_ready) {
|
||||
log_debug(" [DB] app.db title list unavailable for this scan cycle");
|
||||
}
|
||||
|
||||
for (int i = 0; SCAN_PATHS[i] != NULL; i++) {
|
||||
if (should_stop_requested())
|
||||
goto done;
|
||||
@@ -2971,20 +2850,20 @@ static int collect_scan_candidates(scan_candidate_t *candidates,
|
||||
|
||||
if (g_runtime_cfg.recursive_scan) {
|
||||
collect_candidates_recursively(
|
||||
full_path, candidates, max_candidates, &candidate_count, db_cache,
|
||||
&db_cache_count, discovered_param_roots,
|
||||
full_path, candidates, max_candidates, &candidate_count,
|
||||
&app_db_titles, app_db_titles_ready, discovered_param_roots,
|
||||
&discovered_param_root_count);
|
||||
} else {
|
||||
(void)try_collect_candidate_for_directory(
|
||||
full_path, candidates, max_candidates, &candidate_count, db_cache,
|
||||
&db_cache_count, discovered_param_roots,
|
||||
full_path, candidates, max_candidates, &candidate_count,
|
||||
&app_db_titles, app_db_titles_ready, discovered_param_roots,
|
||||
&discovered_param_root_count);
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
done:
|
||||
// Keep app.db open only during a single scan cycle.
|
||||
free_app_db_title_list(&app_db_titles);
|
||||
close_app_db();
|
||||
return candidate_count;
|
||||
}
|
||||
@@ -2993,31 +2872,9 @@ done:
|
||||
bool mount_and_install(const char *src_path, const char *title_id,
|
||||
const char *title_name, bool is_remount,
|
||||
bool should_register) {
|
||||
char system_ex_app[MAX_PATH];
|
||||
char user_app_dir[MAX_PATH];
|
||||
char user_sce_sys[MAX_PATH];
|
||||
char src_sce_sys[MAX_PATH];
|
||||
bool nullfs_mounted = false;
|
||||
|
||||
// MOUNT
|
||||
build_system_ex_app_path(title_id, system_ex_app, sizeof(system_ex_app));
|
||||
mkdir(system_ex_app, 0777);
|
||||
if (unmount(system_ex_app, 0) != 0 && errno != ENOENT && errno != EINVAL) {
|
||||
log_debug(" [MOUNT] pre-unmount failed for %s: %s, trying force...",
|
||||
system_ex_app, strerror(errno));
|
||||
if (unmount(system_ex_app, MNT_FORCE) != 0 && errno != ENOENT &&
|
||||
errno != EINVAL) {
|
||||
log_debug(" [MOUNT] pre-force-unmount failed for %s: %s", system_ex_app,
|
||||
strerror(errno));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (mount_nullfs(src_path, system_ex_app) < 0) {
|
||||
log_debug(" [MOUNT] FAIL: %s", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
nullfs_mounted = true;
|
||||
cache_nullfs_mount(system_ex_app);
|
||||
|
||||
// COPY FILES
|
||||
if (!is_remount) {
|
||||
@@ -3030,8 +2887,6 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
if (copy_dir(src_sce_sys, user_sce_sys) != 0) {
|
||||
log_debug(" [COPY] Failed to copy sce_sys: %s -> %s", src_sce_sys,
|
||||
user_sce_sys);
|
||||
if (nullfs_mounted)
|
||||
unmount_nullfs_mount(system_ex_app);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3040,8 +2895,6 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
snprintf(icon_dst, sizeof(icon_dst), "/user/app/%s/icon0.png", title_id);
|
||||
if (copy_file(icon_src, icon_dst) != 0) {
|
||||
log_debug(" [COPY] Failed to copy icon: %s -> %s", icon_src, icon_dst);
|
||||
if (nullfs_mounted)
|
||||
unmount_nullfs_mount(system_ex_app);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -3054,7 +2907,7 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
bool link_ok = false;
|
||||
FILE *flnk = fopen(lnk_path, "w");
|
||||
if (flnk) {
|
||||
bool write_ok = (fprintf(flnk, "%s", system_ex_app) >= 0);
|
||||
bool write_ok = (fprintf(flnk, "%s", src_path) >= 0);
|
||||
bool flush_ok = (fflush(flnk) == 0);
|
||||
bool close_ok = (fclose(flnk) == 0);
|
||||
if (write_ok && flush_ok && close_ok) {
|
||||
@@ -3066,8 +2919,6 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
log_debug(" [LINK] open failed for %s: %s", lnk_path, strerror(errno));
|
||||
}
|
||||
if (!link_ok) {
|
||||
if (nullfs_mounted)
|
||||
unmount_nullfs_mount(system_ex_app);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3105,8 +2956,6 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
log_debug(" [REG] FAIL: 0x%x", res);
|
||||
notify_system("Register failed: %s (%s)\ncode=0x%08X", title_name, title_id,
|
||||
(uint32_t)res);
|
||||
if (nullfs_mounted)
|
||||
unmount_nullfs_mount(system_ex_app);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3115,11 +2964,6 @@ bool mount_and_install(const char *src_path, const char *title_id,
|
||||
// --- Execution (per discovered candidate) ---
|
||||
static void process_scan_candidates(const scan_candidate_t *candidates,
|
||||
int candidate_count) {
|
||||
if (remount_system_ex() != 0) {
|
||||
log_debug(" [MOUNT] remount_system_ex failed: %s", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < candidate_count; i++) {
|
||||
if (should_stop_requested())
|
||||
return;
|
||||
@@ -3228,7 +3072,6 @@ int main(void) {
|
||||
notify_system("ShadowMount+: Recursive scan enabled.");
|
||||
|
||||
cleanup_mount_dirs();
|
||||
(void)init_app_triggers();
|
||||
|
||||
// --- STARTUP LOGIC ---
|
||||
cleanup_lost_sources_before_scan();
|
||||
@@ -3253,7 +3096,7 @@ int main(void) {
|
||||
|
||||
// Sleep FIRST since we either just finished scan above, or library was
|
||||
// ready.
|
||||
if (sleep_with_stop_check(SCAN_INTERVAL_US)) {
|
||||
if (sleep_with_stop_check(g_runtime_cfg.scan_interval_us)) {
|
||||
log_debug("[SHUTDOWN] stop requested during sleep");
|
||||
break;
|
||||
}
|
||||
@@ -3261,7 +3104,6 @@ int main(void) {
|
||||
scan_all_paths();
|
||||
}
|
||||
|
||||
shutdown_nullfs_mounts();
|
||||
shutdown_ufs_mounts();
|
||||
close_app_db();
|
||||
if (lock >= 0) {
|
||||
|
||||
Reference in new issue
Block a user