1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 13:16:47 +00:00

Supports multi-disk detection

This commit is contained in:
Tw93
2025-11-23 20:20:35 +08:00
parent d5aea990f6
commit 54c30d6b60
4 changed files with 159 additions and 13 deletions

2
.gitignore vendored
View File

@@ -28,6 +28,8 @@ temp/
# Cache
.cache/
*.cache
.gocache/
.gomod/
# Backup files
*.bak

Binary file not shown.

View File

@@ -29,8 +29,8 @@ type MetricsSnapshot struct {
Uptime string
Procs uint64
Hardware HardwareInfo
HealthScore int // 0-100 system health score
HealthScoreMsg string // Brief explanation
HealthScore int // 0-100 system health score
HealthScoreMsg string // Brief explanation
CPU CPUStatus
GPU []GPUStatus
@@ -47,11 +47,11 @@ type MetricsSnapshot struct {
}
type HardwareInfo struct {
Model string // MacBook Pro 14-inch, 2021
CPUModel string // Apple M1 Pro / Intel Core i7
TotalRAM string // 16GB
DiskSize string // 512GB
OSVersion string // macOS Sonoma 14.5
Model string // MacBook Pro 14-inch, 2021
CPUModel string // Apple M1 Pro / Intel Core i7
TotalRAM string // 16GB
DiskSize string // 512GB
OSVersion string // macOS Sonoma 14.5
}
type DiskIOStatus struct {
@@ -94,10 +94,12 @@ type MemoryStatus struct {
type DiskStatus struct {
Mount string
Device string
Used uint64
Total uint64
UsedPercent float64
Fstype string
External bool
}
type NetworkStatus struct {
@@ -157,6 +159,7 @@ const (
systemProfilerTimeout = 4 * time.Second
bluetoothctlTimeout = 1500 * time.Millisecond
macGPUInfoTTL = 10 * time.Minute
cpuSampleInterval = 200 * time.Millisecond
)
var skipDiskMounts = map[string]bool{
@@ -362,7 +365,7 @@ func formatUptime(secs uint64) string {
}
func collectCPU() (CPUStatus, error) {
percents, err := cpu.Percent(0, true)
percents, err := cpu.Percent(cpuSampleInterval, true)
if err != nil {
return CPUStatus{}, err
}
@@ -375,9 +378,26 @@ func collectCPU() (CPUStatus, error) {
}
totalPercent /= float64(len(percents))
loadAvg, _ := load.Avg()
counts, _ := cpu.Counts(false)
logical, _ := cpu.Counts(true)
loadStats, loadErr := load.Avg()
var loadAvg load.AvgStat
if loadStats != nil {
loadAvg = *loadStats
}
if loadErr != nil || isZeroLoad(loadAvg) {
if fallback, err := fallbackLoadAvgFromUptime(); err == nil {
loadAvg = fallback
}
}
counts, countsErr := cpu.Counts(false)
if countsErr != nil || counts == 0 {
counts = runtime.NumCPU()
}
logical, logicalErr := cpu.Counts(true)
if logicalErr != nil || logical == 0 {
logical = runtime.NumCPU()
}
return CPUStatus{
Usage: totalPercent,
@@ -390,6 +410,62 @@ func collectCPU() (CPUStatus, error) {
}, nil
}
func isZeroLoad(avg load.AvgStat) bool {
return avg.Load1 == 0 && avg.Load5 == 0 && avg.Load15 == 0
}
func fallbackLoadAvgFromUptime() (load.AvgStat, error) {
if !commandExists("uptime") {
return load.AvgStat{}, errors.New("uptime command unavailable")
}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
out, err := runCmd(ctx, "uptime")
if err != nil {
return load.AvgStat{}, err
}
markers := []string{"load averages:", "load average:"}
idx := -1
for _, marker := range markers {
if pos := strings.LastIndex(out, marker); pos != -1 {
idx = pos + len(marker)
break
}
}
if idx == -1 {
return load.AvgStat{}, errors.New("load averages not found in uptime output")
}
segment := strings.TrimSpace(out[idx:])
fields := strings.Fields(segment)
var values []float64
for _, field := range fields {
field = strings.Trim(field, ",;")
if field == "" {
continue
}
val, err := strconv.ParseFloat(field, 64)
if err != nil {
continue
}
values = append(values, val)
if len(values) == 3 {
break
}
}
if len(values) < 3 {
return load.AvgStat{}, errors.New("could not parse load averages from uptime output")
}
return load.AvgStat{
Load1: values[0],
Load5: values[1],
Load15: values[2],
}, nil
}
func collectMemory() (MemoryStatus, error) {
vm, err := mem.VirtualMemory()
if err != nil {
@@ -476,6 +552,7 @@ func collectDisks() ([]DiskStatus, error) {
}
disks = append(disks, DiskStatus{
Mount: part.Mountpoint,
Device: part.Device,
Used: usage.Used,
Total: usage.Total,
UsedPercent: usage.UsedPercent,
@@ -485,6 +562,8 @@ func collectDisks() ([]DiskStatus, error) {
seenVolume[volKey] = true
}
annotateDiskTypes(disks)
sort.Slice(disks, func(i, j int) bool {
return disks[i].Total > disks[j].Total
})
@@ -496,6 +575,72 @@ func collectDisks() ([]DiskStatus, error) {
return disks, nil
}
func annotateDiskTypes(disks []DiskStatus) {
if len(disks) == 0 || runtime.GOOS != "darwin" || !commandExists("diskutil") {
return
}
cache := make(map[string]bool)
for i := range disks {
base := baseDeviceName(disks[i].Device)
if base == "" {
base = disks[i].Device
}
if val, ok := cache[base]; ok {
disks[i].External = val
continue
}
external, err := isExternalDisk(base)
if err != nil {
external = strings.HasPrefix(disks[i].Mount, "/Volumes/")
}
disks[i].External = external
cache[base] = external
}
}
func baseDeviceName(device string) string {
device = strings.TrimPrefix(device, "/dev/")
if !strings.HasPrefix(device, "disk") {
return device
}
for i := 4; i < len(device); i++ {
if device[i] == 's' {
return device[:i]
}
}
return device
}
func isExternalDisk(device string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
out, err := runCmd(ctx, "diskutil", "info", device)
if err != nil {
return false, err
}
var (
found bool
external bool
)
for _, line := range strings.Split(out, "\n") {
trim := strings.TrimSpace(line)
if strings.HasPrefix(trim, "Internal:") {
found = true
external = strings.Contains(trim, "No")
break
}
if strings.HasPrefix(trim, "Device Location:") {
found = true
external = strings.Contains(trim, "External")
}
}
if !found {
return false, errors.New("diskutil info missing Internal field")
}
return external, nil
}
func (c *Collector) collectDiskIO(now time.Time) DiskIOStatus {
counters, err := disk.IOCounters()
if err != nil || len(counters) == 0 {
@@ -1247,4 +1392,3 @@ func collectHardware(totalRAM uint64, disks []DiskStatus) HardwareInfo {
OSVersion: osVersion,
}
}

2
mole
View File

@@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
# Version info
VERSION="1.10.6"
VERSION="1.10.7"
MOLE_TAGLINE="can dig deep to clean your Mac."
# Check if Touch ID is already configured