LazyProc.Call must-finds and panics on a missing export; the trimmed Tianyi cloud-PC image has no FreeMibIfTable2 in iphlpapi.dll, which killed the agent on its first collection after a successful register. Optional procs (GetSystemTimes/GlobalMemoryStatusEx/GetIfTable2Ex+ FreeMibIfTable2 as one gated pair) now resolve through sourceGate (Find once, missing = source disabled with one warning); the iface pair is gated together so a missing free proc never leaks the system table. collectMetrics additionally runs every source through guardedCall so any residual panic zeroes that source alone. Pure gate/guard logic lives in probe_guard.go (no build tag) with Linux-runnable regression tests.
57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
// This file holds the probe collector's degrade-don't-die plumbing:
|
|
// the per-source independence discipline as reusable mechanics,
|
|
// shared by every platform sampler but exercised today by the Windows
|
|
// sampler (the Linux sampler reads /proc files, whose failure mode is
|
|
// an error return, not a panic).
|
|
//
|
|
// Two layers:
|
|
//
|
|
// - source gates: an optional OS facility (a Win32 procedure, say)
|
|
// is probed once; when it is missing the source is skipped —
|
|
// zeroed — with a single warning, never re-probed, and never
|
|
// reached through a call path that panics on a missing export the
|
|
// way LazyProc.Call does when it must-finds a procedure;
|
|
// - guarded calls: a panicking source is recovered, zeroed and
|
|
// logged, so one bad source can never take the agent process (and
|
|
// every other source with it) down.
|
|
|
|
// sourceGate caches one availability probe. The zero value is ready.
|
|
type sourceGate struct {
|
|
once sync.Once
|
|
available bool
|
|
}
|
|
|
|
// enabled runs probe on first use and reports whether the source may
|
|
// be used. probe returns nil when the facility is present. A failed
|
|
// probe disables the source for the process lifetime and logs once.
|
|
func (g *sourceGate) enabled(name string, probe func() error) bool {
|
|
g.once.Do(func() {
|
|
if err := probe(); err != nil {
|
|
logger.Warningf("probe: %s unavailable, source disabled: %v", name, err)
|
|
return
|
|
}
|
|
g.available = true
|
|
})
|
|
return g.available
|
|
}
|
|
|
|
// guardedCall runs fn and converts a panic into the zero value plus a
|
|
// warning, so one failing metrics source degrades alone.
|
|
func guardedCall[T any](name string, fn func() T) (out T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logger.Warningf("probe: %s panicked, source zeroed: %v", name, r)
|
|
out = *new(T)
|
|
}
|
|
}()
|
|
return fn()
|
|
}
|