Files
wehub-resource-sync a06f331eb8
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

172 lines
4.4 KiB
Go

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