// 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 #include #include #include #include #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; } bool update_finish(void) { if (!thread) return true; // Läuft die Abfrage noch, nicht freigeben (sonst schreibt der Thread in freigegebenen Speicher). // false = läuft noch, die App muss sich hart beenden (main.c). if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false; threadFree(thread); thread = NULL; return true; }