Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ docs/*
.idea/
.vscode/
legacy
/src/icon_asset.c
/src/config_ini_example_asset.c
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ CFLAGS += -DMICROMOUNT_VERSION=\"$(VERSION_TAG)\"
LDFLAGS := -flto=thin -Wl,--gc-sections
LIBS := -lSceNotification -lSceUserService

ASSET_SRCS := src/icon_asset.c \
src/config_ini_example_asset.c
SRCS := src/main.c \
src/mm_config.c \
src/mm_log.c \
src/mm_mount.c \
src/mm_scan.c \
src/mm_sha256.c \
src/mm_util.c
src/mm_util.c \
$(ASSET_SRCS)
OBJS := $(SRCS:.c=.o)
HEADERS := $(wildcard include/*.h)
TARGET := micromount.elf
Expand All @@ -28,5 +31,11 @@ $(TARGET): $(OBJS)
src/%.o: src/%.c $(HEADERS)
$(CC) $(CFLAGS) -c -o $@ $<

src/icon_asset.c: assets/icon.png
xxd -i $< > $@

src/config_ini_example_asset.c: config.ini.example
xxd -i $< > $@

clean:
rm -f $(TARGET) src/*.o
rm -f $(TARGET) src/*.o src/icon_asset.c src/config_ini_example_asset.c
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ MicroMount runtime behavior:
- Cleans stale managed mounts before each reconcile pass.
- Skips images that are already correctly mounted.
- Writes logs to `/data/micromount/debug.log` on every run.
- Reads config from `/data/micromount/config.ini` (defaults are used when missing).
- Reads config from `/data/micromount/config.ini` (auto-created from template when missing).

## 💖 Sponsorship

Expand Down Expand Up @@ -58,7 +58,7 @@ mkpfs pack file --compress --verify ./GAME1234.exfat ./GAME1234.ffpfsc

## ⚙️ How it works

1. MicroMount starts, loads defaults, then loads `/data/micromount/config.ini` if present.
1. MicroMount starts, loads defaults, creates `/data/micromount/config.ini` from the embedded template when missing, then loads it.
2. It scans `scan_paths` recursively up to `scan_depth` for `.ffpfsc`.
3. For each candidate image, it builds:
- `GAMEID` from filename (PlayStation-style `AAAA0000` or `AAAA00000` when found).
Expand All @@ -78,14 +78,15 @@ mkpfs pack file --compress --verify ./GAME1234.exfat ./GAME1234.ffpfsc
## 🛠️ Configuration

Use `config.ini.example` as the template for `/data/micromount/config.ini`.
If `/data/micromount/config.ini` does not exist at runtime, MicroMount automatically creates it from the embedded `config.ini.example`.

Core keys:

- `target_directory` (default: `/data/homebrew`)
- `scanpath` (repeatable)
- `scan_paths` (comma/semicolon-separated list)
- `scan_depth` (default: `1`)
- `scan_interval_seconds` (default: `15`)
- `scan_interval_seconds` (default: `30`)
- `debug` (default: `1`)

Mount profile keys:
Expand Down
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon.psd
Binary file not shown.
Binary file added assets/icon_large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion config.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# scan_depth=1

# Full scan interval in seconds.
# scan_interval_seconds=15
# scan_interval_seconds=30

# Default scan roots copied from ShadowMountPlus are used automatically.
# To override them, add one or more scanpath entries or a scan_paths list.
Expand Down
2 changes: 2 additions & 0 deletions include/mm_paths.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#define MM_CONFIG_FILE MM_ROOT_DIR "/config.ini"
#define MM_LOG_FILE MM_ROOT_DIR "/debug.log"
#define MM_MANAGED_PREFIX "micromount-"
#define MM_NOTIFY_ICON_DIR "/user/data/micromount"
#define MM_NOTIFY_ICON_FILE MM_NOTIFY_ICON_DIR "/icon.png"

#define MM_DEFAULT_SCAN_PATHS_INITIALIZER \
{ \
Expand Down
8 changes: 8 additions & 0 deletions include/mm_runtime.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef MM_RUNTIME_H
#define MM_RUNTIME_H

#include "mm_platform.h"

bool mm_should_stop(void);

#endif
172 changes: 165 additions & 7 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
#include "mm_mount.h"
#include "mm_paths.h"
#include "mm_platform.h"
#include "mm_runtime.h"
#include "mm_scan.h"

#include <sys/sysctl.h>
#include <sys/user.h>

#define MM_RESTART_WAIT_POLL_US 200000u
#define MM_RESTART_WAIT_TIMEOUT_US 15000000u

static volatile sig_atomic_t g_stop_requested = 0;

static void mm_on_signal(int signal_number) {
Expand All @@ -24,12 +31,12 @@ static void mm_install_signal_handlers(void) {
sigaction(SIGQUIT, &action, NULL);
}

static bool mm_should_stop(void) {
bool mm_should_stop(void) {
return g_stop_requested != 0;
}

static bool mm_sleep_with_stop(unsigned int seconds) {
unsigned int remaining_us = seconds * 1000000u;
static bool mm_sleep_with_stop_us(unsigned int total_us) {
unsigned int remaining_us = total_us;

while (remaining_us > 0) {
unsigned int step = remaining_us > 200000u ? 200000u : remaining_us;
Expand All @@ -42,6 +49,125 @@ static bool mm_sleep_with_stop(unsigned int seconds) {
return mm_should_stop();
}

static bool mm_sleep_with_stop(unsigned int seconds) {
if (seconds > UINT_MAX / 1000000u)
return mm_sleep_with_stop_us(UINT_MAX);
return mm_sleep_with_stop_us(seconds * 1000000u);
}

static pid_t mm_find_pid_by_name(const char *name, bool exclude_self) {
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
size_t buffer_size = 0;
uint8_t *buffer = NULL;
uint8_t *cursor;
uint8_t *end;
size_t name_length;
pid_t self_pid = exclude_self ? getpid() : -1;
pid_t found_pid = 0;

if (!name)
return -1;

name_length = strlen(name);
if (name_length >= sizeof(((struct kinfo_proc *)0)->ki_tdname))
return 0;

if (sysctl(mib, 4, NULL, &buffer_size, NULL, 0) != 0)
return -1;
if (buffer_size == 0)
return 0;

buffer = malloc(buffer_size);
if (!buffer)
return -1;

if (sysctl(mib, 4, buffer, &buffer_size, NULL, 0) != 0) {
free(buffer);
return -1;
}

cursor = buffer;
end = buffer + buffer_size;
while (cursor < end) {
const struct kinfo_proc *entry;
int entry_size;
size_t thread_name_length;

if ((size_t)(end - cursor) < sizeof(int))
break;

entry_size = *(const int *)cursor;
if (entry_size <= 0 || (size_t)entry_size > (size_t)(end - cursor))
break;

if ((size_t)entry_size <
offsetof(struct kinfo_proc, ki_tdname) +
sizeof(((struct kinfo_proc *)0)->ki_tdname)) {
cursor += entry_size;
continue;
}

entry = (const struct kinfo_proc *)(const void *)cursor;
thread_name_length =
strnlen(entry->ki_tdname, sizeof(entry->ki_tdname));
if ((!exclude_self || entry->ki_pid != self_pid) &&
thread_name_length == name_length &&
memcmp(entry->ki_tdname, name, name_length) == 0) {
found_pid = entry->ki_pid;
break;
}

cursor += entry_size;
}

free(buffer);
return found_pid;
}

static bool mm_terminate_existing_processes(void) {
pid_t target_pid;
pid_t active_pid = 0;
unsigned int waited_us = 0;

target_pid = mm_find_pid_by_name(MM_PAYLOAD_NAME, true);
if (target_pid < 0)
return false;
if (target_pid == 0)
return true;

mm_notify_debug(
"Found an existing MicroMount process. Attempting to terminate it.");

while (!mm_should_stop()) {
target_pid = mm_find_pid_by_name(MM_PAYLOAD_NAME, true);
if (target_pid < 0)
return false;
if (target_pid == 0)
return true;

if (target_pid != active_pid) {
active_pid = target_pid;
waited_us = 0;
}

if (waited_us >= MM_RESTART_WAIT_TIMEOUT_US) {
if (kill(target_pid, SIGKILL) != 0 && errno != ESRCH)
return false;
if (waited_us >= MM_RESTART_WAIT_TIMEOUT_US + 2000000u)
return false;
} else {
if (kill(target_pid, SIGTERM) != 0 && errno != ESRCH)
return false;
}

if (mm_sleep_with_stop_us(MM_RESTART_WAIT_POLL_US))
return false;
waited_us += MM_RESTART_WAIT_POLL_US;
}

return false;
}

static void mm_run_cycle(mm_config_t *config, bool first_cycle,
size_t *last_notified_total) {
mm_candidate_list_t candidates;
Expand All @@ -53,17 +179,25 @@ static void mm_run_cycle(mm_config_t *config, bool first_cycle,

if (!mm_config_reload_if_changed(config, &config_reloaded))
summary.errors++;
if (mm_should_stop())
goto cleanup;
if (config_reloaded)
mm_notify_debug("Reloaded %s", MM_CONFIG_FILE);

if (!mm_scan_for_images(config, &candidates))
if (!mm_scan_for_images(config, &candidates) && !mm_should_stop())
summary.errors++;
if (mm_should_stop())
goto cleanup;
summary.discovered = candidates.count;

mm_cleanup_managed_mounts(config, &candidates, &summary.cleaned,
&summary.errors);
if (mm_should_stop())
goto cleanup;
mm_reconcile_mounts(config, &candidates, &summary.mounted, &summary.skipped,
&summary.errors);
if (mm_should_stop())
goto cleanup;
summary.total_mounted = mm_count_managed_mounts(config);

mm_log_info("CYCLE",
Expand All @@ -82,25 +216,49 @@ static void mm_run_cycle(mm_config_t *config, bool first_cycle,
*last_notified_total = summary.total_mounted;
}

cleanup:
mm_candidate_list_free(&candidates);
}

int main(void) {
mm_config_t config;
bool user_service_initialized = false;
bool replaced_previous_process = false;
bool first_cycle = true;
size_t last_notified_total = SIZE_MAX;
pid_t existing_pid;

kernel_set_ucred_authid(-1, MM_AUTHID_BASE);
(void)syscall(SYS_thr_set_name, -1, MM_PAYLOAD_NAME);
mm_install_signal_handlers();

mm_log_init(true);
if (sceUserServiceInitialize(NULL) == 0)
user_service_initialized = true;
else
mm_log_warn("CORE", "sceUserServiceInitialize failed; notifications may be limited");

existing_pid = mm_find_pid_by_name(MM_PAYLOAD_NAME, true);
if (existing_pid < 0) {
mm_log_error("CORE", "failed to enumerate running processes");
if (user_service_initialized)
sceUserServiceTerminate();
return 1;
}
if (existing_pid > 0) {
if (!mm_terminate_existing_processes()) {
mm_log_error("CORE", "failed to stop previous MicroMount process");
if (user_service_initialized)
sceUserServiceTerminate();
return 1;
}
replaced_previous_process = true;
}

(void)syscall(SYS_thr_set_name, -1, MM_PAYLOAD_NAME);
mm_log_init(true);
if (replaced_previous_process) {
mm_notify_debug(
"Previous process terminated. Starting a new micromount process...");
}

if (!mm_config_load(&config)) {
mm_log_error("CORE", "failed to load config, continuing with defaults");
mm_config_init_defaults(&config);
Expand Down
Loading
Loading