diff --git a/gc_bba_test/README.md b/gc_bba_test/README.md index 07185e3..70c4534 100644 --- a/gc_bba_test/README.md +++ b/gc_bba_test/README.md @@ -81,12 +81,25 @@ Press **Z**, then on the host: sudo ./scripts/send-dol.py gc_bba_test/gc_bba_test.dol ``` -The flow is deliberately **receive → write to SD → boot Swiss**: +The flow is deliberately **receive → temp file → verify → rotate → boot Swiss**: 1. The new build arrives over the adapter into a staging buffer. -2. It is written to the SD card (default `/gc_bba_test.dol`, override with - `--dest`) and **read back and compared**. -3. The app then boots **`/boot.dol`** — your known-good Swiss. +2. It is written to `.tmp` and **read back and compared**. +3. Only then is it rotated in: the existing build becomes + **`.prev.dol`** and the temp file becomes `.dol` + (default `/gc_bba_test.dol`, override with `--dest`). +4. The app then boots **`/boot.dol`** — your known-good Swiss. + +**Ordering is the point.** If the transfer, the write or the verify fails, the +build you are currently running is still on the card untouched, so you can +just try again. And once an update succeeds, the previous build is kept as +`.prev.dol` — so if the *new* one turns out to be broken, boot the +previous one from Swiss and transfer again from there. One generation back is +enough to never get stranded needing an SD card reader. + +The temp file deliberately has **no `.dol` extension**: Swiss lists `.dol` +files, and a half-written image Swiss offers to boot is exactly what this +avoids. The backup keeps `.dol` precisely because booting it is the point. **The received image is never launched.** The only thing this ever jumps into is Swiss, which was already known good. So a broken test build costs nothing: diff --git a/gc_bba_test/source/main.c b/gc_bba_test/source/main.c index 0c238c6..f691f63 100644 --- a/gc_bba_test/source/main.c +++ b/gc_bba_test/source/main.c @@ -131,10 +131,12 @@ static void do_netboot(char *status, size_t nstatus) printf("\n" C_HEAD " Self-update over the BBA" C_RESET "\n\n"); printf(" On the host: sudo ./scripts/send-dol.py \n\n"); - printf(C_DIM " The received build is WRITTEN TO THE SD CARD and verified;\n" - " it is never launched directly. This then boots /boot.dol --\n" - " your known-good Swiss -- so a broken test build costs nothing:\n" - " Swiss comes up and can load whatever you like.\n" C_RESET "\n"); + printf(C_DIM " The received build is written to a TEMP file, verified, and\n" + " only then rotated in -- the previous build is kept alongside it\n" + " as .prev.dol, so if the new one is broken you can boot the\n" + " old one from Swiss and try the transfer again.\n" + " Nothing received is ever launched directly: this boots /boot.dol,\n" + " your known-good Swiss.\n" C_RESET "\n"); printf(" Blocks until done or it times out (30 s idle, 5 s mid-xfer).\n\n"); if (!s_stage) { @@ -152,7 +154,8 @@ static void do_netboot(char *status, size_t nstatus) if (rc < 0) { /* Nothing has been launched and nothing is half-written that we * know of, so staying in the menu is the safe outcome. */ - snprintf(status, nstatus, "SD write failed (%d) - nothing launched", rc); + snprintf(status, nstatus, + "SD write failed (%d) - existing build untouched", rc); return; } diff --git a/gc_bba_test/source/netboot.c b/gc_bba_test/source/netboot.c index 329bad8..aafe906 100644 --- a/gc_bba_test/source/netboot.c +++ b/gc_bba_test/source/netboot.c @@ -342,44 +342,66 @@ static int sd_mount(void (*status)(const char *msg)) return -1; } +/* Build ".tmp" and ".prev.dol" from ".dol". + * + * The temp file deliberately does NOT end in .dol: Swiss lists .dol files, and + * a half-written image that Swiss offers to boot is exactly what we are trying + * to prevent. The backup DOES end in .dol, because booting it from Swiss is + * the whole point of keeping it. */ +static void derive_names(const char *path, char *tmp, size_t ntmp, + char *prev, size_t nprev) +{ + char base[80]; + size_t n = strlen(path); + + if (n >= sizeof(base)) n = sizeof(base) - 1; + memcpy(base, path, n); + base[n] = 0; + if (n > 4 && strcasecmp(base + n - 4, ".dol") == 0) + base[n - 4] = 0; + + snprintf(tmp, ntmp, "sd:%s.tmp", base); + snprintf(prev, nprev, "sd:%s.prev.dol", base); +} + int netboot_sd_write(const char *path, const void *buf, u32 len, void (*status)(const char *msg)) { - char full[96], line[128]; + char final[96], tmp[112], prev[112], line[160]; FILE *f; - u32 done = 0; - int rc = -1; + u32 done; + int rc = 0; if (sd_mount(status) < 0) return -1; - snprintf(full, sizeof(full), "sd:%s", path); + snprintf(final, sizeof(final), "sd:%s", path); + derive_names(path, tmp, sizeof(tmp), prev, sizeof(prev)); - if (status) { snprintf(line, sizeof(line), "writing %s", full); status(line); } - f = fopen(full, "wb"); - if (!f) { - if (status) status("open for write failed"); - return -2; - } + /* Write to a TEMP file and verify it BEFORE touching the existing + * build. Ordering matters: if the transfer, the write or the verify + * fails, the copy you are running from is still on the card untouched, + * so you can always try the update again. Only once the new image is + * known good do we rotate. */ + if (status) { snprintf(line, sizeof(line), "writing %s", tmp); status(line); } + f = fopen(tmp, "wb"); + if (!f) { if (status) status("open for write failed"); return -2; } done = (u32)fwrite(buf, 1, len, f); fclose(f); if (done != len) { - if (status) status("short write"); + remove(tmp); + if (status) status("short write - existing build left alone"); return -3; } - /* Read it back. A half-written .dol that still parses is exactly the - * failure this whole design exists to avoid, and the card is the one - * component here with no other integrity check. */ + /* Read it back. A half-written .dol that still parses is the failure + * this design exists to avoid, and the card is the one component here + * with no other integrity check. */ if (status) status("verifying..."); - f = fopen(full, "rb"); - if (!f) { - if (status) status("reopen for verify failed"); - return -4; - } + f = fopen(tmp, "rb"); + if (!f) { remove(tmp); if (status) status("reopen for verify failed"); return -4; } { static u8 chunk[4096]; const u8 *src = (const u8 *)buf; u32 off = 0; - rc = 0; while (off < len) { u32 want = len - off; if (want > sizeof(chunk)) want = sizeof(chunk); @@ -390,11 +412,33 @@ int netboot_sd_write(const char *path, const void *buf, u32 len, } fclose(f); if (rc < 0) { - if (status) status("VERIFY FAILED - card image is not what we sent"); + remove(tmp); + if (status) status("VERIFY FAILED - existing build left alone"); return rc; } - if (status) { snprintf(line, sizeof(line), "wrote and verified %u bytes", - (unsigned)len); status(line); } + + /* Rotate: keep exactly one generation back. remove()/rename() of a + * file that is not there is fine and ignored -- that is just the first + * update on a fresh card. */ + remove(prev); + rename(final, prev); + if (rename(tmp, final) != 0) { + /* Should not happen, but if it does the old build is sitting in + * .prev.dol and is still bootable from Swiss -- say so rather + * than leaving the user to guess. */ + if (status) { + snprintf(line, sizeof(line), + "rename failed; previous build is at %s", prev); + status(line); + } + return -7; + } + + if (status) { + snprintf(line, sizeof(line), "wrote %u bytes; previous build kept as %s", + (unsigned)len, prev); + status(line); + } return 0; }