Skip to content

Commit 8b1049d

Browse files
author
Cosmostima
committed
v1.1.1 Improve EventTap reliability, simplify Spotlight queries.
1 parent e759980 commit 8b1049d

14 files changed

Lines changed: 279 additions & 161 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ concurrency:
1313
jobs:
1414
test:
1515
name: Unit Tests (macOS)
16-
runs-on: macos-15
16+
runs-on: macos-26
1717

1818
steps:
1919
- name: Checkout

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ xcuserdata/
1515
*.xccheckout
1616
*.moved-aside
1717
DerivedData/
18+
.DerivedData/
1819
*.hmap
1920
*.ipa
2021
*.xcuserstate

FileRing.xcodeproj/project.pbxproj

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
attributes = {
146146
BuildIndependentTargetsInParallel = 1;
147147
LastSwiftUpdateCheck = 2600;
148-
LastUpgradeCheck = 2600;
148+
LastUpgradeCheck = 2620;
149149
TargetAttributes = {
150150
49036EF82EB2FC580021BB0A = {
151151
CreatedOnToolsVersion = 26.0;
@@ -221,7 +221,7 @@
221221
buildSettings = {
222222
BUNDLE_LOADER = "$(TEST_HOST)";
223223
CODE_SIGN_STYLE = Automatic;
224-
DEVELOPMENT_TEAM = "";
224+
DEAD_CODE_STRIPPING = YES;
225225
ENABLE_TESTABILITY = YES;
226226
GENERATE_INFOPLIST_FILE = YES;
227227
MACOSX_DEPLOYMENT_TARGET = 13.0;
@@ -268,6 +268,7 @@
268268
CLANG_WARN_UNREACHABLE_CODE = YES;
269269
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
270270
COPY_PHASE_STRIP = NO;
271+
DEAD_CODE_STRIPPING = YES;
271272
DEBUG_INFORMATION_FORMAT = dwarf;
272273
DEVELOPMENT_TEAM = "";
273274
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -293,6 +294,7 @@
293294
MTL_FAST_MATH = YES;
294295
ONLY_ACTIVE_ARCH = YES;
295296
SDKROOT = macosx;
297+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
296298
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
297299
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
298300
};
@@ -332,6 +334,7 @@
332334
CLANG_WARN_UNREACHABLE_CODE = YES;
333335
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
334336
COPY_PHASE_STRIP = NO;
337+
DEAD_CODE_STRIPPING = YES;
335338
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
336339
DEVELOPMENT_TEAM = "";
337340
ENABLE_NS_ASSERTIONS = NO;
@@ -350,6 +353,7 @@
350353
MTL_ENABLE_DEBUG_INFO = NO;
351354
MTL_FAST_MATH = YES;
352355
SDKROOT = macosx;
356+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
353357
SWIFT_COMPILATION_MODE = wholemodule;
354358
};
355359
name = Release;
@@ -365,6 +369,7 @@
365369
CODE_SIGN_STYLE = Automatic;
366370
COMBINE_HIDPI_IMAGES = YES;
367371
CURRENT_PROJECT_VERSION = 1;
372+
DEAD_CODE_STRIPPING = YES;
368373
DEVELOPMENT_TEAM = "";
369374
ENABLE_APP_SANDBOX = YES;
370375
ENABLE_HARDENED_RUNTIME = YES;
@@ -390,7 +395,7 @@
390395
"@executable_path/../Frameworks",
391396
);
392397
MACOSX_DEPLOYMENT_TARGET = 13.0;
393-
MARKETING_VERSION = 1.1.0;
398+
MARKETING_VERSION = 1.1.1;
394399
PRODUCT_BUNDLE_IDENTIFIER = com.cosmos.FileRing;
395400
PRODUCT_NAME = "$(TARGET_NAME)";
396401
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -421,6 +426,7 @@
421426
CODE_SIGN_STYLE = Automatic;
422427
COMBINE_HIDPI_IMAGES = YES;
423428
CURRENT_PROJECT_VERSION = 1;
429+
DEAD_CODE_STRIPPING = YES;
424430
DEVELOPMENT_TEAM = "";
425431
ENABLE_APP_SANDBOX = YES;
426432
ENABLE_HARDENED_RUNTIME = YES;
@@ -446,7 +452,7 @@
446452
"@executable_path/../Frameworks",
447453
);
448454
MACOSX_DEPLOYMENT_TARGET = 13.0;
449-
MARKETING_VERSION = 1.1.0;
455+
MARKETING_VERSION = 1.1.1;
450456
PRODUCT_BUNDLE_IDENTIFIER = com.cosmos.FileRing;
451457
PRODUCT_NAME = "$(TARGET_NAME)";
452458
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -471,7 +477,7 @@
471477
buildSettings = {
472478
BUNDLE_LOADER = "$(TEST_HOST)";
473479
CODE_SIGN_STYLE = Automatic;
474-
DEVELOPMENT_TEAM = "";
480+
DEAD_CODE_STRIPPING = YES;
475481
ENABLE_TESTABILITY = YES;
476482
GENERATE_INFOPLIST_FILE = YES;
477483
MACOSX_DEPLOYMENT_TARGET = 13.0;

FileRing.xcodeproj/xcshareddata/xcschemes/FileRing.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "2600"
3+
LastUpgradeVersion = "2620"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"

FileRing/Managers/EventTapManager.swift

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class EventTapManager {
6969

7070
@MainActor weak var delegate: HotkeyManagerDelegate?
7171
@MainActor private var hotkeyState: HotkeyState = .idle
72+
@MainActor private var healthCheckTimer: Timer?
7273

7374
// MARK: - Initialization
7475

@@ -118,35 +119,120 @@ class EventTapManager {
118119

119120
func cleanup() {
120121
stopAndWait()
122+
Task { @MainActor in
123+
self.stopHealthCheck()
124+
}
121125
NSWorkspace.shared.notificationCenter.removeObserver(self)
122126
NotificationCenter.default.removeObserver(self)
123127
}
124128

125-
// MARK: - Sleep/Wake Notifications
129+
// MARK: - Health Check
130+
131+
@MainActor private func startHealthCheck() {
132+
healthCheckTimer?.invalidate()
133+
let block: @Sendable (Timer) -> Void = { [weak self] _ in
134+
Task { @MainActor [weak self] in
135+
self?.performHealthCheck()
136+
}
137+
}
138+
healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true, block: block)
139+
}
140+
141+
@MainActor private func stopHealthCheck() {
142+
healthCheckTimer?.invalidate()
143+
healthCheckTimer = nil
144+
}
145+
146+
@MainActor private func performHealthCheck() {
147+
guard sharedState.withLock({ $0.isRunning }) else { return }
148+
149+
guard let tap = self.eventTap else {
150+
// Tap is nil but isRunning is true — something went wrong, rebuild
151+
os_log(.info, "Health check: event tap is nil, rebuilding")
152+
rebuildEventTap()
153+
return
154+
}
155+
156+
if !CGEvent.tapIsEnabled(tap: tap) {
157+
// Try re-enabling first
158+
CGEvent.tapEnable(tap: tap, enable: true)
159+
160+
// Check again after re-enable attempt
161+
if !CGEvent.tapIsEnabled(tap: tap) {
162+
// Re-enable failed — full rebuild needed
163+
os_log(.info, "Health check: event tap disabled and re-enable failed, rebuilding")
164+
rebuildEventTap()
165+
} else {
166+
os_log(.info, "Health check: event tap was disabled, re-enabled successfully")
167+
}
168+
}
169+
}
170+
171+
@MainActor private func rebuildEventTap() {
172+
stopAndWait()
173+
start()
174+
}
175+
176+
// MARK: - Sleep/Wake & Screen/Session Notifications
126177

127178
private func setupSleepWakeNotifications() {
128-
NSWorkspace.shared.notificationCenter.addObserver(
179+
let wsnc = NSWorkspace.shared.notificationCenter
180+
181+
// System sleep/wake
182+
wsnc.addObserver(
129183
self,
130184
selector: #selector(handleWillSleep),
131185
name: NSWorkspace.willSleepNotification,
132186
object: nil
133187
)
134-
135-
NSWorkspace.shared.notificationCenter.addObserver(
188+
wsnc.addObserver(
136189
self,
137190
selector: #selector(handleDidWake),
138191
name: NSWorkspace.didWakeNotification,
139192
object: nil
140193
)
194+
195+
// Screen lock/unlock (different from system sleep!)
196+
wsnc.addObserver(
197+
self,
198+
selector: #selector(handleWillSleep),
199+
name: NSWorkspace.screensDidSleepNotification,
200+
object: nil
201+
)
202+
wsnc.addObserver(
203+
self,
204+
selector: #selector(handleDidWake),
205+
name: NSWorkspace.screensDidWakeNotification,
206+
object: nil
207+
)
208+
209+
// Fast user switching
210+
wsnc.addObserver(
211+
self,
212+
selector: #selector(handleDidWake),
213+
name: NSWorkspace.sessionDidBecomeActiveNotification,
214+
object: nil
215+
)
216+
wsnc.addObserver(
217+
self,
218+
selector: #selector(handleWillSleep),
219+
name: NSWorkspace.sessionDidResignActiveNotification,
220+
object: nil
221+
)
141222
}
142223

143224
@objc private func handleWillSleep() {
144-
// System is about to sleep: stop event tap
145-
stop()
225+
// System is about to sleep / screen locked / session resigned:
226+
// stop event tap synchronously.
227+
// Must use stopAndWait() (not stop()) to ensure the old RunLoop and tap
228+
// are fully cleaned up before the system sleeps. Otherwise, on wake the
229+
// old RunLoop's deferred cleanup can race with start() and clobber the
230+
// newly created tap (setting isRunning back to false / invalidating the new tap).
231+
stopAndWait()
146232
}
147233

148234
@objc private func handleDidWake() {
149-
// System woke up: restart event tap
235+
// System woke up / screen unlocked / session became active: restart event tap
150236
// Delay a bit to ensure system is stable
151237
Task { @MainActor in
152238
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
@@ -231,6 +317,11 @@ class EventTapManager {
231317
return hasPermission
232318
}
233319

320+
/// Whether the CGEvent tap is currently running and capturing events.
321+
var isEventTapActive: Bool {
322+
sharedState.withLock { $0.isRunning }
323+
}
324+
234325
func requestPermission() async -> Bool {
235326
Logger.main.info("Requesting Accessibility permission via system prompt...")
236327
let granted = AccessibilityHelper.requestPermission()
@@ -269,6 +360,7 @@ class EventTapManager {
269360
sharedState.withLock { $0.didConsumeKeyDown = false }
270361
Task { @MainActor in
271362
self.hotkeyState = .idle
363+
self.stopHealthCheck()
272364
}
273365

274366
if let runLoop = runLoop {
@@ -288,6 +380,7 @@ class EventTapManager {
288380
sharedState.withLock { $0.didConsumeKeyDown = false }
289381
Task { @MainActor in
290382
self.hotkeyState = .idle
383+
self.stopHealthCheck()
291384
}
292385

293386
let semaphore = DispatchSemaphore(value: 0)
@@ -386,6 +479,15 @@ class EventTapManager {
386479

387480
CGEvent.tapEnable(tap: tap, enable: true)
388481

482+
// EventTap is confirmed active — mark permission for this session and notify UI.
483+
// This is more reliable than AXIsProcessTrustedWithOptions, which can return
484+
// false for sandboxed apps even after the user has granted accessibility permission.
485+
DispatchQueue.main.async {
486+
AccessibilityHelper.markPermissionConfirmed()
487+
NotificationCenter.default.post(name: .eventTapDidStart, object: nil)
488+
self.startHealthCheck()
489+
}
490+
389491
CFRunLoopRun()
390492

391493
// RunLoop exited (either via stop() or unexpectedly).
@@ -402,6 +504,13 @@ class EventTapManager {
402504
if type == .tapDisabledByTimeout {
403505
if let eventTap = self.eventTap {
404506
CGEvent.tapEnable(tap: eventTap, enable: true)
507+
// If re-enable failed, schedule a full rebuild via health check
508+
if !CGEvent.tapIsEnabled(tap: eventTap) {
509+
os_log(.info, "tapDisabledByTimeout: re-enable failed, scheduling rebuild")
510+
DispatchQueue.main.async { [weak self] in
511+
self?.rebuildEventTap()
512+
}
513+
}
405514
}
406515
return Unmanaged.passUnretained(event)
407516
}

0 commit comments

Comments
 (0)