Skip to content

FanControl 1.5.1 —— 修复「开机自启后主窗口打不开」(黑苹果主板风扇调速 / Nuvoton NCT679x)

Latest

Choose a tag to compare

@glassecho glassecho released this 12 Oct 14:01
· 1 commit to main since this release

FanControl 1.5.1

macOS 下的主板风扇调速工具(面向 Hackintosh)。给主板 Super I/O 芯片 Nuvoton NCT679x
补上 SMC 写能力,让黑苹果也能按温度曲线调速。

English? Jump to the English section ↓

这一版是修 bug 的:修掉「开机自启后主窗口打不开」。


修了什么

症状:开机自启(登录项)拉起 app 之后,点菜单栏图标、点「打开主窗口」都没反应,
窗口怎么都出不来。

根因(用 lldb attach 到出问题的进程拿到的硬证据):

NSApp.windows = [ NSStatusBarWindow, NSStatusBarWindow, _NSPopoverWindow ]

主窗口根本不在里面。 开机(登录项)启动时 SwiftUI 有时不会创建主窗口。
而原来的 MainWindow.show() 只会对已有窗口做 makeKeyAndOrderFront ——
窗口不存在时,这句代码无能为力。

更麻烦的是「找不到就重建」这条退路当时也走不通:重建要用的 SwiftUI openWindow
注入在窗口内容的 onAppear 里,而窗口压根没建 → 永远注入不上(鸡生蛋)。

还有第二个坑:窗口认领用的是固定 3 秒轮询(attempt < 12 × 0.25s)。
SwiftUI 建窗时机不定时会漏;漏掉后 delegate 没设上,用户点红叉会把窗口真正关掉,
NSApp.windows 里从此再也找不到它 —— 这才是「关一次就再也打不开」的来源。

修复清单

# 改动
1 MainWindow.show() 找不到窗口时,改请 SwiftUI 的 openWindow(id:) 重建一个
2 openWindow 的注入点提前到 Commands —— app 启动构建菜单时就求值,窗口没建也能拿到(关键)
3 启动 3 秒后兜底自检:没有主窗口就主动建一个
4 窗口认领改为「didBecomeKey / didBecomeMain / didUpdate 通知 + 10 秒轮询」双保险,每次 show() 再补认领
5 主窗口识别不再绑死文案(优先用 SwiftUI 设的 identifier main-AppWindow-* 与窗口类名,标题只作兜底)
6 点程序坞图标不再判断 hasVisibleWindows,无脑尝试显示/重建
7 🆕 菜单栏「文件 → 打开主窗口」(⌘N),窗口万一丢了也能从这里找回来

第 2 条用一个最小 SwiftUI 复现确凿验证过 —— 关键是「窗口没建时 Commands 仍能提供 openWindow」:

[Cmds.Button.onAppear] 菜单项求值了
A) windows=0  fromView=false  fromCommands=true   ← 窗口压根没建,但 Commands 拿到了 openWindow
B) 用 fromCommands 请求打开窗口 ...
   [Probe.onAppear] 窗口内容求值了
C) windows=1  AppKitWindow visible=true            ← 重建成功

另外:修掉了安装包的一个隐患(附件已更新)

打包脚本 make-pkg.sh 里有个会让人「装完提示成功、但 app 不见了」的坑,一并修了。

pkgbuild 只要在 payload 里看到 .app,就会往安装包的 PackageInfo 里自动写上:

<relocate>
    <bundle id="local.hackintosh.FanControl"/>
</relocate>

它等于在告诉 macOS 安装器:「这个 app 可重定位 —— 装的时候先在磁盘上搜有没有同
bundle ID 的副本,找到就装到那个位置去,别装 /Applications」。

于是只要机器上任何地方躺着一份 FanControl.app,双击 pkg 装完会提示「安装成功」,
但 /Applications/FanControl.app 根本不存在 —— 东西被塞进那份副本的位置了。

修复方式:打包时展开 PackageInfo、删掉这段声明再压回去(pkgbuild 没有关掉它
的选项,--help 里没有任何 relocate 相关参数),并在打包结束后清理掉
build/pkg/payload 那份副本。详见提交 2fd8508。

说明:本 Release 的三个附件已于事后替换为修复后的版本,SHA-256 相应更新。
如果你在这之前下载过,建议重新下载。


下载哪个?

