Skip to content

Commit d479112

Browse files
ZhaoChaoqunclaude
andcommitted
fix: address review comments on KeyMonitor safety
- Guard let source from TISCopyCurrentKeyboardLayoutInputSource to avoid force unwrap crash - Guard let UnicodeScalar to avoid force unwrap crash - Protect isRecordingKey with OSAllocatedUnfairLock for thread safety between CGEvent tap callback and main thread - Add safety comments on restartWithNewTriggerKey and handleTriggerKeyChanged about async requirement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 635bcb0 commit d479112

2 files changed

Lines changed: 16 additions & 6 deletions

File tree

Sources/KeyMonitor.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ class KeyMonitor {
8888
/// 当前配置的触发键
8989
private var triggerConfig: TriggerKeyConfig = .current
9090

91-
/// 是否处于按键录制模式
92-
var isRecordingKey = false
91+
/// 是否处于按键录制模式(通过锁保护,因为该值在 CGEvent tap 回调线程读取、主线程写入)
92+
private let _isRecordingKey = OSAllocatedUnfairLock(initialState: false)
93+
var isRecordingKey: Bool {
94+
get { _isRecordingKey.withLock { $0 } }
95+
set { _isRecordingKey.withLock { $0 = newValue } }
96+
}
9397

9498
func startMonitoring() {
9599
// 单元测试环境下跳过监听(避免弹出辅助功能权限窗口)
@@ -156,7 +160,9 @@ class KeyMonitor {
156160
logger.info("触发键监听已启动(\(self.triggerConfig.displayName, privacy: .public)")
157161
}
158162

159-
/// 切换触发键后重新启动监听
163+
/// 切换触发键后重新启动监听。
164+
/// ⚠️ 必须通过 DispatchQueue.main.async 异步调用,禁止在 CGEvent tap 回调中同步调用,
165+
/// 因为 stopMonitoring() 会销毁当前正在执行回调的 event tap,导致未定义行为。
160166
func restartWithNewTriggerKey() {
161167
stopMonitoring()
162168
isTriggerPressed = false
@@ -290,8 +296,8 @@ class KeyMonitor {
290296

291297
/// 将 keyCode 转为字符(用于字母/数字键)
292298
private func keyCodeToCharacter(_ keyCode: Int64) -> Character? {
293-
let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue()
294-
guard let layoutData = TISGetInputSourceProperty(source!, kTISPropertyUnicodeKeyLayoutData) else { return nil }
299+
guard let source = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue() else { return nil }
300+
guard let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { return nil }
295301
let layout = unsafeBitCast(layoutData, to: CFData.self)
296302
let keyboardLayout = unsafeBitCast(CFDataGetBytePtr(layout), to: UnsafePointer<UCKeyboardLayout>.self)
297303

@@ -313,7 +319,8 @@ class KeyMonitor {
313319
)
314320

315321
guard status == noErr, length > 0 else { return nil }
316-
return Character(UnicodeScalar(chars[0])!)
322+
guard let scalar = UnicodeScalar(chars[0]) else { return nil }
323+
return Character(scalar)
317324
}
318325

319326
/// 统一处理触发键状态变化

Sources/TypelessApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7171
AnalyticsService.track("App.Launched")
7272
}
7373

74+
/// 触发键变更后重启监听。
75+
/// ⚠️ 必须在主线程异步调用(DispatchQueue.main.async),不可在 CGEvent tap 回调中同步调用,
76+
/// 否则 stopMonitoring() 会在回调内部销毁 event tap,导致未定义行为。
7477
@objc private func handleTriggerKeyChanged() {
7578
keyMonitor?.restartWithNewTriggerKey()
7679
updateTriggerKeyMenuText()

0 commit comments

Comments
 (0)