// Werkzeuge > Logs: Log-Zeilen aller MBMNET-Server, wie auf der Log-Webseite (MBMNET-Tools/logserver). // // Der Log-Server ist nur übers Accounting erreichbar (/mbmnet/logs/..., logsproxy.js), und zwar nur über // HTTPS mit der MBMNET-CA (dieselbe wie für Miiverse): Das Log-Passwort geht nie im Klartext übers Netz. // Oben die Zeilen (neueste unten, Steuerkreuz blättert, ganz oben werden ältere nachgeladen), unten die // Quellen zum Filtern. Ist man ganz unten, lädt die Ansicht alle 5 Sekunden neu (wie "live" im Browser). #include <3ds.h> #include #include #include #include #include #include "logview.h" #include "https.h" #include "ui.h" #define TOKEN_FILE "sdmc:/3ds/mbmnet/logs-token" #define MAX_LINES 600 #define MAX_SOURCES 48 #define ROW_H 13 #define ROWS 16 #define SRC_ROW_H 24 #define SRC_ROWS 7 #define RESP_MAX (192 * 1024) typedef struct { u32 id; u64 t; // ms seit 1970 (UTC, vom Server) char level; // 'i', 'w', 'e' char src[28]; // Quelle (ohne Host) char msg[404]; } Line; typedef struct { char key[80], name[32], host[32]; bool online; u32 errors, warns; } Source; typedef enum { LV_NEED_PASSWORD, LV_LOADING, LV_READY, LV_ERROR } LvState; enum { JOB_NONE, JOB_LOGIN, JOB_RELOAD, JOB_OLDER }; // --- geteilt mit dem Lade-Thread (lock) --- static LightLock lock; static LightEvent wake; static Thread thread; static volatile bool quit, busy; static volatile int job; static char server[64], token[72], password[72]; static Line *lines; static int lineCount; static Source sources[MAX_SOURCES]; static int sourceCount; static volatile LvState state = LV_LOADING; static char status[128]; static s64 timeOffset; // Sekunden: Konsolenuhr - Serveruhr (für die Uhrzeit der Zeilen) static u64 lastLoad; // osGetTime() des letzten Ladens // --- nur Hauptthread --- static int cursor; // gewählte Zeile static int top; // erste sichtbare Zeile static int srcSel; // 0 = alle, sonst sources[srcSel - 1] static int srcTop; static bool errorsOnly, detail, following = true; static char filterKey[80]; // gewählte Quelle (key), leer = alle // --- HTTP ---------------------------------------------------------------------------- static int parse_month(const char *m) { static const char *names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; for (int i = 0; i < 12; i++) if (!strncmp(m, names[i], 3)) return i + 1; return 0; } // Tage seit 1970-01-01 (proleptischer gregorianischer Kalender) static s64 days_from_civil(int y, int m, int d) { y -= m <= 2; s64 era = (y >= 0 ? y : y - 399) / 400; s64 yoe = y - era * 400; s64 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; s64 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + doe - 719468; } // Server-Uhrzeit aus der Date-Kopfzeile ("Mon, 26 Oct 2026 12:34:56 GMT") -> Abstand zur Konsolenuhr static void sync_clock(const char *date) { int d, y, hh, mm, ss; char mon[4] = ""; if (sscanf(date, "%*3s, %d %3s %d %d:%d:%d", &d, mon, &y, &hh, &mm, &ss) != 6) return; int m = parse_month(mon); if (!m) return; s64 serverNow = days_from_civil(y, m, d) * 86400 + hh * 3600 + mm * 60 + ss; timeOffset = (s64)time(NULL) - serverNow; } // HTTPS-Anfrage ans Accounting (https.c, MBMNET-CA). Status 0 = keine Verbindung (status bekommt den Grund). static u32 request(const char *path, const char *form, char *out, u32 cap, u32 *outLen) { char date[64]; Result rc = 0; u32 code = https_request(server, path, form, token[0] ? "X-Logs-Token" : NULL, token, out, cap, outLen, date, sizeof(date), &rc); if (date[0]) sync_clock(date); if (!code) snprintf(status, sizeof(status), rc == -1 ? "MBMNET-Zertifikat fehlt (Server nicht erreichbar?)" : "Keine Verbindung (%08lX)", (unsigned long)rc); return code; } static void url_encode(char *out, size_t len, const char *in) { static const char hex[] = "0123456789ABCDEF"; size_t n = 0; for (; *in && n + 4 < len; in++) { unsigned char c = *in; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') out[n++] = c; else { out[n++] = '%'; out[n++] = hex[c >> 4]; out[n++] = hex[c & 15]; } } out[n] = 0; } static void save_token(const char *t) { mkdir("sdmc:/3ds/mbmnet", 0777); FILE *f = fopen(TOKEN_FILE, "w"); if (f) { fputs(t, f); fclose(f); } } static void load_token(void) { token[0] = 0; FILE *f = fopen(TOKEN_FILE, "r"); if (!f) return; if (fgets(token, sizeof(token), f)) token[strcspn(token, "\r\n")] = 0; fclose(f); if (strlen(token) != 64) token[0] = 0; } static void forget_token(void) { token[0] = 0; remove(TOKEN_FILE); } // Zeile mit Tabs zerlegen static int split(char *line, char **f, int max) { int n = 0; while (n < max) { f[n++] = line; char *tab = strchr(line, '\t'); if (!tab) break; *tab = 0; line = tab + 1; } return n; } static void parse_line(Line *l, char **f) { l->id = strtoul(f[0], NULL, 10); l->t = strtoull(f[1], NULL, 10); l->level = f[2][0] == 'e' ? 'e' : f[2][0] == 'w' ? 'w' : 'i'; char *bar = strchr(f[3], '|'); // key = "quelle|host" if (bar) *bar = 0; snprintf(l->src, sizeof(l->src), "%s", f[3]); snprintf(l->msg, sizeof(l->msg), "%s", f[4]); } // --- Lade-Thread --------------------------------------------------------------------- static bool load_sources(char *buf) { u32 len; u32 code = request("/mbmnet/logs/sources", NULL, buf, RESP_MAX, &len); if (code == 401) return false; if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; } Source tmp[MAX_SOURCES]; int n = 0; char *save = NULL, *f[8]; for (char *line = strtok_r(buf, "\n", &save); line && n < MAX_SOURCES; line = strtok_r(NULL, "\n", &save)) { if (split(line, f, 8) < 6) continue; Source *s = &tmp[n++]; snprintf(s->key, sizeof(s->key), "%s", f[0]); snprintf(s->name, sizeof(s->name), "%s", f[1]); snprintf(s->host, sizeof(s->host), "%s", f[2]); s->online = f[3][0] == '1'; s->errors = strtoul(f[4], NULL, 10); s->warns = strtoul(f[5], NULL, 10); } LightLock_Lock(&lock); memcpy(sources, tmp, sizeof(Source) * n); sourceCount = n; LightLock_Unlock(&lock); return true; } // older = vor der ältesten Zeile weiterlesen (sonst die neuesten). false = nicht angemeldet. static bool load_lines(char *buf, bool older) { char path[200], key[96]; url_encode(key, sizeof(key), filterKey); LightLock_Lock(&lock); u32 before = older && lineCount ? lines[0].id : 0; LightLock_Unlock(&lock); snprintf(path, sizeof(path), "/mbmnet/logs/lines?limit=%d%s%s%s%s", older ? 100 : 200, key[0] ? "&source=" : "", key, errorsOnly ? "&level=error" : "", ""); if (before) snprintf(path + strlen(path), sizeof(path) - strlen(path), "&before=%lu", (unsigned long)before); u32 len; u32 code = request(path, NULL, buf, RESP_MAX, &len); if (code == 401) return false; if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; } // Neue Zeilen zählen und einlesen int n = 0; for (char *p = buf; *p; p++) if (*p == '\n') n++; if (len && buf[len - 1] != '\n') n++; Line *fresh = n ? malloc(sizeof(Line) * n) : NULL; int got = 0; char *save = NULL, *f[6]; for (char *line = strtok_r(buf, "\n", &save); line && got < n; line = strtok_r(NULL, "\n", &save)) { if (split(line, f, 6) < 5) continue; parse_line(&fresh[got++], f); } LightLock_Lock(&lock); if (older) { // vorne anhängen, höchstens MAX_LINES (die neuesten fallen dann weg) int keep = lineCount; if (got + keep > MAX_LINES) keep = MAX_LINES - got; if (keep < 0) keep = 0; memmove(lines + got, lines, sizeof(Line) * keep); memcpy(lines, fresh, sizeof(Line) * (got > MAX_LINES ? MAX_LINES : got)); lineCount = got + keep; cursor += got; top += got; } else { if (got > MAX_LINES) got = MAX_LINES; memcpy(lines, fresh, sizeof(Line) * got); lineCount = got; } LightLock_Unlock(&lock); free(fresh); snprintf(status, sizeof(status), "%s", ""); lastLoad = osGetTime(); return true; } static void worker(void *arg) { (void)arg; char *buf = malloc(RESP_MAX); while (!quit) { LightEvent_Wait(&wake); if (quit) break; int j = job; job = JOB_NONE; if (j == JOB_NONE || !buf) continue; busy = true; if (j == JOB_LOGIN) { // Passwort in der Kopfzeile X-Logs-Password (URL-kodiert, nur HTTPS). Als POST-Inhalt kam es nicht an: // Der HTTP-Dienst des 3DS schickte 0 Bytes. char enc[200]; url_encode(enc, sizeof(enc), password); memset(password, 0, sizeof(password)); token[0] = 0; u32 len; char date[64]; Result hrc = 0; u32 code = https_request(server, "/mbmnet/logs/login", "", "X-Logs-Password", enc, buf, 256, &len, date, sizeof(date), &hrc); memset(enc, 0, sizeof(enc)); if (date[0]) sync_clock(date); if (!code) snprintf(status, sizeof(status), "Keine Verbindung (%08lX)", (unsigned long)hrc); if (code == 200 && len == 64) { memcpy(token, buf, 64); token[64] = 0; save_token(token); j = JOB_RELOAD; } else { if (code == 401) snprintf(status, sizeof(status), "Falsches Passwort."); else if (code == 429) snprintf(status, sizeof(status), "Zu viele Versuche, 15 Minuten warten."); else if (code) snprintf(status, sizeof(status), "Anmeldung fehlgeschlagen (HTTP %lu)", (unsigned long)code); // code 0: status sagt schon, warum keine Verbindung state = code == 401 ? LV_NEED_PASSWORD : LV_ERROR; busy = false; continue; } } bool ok = j == JOB_OLDER ? load_lines(buf, true) : load_sources(buf) && load_lines(buf, false); if (!ok) { forget_token(); state = LV_NEED_PASSWORD; snprintf(status, sizeof(status), "Bitte anmelden."); } else state = LV_READY; busy = false; } free(buf); } static void start_job(int j) { if (busy || job != JOB_NONE) return; job = j; if (state != LV_READY) state = LV_LOADING; LightEvent_Signal(&wake); } void logview_open(const char *srv) { snprintf(server, sizeof(server), "%s", srv); if (!lines) lines = calloc(MAX_LINES, sizeof(Line)); if (!thread) { LightLock_Init(&lock); LightEvent_Init(&wake, RESET_ONESHOT); s32 prio = 0x30; svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); thread = threadCreate(worker, NULL, 32 * 1024, prio + 1, -2, false); } detail = false; following = true; load_token(); status[0] = 0; if (!token[0]) { state = LV_NEED_PASSWORD; return; } state = LV_LOADING; start_job(JOB_RELOAD); } bool logview_finish(void) { if (!thread) return true; quit = true; LightEvent_Signal(&wake); if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false; threadFree(thread); thread = NULL; free(lines); lines = NULL; return true; } // --- Eingaben -------------------------------------------------------------------------- static u64 srcPendingAt; // Quelle mit dem Steuerkreuz gewechselt: kurz warten, dann laden (nicht bei jedem Schritt) // Quelle wählen. now = sofort laden (L/R, Antippen), sonst nach einer kurzen Pause (Steuerkreuz) static void select_source(int sel, bool now) { if (sel < 0) sel = sourceCount; if (sel > sourceCount) sel = 0; srcSel = sel; snprintf(filterKey, sizeof(filterKey), "%s", sel ? sources[sel - 1].key : ""); if (srcSel < srcTop) srcTop = srcSel; if (srcSel >= srcTop + SRC_ROWS) srcTop = srcSel - SRC_ROWS + 1; following = true; if (now) { srcPendingAt = 0; start_job(JOB_RELOAD); } else srcPendingAt = osGetTime(); } static bool ask_password(void) { SwkbdState kb; char input[64] = ""; swkbdInit(&kb, SWKBD_TYPE_NORMAL, 2, sizeof(input) - 1); swkbdSetPasswordMode(&kb, SWKBD_PASSWORD_HIDE_DELAY); swkbdSetHintText(&kb, "Passwort der Log-Seite"); swkbdSetButton(&kb, SWKBD_BUTTON_LEFT, "Abbrechen", false); swkbdSetButton(&kb, SWKBD_BUTTON_RIGHT, "Anmelden", true); if (swkbdInputText(&kb, input, sizeof(input)) != SWKBD_BUTTON_RIGHT || !input[0]) return false; snprintf(password, sizeof(password), "%s", input); memset(input, 0, sizeof(input)); state = LV_LOADING; snprintf(status, sizeof(status), "Melde an ..."); job = JOB_LOGIN; LightEvent_Signal(&wake); return true; } typedef struct { float x, y, w, h; } Rect; static const Rect btnFilter = { 8, 204, 100, 30 }, btnReload = { 112, 204, 96, 30 }, btnBack = { 212, 204, 100, 30 }; static bool in(Rect r, touchPosition tp) { return tp.px >= r.x && tp.px < r.x + r.w && tp.py >= r.y && tp.py < r.y + r.h; } bool logview_update(u32 down, u32 repeat, u32 held) { (void)held; if (state == LV_NEED_PASSWORD && !busy && job == JOB_NONE) { if (!ask_password()) return false; // abgebrochen: zurück zu den Werkzeugen } touchPosition tp = { 0 }; if (down & KEY_TOUCH) hidTouchRead(&tp); if (down & KEY_B || (down & KEY_TOUCH && in(btnBack, tp))) { if (detail) detail = false; else return false; } if (down & KEY_A) detail = !detail && lineCount > 0; if (down & KEY_X || (down & KEY_TOUCH && in(btnFilter, tp))) { errorsOnly = !errorsOnly; following = true; start_job(JOB_RELOAD); } if (down & KEY_Y || (down & KEY_TOUCH && in(btnReload, tp))) { following = true; start_job(JOB_RELOAD); } if (down & KEY_R) select_source(srcSel + 1, true); if (down & KEY_L) select_source(srcSel - 1, true); // Steuerkreuz (unten): Quellen auf dem unteren Bildschirm if (repeat & KEY_DDOWN) select_source(srcSel + 1, false); if (repeat & KEY_DUP) select_source(srcSel - 1, false); if (srcPendingAt && osGetTime() - srcPendingAt > 400 && !busy && job == JOB_NONE) { srcPendingAt = 0; start_job(JOB_RELOAD); } if (down & KEY_TOUCH && tp.py >= 24 && tp.py < 24 + SRC_ROWS * SRC_ROW_H) { int row = srcTop + (tp.py - 24) / SRC_ROW_H; if (row <= sourceCount) select_source(row, true); } LightLock_Lock(&lock); int count = lineCount; // Schiebepad (oben): Zeilen auf dem oberen Bildschirm, links/rechts seitenweise bool moved = false; if (!detail) { if (repeat & KEY_CPAD_UP) { moved = true; if (cursor > 0) cursor--; else if (count && state == LV_READY) start_job(JOB_OLDER); // ganz oben: ältere nachladen } if (repeat & KEY_CPAD_DOWN) { cursor++; moved = true; } if (repeat & KEY_CPAD_LEFT) { cursor -= ROWS; moved = true; } if (repeat & KEY_CPAD_RIGHT) { cursor += ROWS; moved = true; } } // Live mitlaufen nur, solange man nicht selbst blättert (sonst sprang es sofort wieder nach unten) if (following && !moved) cursor = count - 1; if (cursor >= count) cursor = count - 1; if (cursor < 0) cursor = 0; following = count == 0 || cursor == count - 1; if (cursor < top) top = cursor; if (cursor >= top + ROWS) top = cursor - ROWS + 1; if (top > count - ROWS) top = count - ROWS; if (top < 0) top = 0; LightLock_Unlock(&lock); // Live: unten angekommen, alle 5 Sekunden neu laden if (following && state == LV_READY && osGetTime() - lastLoad > 5000) start_job(JOB_RELOAD); return true; } // --- Zeichnen --------------------------------------------------------------------------- static u32 level_color(char level) { return level == 'e' ? COL_RED : level == 'w' ? COL_AMBER : COL_CARD_HI; } void logview_draw_top(void) { C2D_DrawRectSolid(0, 0, 0, TOP_W, SCR_H, COL_BG); C2D_DrawRectSolid(0, 0, 0.5f, TOP_W, 20, COL_BG2); char head[96], line[160]; snprintf(head, sizeof(head), "Logs · %s%s", srcSel ? sources[srcSel - 1].name : "alle Quellen", errorsOnly ? " · nur Fehler" : ""); ui_text(head, 6, 3, 0.45f, COL_WHITE, 0); const char *right = busy || state == LV_LOADING ? "lädt ..." : status[0] ? status : following ? "live" : "angehalten"; ui_fit(line, sizeof(line), right, 0.38f, 180); ui_text(line, TOP_W - 6, 4, 0.38f, status[0] ? COL_AMBER : following ? COL_GREEN : COL_MUTED, C2D_AlignRight); if (state == LV_NEED_PASSWORD) { ui_text_wrap(status[0] ? status : "Anmeldung mit dem Passwort der Log-Seite.", 20, 100, 0.5f, COL_MUTED, TOP_W - 40); return; } LightLock_Lock(&lock); if (!lineCount) { ui_text(state == LV_LOADING ? "Einen Moment ..." : "Keine Zeilen.", TOP_W / 2, 110, 0.5f, COL_MUTED, C2D_AlignCenter); LightLock_Unlock(&lock); return; } for (int r = 0; r < ROWS && top + r < lineCount; r++) { const Line *l = &lines[top + r]; float y = 22 + r * ROW_H; if (top + r == cursor) C2D_DrawRectSolid(0, y, 0.5f, TOP_W, ROW_H, COL_CARD); time_t when = (time_t)(l->t / 1000 + timeOffset); struct tm *tm = gmtime(&when); char hhmm[12]; snprintf(hhmm, sizeof(hhmm), "%02d:%02d:%02d", tm ? tm->tm_hour : 0, tm ? tm->tm_min : 0, tm ? tm->tm_sec : 0); ui_text(hhmm, 4, y + 1, 0.34f, COL_MUTED, 0); C2D_DrawRectSolid(46, y + 1, 0.5f, 3, ROW_H - 2, level_color(l->level)); ui_fit(line, sizeof(line), l->src, 0.34f, 56); ui_text(line, 53, y + 1, 0.34f, COL_MUTED, 0); ui_fit(line, sizeof(line), l->msg, 0.36f, TOP_W - 116); ui_text(line, 112, y + 1, 0.36f, l->level == 'e' ? C2D_Color32(0xFF, 0x8A, 0x8A, 0xFF) : COL_WHITE, 0); } if (detail && cursor < lineCount) { const Line *l = &lines[cursor]; ui_round_rect(14, 30, TOP_W - 28, 200, 10, COL_BG2); C2D_DrawRectSolid(14, 40, 0.5f, 4, 180, level_color(l->level)); snprintf(head, sizeof(head), "%s · Zeile %lu", l->src, (unsigned long)l->id); ui_text(head, 26, 36, 0.42f, COL_MUTED, 0); ui_text_wrap(l->msg, 26, 56, 0.42f, COL_WHITE, TOP_W - 60); } LightLock_Unlock(&lock); } static void button(Rect r, const char *label, bool on) { ui_round_rect(r.x, r.y, r.w, r.h, 10, on ? COL_RED : COL_CARD); ui_text(label, r.x + r.w / 2, r.y + 8, 0.42f, COL_WHITE, C2D_AlignCenter); } void logview_draw_bottom(void) { C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); C2D_DrawRectSolid(0, 0, 0.5f, BOT_W, 22, COL_BG2); ui_text("Quellen", 6, 3, 0.45f, COL_WHITE, 0); ui_text("Steuerkreuz/L/R wechseln · A Details", BOT_W - 6, 5, 0.34f, COL_MUTED, C2D_AlignRight); char line[96]; LightLock_Lock(&lock); for (int r = 0; r < SRC_ROWS && srcTop + r <= sourceCount; r++) { int i = srcTop + r; float y = 24 + r * SRC_ROW_H; if (i == srcSel) { C2D_DrawRectSolid(0, y, 0.5f, BOT_W, SRC_ROW_H, COL_CARD_HI); C2D_DrawRectSolid(0, y + 4, 0.5f, 3, SRC_ROW_H - 8, COL_RED); } if (i == 0) { ui_text("Alle Quellen", 14, y + 5, 0.45f, COL_WHITE, 0); continue; } const Source *s = &sources[i - 1]; C2D_DrawCircleSolid(14, y + SRC_ROW_H / 2, 0.5f, 4, s->online ? COL_GREEN : COL_MUTED); snprintf(line, sizeof(line), "%s · %s", s->name, s->host); char fitted[96]; ui_fit(fitted, sizeof(fitted), line, 0.42f, s->errors ? 200 : 270); ui_text(fitted, 24, y + 5, 0.42f, COL_WHITE, 0); if (s->errors) { snprintf(line, sizeof(line), "%lu Fehler", (unsigned long)s->errors); float w = ui_text_width(line, 0.36f) + 12; ui_round_rect(BOT_W - 8 - w, y + 4, w, 16, 8, COL_RED); ui_text(line, BOT_W - 8 - w / 2, y + 5, 0.36f, COL_WHITE, C2D_AlignCenter); } } LightLock_Unlock(&lock); button(btnFilter, errorsOnly ? "Alle Stufen" : "Nur Fehler", errorsOnly); button(btnReload, "Neu laden", false); button(btnBack, detail ? "Schließen" : "Zurück", false); }