文件 大小 给谁用
FanControl-1.5.1.dmg 1.9 MB 普通用户装这个 —— 双击即可
FanControl-1.5.1.pkg 1.4 MB 同上,也可以用命令行装
FanControl-1.5.1-src.zip 3.2 MB 想自己编译的人

⚠️ 本软件没有 Apple 开发者签名(作者没有证书),Gatekeeper 会拦。
右键点安装包 → 打开 → 再点一次「打开」即可;
或到「系统设置 → 隐私与安全性」拉到底点「仍要打开」。

如果你正在用 1.5.0:直接装 1.5.1 覆盖升级即可,风扇配置与水泵通道设置都会保留。

另外建议顺手做一次:升级后到菜单栏面板里把「开机自启」关掉再打开 ——
如果你之前是从别的路径启动过 app,登录项里记的可能还是旧位置,重开一次就指向
/Applications/FanControl.app 了。


适用条件(缺一不可)

条件 要求
系统 Hackintosh(能改 OpenCore 的 config.plist)—— 真 Mac 不适用
主板 Super I/O Nuvoton NCT679x:NCT6790D…NCT6798D、NCT679BD,或 NCT6779D
架构 Intel —— Apple Silicon 不适用

macOS 13 / 14 / 15 / 26 均可。


装完必须做两件事

  1. 把 /usr/local/share/fancontrol/SMCSuperIO.kext 放进 EFI/OC/Kexts,替换同名文件,
    并在 config.plist → Kernel → Add 里注册 ——
    它必须排在 VirtualSMC.kext 之后(OpenCore 按顺序注入,依赖没就位的会被静默跳过)。
  2. 核对水泵通道:pump_channel 默认是 ch6(作者本机布局),换机器必须确认。

装完跑一次自检确认:

zsh /usr/local/sbin/fancontrol-check

校验(SHA-256)

63a9c6311681ca9408df41ed533af470be08f719a1b4b8a60addc89b33481e8d  FanControl-1.5.1.dmg
a3890a71e2db2cd50fdfd7b3c1d82157866b500e091c5cbd0f9ee43e09c88443  FanControl-1.5.1.pkg
896f3a5ca44d74a1adbfe293c7385bdfdbec8d5b7ce0bf5e2928621de51217c9  FanControl-1.5.1-src.zip

版本历史

1.5.0

  • 🆕 界面中英双语,跟随系统语言自动切换 —— 英文系统上第一次打开就是英文,
    不需要任何设置项。约 150 条文案全部双语,连 Finder / 程序坞里显示的 app 名字也跟着变。
  • 🔧 顺手修掉三处内部隐患(都是「界面一旦变英文就必然出问题」的):
    needsInstall 不再靠匹配提示文字(改由 fanctl 退出码判断)、
    gpufand 配置注释头改 ASCII、手动模式下写失败也会正确提示。

1.4.1

  • 🆕 菜单栏面板里可以直接开关「开机自启」—— 走系统的登录项机制(SMAppService),
    在「系统设置 → 通用 → 登录项」里也能看到和关掉。
  • 🔧 修了五处内部隐患(版本号单一来源、子进程管道死锁、传感器面板动态发现 CPU 核心、
    临时配置文件唯一文件名、发布版二进制不含本机开发路径)。

1.4.0

  • 🆕 菜单栏常驻显示:可任选「风扇图标 / CPU 温度 / GPU 温度 / CPU 占用 / 内存 /
    风扇转速 / 显卡风扇 / 网络」中的几项显示在菜单栏上,每项各自带图标。

1.3.0

  • 🆕 「监控」页:8 个可独立开关的面板 —— CPU(总占用 / 每核心 / 负载 / 走势图)、
    显卡、内存、存储、网络、进程、传感器、系统。开关会记住,全部只读、不需要 root。

1.2.0

  • 🆕 显卡监控(只读):风扇转速 / 占空比 / 温度 / 功耗 / 核心频率 / 占用率。

已知限制

  • 显卡风扇只能看、不能调 —— 由显卡固件的 PowerPlay 表 + 驱动管理,
    macOS 没有对外开放的写入接口。
  • 真 Mac、Apple Silicon 不适用。
  • 繁体中文的界面文案仍是简体(app 名是繁体)。完整 zh-Hant 翻译在计划中。
  • 界面是固定深色,跟随系统外观在计划中。
  • 没有 Apple 开发者签名,Gatekeeper 会拦。

