fix(webui): repair broken scenario Run button onclick, dedupe build.sh overlay staging
- 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>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user