Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 74 additions & 7 deletions Sources/ProcessBarMonitor/SystemMetricsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ actor SystemMetricsProvider {
private var lastTemperatureRefresh = Date.distantPast
private var lastTemperatureMode: TemperatureMode = .hottestCPU
private let appleSiliconProvider = AppleSiliconTemperatureProvider()
/// Caches the confirmed temperature tool state to avoid repeated shell detection.
private var temperatureToolState: TemperatureToolState = .unchecked

/// 3-state cache for temperature command resolution.
/// - unchecked: initial state, needs detection
/// - unavailable: both tools confirmed missing, skip detection
/// - resolved: tool found, store its kind and the resolved executable path
private enum TemperatureToolState {
case unchecked
case unavailable
case resolved(kind: TemperatureToolKind, path: String)
}

/// Supported temperature measurement tools.
private enum TemperatureToolKind {
case istats
case osxCpuTemp
}

func snapshot(temperatureMode: TemperatureMode) -> SystemSummary {
let temperature = bestEffortCPUTemperature(mode: temperatureMode)
Expand Down Expand Up @@ -84,17 +102,66 @@ actor SystemMetricsProvider {
return appleTemp
}

cachedTemperature = commandTemperature(
"/bin/zsh",
["-lc", "if command -v istats >/dev/null 2>&1; then istats cpu temp --value-only; elif command -v osx-cpu-temp >/dev/null 2>&1; then osx-cpu-temp; else exit 1; fi"]
)
// Skip detection entirely if both tools were previously confirmed unavailable
if case .unchecked = temperatureToolState {
temperatureToolState = resolveTemperatureTool()
}

switch temperatureToolState {
case .unavailable:
cachedTemperature = nil
case .resolved(let kind, let path):
cachedTemperature = runTemperatureCommand(kind: kind, path: path)
case .unchecked:
cachedTemperature = nil
}
return cachedTemperature
}

private func commandTemperature(_ launchPath: String, _ arguments: [String]) -> Double? {
/// Detects which temperature tool is available and caches the resolved executable path.
/// Called at most once per app run when cache is cold.
private func resolveTemperatureTool() -> TemperatureToolState {
let process = Process()
process.executableURL = URL(fileURLWithPath: launchPath)
process.arguments = arguments
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = ["-lc", """
if command -v istats >/dev/null 2>&1; then
echo "istats:$(command -v istats)"
elif command -v osx-cpu-temp >/dev/null 2>&1; then
echo "osx-cpu-temp:$(command -v osx-cpu-temp)"
else
echo "none"
fi
"""]
let output = Pipe()
process.standardOutput = output
process.standardError = Pipe()

do { try process.run() } catch { return .unavailable }
process.waitUntilExit()

guard process.terminationStatus == 0 else { return .unavailable }
let data = output.fileHandleForReading.readDataToEndOfFile()
guard let raw = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else { return .unavailable }

if raw.hasPrefix("istats:") {
let path = String(raw.dropFirst(7))
return .resolved(kind: .istats, path: path)
} else if raw.hasPrefix("osx-cpu-temp:") {
let path = String(raw.dropFirst(13))
return .resolved(kind: .osxCpuTemp, path: path)
}
return .unavailable
}

private func runTemperatureCommand(kind: TemperatureToolKind, path: String) -> Double? {
let process = Process()
process.executableURL = URL(fileURLWithPath: path)
switch kind {
case .istats:
process.arguments = ["cpu", "temp", "--value-only"]
case .osxCpuTemp:
process.arguments = []
}
let output = Pipe()
process.standardOutput = output
process.standardError = Pipe()
Expand Down