Files
OpenStats/Helper/main.swift
T
gentpan 1205483b5e 新增清理与系统维护,修复面板抖动,更换应用图标
- 清理:应用缓存、日志、浏览器缓存、Xcode/模拟器/npm 缓存、下载残留、安装包、废纸篓;
  白名单目录 + 安全守卫,运行中的应用自动跳过,执行前逐项复核,操作日志
- 系统维护:刷新 DNS 缓存、释放内存(辅助工具或管理员授权)
- 面板:去掉打开后的二次调整高度与窗口高度动画,内容放得下时不回弹,
  修复点击菜单栏图标关闭时被重新打开
- 更换应用图标,面板与关于页使用应用图标作为 Logo
- 清理未使用的组件、格式化方法、设计 token 与辅助工具接口
2026-09-13 03:03:08 +05:00

218 lines
7.5 KiB
Swift

//
// OpenStatsHelper
//
// 以 root 运行的辅助工具,由 SMAppService.daemon 注册。
// 只做固定的几件事:设置风扇转速、切换“合盖不睡眠”、刷新 DNS、释放内存,
// 并在客户端断开时恢复风扇与睡眠设置。
//
import Foundation
import HelperShared
import SMC
final class HelperService: NSObject, NSXPCListenerDelegate, OpenStatsHelperProtocol, @unchecked Sendable {
/// 所有可变状态只在该队列上访问
private let queue = DispatchQueue(label: "com.openstats.helper.state")
private let listener = NSXPCListener(machServiceName: HelperConstants.machServiceName)
private let stateURL = URL(fileURLWithPath: "/Library/Application Support/OpenStats/helper-state.plist")
private let idleTimeout: TimeInterval = 30
private var connections: Set<ObjectIdentifier> = []
private var fans: FanControl?
private var manualFans: Set<Int> = []
private var sleepDisabledByHelper = false
private var idleExit: DispatchWorkItem?
func run() {
queue.sync {
fans = (try? SMCConnection()).map(FanControl.init)
recoverFromPreviousRun()
scheduleIdleExit()
}
listener.delegate = self
listener.resume()
dispatchMain()
}
// MARK: NSXPCListenerDelegate
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
// 系统在收到消息前校验调用方签名,不符合要求的连接会被直接失效
connection.setCodeSigningRequirement(HelperConstants.clientRequirement)
connection.exportedInterface = NSXPCInterface(with: OpenStatsHelperProtocol.self)
connection.exportedObject = self
let id = ObjectIdentifier(connection)
connection.invalidationHandler = { [weak self] in
guard let self else { return }
self.queue.async { self.connectionClosed(id) }
}
queue.sync {
connections.insert(id)
idleExit?.cancel()
}
connection.resume()
return true
}
// MARK: OpenStatsHelperProtocol
func protocolVersion(reply: @escaping @Sendable (Int) -> Void) {
reply(HelperConstants.protocolVersion)
}
func setFanTarget(fan: Int, rpm: Double, reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in
guard let fans else { return reply("无法访问 SMC") }
guard (0..<fans.fanCount).contains(fan), rpm.isFinite, rpm > 0 else { return reply("无效的风扇参数") }
do {
try fans.setManual(fan: fan, rpm: rpm)
manualFans.insert(fan)
reply(nil)
} catch {
reply("设置风扇失败:\(error)")
}
}
}
func setFanAutomatic(fan: Int, reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in
guard let fans else { return reply("无法访问 SMC") }
guard (0..<fans.fanCount).contains(fan) else { return reply("无效的风扇编号") }
do {
try fans.setAutomatic(fan: fan)
manualFans.remove(fan)
reply(nil)
} catch {
reply("恢复风扇失败:\(error)")
}
}
}
func resetAllFans(reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in
reply(resetFans())
}
}
func setSleepDisabled(_ disabled: Bool, reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in
reply(applySleepDisabled(disabled))
}
}
func flushDNSCache(reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in reply(run(.flushDNS)) }
}
func purgeMemory(reply: @escaping @Sendable (String?) -> Void) {
queue.async { [self] in reply(run(.purgeMemory)) }
}
// MARK: 状态恢复
private func connectionClosed(_ id: ObjectIdentifier) {
connections.remove(id)
guard connections.isEmpty else { return }
// 应用已退出或崩溃:把系统恢复到默认状态
_ = resetFans()
if sleepDisabledByHelper { _ = applySleepDisabled(false) }
scheduleIdleExit()
}
private func recoverFromPreviousRun() {
guard let data = try? Data(contentsOf: stateURL),
let state = try? PropertyListDecoder().decode(PersistedState.self, from: data) else { return }
if state.sleepDisabled {
sleepDisabledByHelper = true
_ = applySleepDisabled(false)
}
if state.fansOverridden { _ = resetFans() }
}
private func scheduleIdleExit() {
idleExit?.cancel()
let work = DispatchWorkItem { [weak self] in
guard let self, self.connections.isEmpty else { return }
exit(0)
}
idleExit = work
queue.asyncAfter(deadline: .now() + idleTimeout, execute: work)
}
// MARK: 风扇
private func resetFans() -> String? {
guard let fans else { return manualFans.isEmpty ? nil : "无法访问 SMC" }
do {
try fans.resetAll()
manualFans.removeAll()
persist()
return nil
} catch {
return "恢复风扇失败:\(error)"
}
}
// MARK: 睡眠
private func applySleepDisabled(_ disabled: Bool) -> String? {
let result = runTool("/usr/bin/pmset", ["-a", "disablesleep", disabled ? "1" : "0"])
guard result.status == 0 else {
return "pmset 执行失败:\(result.output.trimmingCharacters(in: .whitespacesAndNewlines))"
}
sleepDisabledByHelper = disabled
persist()
return nil
}
// MARK: 系统维护
private func run(_ command: MaintenanceCommand) -> String? {
for step in command.steps {
let result = runTool(step.path, step.arguments)
// killall 在 mDNSResponder 恰好重启时可能返回非 0,不视为失败
if result.status != 0, step.path != "/usr/bin/killall" {
return "\((step.path as NSString).lastPathComponent) 执行失败:\(result.output.trimmingCharacters(in: .whitespacesAndNewlines))"
}
}
return nil
}
private func runTool(_ path: String, _ arguments: [String]) -> (status: Int32, output: String) {
let process = Process()
process.executableURL = URL(fileURLWithPath: path)
process.arguments = arguments
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
try process.run()
} catch {
return (-1, error.localizedDescription)
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return (process.terminationStatus, String(decoding: data, as: UTF8.self))
}
// MARK: 持久化
private struct PersistedState: Codable {
var sleepDisabled: Bool
var fansOverridden: Bool
}
private func persist() {
let state = PersistedState(sleepDisabled: sleepDisabledByHelper, fansOverridden: !manualFans.isEmpty)
guard state.sleepDisabled || state.fansOverridden else {
try? FileManager.default.removeItem(at: stateURL)
return
}
try? FileManager.default.createDirectory(at: stateURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try? PropertyListEncoder().encode(state).write(to: stateURL, options: .atomic)
}
}
HelperService().run()