反馈

作者手上只有一台机器(ASUS 主板 + NCT679BD),特别希望有不同主板、不同芯片的朋友帮忙一起测。
请到 Issues 反馈芯片型号、自检结果、
能不能调速、水泵通道是几号。


English

FanControl 1.5.1 for Hackintosh — motherboard fan control on macOS.

It gives hackintoshes the ability to control motherboard fans by temperature curve, by patching a gap in
VirtualSMC's SMCSuperIO. Six modes (silent / standard / performance / full / manual / custom curve),
plus read-only system and GPU monitoring and a menu-bar readout.

This release is a bug fix: the main window could not be opened after a launch-at-login start.

What was broken

Symptom: after the app is started by the login item at boot, clicking the menu-bar icon or
"Open main window" did nothing at all — the window would never come up.

Root cause (hard evidence from attaching lldb to the broken process):

NSApp.windows = [ NSStatusBarWindow, NSStatusBarWindow, _NSPopoverWindow ]

The main window was simply not there. When SwiftUI launches the app as a login item it
sometimes does not create the main window. MainWindow.show() only did
makeKeyAndOrderFront on an existing window — with no window to find, that call does nothing.

The fallback ("recreate it if missing") also could not work, because the SwiftUI openWindow
needed for recreation was injected in the window content's onAppear — and with no window
created, that never runs (chicken and egg).

A second trap: window adoption used a fixed 3-second poll (attempt < 12 × 0.25 s).
SwiftUI's window creation timing is not deterministic, so it sometimes missed; when it missed, the
delegate was never set, and clicking the red button actually destroyed the window — after which
NSApp.windows never contained it again. That is what made it "close it once and it's gone forever".

Fixes

# Change
1 MainWindow.show() now asks SwiftUI's openWindow(id:) to recreate the window when none is found
2 openWindow is now injected from Commands — evaluated when the app builds its menu, so it is available even with no window (the key fix)
3 A fallback self-check 3 s after launch: if there is no main window, create one
4 Window adoption is now "notifications (didBecomeKey / didBecomeMain / didUpdate) + a 10 s poll", and every show() re-adopts
5 Main-window detection no longer depends on the title text (it prefers SwiftUI's identifier main-AppWindow-* and the window class; the title is only a fallback)
6 Clicking the Dock icon no longer checks hasVisibleWindows — it always tries to show/recreate
7 🆕 Menu item File → Open Main Window (⌘N), so you can recover the window from the menu bar too

Fix #2 was verified with a minimal SwiftUI repro — the key question being "can Commands supply
openWindow when no window was ever created":

[Cmds.Button.onAppear] menu item evaluated
A) windows=0  fromView=false  fromCommands=true   ← no window at all, yet Commands had openWindow
B) requesting the window via fromCommands ...
   [Probe.onAppear] window content evaluated
C) windows=1  AppKitWindow visible=true            ← recreated successfully

Also fixed: a packaging trap (attachments have been updated)

make-pkg.sh had a defect that could make an install report success while leaving no app behind.

Whenever pkgbuild sees an .app in the payload, it writes this into the package's PackageInfo:

<relocate>
    <bundle id="local.hackintosh.FanControl"/>
</relocate>

That tells the macOS Installer: "this app is relocatable — look for an existing copy with the same
bundle ID and install there instead of /Applications."

So if any copy of FanControl.app exists anywhere on the machine, double-clicking the pkg reports
success but /Applications/FanControl.app is never created — everything lands where that other copy
lives.

The fix strips that declaration while packaging (there is no pkgbuild flag to disable it —
--help has nothing relocate-related) and removes the build/pkg/payload copy afterwards.
See commit 2fd8508.

Note: the three assets of this release were replaced with the fixed builds after the fact, and
the SHA-256 values below were updated accordingly. If you downloaded them earlier, please re-download.

Which file?

File Size For whom
FanControl-1.5.1.dmg 1.9 MB Normal use — just double-click
FanControl-1.5.1.pkg 1.4 MB Same thing, installable from the command line
FanControl-1.5.1-src.zip 3.2 MB If you want to build it yourself

⚠️ There is no Apple Developer signature (the author has no certificate), so Gatekeeper will
block it. Right-click the installer → Open → Open again, or use
System Settings → Privacy & Security → Open Anyway.

