Files
bee/audit/internal/app/blackbox_archive.go
T
mchusandClaude Sonnet 5 8a91f0f783 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>
2026-07-29 18:11:24 +03:00

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()
}