a73x

internal/agent/hostinfo/hostinfo_darwin.go

Ref:   Size: 4.3 KiB   History

//go:build darwin

package hostinfo

import (
	"context"
	"fmt"
	"time"

	"golang.org/x/sys/unix"

	agentexec "github.com/a73x/eitri/internal/agent/exec"
	"github.com/a73x/eitri/internal/pb"
)

// systemVersionPath is macOS's identity file, osIdentity's source. Unlike the
// Linux path seams in hostinfo.go it lives with its platform: nothing untagged
// reads it, so on Linux it would be an unused variable.
var systemVersionPath = "/System/Library/CoreServices/SystemVersion.plist"

// readBootID derives an opaque per-boot token from the kern.boottime sysctl.
// BootID only needs equality across a reboot (see its doc comment), and
// boottime changes on every reboot and holds steady within one, so it
// satisfies the contract without needing a dedicated boot-id concept the way
// Linux's kernel does. "" on sysctl failure, per the package's best-effort
// contract.
func readBootID() string {
	tv, err := unix.SysctlTimeval("kern.boottime")
	if err != nil {
		return ""
	}
	return fmt.Sprintf("boottime-%d.%d", tv.Sec, tv.Usec)
}

func totalMemMB() int64 {
	v, err := unix.SysctlUint64("hw.memsize")
	if err != nil {
		return 0
	}
	return int64(v) / mb
}

func totalDiskGB(stateDir string) int64 {
	var fs unix.Statfs_t
	if err := unix.Statfs(stateDir, &fs); err != nil {
		return 0
	}
	// Bsize is uint32 on Darwin (int64 on Linux) — convert, never multiply raw.
	return int64(fs.Blocks) * int64(fs.Bsize) / gb
}

// osIdentity is Facts' Darwin source for the OS fields: macOS's own version
// plist, mapped to the same shape /etc/os-release gives on Linux.
func osIdentity() (id, pretty, version string) {
	return macOSIdentity(readFile(systemVersionPath))
}

// virtSource is Facts' Darwin answer for Virt. A Mac in the fleet is bare
// metal — Virtualization.framework guests can't nest — so the answer is the
// constant "none", the same string a bare-metal Linux host reports. No
// subprocess runs: there is no Darwin systemd-detect-virt, and spawning
// something per Hello to learn nothing is the machinery this design removes.
func virtSource(_ context.Context, _ agentexec.Runner) string { return "none" }

// kernelVersion is Facts' Darwin source for Kernel: the XNU release string
// (e.g. "23.6.0") via the kern.osrelease sysctl — Darwin's equivalent of
// Linux's /proc/sys/kernel/osrelease.
func kernelVersion() string {
	s, err := unix.Sysctl("kern.osrelease")
	if err != nil {
		return ""
	}
	return s
}

// cpuModel is Facts' Darwin source for CpuModel via the machdep.cpu.brand_string
// sysctl — Darwin's equivalent of Linux's /proc/cpuinfo "model name" line.
func cpuModel() string {
	s, err := unix.Sysctl("machdep.cpu.brand_string")
	if err != nil {
		return ""
	}
	return s
}

// readMetrics is Metrics' Darwin implementation: uptime and load from
// sysctls, memory headroom from free pages, disk from statfs on the state dir.
// Each probe is independent — one failing sysctl zeroes its own dimension and
// nothing else.
func readMetrics(stateDir string) *pb.HostMetrics {
	m := &pb.HostMetrics{}

	if tv, err := unix.SysctlTimeval("kern.boottime"); err == nil {
		if uptime := time.Now().Unix() - tv.Sec; uptime > 0 {
			m.UptimeS = uptime
		}
	}

	if raw, err := unix.SysctlRaw("vm.loadavg"); err == nil {
		if loads, ok := decodeLoadavg(raw); ok {
			m.Load1, m.Load5, m.Load15 = loads[0], loads[1], loads[2]
		}
	}

	if avail := memAvailableMB(); avail > 0 {
		m.MemAvailableMb = avail
		if used := totalMemMB() - avail; used > 0 {
			m.MemUsedMb = used
		}
	}

	var fs unix.Statfs_t
	if err := unix.Statfs(stateDir, &fs); err == nil {
		// Bsize is uint32 on Darwin (int64 on Linux) — convert, never multiply raw.
		bsize := int64(fs.Bsize)
		m.DiskFreeGb = int64(fs.Bavail) * bsize / gb
		m.DiskUsedGb = (int64(fs.Blocks) - int64(fs.Bfree)) * bsize / gb
	}
	return m
}

// memAvailableMB is Darwin's best-effort headroom: free pages only. macOS also
// reclaims inactive and purgeable memory on demand, but counting those needs
// host_statistics64 and therefore cgo, which this package does not take. The
// number under-reports headroom; it never over-reports it.
//
// Pages are 16 KiB on Apple Silicon, so the page size is read, not assumed.
func memAvailableMB() int64 {
	free, err := unix.SysctlUint32("vm.page_free_count")
	if err != nil {
		return 0
	}
	pageSize, err := unix.SysctlUint32("hw.pagesize")
	if err != nil {
		return 0
	}
	return int64(free) * int64(pageSize) / mb
}