MBMNET-App v2.8.2: Quellcode (GPLv3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+277
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user