Skip to content

Commit

Permalink
Fix .setShortcut() crashing when called from a `NSBackgroundActivit…
Browse files Browse the repository at this point in the history
…yScheduler` task (#9)
  • Loading branch information
sindresorhus committed Jun 5, 2020
1 parent edae643 commit eb68608
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 7 deletions.
9 changes: 8 additions & 1 deletion Sources/KeyboardShortcuts/NSMenuItem++.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ extension NSMenuItem {
keyEquivalentModifierMask = shortcut.modifiers
}

set()
// `TISCopyCurrentASCIICapableKeyboardLayoutInputSource` works on a background thread, but crashes when used in a `NSBackgroundActivityScheduler` task, so we ensure it's not run in that queue.
if DispatchQueue.isCurrentQueueNSBackgroundActivitySchedulerQueue {
DispatchQueue.main.async {
set()
}
} else {
set()
}

AssociatedKeys.observer[self] = NotificationCenter.default.addObserver(forName: .shortcutByNameDidChange, object: nil, queue: nil) { notification in
guard
Expand Down
20 changes: 14 additions & 6 deletions Sources/KeyboardShortcuts/Shortcut.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ private var keyToKeyEquivalentString: [KeyboardShortcuts.Key: String] = [

extension KeyboardShortcuts.Shortcut {
fileprivate func keyToCharacter() -> String? {
// `TISCopyCurrentASCIICapableKeyboardLayoutInputSource` works on a background thread, but crashes when used in a `NSBackgroundActivityScheduler` task, so we guard against that. It only crashes when running from Xcode, not in release builds, but it's probably safest to not call it from a `NSBackgroundActivityScheduler` no matter what.
assert(!DispatchQueue.isCurrentQueueNSBackgroundActivitySchedulerQueue, "This method cannot be used in a `NSBackgroundActivityScheduler` task")

// Some characters cannot be automatically translated.
if
let key = key,
Expand All @@ -178,14 +181,19 @@ extension KeyboardShortcuts.Shortcut {
return character
}

let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource().takeUnretainedValue()
let layoutData = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
let dataRef = unsafeBitCast(layoutData, to: CFData.self)
guard
let source = TISCopyCurrentASCIICapableKeyboardLayoutInputSource()?.takeRetainedValue(),
let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
else {
return nil
}

let dataRef = unsafeBitCast(layoutDataPointer, to: CFData.self)
let keyLayout = unsafeBitCast(CFDataGetBytePtr(dataRef), to: UnsafePointer<CoreServices.UCKeyboardLayout>.self)
var deadKeyState: UInt32 = 0
let maxCharacters = 256
let maxLength = 4
var length = 0
var characters = [UniChar](repeating: 0, count: maxCharacters)
var characters = [UniChar](repeating: 0, count: maxLength)

let error = CoreServices.UCKeyTranslate(
keyLayout,
Expand All @@ -195,7 +203,7 @@ extension KeyboardShortcuts.Shortcut {
UInt32(LMGetKbdType()),
OptionBits(CoreServices.kUCKeyTranslateNoDeadKeysBit),
&deadKeyState,
maxCharacters,
maxLength,
&length,
&characters
)
Expand Down
18 changes: 18 additions & 0 deletions Sources/KeyboardShortcuts/util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,21 @@ final class ObjectAssociation<T: Any> {
}
}
}


extension DispatchQueue {
/**
Label of the current dispatch queue.
- Important: Only meant for debugging purposes.
```
DispatchQueue.currentQueueLabel
//=> "com.apple.main-thread"
```
*/
static var currentQueueLabel: String { String(cString: __dispatch_queue_get_label(nil)) }

/// Whether the current queue is a `NSBackgroundActivityScheduler` task.
static var isCurrentQueueNSBackgroundActivitySchedulerQueue: Bool { currentQueueLabel.hasPrefix("com.apple.xpc.activity.") }
}

0 comments on commit eb68608

Please sign in to comment.