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:
@@ -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