mirror of
https://github.com/drakmor/ShadowMountPlus.git
synced 2026-10-21 22:22:28 -05:00
1675 lines
54 KiB
C
1675 lines
54 KiB
C
#include "sm_platform.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <sys/event.h>
|
|
#include <sys/select.h>
|
|
|
|
#include "sm_config_mount.h"
|
|
#include "sm_appdb.h"
|
|
#include "sm_fakelib.h"
|
|
#include "sm_filesystem.h"
|
|
#include "sm_game_lifecycle.h"
|
|
#include "sm_hash.h"
|
|
#include "sm_image.h"
|
|
#include "sm_install.h"
|
|
#include "sm_install_queue.h"
|
|
#include "sm_kstuff.h"
|
|
#include "sm_limits.h"
|
|
#include "sm_log.h"
|
|
#include "sm_path_utils.h"
|
|
#include "sm_paths.h"
|
|
#include "sm_runtime.h"
|
|
#include "sm_scan.h"
|
|
#include "sm_scan_tree.h"
|
|
#include "sm_scanner.h"
|
|
#include "sm_time.h"
|
|
#include "sm_types.h"
|
|
|
|
#define SCANNER_EVENT_BATCH 32
|
|
#define SCANNER_EVENT_DRAIN_BATCHES 8
|
|
#define SCANNER_WATCH_INDEX_NONE ((size_t)-1)
|
|
#define SCANNER_CONFIG_RELOAD_DEBOUNCE_US 250000ull
|
|
#define SCANNER_CONFIG_PROBE_INTERVAL_US 10000000ull
|
|
#define SCANNER_MANUAL_RELOAD_DEBOUNCE_US 250000ull
|
|
#define SCANNER_MANUAL_PROBE_INTERVAL_US 10000000ull
|
|
|
|
typedef enum {
|
|
SCANNER_WATCH_SCAN_ROOT = 0,
|
|
SCANNER_WATCH_SCAN_ROOT_PARENT,
|
|
SCANNER_WATCH_SCAN_BACKPORT_ROOT,
|
|
SCANNER_WATCH_SCAN_SUBDIR,
|
|
SCANNER_WATCH_SCAN_IMAGE_FILE,
|
|
} scanner_watch_kind_t;
|
|
|
|
typedef struct {
|
|
int fd;
|
|
int scan_root_index;
|
|
uint8_t depth;
|
|
scanner_watch_kind_t kind;
|
|
size_t prev_root_watch_index;
|
|
size_t next_root_watch_index;
|
|
char path[MAX_PATH];
|
|
} scanner_watch_entry_t;
|
|
|
|
typedef struct {
|
|
bool dirty;
|
|
bool cleanup_pending;
|
|
bool watch_tree_stale;
|
|
bool root_present;
|
|
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;
|
|
char watch_tree_rebuild_path[MAX_PATH];
|
|
} scanner_root_state_t;
|
|
|
|
static int g_scanner_wake_pipe[2] = {-1, -1};
|
|
static int g_scanner_config_fd = -1;
|
|
static int g_scanner_manual_fd = -1;
|
|
static volatile sig_atomic_t g_scanner_wake_write_fd = -1;
|
|
static scan_candidate_t g_scanner_scan_candidates[MAX_PENDING];
|
|
static scanner_watch_entry_t *g_scanner_watch_entries = NULL;
|
|
static size_t g_scanner_watch_count = 0;
|
|
static size_t g_scanner_watch_capacity = 0;
|
|
static size_t g_scanner_root_watch_heads[MAX_SCAN_PATHS];
|
|
static size_t *g_scanner_watch_fd_index = NULL;
|
|
static size_t g_scanner_watch_fd_index_capacity = 0;
|
|
static scanner_root_state_t g_scanner_root_states[MAX_SCAN_PATHS];
|
|
static bool g_scanner_config_reload_pending = false;
|
|
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 uint64_t scanner_stability_wait_us(void) {
|
|
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;
|
|
}
|
|
|
|
static void schedule_config_reload(uint64_t now_us) {
|
|
g_scanner_config_reload_pending = true;
|
|
g_scanner_config_reload_ready_after_us =
|
|
now_us + SCANNER_CONFIG_RELOAD_DEBOUNCE_US;
|
|
}
|
|
|
|
static void schedule_config_probe(uint64_t now_us) {
|
|
g_scanner_config_probe_due_us = now_us + SCANNER_CONFIG_PROBE_INTERVAL_US;
|
|
}
|
|
|
|
static void schedule_manual_scan(uint64_t now_us) {
|
|
g_scanner_manual_scan_due_us = now_us + SCANNER_MANUAL_RELOAD_DEBOUNCE_US;
|
|
}
|
|
|
|
static void schedule_manual_probe(uint64_t now_us) {
|
|
g_scanner_manual_probe_due_us = now_us + SCANNER_MANUAL_PROBE_INTERVAL_US;
|
|
}
|
|
|
|
static bool set_fd_nonblocking(int fd) {
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0)
|
|
return false;
|
|
|
|
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
|
}
|
|
|
|
static void reset_scanner_root_states(void) {
|
|
memset(g_scanner_root_states, 0, sizeof(g_scanner_root_states));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static void clear_scan_root_watch_tree_state(int scan_root_index) {
|
|
g_scanner_root_states[scan_root_index].watch_tree_stale = false;
|
|
g_scanner_root_states[scan_root_index].watch_tree_rebuild_depth = 0;
|
|
g_scanner_root_states[scan_root_index].watch_tree_rebuild_kind =
|
|
SCANNER_WATCH_SCAN_ROOT;
|
|
g_scanner_root_states[scan_root_index].watch_tree_rebuild_path[0] = '\0';
|
|
}
|
|
|
|
static void close_scanner_wake_pipe(void) {
|
|
g_scanner_wake_write_fd = -1;
|
|
if (g_scanner_wake_pipe[0] >= 0) {
|
|
close(g_scanner_wake_pipe[0]);
|
|
g_scanner_wake_pipe[0] = -1;
|
|
}
|
|
if (g_scanner_wake_pipe[1] >= 0) {
|
|
close(g_scanner_wake_pipe[1]);
|
|
g_scanner_wake_pipe[1] = -1;
|
|
}
|
|
}
|
|
|
|
static void close_scanner_config_file(void) {
|
|
if (g_scanner_config_fd >= 0) {
|
|
close(g_scanner_config_fd);
|
|
g_scanner_config_fd = -1;
|
|
}
|
|
}
|
|
|
|
static void close_scanner_manual_file(void) {
|
|
if (g_scanner_manual_fd >= 0) {
|
|
close(g_scanner_manual_fd);
|
|
g_scanner_manual_fd = -1;
|
|
}
|
|
}
|
|
|
|
static void clear_scanner_watch_entries(void) {
|
|
for (size_t i = 0; i < g_scanner_watch_count; i++) {
|
|
if (g_scanner_watch_entries[i].fd >= 0)
|
|
close(g_scanner_watch_entries[i].fd);
|
|
}
|
|
free(g_scanner_watch_entries);
|
|
free(g_scanner_watch_fd_index);
|
|
g_scanner_watch_entries = NULL;
|
|
g_scanner_watch_fd_index = NULL;
|
|
g_scanner_watch_count = 0;
|
|
g_scanner_watch_capacity = 0;
|
|
g_scanner_watch_fd_index_capacity = 0;
|
|
reset_scanner_root_watch_heads();
|
|
}
|
|
|
|
static void drain_scanner_wake_pipe(void) {
|
|
if (g_scanner_wake_pipe[0] < 0)
|
|
return;
|
|
|
|
char buf[64];
|
|
while (read(g_scanner_wake_pipe[0], buf, sizeof(buf)) > 0) {
|
|
}
|
|
}
|
|
|
|
static void log_immediate_scan_reason(const char *reason) {
|
|
if (!reason || reason[0] == '\0')
|
|
return;
|
|
log_debug("[SCAN] running immediate full scan (%s)", reason);
|
|
}
|
|
|
|
static bool ensure_scanner_watch_capacity(size_t needed_count) {
|
|
if (needed_count <= g_scanner_watch_capacity)
|
|
return true;
|
|
|
|
size_t new_capacity = g_scanner_watch_capacity ? g_scanner_watch_capacity : 64;
|
|
while (new_capacity < needed_count)
|
|
new_capacity *= 2u;
|
|
|
|
scanner_watch_entry_t *new_entries =
|
|
realloc(g_scanner_watch_entries, new_capacity * sizeof(*new_entries));
|
|
if (!new_entries) {
|
|
log_debug(" [SCAN] watcher registry allocation failed");
|
|
return false;
|
|
}
|
|
|
|
g_scanner_watch_entries = new_entries;
|
|
g_scanner_watch_capacity = new_capacity;
|
|
return true;
|
|
}
|
|
|
|
static void clear_scanner_config_reload_state(void) {
|
|
g_scanner_config_reload_pending = false;
|
|
g_scanner_config_reload_ready_after_us = 0;
|
|
g_scanner_config_probe_due_us = 0;
|
|
}
|
|
|
|
static void clear_scanner_manual_scan_state(void) {
|
|
g_scanner_manual_scan_due_us = 0;
|
|
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_mount_first !=
|
|
new_cfg->global_fakelib_mount_first ||
|
|
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};
|
|
|
|
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
|
for (unsigned shift = 0; shift < 32u; shift += 8u) {
|
|
hash ^= (values[i] >> shift) & 0xffu;
|
|
hash *= 16777619u;
|
|
}
|
|
}
|
|
for (int i = 0; i < scan_path_count; i++) {
|
|
hash ^= sm_fnv1a32(get_scan_path(i));
|
|
hash *= 16777619u;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
static size_t scanner_watch_fd_hash(uintptr_t ident) {
|
|
return (size_t)((ident * 11400714819323198485ull) >>
|
|
(sizeof(uintptr_t) >= sizeof(uint64_t) ? 0 : 1));
|
|
}
|
|
|
|
static bool insert_scanner_watch_fd_index_entry(uintptr_t ident,
|
|
size_t watch_index) {
|
|
if (!g_scanner_watch_fd_index || g_scanner_watch_fd_index_capacity == 0)
|
|
return false;
|
|
|
|
size_t mask = g_scanner_watch_fd_index_capacity - 1u;
|
|
size_t slot = scanner_watch_fd_hash(ident) & mask;
|
|
while (g_scanner_watch_fd_index[slot] != SCANNER_WATCH_INDEX_NONE)
|
|
slot = (slot + 1u) & mask;
|
|
|
|
g_scanner_watch_fd_index[slot] = watch_index;
|
|
return true;
|
|
}
|
|
|
|
static bool rebuild_scanner_watch_fd_index_with_count(size_t watch_count) {
|
|
size_t needed_capacity = 16u;
|
|
while (needed_capacity < (watch_count * 2u))
|
|
needed_capacity *= 2u;
|
|
|
|
if (g_scanner_watch_fd_index_capacity != needed_capacity) {
|
|
size_t *new_index =
|
|
realloc(g_scanner_watch_fd_index, needed_capacity * sizeof(*new_index));
|
|
if (!new_index) {
|
|
log_debug(" [SCAN] watcher fd index allocation failed");
|
|
return false;
|
|
}
|
|
g_scanner_watch_fd_index = new_index;
|
|
g_scanner_watch_fd_index_capacity = needed_capacity;
|
|
}
|
|
|
|
for (size_t i = 0; i < g_scanner_watch_fd_index_capacity; i++)
|
|
g_scanner_watch_fd_index[i] = SCANNER_WATCH_INDEX_NONE;
|
|
|
|
for (size_t i = 0; i < g_scanner_watch_count; i++) {
|
|
if (!insert_scanner_watch_fd_index_entry(
|
|
(uintptr_t)g_scanner_watch_entries[i].fd, i)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool ensure_scanner_watch_fd_index_capacity(size_t watch_count) {
|
|
size_t needed_capacity = 16u;
|
|
while (needed_capacity < (watch_count * 2u))
|
|
needed_capacity *= 2u;
|
|
|
|
if (g_scanner_watch_fd_index && g_scanner_watch_fd_index_capacity >= needed_capacity)
|
|
return true;
|
|
|
|
return rebuild_scanner_watch_fd_index_with_count(watch_count);
|
|
}
|
|
|
|
static bool rebuild_scanner_watch_fd_index(void) {
|
|
return rebuild_scanner_watch_fd_index_with_count(g_scanner_watch_count);
|
|
}
|
|
|
|
static void link_scanner_watch_entry_to_root(size_t index) {
|
|
scanner_watch_entry_t *entry = &g_scanner_watch_entries[index];
|
|
size_t head = g_scanner_root_watch_heads[entry->scan_root_index];
|
|
entry->prev_root_watch_index = SCANNER_WATCH_INDEX_NONE;
|
|
entry->next_root_watch_index = head;
|
|
if (head != SCANNER_WATCH_INDEX_NONE)
|
|
g_scanner_watch_entries[head].prev_root_watch_index = index;
|
|
g_scanner_root_watch_heads[entry->scan_root_index] = index;
|
|
}
|
|
|
|
static void unlink_scanner_watch_entry_from_root(size_t index) {
|
|
scanner_watch_entry_t *entry = &g_scanner_watch_entries[index];
|
|
if (entry->prev_root_watch_index != SCANNER_WATCH_INDEX_NONE) {
|
|
g_scanner_watch_entries[entry->prev_root_watch_index].next_root_watch_index =
|
|
entry->next_root_watch_index;
|
|
} else {
|
|
g_scanner_root_watch_heads[entry->scan_root_index] =
|
|
entry->next_root_watch_index;
|
|
}
|
|
if (entry->next_root_watch_index != SCANNER_WATCH_INDEX_NONE) {
|
|
g_scanner_watch_entries[entry->next_root_watch_index].prev_root_watch_index =
|
|
entry->prev_root_watch_index;
|
|
}
|
|
entry->prev_root_watch_index = SCANNER_WATCH_INDEX_NONE;
|
|
entry->next_root_watch_index = SCANNER_WATCH_INDEX_NONE;
|
|
}
|
|
|
|
static void rebind_scanner_watch_entry_root_index(size_t old_index,
|
|
size_t new_index) {
|
|
scanner_watch_entry_t *entry = &g_scanner_watch_entries[new_index];
|
|
if (g_scanner_root_watch_heads[entry->scan_root_index] == old_index)
|
|
g_scanner_root_watch_heads[entry->scan_root_index] = new_index;
|
|
if (entry->prev_root_watch_index != SCANNER_WATCH_INDEX_NONE) {
|
|
g_scanner_watch_entries[entry->prev_root_watch_index].next_root_watch_index =
|
|
new_index;
|
|
}
|
|
if (entry->next_root_watch_index != SCANNER_WATCH_INDEX_NONE) {
|
|
g_scanner_watch_entries[entry->next_root_watch_index].prev_root_watch_index =
|
|
new_index;
|
|
}
|
|
}
|
|
|
|
static bool register_scanner_watch_entry(int kq, int scan_root_index,
|
|
const char *path,
|
|
scanner_watch_kind_t kind,
|
|
uint8_t depth) {
|
|
int open_flags = O_RDONLY;
|
|
if (kind != SCANNER_WATCH_SCAN_IMAGE_FILE)
|
|
open_flags |= O_DIRECTORY;
|
|
|
|
int fd = open(path, open_flags);
|
|
if (fd < 0) {
|
|
if (errno != ENOENT && errno != ENOTDIR) {
|
|
log_debug(" [SCAN] watcher open failed for %s: %s", path,
|
|
strerror(errno));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!ensure_scanner_watch_capacity(g_scanner_watch_count + 1u)) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
if (!ensure_scanner_watch_fd_index_capacity(g_scanner_watch_count + 1u)) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
|
|
struct kevent kev;
|
|
EV_SET(&kev, (uintptr_t)fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR,
|
|
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
|
|
NOTE_REVOKE,
|
|
0, NULL);
|
|
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
|
log_debug(" [SCAN] watcher registration failed for %s: %s", path,
|
|
strerror(errno));
|
|
close(fd);
|
|
return false;
|
|
}
|
|
|
|
scanner_watch_entry_t *entry = &g_scanner_watch_entries[g_scanner_watch_count++];
|
|
memset(entry, 0, sizeof(*entry));
|
|
entry->fd = fd;
|
|
entry->scan_root_index = scan_root_index;
|
|
entry->depth = depth;
|
|
entry->kind = kind;
|
|
entry->prev_root_watch_index = SCANNER_WATCH_INDEX_NONE;
|
|
entry->next_root_watch_index = SCANNER_WATCH_INDEX_NONE;
|
|
(void)strlcpy(entry->path, path, sizeof(entry->path));
|
|
link_scanner_watch_entry_to_root(g_scanner_watch_count - 1u);
|
|
if (!insert_scanner_watch_fd_index_entry((uintptr_t)fd,
|
|
g_scanner_watch_count - 1u)) {
|
|
unlink_scanner_watch_entry_from_root(g_scanner_watch_count - 1u);
|
|
memset(entry, 0, sizeof(*entry));
|
|
close(fd);
|
|
g_scanner_watch_count--;
|
|
(void)rebuild_scanner_watch_fd_index();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void remove_scanner_watch_entry_at(size_t index) {
|
|
if (index >= g_scanner_watch_count)
|
|
return;
|
|
|
|
unlink_scanner_watch_entry_from_root(index);
|
|
if (g_scanner_watch_entries[index].fd >= 0)
|
|
close(g_scanner_watch_entries[index].fd);
|
|
|
|
size_t last_index = g_scanner_watch_count - 1u;
|
|
if (index != last_index) {
|
|
g_scanner_watch_entries[index] = g_scanner_watch_entries[last_index];
|
|
rebind_scanner_watch_entry_root_index(last_index, index);
|
|
}
|
|
memset(&g_scanner_watch_entries[last_index], 0,
|
|
sizeof(g_scanner_watch_entries[last_index]));
|
|
g_scanner_watch_count--;
|
|
}
|
|
|
|
static bool remove_scan_root_watch_entries(int scan_root_index) {
|
|
bool removed_any = false;
|
|
while (g_scanner_root_watch_heads[scan_root_index] != SCANNER_WATCH_INDEX_NONE) {
|
|
remove_scanner_watch_entry_at(g_scanner_root_watch_heads[scan_root_index]);
|
|
removed_any = true;
|
|
}
|
|
|
|
return !removed_any || rebuild_scanner_watch_fd_index();
|
|
}
|
|
|
|
static bool remove_scan_root_watch_entries_for_path(int scan_root_index,
|
|
const char *path) {
|
|
size_t index = g_scanner_root_watch_heads[scan_root_index];
|
|
bool removed_any = false;
|
|
while (index != SCANNER_WATCH_INDEX_NONE) {
|
|
if (path_matches_root_or_child(g_scanner_watch_entries[index].path, path)) {
|
|
remove_scanner_watch_entry_at(index);
|
|
removed_any = true;
|
|
index = g_scanner_root_watch_heads[scan_root_index];
|
|
continue;
|
|
}
|
|
index = g_scanner_watch_entries[index].next_root_watch_index;
|
|
}
|
|
|
|
return !removed_any || rebuild_scanner_watch_fd_index();
|
|
}
|
|
|
|
static scanner_watch_entry_t *find_scanner_watch_entry_by_fd(uintptr_t ident) {
|
|
if (!g_scanner_watch_fd_index || g_scanner_watch_fd_index_capacity == 0)
|
|
return NULL;
|
|
|
|
size_t mask = g_scanner_watch_fd_index_capacity - 1u;
|
|
size_t slot = scanner_watch_fd_hash(ident) & mask;
|
|
while (g_scanner_watch_fd_index[slot] != SCANNER_WATCH_INDEX_NONE) {
|
|
size_t watch_index = g_scanner_watch_fd_index[slot];
|
|
if ((uintptr_t)g_scanner_watch_entries[watch_index].fd == ident)
|
|
return &g_scanner_watch_entries[watch_index];
|
|
slot = (slot + 1u) & mask;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool build_parent_directory_path(const char *path,
|
|
char parent_path[MAX_PATH]) {
|
|
const char *slash = strrchr(path, '/');
|
|
if (!slash)
|
|
return false;
|
|
if (slash == path) {
|
|
(void)strlcpy(parent_path, "/", MAX_PATH);
|
|
return true;
|
|
}
|
|
|
|
size_t parent_len = (size_t)(slash - path);
|
|
if (parent_len >= MAX_PATH)
|
|
parent_len = MAX_PATH - 1u;
|
|
memcpy(parent_path, path, parent_len);
|
|
parent_path[parent_len] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static bool stat_directory_identity(const char *path, uint64_t *device_out,
|
|
uint64_t *inode_out) {
|
|
struct stat st;
|
|
if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode))
|
|
return false;
|
|
|
|
if (device_out)
|
|
*device_out = (uint64_t)st.st_dev;
|
|
if (inode_out)
|
|
*inode_out = (uint64_t)st.st_ino;
|
|
return true;
|
|
}
|
|
|
|
static bool update_scan_root_presence_state(int scan_root_index,
|
|
const char *scan_root,
|
|
bool *present_out) {
|
|
uint64_t root_device = 0;
|
|
uint64_t root_inode = 0;
|
|
bool present =
|
|
stat_directory_identity(scan_root, &root_device, &root_inode);
|
|
scanner_root_state_t *state = &g_scanner_root_states[scan_root_index];
|
|
bool changed = state->root_present != present;
|
|
if (present && state->root_present &&
|
|
(state->root_device != root_device || state->root_inode != root_inode)) {
|
|
changed = true;
|
|
}
|
|
|
|
state->root_present = present;
|
|
state->root_device = present ? root_device : 0;
|
|
state->root_inode = present ? root_inode : 0;
|
|
*present_out = present;
|
|
return changed;
|
|
}
|
|
|
|
static bool resolve_existing_parent_directory_path(
|
|
const char *path, char parent_path[MAX_PATH]) {
|
|
char current_path[MAX_PATH];
|
|
(void)strlcpy(current_path, path, sizeof(current_path));
|
|
|
|
while (true) {
|
|
char candidate_parent[MAX_PATH];
|
|
if (!build_parent_directory_path(current_path, candidate_parent))
|
|
return false;
|
|
if (strcmp(candidate_parent, current_path) == 0)
|
|
return false;
|
|
if (stat_directory_identity(candidate_parent, NULL, NULL)) {
|
|
(void)strlcpy(parent_path, candidate_parent, MAX_PATH);
|
|
return true;
|
|
}
|
|
(void)strlcpy(current_path, candidate_parent, sizeof(current_path));
|
|
}
|
|
}
|
|
|
|
static bool resolve_watch_tree_rebuild_target(const scanner_watch_entry_t *entry,
|
|
char rebuild_path[MAX_PATH],
|
|
uint8_t *rebuild_depth_out,
|
|
scanner_watch_kind_t *kind_out) {
|
|
switch (entry->kind) {
|
|
case SCANNER_WATCH_SCAN_ROOT:
|
|
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
|
|
case SCANNER_WATCH_SCAN_SUBDIR:
|
|
(void)strlcpy(rebuild_path, entry->path, MAX_PATH);
|
|
*rebuild_depth_out = entry->depth;
|
|
*kind_out = entry->kind;
|
|
return true;
|
|
case SCANNER_WATCH_SCAN_ROOT_PARENT:
|
|
(void)strlcpy(rebuild_path, get_scan_path(entry->scan_root_index),
|
|
MAX_PATH);
|
|
*rebuild_depth_out = 0;
|
|
*kind_out = SCANNER_WATCH_SCAN_ROOT;
|
|
return true;
|
|
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
|
if (!build_parent_directory_path(entry->path, rebuild_path))
|
|
return false;
|
|
*rebuild_depth_out = (entry->depth > 0u) ? (uint8_t)(entry->depth - 1u) : 0u;
|
|
*kind_out =
|
|
(entry->depth <= 1u) ? SCANNER_WATCH_SCAN_ROOT : SCANNER_WATCH_SCAN_SUBDIR;
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
int kq;
|
|
int scan_root_index;
|
|
} register_watch_tree_ctx_t;
|
|
|
|
static sm_scan_tree_dir_visit_t register_watch_directory_visit(
|
|
const char *dir_path, unsigned int depth_from_root, void *ctx_ptr) {
|
|
register_watch_tree_ctx_t *ctx = (register_watch_tree_ctx_t *)ctx_ptr;
|
|
scanner_watch_kind_t kind =
|
|
(depth_from_root == 0u) ? SCANNER_WATCH_SCAN_ROOT
|
|
: SCANNER_WATCH_SCAN_SUBDIR;
|
|
if (!register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, dir_path, kind,
|
|
(uint8_t)depth_from_root)) {
|
|
return SM_SCAN_TREE_DIR_ABORT;
|
|
}
|
|
|
|
return SM_SCAN_TREE_DIR_DESCEND;
|
|
}
|
|
|
|
static bool register_watch_image_visit(const char *image_path,
|
|
const char *image_name,
|
|
unsigned int depth_from_root,
|
|
void *ctx_ptr) {
|
|
(void)image_name;
|
|
|
|
register_watch_tree_ctx_t *ctx = (register_watch_tree_ctx_t *)ctx_ptr;
|
|
return register_scanner_watch_entry(ctx->kq, ctx->scan_root_index, image_path,
|
|
SCANNER_WATCH_SCAN_IMAGE_FILE,
|
|
(uint8_t)depth_from_root);
|
|
}
|
|
|
|
static bool register_scan_root_parent_watch(int kq, int scan_root_index,
|
|
const char *scan_root) {
|
|
char parent_path[MAX_PATH];
|
|
if (!resolve_existing_parent_directory_path(scan_root, parent_path))
|
|
return true;
|
|
|
|
return register_scanner_watch_entry(kq, scan_root_index, parent_path,
|
|
SCANNER_WATCH_SCAN_ROOT_PARENT, 0u);
|
|
}
|
|
|
|
static bool rebuild_scan_root_watch_tree(int kq, int scan_root_index) {
|
|
const char *scan_root = get_scan_path(scan_root_index);
|
|
if (!remove_scan_root_watch_entries(scan_root_index))
|
|
return false;
|
|
|
|
bool root_present = false;
|
|
(void)update_scan_root_presence_state(scan_root_index, scan_root,
|
|
&root_present);
|
|
if (!root_present) {
|
|
if (!register_scan_root_parent_watch(kq, scan_root_index, scan_root))
|
|
return false;
|
|
clear_scan_root_watch_tree_state(scan_root_index);
|
|
return true;
|
|
}
|
|
|
|
unsigned int scan_depth = runtime_config()->scan_depth;
|
|
if (scan_depth < MIN_SCAN_DEPTH)
|
|
scan_depth = MIN_SCAN_DEPTH;
|
|
|
|
register_watch_tree_ctx_t walk_ctx = {
|
|
.kq = kq,
|
|
.scan_root_index = scan_root_index,
|
|
};
|
|
sm_scan_tree_callbacks_t callbacks = {
|
|
.on_directory = register_watch_directory_visit,
|
|
.on_image_file = register_watch_image_visit,
|
|
};
|
|
if (!sm_scan_tree_walk(scan_root, scan_root, 0u, scan_depth, &callbacks,
|
|
&walk_ctx)) {
|
|
return false;
|
|
}
|
|
|
|
char backport_root[MAX_PATH];
|
|
if (build_backports_root_path(scan_root, backport_root)) {
|
|
if (!register_scanner_watch_entry(kq, scan_root_index, backport_root,
|
|
SCANNER_WATCH_SCAN_BACKPORT_ROOT, 1u)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!register_scan_root_parent_watch(kq, scan_root_index, scan_root))
|
|
return false;
|
|
|
|
clear_scan_root_watch_tree_state(scan_root_index);
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
if (!rebuild_path || rebuild_path[0] == '\0' ||
|
|
rebuild_kind == SCANNER_WATCH_SCAN_ROOT ||
|
|
strcmp(rebuild_path, scan_root) == 0) {
|
|
return rebuild_scan_root_watch_tree(kq, scan_root_index);
|
|
}
|
|
|
|
if (rebuild_kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT) {
|
|
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
|
return false;
|
|
if (!register_scanner_watch_entry(kq, scan_root_index, rebuild_path,
|
|
SCANNER_WATCH_SCAN_BACKPORT_ROOT,
|
|
rebuild_depth)) {
|
|
return false;
|
|
}
|
|
clear_scan_root_watch_tree_state(scan_root_index);
|
|
return true;
|
|
}
|
|
|
|
unsigned int scan_depth = runtime_config()->scan_depth;
|
|
if (scan_depth < MIN_SCAN_DEPTH)
|
|
scan_depth = MIN_SCAN_DEPTH;
|
|
|
|
if (!remove_scan_root_watch_entries_for_path(scan_root_index, rebuild_path))
|
|
return false;
|
|
if (rebuild_depth > scan_depth) {
|
|
clear_scan_root_watch_tree_state(scan_root_index);
|
|
return true;
|
|
}
|
|
|
|
register_watch_tree_ctx_t walk_ctx = {
|
|
.kq = kq,
|
|
.scan_root_index = scan_root_index,
|
|
};
|
|
sm_scan_tree_callbacks_t callbacks = {
|
|
.on_directory = register_watch_directory_visit,
|
|
.on_image_file = register_watch_image_visit,
|
|
};
|
|
if (!sm_scan_tree_walk(scan_root, rebuild_path, rebuild_depth,
|
|
scan_depth - rebuild_depth, &callbacks, &walk_ctx)) {
|
|
return false;
|
|
}
|
|
|
|
clear_scan_root_watch_tree_state(scan_root_index);
|
|
return true;
|
|
}
|
|
|
|
static bool rebuild_all_scan_root_watch_trees(int kq) {
|
|
for (int i = 0; i < get_scan_path_count(); i++) {
|
|
if (!rebuild_scan_root_watch_tree(kq, i))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void clear_all_dirty_scan_roots(void) {
|
|
for (int i = 0; i < get_scan_path_count(); i++) {
|
|
g_scanner_root_states[i].dirty = false;
|
|
g_scanner_root_states[i].cleanup_pending = false;
|
|
g_scanner_root_states[i].ready_after_us = 0;
|
|
clear_scan_root_watch_tree_state(i);
|
|
}
|
|
}
|
|
|
|
static void schedule_scan_root_cleanup(int scan_root_index) {
|
|
g_scanner_root_states[scan_root_index].cleanup_pending = true;
|
|
}
|
|
|
|
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];
|
|
uint64_t ready_after_us =
|
|
immediate ? now_us : now_us + scanner_stability_wait_us();
|
|
|
|
if (!state->dirty) {
|
|
state->dirty = true;
|
|
state->ready_after_us = ready_after_us;
|
|
return;
|
|
}
|
|
|
|
if (immediate) {
|
|
if (ready_after_us < state->ready_after_us)
|
|
state->ready_after_us = ready_after_us;
|
|
return;
|
|
}
|
|
|
|
if (ready_after_us > state->ready_after_us)
|
|
state->ready_after_us = ready_after_us;
|
|
}
|
|
|
|
static bool scanner_event_requires_consistency_cleanup(
|
|
const scanner_watch_entry_t *entry, uint32_t fflags) {
|
|
if (entry->kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT)
|
|
return false;
|
|
|
|
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
|
}
|
|
|
|
static bool scanner_event_requires_watch_tree_refresh(
|
|
const scanner_watch_entry_t *entry, uint32_t fflags) {
|
|
uint32_t tree_change_flags =
|
|
NOTE_WRITE | NOTE_EXTEND | NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE;
|
|
|
|
switch (entry->kind) {
|
|
case SCANNER_WATCH_SCAN_ROOT:
|
|
return (fflags & tree_change_flags) != 0;
|
|
case SCANNER_WATCH_SCAN_BACKPORT_ROOT:
|
|
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
|
case SCANNER_WATCH_SCAN_SUBDIR:
|
|
return entry->depth < runtime_config()->scan_depth &&
|
|
(fflags & tree_change_flags) != 0;
|
|
case SCANNER_WATCH_SCAN_IMAGE_FILE:
|
|
return (fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void schedule_scan_root_watch_tree_rebuild(
|
|
const scanner_watch_entry_t *entry) {
|
|
char rebuild_path[MAX_PATH];
|
|
uint8_t rebuild_depth = 0;
|
|
scanner_watch_kind_t rebuild_kind = SCANNER_WATCH_SCAN_ROOT;
|
|
if (!resolve_watch_tree_rebuild_target(entry, rebuild_path, &rebuild_depth,
|
|
&rebuild_kind)) {
|
|
return;
|
|
}
|
|
|
|
scanner_root_state_t *state = &g_scanner_root_states[entry->scan_root_index];
|
|
if (!state->watch_tree_stale || state->watch_tree_rebuild_path[0] == '\0') {
|
|
state->watch_tree_stale = true;
|
|
state->watch_tree_rebuild_depth = rebuild_depth;
|
|
state->watch_tree_rebuild_kind = rebuild_kind;
|
|
(void)strlcpy(state->watch_tree_rebuild_path, rebuild_path,
|
|
sizeof(state->watch_tree_rebuild_path));
|
|
return;
|
|
}
|
|
|
|
if (strcmp(state->watch_tree_rebuild_path, rebuild_path) == 0) {
|
|
if (rebuild_depth < state->watch_tree_rebuild_depth)
|
|
state->watch_tree_rebuild_depth = rebuild_depth;
|
|
if (rebuild_kind == SCANNER_WATCH_SCAN_ROOT ||
|
|
(rebuild_kind == SCANNER_WATCH_SCAN_BACKPORT_ROOT &&
|
|
state->watch_tree_rebuild_kind != SCANNER_WATCH_SCAN_ROOT)) {
|
|
state->watch_tree_rebuild_kind = rebuild_kind;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (path_matches_root_or_child(rebuild_path, state->watch_tree_rebuild_path))
|
|
return;
|
|
|
|
if (path_matches_root_or_child(state->watch_tree_rebuild_path, rebuild_path)) {
|
|
state->watch_tree_rebuild_depth = rebuild_depth;
|
|
state->watch_tree_rebuild_kind = rebuild_kind;
|
|
(void)strlcpy(state->watch_tree_rebuild_path, rebuild_path,
|
|
sizeof(state->watch_tree_rebuild_path));
|
|
return;
|
|
}
|
|
|
|
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(entry->scan_root_index),
|
|
sizeof(state->watch_tree_rebuild_path));
|
|
}
|
|
|
|
static void register_config_file_watch(int kq, uint64_t now_us) {
|
|
if (g_scanner_config_fd < 0)
|
|
return;
|
|
|
|
struct kevent kev;
|
|
EV_SET(&kev, (uintptr_t)g_scanner_config_fd, EVFILT_VNODE,
|
|
EV_ADD | EV_ENABLE | EV_CLEAR,
|
|
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
|
|
NOTE_REVOKE,
|
|
0, NULL);
|
|
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
|
log_debug(" [CFG] config watcher registration failed: %s",
|
|
strerror(errno));
|
|
close_scanner_config_file();
|
|
schedule_config_probe(now_us);
|
|
return;
|
|
}
|
|
|
|
g_scanner_config_probe_due_us = 0;
|
|
}
|
|
|
|
static void reopen_config_file_watch(int kq, uint64_t now_us) {
|
|
close_scanner_config_file();
|
|
|
|
g_scanner_config_fd = open(CONFIG_FILE, O_RDONLY);
|
|
if (g_scanner_config_fd < 0) {
|
|
if (errno != ENOENT) {
|
|
log_debug(" [CFG] config watcher unavailable for %s: %s", CONFIG_FILE,
|
|
strerror(errno));
|
|
}
|
|
schedule_config_probe(now_us);
|
|
return;
|
|
}
|
|
|
|
register_config_file_watch(kq, now_us);
|
|
}
|
|
|
|
static void register_manual_file_watch(int kq, uint64_t now_us) {
|
|
if (g_scanner_manual_fd < 0)
|
|
return;
|
|
|
|
struct kevent kev;
|
|
EV_SET(&kev, (uintptr_t)g_scanner_manual_fd, EVFILT_VNODE,
|
|
EV_ADD | EV_ENABLE | EV_CLEAR,
|
|
NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_DELETE | NOTE_RENAME |
|
|
NOTE_REVOKE,
|
|
0, NULL);
|
|
if (kevent(kq, &kev, 1, NULL, 0, NULL) != 0) {
|
|
log_debug(" [MANUAL] manual.lst watcher registration failed: %s",
|
|
strerror(errno));
|
|
close_scanner_manual_file();
|
|
schedule_manual_probe(now_us);
|
|
return;
|
|
}
|
|
|
|
g_scanner_manual_probe_due_us = 0;
|
|
}
|
|
|
|
static int open_manual_list_file(void) {
|
|
mkdir(LOG_DIR, 0777);
|
|
int fd = open(MANUAL_LIST_FILE, O_RDONLY | O_CREAT, 0666);
|
|
if (fd < 0) {
|
|
log_debug(" [MANUAL] manual.lst open/create failed for %s: %s",
|
|
MANUAL_LIST_FILE, strerror(errno));
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
static void reopen_manual_file_watch(int kq, uint64_t now_us) {
|
|
close_scanner_manual_file();
|
|
|
|
g_scanner_manual_fd = open_manual_list_file();
|
|
if (g_scanner_manual_fd < 0) {
|
|
schedule_manual_probe(now_us);
|
|
return;
|
|
}
|
|
|
|
register_manual_file_watch(kq, now_us);
|
|
}
|
|
|
|
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 (old_cfg->backport_fakelib_enabled &&
|
|
fakelib_runtime_config_changed(old_cfg, new_cfg))
|
|
sm_fakelib_game_shutdown();
|
|
|
|
sm_kstuff_on_config_reload();
|
|
|
|
if (scan_topology_changed) {
|
|
clear_scanner_watch_entries();
|
|
reset_scanner_root_states();
|
|
if (!rebuild_all_scan_root_watch_trees(kq)) {
|
|
log_debug(" [CFG] scanner watch rebuild failed after config reload");
|
|
return false;
|
|
} else {
|
|
log_debug(" [CFG] scanner watches rebuilt after scan config reload");
|
|
}
|
|
}
|
|
|
|
if (!refresh_game_lifecycle_watcher()) {
|
|
log_debug(" [CFG] lifecycle watcher refresh failed after config reload");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
scan_candidate_t *candidates = g_scanner_scan_candidates;
|
|
|
|
log_immediate_scan_reason(reason);
|
|
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
bool unstable_found = false;
|
|
cleanup_lost_sources_before_scan();
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
int total_found_games = 0;
|
|
int *total_found_ptr = startup_sync ? &total_found_games : NULL;
|
|
int candidate_count = collect_scan_candidates(candidates, MAX_PENDING,
|
|
total_found_ptr,
|
|
&unstable_found);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
if (candidate_count > 0 && startup_sync) {
|
|
int new_games = 0;
|
|
for (int i = 0; i < candidate_count; i++) {
|
|
if (!candidates[i].installed)
|
|
new_games++;
|
|
}
|
|
if (new_games > 0)
|
|
notify_system_info("Found %d new games. Executing...", new_games);
|
|
}
|
|
|
|
process_scan_candidates(candidates, candidate_count);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
mount_backport_overlays(&unstable_found);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
if (unstable_found_out)
|
|
*unstable_found_out = unstable_found;
|
|
|
|
if (startup_sync && !should_abort_scan_cycle()) {
|
|
notify_system_rich(true, "Library Synchronized.\nFound %d games.",
|
|
total_found_games);
|
|
}
|
|
|
|
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;
|
|
|
|
log_debug("[SCAN] running targeted scan for %s", scan_root);
|
|
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
bool unstable_found = false;
|
|
cleanup_lost_sources_for_scan_root(scan_root);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
int candidate_count = collect_scan_candidates_for_scan_root(
|
|
scan_root, candidates, MAX_PENDING, NULL, &unstable_found);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
process_scan_candidates(candidates, candidate_count);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
mount_backport_overlays(&unstable_found);
|
|
if (should_abort_scan_cycle())
|
|
return false;
|
|
|
|
if (unstable_found_out)
|
|
*unstable_found_out = unstable_found;
|
|
|
|
return !should_abort_scan_cycle();
|
|
}
|
|
|
|
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)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static bool config_reload_due(uint64_t now_us) {
|
|
return g_scanner_config_reload_pending &&
|
|
now_us >= g_scanner_config_reload_ready_after_us;
|
|
}
|
|
|
|
static bool config_probe_due(uint64_t now_us) {
|
|
return g_scanner_config_fd < 0 && g_scanner_config_probe_due_us != 0 &&
|
|
now_us >= g_scanner_config_probe_due_us;
|
|
}
|
|
|
|
static int find_due_dirty_scan_root(uint64_t now_us) {
|
|
int selected_root = -1;
|
|
uint64_t selected_deadline = 0;
|
|
|
|
for (int i = 0; i < get_scan_path_count(); i++) {
|
|
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
|
if (!state->dirty)
|
|
continue;
|
|
if (state->ready_after_us > now_us)
|
|
continue;
|
|
if (selected_root < 0 || state->ready_after_us < selected_deadline) {
|
|
selected_root = i;
|
|
selected_deadline = state->ready_after_us;
|
|
}
|
|
}
|
|
|
|
return selected_root;
|
|
}
|
|
|
|
static uint64_t compute_next_scan_deadline_us(uint64_t now_us,
|
|
uint64_t full_resync_due_us) {
|
|
uint64_t next_deadline = full_resync_due_us;
|
|
uint64_t install_wake_us = sm_install_next_wake_us(now_us);
|
|
|
|
if (install_wake_us != 0 &&
|
|
(next_deadline == 0 || install_wake_us < next_deadline)) {
|
|
next_deadline = install_wake_us;
|
|
}
|
|
|
|
if (g_scanner_config_reload_pending &&
|
|
(next_deadline == 0 ||
|
|
g_scanner_config_reload_ready_after_us < next_deadline)) {
|
|
next_deadline = g_scanner_config_reload_ready_after_us;
|
|
}
|
|
|
|
if (g_scanner_config_fd < 0 && g_scanner_config_probe_due_us != 0 &&
|
|
(next_deadline == 0 || g_scanner_config_probe_due_us < next_deadline)) {
|
|
next_deadline = g_scanner_config_probe_due_us;
|
|
}
|
|
|
|
if (g_scanner_manual_scan_due_us != 0 &&
|
|
(next_deadline == 0 || g_scanner_manual_scan_due_us < next_deadline)) {
|
|
next_deadline = g_scanner_manual_scan_due_us;
|
|
}
|
|
|
|
if (g_scanner_manual_fd < 0 && g_scanner_manual_probe_due_us != 0 &&
|
|
(next_deadline == 0 || g_scanner_manual_probe_due_us < next_deadline)) {
|
|
next_deadline = g_scanner_manual_probe_due_us;
|
|
}
|
|
|
|
for (int i = 0; i < get_scan_path_count(); i++) {
|
|
const scanner_root_state_t *state = &g_scanner_root_states[i];
|
|
if (!state->dirty)
|
|
continue;
|
|
if (next_deadline == 0 || state->ready_after_us < next_deadline)
|
|
next_deadline = state->ready_after_us;
|
|
}
|
|
|
|
return next_deadline;
|
|
}
|
|
|
|
static const struct timespec *build_wait_timeout(struct timespec *timeout,
|
|
uint64_t now_us,
|
|
uint64_t deadline_us) {
|
|
if (deadline_us == 0)
|
|
return NULL;
|
|
|
|
memset(timeout, 0, sizeof(*timeout));
|
|
if (deadline_us <= now_us)
|
|
return timeout;
|
|
|
|
uint64_t delta_us = deadline_us - now_us;
|
|
timeout->tv_sec = (time_t)(delta_us / 1000000ull);
|
|
timeout->tv_nsec = (long)((delta_us % 1000000ull) * 1000ull);
|
|
return timeout;
|
|
}
|
|
|
|
static bool handle_scan_root_parent_event(
|
|
int kq, const scanner_watch_entry_t *entry, uint64_t now_us) {
|
|
const char *scan_root = get_scan_path(entry->scan_root_index);
|
|
bool root_present = false;
|
|
bool root_changed = update_scan_root_presence_state(
|
|
entry->scan_root_index, scan_root, &root_present);
|
|
if (root_present && root_changed) {
|
|
schedule_scan_root_dirty(entry->scan_root_index, now_us, false);
|
|
schedule_scan_root_watch_tree_rebuild(entry);
|
|
return true;
|
|
}
|
|
if (root_present)
|
|
return true;
|
|
if (root_changed) {
|
|
schedule_scan_root_cleanup(entry->scan_root_index);
|
|
schedule_scan_root_dirty(entry->scan_root_index, now_us, true);
|
|
schedule_scan_root_watch_tree_rebuild(entry);
|
|
return true;
|
|
}
|
|
|
|
char parent_path[MAX_PATH];
|
|
if (!resolve_existing_parent_directory_path(scan_root, parent_path) ||
|
|
strcmp(parent_path, entry->path) != 0) {
|
|
return rebuild_scan_root_watch_tree(kq, entry->scan_root_index);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool process_scanner_events(int kq, const struct timespec *timeout,
|
|
bool *timed_out_out) {
|
|
*timed_out_out = false;
|
|
|
|
struct kevent events[SCANNER_EVENT_BATCH];
|
|
int nev = kevent(kq, NULL, 0, events, SCANNER_EVENT_BATCH, timeout);
|
|
if (nev < 0) {
|
|
if (errno == EINTR)
|
|
return true;
|
|
|
|
log_debug(" [SCAN] kevent wait failed: %s", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (nev == 0) {
|
|
*timed_out_out = true;
|
|
return true;
|
|
}
|
|
|
|
uint64_t now_us = monotonic_time_us();
|
|
|
|
for (int i = 0; i < nev; i++) {
|
|
const struct kevent *event = &events[i];
|
|
|
|
if (event->filter == EVFILT_READ &&
|
|
event->ident == (uintptr_t)g_scanner_wake_pipe[0]) {
|
|
drain_scanner_wake_pipe();
|
|
continue;
|
|
}
|
|
|
|
if (event->filter != EVFILT_VNODE)
|
|
continue;
|
|
|
|
if (event->ident == (uintptr_t)g_scanner_config_fd) {
|
|
if ((event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0)
|
|
reopen_config_file_watch(kq, now_us);
|
|
schedule_config_reload(now_us);
|
|
continue;
|
|
}
|
|
|
|
if (event->ident == (uintptr_t)g_scanner_manual_fd) {
|
|
if ((event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0) {
|
|
close_scanner_manual_file();
|
|
g_scanner_manual_probe_due_us = now_us == 0 ? 1 : now_us;
|
|
continue;
|
|
}
|
|
schedule_manual_scan(now_us);
|
|
continue;
|
|
}
|
|
|
|
scanner_watch_entry_t *watch_entry =
|
|
find_scanner_watch_entry_by_fd(event->ident);
|
|
if (!watch_entry)
|
|
continue;
|
|
|
|
if (watch_entry->kind == SCANNER_WATCH_SCAN_ROOT_PARENT) {
|
|
if (!handle_scan_root_parent_event(kq, watch_entry, now_us))
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
bool immediate =
|
|
(event->fflags & (NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE)) != 0;
|
|
if (scanner_event_requires_consistency_cleanup(watch_entry, event->fflags))
|
|
schedule_scan_root_cleanup(watch_entry->scan_root_index);
|
|
schedule_scan_root_dirty(watch_entry->scan_root_index, now_us, immediate);
|
|
|
|
if (scanner_event_requires_watch_tree_refresh(watch_entry, event->fflags))
|
|
schedule_scan_root_watch_tree_rebuild(watch_entry);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool drain_scanner_events_nowait(int kq) {
|
|
struct timespec timeout;
|
|
memset(&timeout, 0, sizeof(timeout));
|
|
|
|
for (int batch = 0; batch < SCANNER_EVENT_DRAIN_BATCHES; batch++) {
|
|
bool timed_out = false;
|
|
if (!process_scanner_events(kq, &timeout, &timed_out))
|
|
return false;
|
|
if (timed_out)
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool discard_scanner_events_nowait(int kq) {
|
|
struct kevent events[SCANNER_EVENT_BATCH];
|
|
struct timespec timeout;
|
|
memset(&timeout, 0, sizeof(timeout));
|
|
|
|
while (true) {
|
|
int nev = kevent(kq, NULL, 0, events, SCANNER_EVENT_BATCH, &timeout);
|
|
if (nev < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
log_debug(" [SCAN] stale event drain failed: %s", strerror(errno));
|
|
return false;
|
|
}
|
|
if (nev == 0)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static char g_scanner_shutdown_reason[128];
|
|
|
|
static void request_scanner_shutdown(const char *reason) {
|
|
const char *resolved_reason =
|
|
(reason && reason[0] != '\0') ? reason : "scanner failure";
|
|
(void)strlcpy(g_scanner_shutdown_reason, resolved_reason,
|
|
sizeof(g_scanner_shutdown_reason));
|
|
log_debug(" [SCAN] %s; stopping scanner", g_scanner_shutdown_reason);
|
|
request_shutdown_stop(g_scanner_shutdown_reason);
|
|
}
|
|
|
|
bool sm_scanner_init(void) {
|
|
close_scanner_wake_pipe();
|
|
close_scanner_config_file();
|
|
close_scanner_manual_file();
|
|
clear_scanner_watch_entries();
|
|
reset_scanner_root_states();
|
|
clear_scanner_config_reload_state();
|
|
clear_scanner_manual_scan_state();
|
|
|
|
if (pipe(g_scanner_wake_pipe) != 0) {
|
|
log_debug(" [SCAN] wake pipe creation failed: %s", strerror(errno));
|
|
close_scanner_wake_pipe();
|
|
return false;
|
|
}
|
|
if (!set_fd_nonblocking(g_scanner_wake_pipe[0]) ||
|
|
!set_fd_nonblocking(g_scanner_wake_pipe[1])) {
|
|
log_debug(" [SCAN] wake pipe nonblocking setup failed: %s",
|
|
strerror(errno));
|
|
close_scanner_wake_pipe();
|
|
return false;
|
|
}
|
|
g_scanner_wake_write_fd = (sig_atomic_t)g_scanner_wake_pipe[1];
|
|
|
|
g_scanner_config_fd = open(CONFIG_FILE, O_RDONLY);
|
|
if (g_scanner_config_fd < 0 && errno != ENOENT) {
|
|
log_debug(" [CFG] config watch unavailable for %s: %s", CONFIG_FILE,
|
|
strerror(errno));
|
|
}
|
|
if (g_scanner_config_fd < 0)
|
|
schedule_config_probe(monotonic_time_us());
|
|
|
|
g_scanner_manual_fd = open_manual_list_file();
|
|
if (g_scanner_manual_fd < 0)
|
|
schedule_manual_probe(monotonic_time_us());
|
|
|
|
return true;
|
|
}
|
|
|
|
void sm_scanner_wake(void) {
|
|
sig_atomic_t wake_fd = g_scanner_wake_write_fd;
|
|
if (wake_fd < 0)
|
|
return;
|
|
|
|
static const char token = 'S';
|
|
(void)write((int)wake_fd, &token, sizeof(token));
|
|
}
|
|
|
|
bool sm_scanner_run_startup_sync(void) {
|
|
while (!should_stop_requested()) {
|
|
while (runtime_sleep_mode_active() && !should_stop_requested())
|
|
sceKernelUsleep(200000);
|
|
|
|
if (should_stop_requested())
|
|
return false;
|
|
if (run_full_scan_cycle(true, NULL, NULL))
|
|
return true;
|
|
if (!runtime_sleep_mode_active())
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void sm_scanner_run_loop(void) {
|
|
if (g_scanner_wake_pipe[0] < 0 || g_scanner_wake_pipe[1] < 0) {
|
|
request_scanner_shutdown("scanner wake pipe unavailable");
|
|
return;
|
|
}
|
|
|
|
int kq = kqueue();
|
|
if (kq < 0) {
|
|
char reason[128];
|
|
snprintf(reason, sizeof(reason), "scanner kqueue init failed: %s",
|
|
strerror(errno));
|
|
request_scanner_shutdown(reason);
|
|
return;
|
|
}
|
|
|
|
struct kevent wake_event;
|
|
EV_SET(&wake_event, (uintptr_t)g_scanner_wake_pipe[0], EVFILT_READ,
|
|
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, NULL);
|
|
if (kevent(kq, &wake_event, 1, NULL, 0, NULL) != 0) {
|
|
char reason[128];
|
|
snprintf(reason, sizeof(reason), "scanner wake pipe registration failed: %s",
|
|
strerror(errno));
|
|
close(kq);
|
|
request_scanner_shutdown(reason);
|
|
return;
|
|
}
|
|
|
|
register_config_file_watch(kq, monotonic_time_us());
|
|
register_manual_file_watch(kq, monotonic_time_us());
|
|
if (!rebuild_all_scan_root_watch_trees(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
close_scanner_config_file();
|
|
close_scanner_manual_file();
|
|
request_scanner_shutdown("scanner watcher initialization failed");
|
|
return;
|
|
}
|
|
|
|
uint64_t next_full_resync_us =
|
|
monotonic_time_us() + scanner_full_resync_interval_us();
|
|
bool was_sleeping = false;
|
|
|
|
while (true) {
|
|
if (should_stop_requested()) {
|
|
log_debug("[SHUTDOWN] stop requested");
|
|
break;
|
|
}
|
|
|
|
if (runtime_sleep_mode_active()) {
|
|
was_sleeping = true;
|
|
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] sleep wait failed: %s", strerror(errno));
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner sleep wait failed");
|
|
return;
|
|
}
|
|
drain_scanner_wake_pipe();
|
|
continue;
|
|
}
|
|
if (was_sleeping) {
|
|
was_sleeping = false;
|
|
if (!discard_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner stale event drain failed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
char scan_reason[128];
|
|
if (consume_scan_now_request(scan_reason, sizeof(scan_reason))) {
|
|
bool unstable_found = false;
|
|
if (!run_full_scan_cycle(false, scan_reason, &unstable_found)) {
|
|
if (runtime_sleep_mode_active())
|
|
continue;
|
|
break;
|
|
}
|
|
clear_all_dirty_scan_roots();
|
|
if (!rebuild_all_scan_root_watch_trees(kq) ||
|
|
!drain_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner watcher refresh failed");
|
|
return;
|
|
}
|
|
|
|
uint64_t now_us = monotonic_time_us();
|
|
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
|
if (unstable_found) {
|
|
uint64_t retry_due = now_us + scanner_stability_wait_us();
|
|
if (retry_due < next_full_resync_us)
|
|
next_full_resync_us = retry_due;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
uint64_t now_us = monotonic_time_us();
|
|
if (sm_game_lifecycle_has_active_game()) {
|
|
next_full_resync_us = 0;
|
|
} else if (next_full_resync_us == 0) {
|
|
next_full_resync_us = now_us;
|
|
}
|
|
|
|
if (config_probe_due(now_us)) {
|
|
g_scanner_config_probe_due_us = 0;
|
|
reopen_config_file_watch(kq, now_us);
|
|
if (g_scanner_config_fd >= 0)
|
|
schedule_config_reload(now_us);
|
|
continue;
|
|
}
|
|
|
|
if (g_scanner_manual_fd < 0 && g_scanner_manual_probe_due_us != 0 &&
|
|
now_us >= g_scanner_manual_probe_due_us) {
|
|
g_scanner_manual_probe_due_us = 0;
|
|
reopen_manual_file_watch(kq, now_us);
|
|
if (g_scanner_manual_fd >= 0)
|
|
schedule_manual_scan(now_us);
|
|
continue;
|
|
}
|
|
|
|
if (config_reload_due(now_us)) {
|
|
g_scanner_config_reload_pending = false;
|
|
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();
|
|
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();
|
|
bool scan_topology_changed =
|
|
old_scan_topology_hash != scanner_config_topology_hash();
|
|
if (!apply_runtime_config_reload_effects(kq, &old_cfg, new_cfg,
|
|
scan_topology_changed)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner watcher rebuild after config reload failed");
|
|
return;
|
|
}
|
|
if (scan_topology_changed && !discard_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner stale event drain after config reload failed");
|
|
return;
|
|
}
|
|
notify_system("ShadowMount+: config reloaded.");
|
|
log_debug(" [CFG] runtime config reloaded");
|
|
now_us = monotonic_time_us();
|
|
next_full_resync_us =
|
|
scan_topology_changed ? now_us
|
|
: now_us + scanner_full_resync_interval_us();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (g_scanner_manual_scan_due_us != 0 &&
|
|
now_us >= g_scanner_manual_scan_due_us) {
|
|
g_scanner_manual_scan_due_us = 0;
|
|
invalidate_app_db_title_cache();
|
|
|
|
bool unstable_found = false;
|
|
if (!run_full_scan_cycle(false, "manual.lst changed", &unstable_found)) {
|
|
if (runtime_sleep_mode_active())
|
|
continue;
|
|
break;
|
|
}
|
|
clear_all_dirty_scan_roots();
|
|
if (!rebuild_all_scan_root_watch_trees(kq) ||
|
|
!drain_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner watcher refresh after manual scan failed");
|
|
return;
|
|
}
|
|
|
|
now_us = monotonic_time_us();
|
|
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
|
if (unstable_found) {
|
|
uint64_t retry_due = now_us + scanner_stability_wait_us();
|
|
if (retry_due < next_full_resync_us)
|
|
next_full_resync_us = retry_due;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
uint64_t install_wake_us = sm_install_next_wake_us(now_us);
|
|
if (install_wake_us != 0 && now_us >= install_wake_us) {
|
|
sm_install_service_pending();
|
|
continue;
|
|
}
|
|
|
|
if (next_full_resync_us != 0 && now_us >= next_full_resync_us) {
|
|
bool unstable_found = false;
|
|
if (!run_full_scan_cycle(false, NULL, &unstable_found)) {
|
|
if (runtime_sleep_mode_active())
|
|
continue;
|
|
break;
|
|
}
|
|
clear_all_dirty_scan_roots();
|
|
if (!rebuild_all_scan_root_watch_trees(kq) ||
|
|
!drain_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner watcher refresh after full resync failed");
|
|
return;
|
|
}
|
|
|
|
now_us = monotonic_time_us();
|
|
next_full_resync_us = now_us + scanner_full_resync_interval_us();
|
|
if (unstable_found) {
|
|
uint64_t retry_due = now_us + scanner_stability_wait_us();
|
|
if (retry_due < next_full_resync_us)
|
|
next_full_resync_us = retry_due;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
int cleanup_root_index = find_pending_cleanup_scan_root();
|
|
if (cleanup_root_index >= 0) {
|
|
g_scanner_root_states[cleanup_root_index].cleanup_pending = false;
|
|
cleanup_lost_sources_for_scan_root(get_scan_path(cleanup_root_index));
|
|
continue;
|
|
}
|
|
|
|
int dirty_root_index = find_due_dirty_scan_root(now_us);
|
|
if (dirty_root_index >= 0) {
|
|
bool cleanup_pending =
|
|
g_scanner_root_states[dirty_root_index].cleanup_pending;
|
|
bool rebuild_watch_tree =
|
|
g_scanner_root_states[dirty_root_index].watch_tree_stale;
|
|
uint8_t rebuild_watch_tree_depth =
|
|
g_scanner_root_states[dirty_root_index].watch_tree_rebuild_depth;
|
|
scanner_watch_kind_t rebuild_watch_tree_kind =
|
|
g_scanner_root_states[dirty_root_index].watch_tree_rebuild_kind;
|
|
char rebuild_watch_tree_path[MAX_PATH];
|
|
(void)strlcpy(rebuild_watch_tree_path,
|
|
g_scanner_root_states[dirty_root_index]
|
|
.watch_tree_rebuild_path,
|
|
sizeof(rebuild_watch_tree_path));
|
|
g_scanner_root_states[dirty_root_index].cleanup_pending = false;
|
|
g_scanner_root_states[dirty_root_index].dirty = false;
|
|
g_scanner_root_states[dirty_root_index].ready_after_us = 0;
|
|
clear_scan_root_watch_tree_state(dirty_root_index);
|
|
|
|
bool unstable_found = false;
|
|
if (!run_targeted_scan_cycle(dirty_root_index, &unstable_found)) {
|
|
if (runtime_sleep_mode_active()) {
|
|
scanner_root_state_t *state =
|
|
&g_scanner_root_states[dirty_root_index];
|
|
if (cleanup_pending)
|
|
schedule_scan_root_cleanup(dirty_root_index);
|
|
schedule_scan_root_dirty(dirty_root_index, monotonic_time_us(), true);
|
|
if (rebuild_watch_tree) {
|
|
state->watch_tree_stale = true;
|
|
state->watch_tree_rebuild_depth = rebuild_watch_tree_depth;
|
|
state->watch_tree_rebuild_kind = rebuild_watch_tree_kind;
|
|
(void)strlcpy(state->watch_tree_rebuild_path,
|
|
rebuild_watch_tree_path,
|
|
sizeof(state->watch_tree_rebuild_path));
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (rebuild_watch_tree &&
|
|
!rebuild_scan_root_watch_subtree(kq, dirty_root_index,
|
|
rebuild_watch_tree_path,
|
|
rebuild_watch_tree_depth,
|
|
rebuild_watch_tree_kind)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner root watcher rebuild failed");
|
|
return;
|
|
}
|
|
if (!drain_scanner_events_nowait(kq)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner event drain failed");
|
|
return;
|
|
}
|
|
|
|
if (unstable_found)
|
|
schedule_scan_root_dirty(dirty_root_index, monotonic_time_us(), false);
|
|
continue;
|
|
}
|
|
|
|
uint64_t deadline_us =
|
|
compute_next_scan_deadline_us(now_us, next_full_resync_us);
|
|
struct timespec timeout;
|
|
const struct timespec *timeout_ptr =
|
|
build_wait_timeout(&timeout, now_us, deadline_us);
|
|
|
|
bool timed_out = false;
|
|
if (!process_scanner_events(kq, timeout_ptr, &timed_out)) {
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
request_scanner_shutdown("scanner kevent wait failed");
|
|
return;
|
|
}
|
|
if (should_stop_requested()) {
|
|
log_debug("[SHUTDOWN] stop requested during scanner wait");
|
|
break;
|
|
}
|
|
|
|
(void)timed_out;
|
|
}
|
|
|
|
close(kq);
|
|
clear_scanner_watch_entries();
|
|
}
|
|
|
|
void sm_scanner_shutdown(void) {
|
|
clear_scanner_watch_entries();
|
|
close_scanner_config_file();
|
|
close_scanner_manual_file();
|
|
close_scanner_wake_pipe();
|
|
reset_scanner_root_states();
|
|
clear_scanner_config_reload_state();
|
|
clear_scanner_manual_scan_state();
|
|
}
|