Keep one generation back, and verify before touching the existing build
Without this, a successful transfer of a BROKEN build stranded you: the only
copy of a working test program had already been overwritten, so there was no
way to attempt another transfer without an SD card reader.
Write order is now temp -> verify -> rotate, and the ordering is the point:
- The image goes to "<name>.tmp" and is read back and compared BEFORE
anything existing is touched. If the transfer, the write or the verify
fails, the build you are running is still on the card untouched and you
can simply try again. Previously a failed write left the destination
half-overwritten.
- On success the existing build is rotated to "<name>.prev.dol" and the temp
file takes its place. So if the NEW build turns out to be broken, boot
the previous one from Swiss and transfer again from there. One generation
back is enough never to need a card reader.
The two extensions are chosen deliberately:
- the temp file has NO .dol extension, because Swiss lists .dol files and a
half-written image Swiss offers to boot is exactly what this avoids;
- the backup KEEPS .dol, because booting it from Swiss is the whole point of
keeping it.
remove()/rename() on a file that is not there is ignored -- that is just the
first update on a fresh card. If the final rename somehow fails, the message
names the .prev.dol path rather than leaving the user to guess where their
working build went.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+17
-4
@@ -81,12 +81,25 @@ Press **Z**, then on the host:
|
||||
sudo ./scripts/send-dol.py <iface> 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 `<name>.tmp` and **read back and compared**.
|
||||
3. Only then is it rotated in: the existing build becomes
|
||||
**`<name>.prev.dol`** and the temp file becomes `<name>.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
|
||||
`<name>.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:
|
||||
|
||||
@@ -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 <iface> <file.dol>\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 <name>.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -342,44 +342,66 @@ static int sd_mount(void (*status)(const char *msg))
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Build "<base>.tmp" and "<base>.prev.dol" from "<base>.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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user