chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package crashpool
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// QuarantineEntry records one file that crashed, hung, or panicked the
|
||||
// parser.
|
||||
type QuarantineEntry struct {
|
||||
RelPath string `json:"rel_path"`
|
||||
Reason string `json:"reason"`
|
||||
// MtimeNano pins the file revision that failed. When the file's
|
||||
// current mtime differs the file is treated as changed and gets
|
||||
// one retry instead of being skipped.
|
||||
MtimeNano int64 `json:"mtime_nano"`
|
||||
FirstSeen string `json:"first_seen"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
Attempts int `json:"attempts"`
|
||||
}
|
||||
|
||||
// Quarantine is a persistent set of files that have crashed parsing. It
|
||||
// is JSON-backed so a file that SIGSEGVs the parser survives daemon
|
||||
// restarts and is skipped until its content changes.
|
||||
//
|
||||
// The zero value is not usable; construct with LoadQuarantine. A nil
|
||||
// *Quarantine is safe for every method (no-op / not-quarantined), so
|
||||
// callers can keep crash isolation optional without nil checks.
|
||||
type Quarantine struct {
|
||||
mu sync.Mutex
|
||||
path string
|
||||
entries map[string]*QuarantineEntry
|
||||
}
|
||||
|
||||
// LoadQuarantine reads the quarantine file at path, tolerating a
|
||||
// missing or corrupt file (treated as empty). path is where Save will
|
||||
// persist; an empty path makes Save a no-op (in-memory only).
|
||||
func LoadQuarantine(path string) *Quarantine {
|
||||
q := &Quarantine{path: path, entries: map[string]*QuarantineEntry{}}
|
||||
if path == "" {
|
||||
return q
|
||||
}
|
||||
data, err := os.ReadFile(path) //nolint:gosec // path is daemon-internal
|
||||
if err != nil {
|
||||
return q
|
||||
}
|
||||
var list []*QuarantineEntry
|
||||
if json.Unmarshal(data, &list) != nil {
|
||||
return q
|
||||
}
|
||||
for _, e := range list {
|
||||
if e != nil && e.RelPath != "" {
|
||||
q.entries[e.RelPath] = e
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// IsQuarantined reports whether relPath is a known-bad file at the
|
||||
// given mtime. It returns true only when the file is unchanged since
|
||||
// it failed — a changed file (different mtime) is not quarantined so
|
||||
// it gets retried.
|
||||
func (q *Quarantine) IsQuarantined(relPath string, mtimeNano int64) bool {
|
||||
if q == nil {
|
||||
return false
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
e, ok := q.entries[relPath]
|
||||
return ok && e.MtimeNano == mtimeNano
|
||||
}
|
||||
|
||||
// Add records relPath as quarantined. Re-adding bumps the attempt
|
||||
// count and refreshes the mtime + reason.
|
||||
func (q *Quarantine) Add(relPath, reason string, mtimeNano int64) {
|
||||
if q == nil || relPath == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
if e, ok := q.entries[relPath]; ok {
|
||||
e.Reason = reason
|
||||
e.MtimeNano = mtimeNano
|
||||
e.LastSeen = now
|
||||
e.Attempts++
|
||||
return
|
||||
}
|
||||
q.entries[relPath] = &QuarantineEntry{
|
||||
RelPath: relPath,
|
||||
Reason: reason,
|
||||
MtimeNano: mtimeNano,
|
||||
FirstSeen: now,
|
||||
LastSeen: now,
|
||||
Attempts: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Forget drops relPath from the quarantine — used when a previously
|
||||
// bad file parses cleanly on retry.
|
||||
func (q *Quarantine) Forget(relPath string) {
|
||||
if q == nil {
|
||||
return
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
delete(q.entries, relPath)
|
||||
}
|
||||
|
||||
// Entries returns a stable, sorted snapshot of the quarantine.
|
||||
func (q *Quarantine) Entries() []QuarantineEntry {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
out := make([]QuarantineEntry, 0, len(q.entries))
|
||||
for _, e := range q.entries {
|
||||
out = append(out, *e)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].RelPath < out[j].RelPath })
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of quarantined files.
|
||||
func (q *Quarantine) Len() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return len(q.entries)
|
||||
}
|
||||
|
||||
// Save persists the quarantine to its backing file. A no-op when the
|
||||
// quarantine was loaded with an empty path.
|
||||
func (q *Quarantine) Save() error {
|
||||
if q == nil || q.path == "" {
|
||||
return nil
|
||||
}
|
||||
entries := q.Entries()
|
||||
data, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(q.path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unique temp name so concurrent Saves — the indexer re-indexes
|
||||
// files in parallel through one shared quarantine — never clobber
|
||||
// each other's in-flight write before the rename.
|
||||
f, err := os.CreateTemp(filepath.Dir(q.path), ".quarantine-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := f.Name()
|
||||
if _, err := f.Write(data); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, q.path)
|
||||
}
|
||||
Reference in New Issue
Block a user