MBMNET-App v2.8.2: Quellcode (GPLv3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:05:44 +02:00
co-authored by Claude Opus 5.5
commit d264cd93cd
42 changed files with 7054 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
// Auto-Update. Die App fragt den MBMNET-Server (per HTTP, das kann der 3DS zuverlässig):
// GET /app/version -> z. B. "1.2.0"
// GET /app/mbmnet.3dsx -> die neue App (Homebrew-Launcher-Version)
// GET /app/mbmnet.cia -> die neue App (installierte CIA-Version)
// Der Server holt beides von deinem Git-Server (siehe config.json / README).
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/stat.h>
#include "update.h"
#define MAX_DOWNLOAD (8 * 1024 * 1024)
static volatile UpdateState state = UPD_IDLE;
static char server[64];
static char selfPath[256];
static char newVersion[32];
static Thread thread;
static bool runningAsCia;
// Fehlersuche: Schritt und Code des letzten Fehlers, dazu ein Protokoll auf der SD
#define LOG_FILE "sdmc:/3ds/mbmnet/update.log"
static char errStep[48];
static Result errCode;
static volatile int downloadAttempt; // laufender Versuch (für die Anzeige)
static void ulog(const char *fmt, ...)
{
FILE *f = fopen(LOG_FILE, "a");
if (!f) return;
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
fclose(f);
}
// Merkt sich den Fehler. Den Zustand setzt erst der Worker, wenn alle Versuche erfolglos waren.
static void fail(const char *stepName, Result rc)
{
snprintf(errStep, sizeof(errStep), "%s", stepName);
errCode = rc;
ulog("FEHLER bei %s: %08lX", stepName, (unsigned long)rc);
}
#define ATTEMPTS 3
static void pause_seconds(int s) { svcSleepThread((s64)s * 1000 * 1000 * 1000); }
int update_attempt(void) { return downloadAttempt; }
void update_error(char *out, size_t len)
{
snprintf(out, len, "%s (Fehler %08lX)", errStep[0] ? errStep : "?", (unsigned long)errCode);
}
UpdateState update_state(void) { return state; }
const char *update_new_version(void) { return newVersion; }
// Lädt eine URL komplett in den Speicher. Gibt den HTTP-Status zurück (0 bei Netzwerkfehler).
static Result lastHttpRc; // Fehlercode, falls http_get 0 zurückgibt
static u32 http_get(const char *url, u8 **out, u32 *outSize)
{
httpcContext ctx;
u32 status = 0;
*out = NULL;
*outSize = 0;
lastHttpRc = httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1);
if (R_FAILED(lastHttpRc)) return 0;
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
httpcSetKeepAlive(&ctx, HTTPC_KEEPALIVE_DISABLED);
if (R_SUCCEEDED(lastHttpRc = httpcBeginRequest(&ctx)) &&
R_SUCCEEDED(lastHttpRc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 10ULL * 1000 * 1000 * 1000)) &&
status == 200) {
u32 cap = 64 * 1024, size = 0, got;
u8 *buf = malloc(cap);
Result rc;
do {
if (!buf) break;
if (size == cap) {
if (cap >= MAX_DOWNLOAD) { free(buf); buf = NULL; break; }
u8 *bigger = realloc(buf, cap * 2);
if (!bigger) { free(buf); buf = NULL; break; }
buf = bigger;
cap *= 2;
}
got = 0;
rc = httpcDownloadData(&ctx, buf + size, cap - size, &got);
size += got;
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
if (buf && R_SUCCEEDED(rc)) {
*out = buf;
*outSize = size;
} else {
lastHttpRc = buf ? rc : -2; // -2 = zu wenig Speicher
*outSize = size;
free(buf);
status = 0;
}
}
httpcCloseContext(&ctx);
return status;
}
// Installiert eine CIA über den Systemdienst am:net (wie FBI)
static bool install_cia(const u8 *data, u32 size)
{
Handle h;
Result rc = AM_StartCiaInstall(MEDIATYPE_SD, &h);
ulog("CIA-Installation starten: %08lX", (unsigned long)rc);
if (R_FAILED(rc)) { fail("CIA-Installation starten", rc); return false; }
for (u32 off = 0; off < size;) {
u32 chunk = size - off > 0x10000 ? 0x10000 : size - off;
u32 written = 0;
rc = FSFILE_Write(h, &written, off, data + off, chunk, 0);
if (R_FAILED(rc) || written != chunk) {
ulog("Schreiben bei %lu: %08lX (%lu von %lu Bytes)", (unsigned long)off, (unsigned long)rc,
(unsigned long)written, (unsigned long)chunk);
AM_CancelCIAInstall(h);
fail("CIA schreiben", rc);
return false;
}
off += chunk;
}
rc = AM_FinishCiaInstall(h);
ulog("CIA-Installation abschließen: %08lX", (unsigned long)rc);
if (R_FAILED(rc)) { fail("CIA-Installation abschließen", rc); return false; }
return true;
}
// Schreibt die neue .3dsx über die laufende
static bool install_3dsx(const u8 *data, u32 size)
{
char tmpPath[sizeof(selfPath) + 8];
snprintf(tmpPath, sizeof(tmpPath), "%s.neu", selfPath);
FILE *f = fopen(tmpPath, "wb");
bool ok = f && fwrite(data, 1, size, f) == size;
if (f) fclose(f);
ulog("3DSX schreiben nach %s: %s", tmpPath, ok ? "OK" : "FEHLER");
if (!ok) { fail("3DSX schreiben", -1); return false; }
// Erst komplett schreiben, dann austauschen: so bleibt bei einem Abbruch die alte App heil.
remove(selfPath);
ok = rename(tmpPath, selfPath) == 0;
ulog("3DSX austauschen (%s): %s", selfPath, ok ? "OK" : "FEHLER");
if (!ok) fail("3DSX austauschen", -1);
return ok;
}
static bool is_newer(const char *remote, const char *local)
{
int r[3] = { 0 }, l[3] = { 0 };
sscanf(remote, "%d.%d.%d", &r[0], &r[1], &r[2]);
sscanf(local, "%d.%d.%d", &l[0], &l[1], &l[2]);
for (int i = 0; i < 3; i++) {
if (r[i] != l[i]) return r[i] > l[i];
}
return false;
}
// Schritt 1: gibt es eine neuere Version? (bei Netzwerkfehlern bis zu 3 Versuche)
static void check_worker(void *arg)
{
char url[128];
u8 *data;
u32 size;
snprintf(url, sizeof(url), "http://%s/app/version", server);
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
u32 status = http_get(url, &data, &size);
if (status == 404) { state = UPD_NONE; return; }
if (status == 200) {
snprintf(newVersion, sizeof(newVersion), "%.*s", (int)(size < 31 ? size : 31), (char *)data);
newVersion[strcspn(newVersion, " \r\n")] = 0;
if (newVersion[0] == 'v') memmove(newVersion, newVersion + 1, strlen(newVersion));
free(data);
state = is_newer(newVersion, APP_VERSION) ? UPD_AVAILABLE : UPD_LATEST;
return;
}
free(data);
if (attempt < ATTEMPTS) pause_seconds(2);
}
state = UPD_ERROR;
}
// Ein Versuch: laden, prüfen, installieren
static bool download_and_install(const char *url)
{
u8 *data;
u32 size;
u32 status = http_get(url, &data, &size);
ulog("Download: HTTP %lu, %lu Bytes", (unsigned long)status, (unsigned long)size);
if (status != 200) {
free(data);
fail(status ? "Herunterladen (HTTP-Status)" : "Herunterladen", status ? (Result)status : lastHttpRc);
return false;
}
// Nur echte Dateien installieren: CIA beginnt mit Headergröße 0x2020, 3DSX mit "3DSX"
bool valid = size >= 32 && (runningAsCia
? data[0] == 0x20 && data[1] == 0x20 && data[2] == 0 && data[3] == 0
: memcmp(data, "3DSX", 4) == 0);
if (!valid) { free(data); fail("Datei prüfen", -1); return false; }
bool ok = runningAsCia ? install_cia(data, size) : install_3dsx(data, size);
free(data);
return ok;
}
// Schritt 2: neue Version laden und installieren (bei Fehlern bis zu 3 Versuche)
static void download_worker(void *arg)
{
char url[128];
mkdir("sdmc:/3ds/mbmnet", 0777);
remove(LOG_FILE);
errStep[0] = 0;
snprintf(url, sizeof(url), "http://%s/app/%s", server, runningAsCia ? "mbmnet.cia" : "mbmnet.3dsx");
ulog("MBMNET-Update v%s -> v%s (%s)", APP_VERSION, newVersion, runningAsCia ? "CIA" : "3DSX");
ulog("Lade %s", url);
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
if (attempt > 1) ulog("--- Versuch %d von %d", attempt, ATTEMPTS);
downloadAttempt = attempt;
if (download_and_install(url)) {
ulog("Ergebnis: OK");
state = UPD_INSTALLED;
return;
}
if (attempt < ATTEMPTS) pause_seconds(3);
}
ulog("Ergebnis: FEHLER nach %d Versuchen", ATTEMPTS);
state = UPD_ERROR;
}
static void start_thread(ThreadFunc fn)
{
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(fn, NULL, 32 * 1024, prio + 1, -2, false);
if (!thread) state = UPD_ERROR;
}
void update_start(const char *srv, const char *self)
{
if (!srv[0] || state == UPD_CHECKING || state == UPD_DOWNLOADING) return;
snprintf(server, sizeof(server), "%s", srv);
snprintf(selfPath, sizeof(selfPath), "%s", self);
runningAsCia = !envIsHomebrew();
state = UPD_CHECKING;
start_thread(check_worker);
}
void update_download(void)
{
if (state != UPD_AVAILABLE) return;
state = UPD_DOWNLOADING;
start_thread(download_worker);
}
bool update_pending(void)
{
return state == UPD_AVAILABLE || state == UPD_DOWNLOADING || state == UPD_INSTALLED;
}
void update_finish(void)
{
if (!thread) return;
// Läuft der Download noch, nicht freigeben: Die App beendet sich ohnehin.
if (R_SUCCEEDED(threadJoin(thread, 3ULL * 1000 * 1000 * 1000))) threadFree(thread);
thread = NULL;
}