Already on 1.5.0? Just install 1.5.1 over it — your fan configuration and pump channel are kept.

One thing worth doing: after upgrading, toggle Launch at login off and on again in the
menu-bar panel. If the app was ever started from a different path, the login item may still point at
the old location; re-registering makes it point at /Applications/FanControl.app.

Requirements — all three, no exceptions

  • Hackintosh (you must be able to edit OpenCore's config.plist) — not for real Macs
  • Nuvoton NCT679xD or NCT6779D Super I/O chip
  • Intel — the kext is built for x86_64; Apple Silicon is not supported
  • macOS 13 / 14 / 15 / 26

Two things you must do after installing

  1. Copy /usr/local/share/fancontrol/SMCSuperIO.kext into EFI/OC/Kexts, replacing the existing file,
    and register it in config.plist → Kernel → Add. It must come after VirtualSMC.kext —
    OpenCore injects in order, and a kext whose dependencies aren't in place yet is silently skipped.
  2. Verify the pump channel. The default is ch6 (the author's own board layout) and must be
    re-checked on any other board.

Then run the self-check to confirm your chip is supported:

zsh /usr/local/sbin/fancontrol-check

Full walkthrough: PORTING.md
(English) · 移植说明.md (中文)

Checksums (SHA-256)

63a9c6311681ca9408df41ed533af470be08f719a1b4b8a60addc89b33481e8d  FanControl-1.5.1.dmg
a3890a71e2db2cd50fdfd7b3c1d82157866b500e091c5cbd0f9ee43e09c88443  FanControl-1.5.1.pkg
896f3a5ca44d74a1adbfe293c7385bdfdbec8d5b7ce0bf5e2928621de51217c9  FanControl-1.5.1-src.zip

Version history

  • 1.5.0 — Bilingual UI (English / Simplified Chinese) that follows your system language, with no
    setting to change — including the app name in Finder and the Dock. Plus three internal fixes that
    the English UI would otherwise have broken: needsInstall no longer keys off matching a Chinese
    prompt string (it uses the helper's exit code instead), the generated gpufand config header is now
    ASCII, and manual mode now sets the "helper missing" flag when a write fails.
  • 1.4.1 — Launch-at-login toggle right in the menu bar panel (uses the system login-items
    mechanism, so it also shows up under System Settings → General → Login Items). Plus five internal
    fixes: single-source version number, subprocess pipe deadlock, dynamic CPU-core discovery in the
    sensor panel, unique temp config filenames, and no more dev paths baked into the release binary.
  • 1.4.0 — Persistent menu bar readout: fan icon / CPU temp / GPU temp / CPU load / memory /
    fan RPM / GPU fan / network — each item with its own icon, toggleable from the popover.
  • 1.3.0 — A "Monitor" page with 8 independently toggleable panels (CPU / GPU / memory / storage /
    network / processes / sensors / system). Read-only, no root needed.
  • 1.2.0 — Read-only GPU monitoring: fan RPM, duty, temperature, power, core clock, utilization.

Known limitations

  • GPU fans are read-only — managed by the GPU firmware's PowerPlay table; macOS exposes no writable
    interface.
  • Real Macs and Apple Silicon are not supported.
  • The Traditional Chinese UI text is still Simplified (the app name is Traditional). A full
    zh-Hant translation is planned.
  • The UI is dark-only; following the system appearance is planned.
  • No Apple Developer signature — Gatekeeper will block it.

Why it needs the kext

Installing a kext into your EFI is a physical prerequisite for controlling motherboard fans on a
hackintosh. There is no way around it — the fan keys simply do not exist in SMC until SMCSuperIO
publishes them. If a tool claims to control motherboard fans on a hackintosh without touching the EFI,
it is either reading the GPU or it isn't working.

Credits

Kext derived from shohart/gpufan →
VirtualSMC (SMCSuperIO by vit9696, joedm). BSD-3-Clause.
See NOTICE and THIRD-PARTY/ for attribution. Not affiliated with, or endorsed by, the upstream authors.

Disclaimer

This software changes motherboard fan speeds and requires you to modify your EFI.
Any change to your EFI / OpenCore configuration risks making the machine unbootable — back up first.
Provided "as is", without warranty of any kind. Do not put a water-cooling pump under speed control.