diff --git a/cpu/cpu_darwin_arm64.go b/cpu/cpu_darwin_arm64.go index 2effcadf8d..c9628a6d72 100644 --- a/cpu/cpu_darwin_arm64.go +++ b/cpu/cpu_darwin_arm64.go @@ -6,24 +6,38 @@ package cpu import ( "encoding/binary" "fmt" + "sync" "unsafe" "github.com/shirou/gopsutil/v4/internal/common" ) +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + cpuLibOnce sync.Once + cpuIOKit *common.IOKitLib + cpuCF *common.CoreFoundationLib + cpuLibErr error +) + +func initCPULibraries() { + cpuIOKit, cpuLibErr = common.NewIOKitLib() + if cpuLibErr != nil { + return + } + cpuCF, cpuLibErr = common.NewCoreFoundationLib() +} + // https://github.com/shoenig/go-m1cpu/blob/v0.1.6/cpu.go func getFrequency() (float64, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return 0, err + cpuLibOnce.Do(initCPULibraries) + if cpuLibErr != nil { + return 0, cpuLibErr } - defer iokit.Close() - corefoundation, err := common.NewCoreFoundationLib() - if err != nil { - return 0, err - } - defer corefoundation.Close() + iokit := cpuIOKit + corefoundation := cpuCF matching := iokit.IOServiceMatching("AppleARMIODevice") diff --git a/disk/disk_darwin.go b/disk/disk_darwin.go index 5b4a24006b..73c5f5300d 100644 --- a/disk/disk_darwin.go +++ b/disk/disk_darwin.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "strings" + "sync" "unsafe" "golang.org/x/sys/unix" @@ -157,18 +158,31 @@ func LabelWithContext(_ context.Context, _ string) (string, error) { return "", common.ErrNotImplementedError } -func IOCountersWithContext(_ context.Context, names ...string) (map[string]IOCountersStat, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return nil, err +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + diskLibOnce sync.Once + diskIOKit *common.IOKitLib + diskCF *common.CoreFoundationLib + diskLibErr error +) + +func initDiskLibraries() { + diskIOKit, diskLibErr = common.NewIOKitLib() + if diskLibErr != nil { + return } - defer iokit.Close() + diskCF, diskLibErr = common.NewCoreFoundationLib() +} - corefoundation, err := common.NewCoreFoundationLib() - if err != nil { - return nil, err +func IOCountersWithContext(_ context.Context, names ...string) (map[string]IOCountersStat, error) { + diskLibOnce.Do(initDiskLibraries) + if diskLibErr != nil { + return nil, diskLibErr } - defer corefoundation.Close() + + iokit := diskIOKit + corefoundation := diskCF match := iokit.IOServiceMatching("IOMedia") diff --git a/internal/common/common_darwin.go b/internal/common/common_darwin.go index 384b4c5a72..caa1b8d981 100644 --- a/internal/common/common_darwin.go +++ b/internal/common/common_darwin.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "math" + "sync" "unsafe" "github.com/ebitengine/purego" @@ -16,6 +17,7 @@ import ( type library struct { handle uintptr fnMap map[string]any + mu sync.RWMutex } // library paths @@ -41,15 +43,29 @@ func (lib *library) Dlsym(symbol string) (uintptr, error) { return purego.Dlsym(lib.handle, symbol) } +// getFunc resolves a function pointer from the library, caching it in fnMap. +// Thread-safe via double-checked locking to support shared library handles. func getFunc[T any](lib *library, symbol string) T { - var dlfun *dlFunc[T] + // Fast path: read lock only + lib.mu.RLock() if f, ok := lib.fnMap[symbol].(*dlFunc[T]); ok { - dlfun = f - } else { - dlfun = newDlfunc[T](symbol) - dlfun.init(lib.handle) - lib.fnMap[symbol] = dlfun + lib.mu.RUnlock() + return f.fn } + lib.mu.RUnlock() + + // Slow path: write lock for first-time resolution + lib.mu.Lock() + defer lib.mu.Unlock() + + // Double-check after acquiring write lock + if f, ok := lib.fnMap[symbol].(*dlFunc[T]); ok { + return f.fn + } + + dlfun := newDlfunc[T](symbol) + dlfun.init(lib.handle) + lib.fnMap[symbol] = dlfun return dlfun.fn } diff --git a/sensors/sensors_darwin_arm64.go b/sensors/sensors_darwin_arm64.go index ab52fcc3c8..d58881a0e5 100644 --- a/sensors/sensors_darwin_arm64.go +++ b/sensors/sensors_darwin_arm64.go @@ -5,6 +5,7 @@ package sensors import ( "context" + "sync" "unsafe" "github.com/shirou/gopsutil/v4/internal/common" @@ -15,35 +16,48 @@ const ( kHIDPageAppleVendorTemperatureSensor = 5 ) +// Keep IOKit and CoreFoundation libraries open for the process lifetime. +// Opening and closing them on every call causes SIGBUS/SIGSEGV crashes +// because the Go runtime (GC, timers) can interact with invalidated +// library handles after Dlclose. +// See: https://github.com/shirou/gopsutil/issues/1832 +var ( + sensorLibOnce sync.Once + sensorIOKit *common.IOKitLib + sensorCF *common.CoreFoundationLib + sensorLibErr error +) + +func initSensorLibraries() { + sensorIOKit, sensorLibErr = common.NewIOKitLib() + if sensorLibErr != nil { + return + } + sensorCF, sensorLibErr = common.NewCoreFoundationLib() +} + func ReadTemperaturesArm() []TemperatureStat { temperatures, _ := TemperaturesWithContext(context.Background()) return temperatures } func TemperaturesWithContext(_ context.Context) ([]TemperatureStat, error) { - iokit, err := common.NewIOKitLib() - if err != nil { - return nil, err - } - defer iokit.Close() - - cf, err := common.NewCoreFoundationLib() - if err != nil { - return nil, err + sensorLibOnce.Do(initSensorLibraries) + if sensorLibErr != nil { + return nil, sensorLibErr } - defer cf.Close() ta := &temperatureArm{ - iokit: iokit, - cf: cf, + iokit: sensorIOKit, + cf: sensorCF, } sensors := ta.matching(kHIDPageAppleVendor, kHIDPageAppleVendorTemperatureSensor) - defer cf.CFRelease(uintptr(sensors)) + defer sensorCF.CFRelease(uintptr(sensors)) // Create HID system client - system := iokit.IOHIDEventSystemClientCreate(common.KCFAllocatorDefault) - defer cf.CFRelease(uintptr(system)) + system := sensorIOKit.IOHIDEventSystemClientCreate(common.KCFAllocatorDefault) + defer sensorCF.CFRelease(uintptr(system)) return ta.getSensors(system, sensors), nil }