- page_scenario.go: onclick built via JSON.stringify() embedded raw double
quotes inside a double-quoted HTML attribute, truncating the attribute so
the click handler never compiled; pass the name through an escaped
data-scenario-name attribute instead.
- build.sh: overlay staging rsyncs (OVERLAY_DIR->stage, stage->includes.chroot)
ran without --delete, so a scenario removed from the repo (a9924b0) stayed
baked into every ISO built from the persistent stage cache since — the
"second script" in the Scenario page's list.
- blackbox: rewritten around a deterministic local zip + incremental
patch-the-changed-suffix onto removable media, instead of walking/copying
~90 files through a synchronous ntfs-3g FUSE mount every cycle. journalctl
captures are now "--since last sync" (were "--since boot", growing with
uptime) and metrics.db is excluded (was copied whole every cycle).
- scenario: nvbandwidth-acs-ab now escalates GPU count (same-socket pair,
other socket's pair, one cross-socket pair, all GPUs) under each ACS state
instead of always running all 6 GPUs at once, using a new `bee
gpu-bandwidth-groups` subcommand that discovers socket layout from
`nvidia-smi topo -m` at runtime — gpu_indices is host-specific, so this
can't be baked into the scenario file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
package app
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// blackboxArchiveCompareChunk is the buffer size used when diffing the newly
|
|
// built local zip against the last one successfully written to removable
|
|
// media, to find how many leading bytes are unchanged.
|
|
const blackboxArchiveCompareChunk = 256 * 1024
|
|
|
|
// buildZipArchive walks root (a locally-staged, fast-storage copy of the
|
|
// blackbox tree — never the slow removable-media mountpoint) and writes a
|
|
// single deterministic zip to destPath: same input tree -> byte-identical
|
|
// output, so two cycles that changed nothing produce identical archives and
|
|
// patchArchiveOnTarget (below) can skip re-writing the unchanged prefix to
|
|
// the slow target. Determinism relies on fs.WalkDir's guaranteed lexical
|
|
// order and each entry's Modified time coming from the source file's mtime
|
|
// (stable across cycles for files nothing touched).
|
|
func buildZipArchive(root, destPath string) error {
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
zw := zip.NewWriter(f)
|
|
|
|
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = filepath.ToSlash(rel)
|
|
header.Method = zip.Deflate
|
|
w, err := zw.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
_, err = io.Copy(w, src)
|
|
return err
|
|
})
|
|
if walkErr != nil {
|
|
_ = zw.Close()
|
|
_ = f.Close()
|
|
return walkErr
|
|
}
|
|
if err := zw.Close(); err != nil {
|
|
_ = f.Close()
|
|
return err
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
// commonPrefixLen returns how many leading bytes two files share. Used to
|
|
// find how much of a freshly-rebuilt local zip is identical to the previous
|
|
// cycle's, so patchArchiveOnTarget only has to write the changed suffix to
|
|
// removable media.
|
|
func commonPrefixLen(pathA, pathB string) (int64, error) {
|
|
fa, err := os.Open(pathA)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer fa.Close()
|
|
fb, err := os.Open(pathB)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer fb.Close()
|
|
|
|
ra := bufio.NewReaderSize(fa, blackboxArchiveCompareChunk)
|
|
rb := bufio.NewReaderSize(fb, blackboxArchiveCompareChunk)
|
|
bufA := make([]byte, blackboxArchiveCompareChunk)
|
|
bufB := make([]byte, blackboxArchiveCompareChunk)
|
|
|
|
var total int64
|
|
for {
|
|
na, errA := io.ReadFull(ra, bufA)
|
|
nb, errB := io.ReadFull(rb, bufB)
|
|
n := na
|
|
if nb < n {
|
|
n = nb
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
if bufA[i] != bufB[i] {
|
|
return total + int64(i), nil
|
|
}
|
|
}
|
|
total += int64(n)
|
|
if na != nb || isEOFLike(errA) || isEOFLike(errB) {
|
|
return total, nil
|
|
}
|
|
if errA != nil {
|
|
return total, errA
|
|
}
|
|
if errB != nil {
|
|
return total, errB
|
|
}
|
|
}
|
|
}
|
|
|
|
func isEOFLike(err error) bool {
|
|
return err == io.EOF || err == io.ErrUnexpectedEOF
|
|
}
|
|
|
|
// patchArchiveOnTarget makes targetPath (on removable media, possibly
|
|
// FUSE-mounted with synchronous writes) byte-identical to newLocalZipPath (on
|
|
// fast local storage), writing only the changed suffix instead of the whole
|
|
// file. cachedPath is our own local record of what we last wrote to
|
|
// targetPath; if targetPath's size doesn't match what cachedPath implies
|
|
// (first run, external tampering, a previous crash mid-write), it falls back
|
|
// to writing the whole archive rather than risk corrupting it with a wrong
|
|
// truncate point.
|
|
func patchArchiveOnTarget(targetPath, newLocalZipPath, cachedPath string) error {
|
|
newInfo, err := os.Stat(newLocalZipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var prefixLen int64
|
|
if cachedInfo, err := os.Stat(cachedPath); err == nil {
|
|
if targetInfo, err := os.Stat(targetPath); err == nil && targetInfo.Size() == cachedInfo.Size() {
|
|
prefixLen, err = commonPrefixLen(cachedPath, newLocalZipPath)
|
|
if err != nil {
|
|
prefixLen = 0
|
|
}
|
|
}
|
|
}
|
|
if prefixLen > newInfo.Size() {
|
|
prefixLen = 0
|
|
}
|
|
|
|
target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer target.Close()
|
|
|
|
if err := target.Truncate(prefixLen); err != nil {
|
|
return err
|
|
}
|
|
if _, err := target.Seek(prefixLen, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
|
|
src, err := os.Open(newLocalZipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
if _, err := src.Seek(prefixLen, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(target, src); err != nil {
|
|
return err
|
|
}
|
|
return target.Sync()
|
|
}
|