这些个密码簿 App 真的越来越猖狂了,滥用 SecureEventInput ,完全不把中文用户的打字需求当回事。
理想情况:当且仅当密码输入框处于正在接收文字输入的这种激活状态时,才启用 EnableSecureEventInput()。只要密码输入框是非当前 App 、或者只要密码输入框失去输入状态,那就应该立刻执行 DisableSecureEventInput(),免得害得整个系统都用不了第三方输入法。
实际情况:欧美厂商设计的这类 App 很难会去主动考虑中文副厂输入法使用者的死活。
我写了一段工具,可以供副厂输入法开发者们集成在输入法内、每隔 15 秒跑一次以自动侦测是否有这个情况。有的话,可以推系统通知告诉使用者「此时输入法因为这个原因在输入法选单当中是灰色的、无法使用」。如果你输入法没有 Sandbox 的话,你还可以直接强制结束对应的密码簿应用。
// (c) 2023 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
/// A Swift script to check whether a non-system process is abusing the SecureEventInput.
import AppKit
import Combine
import IOKit
public class SecureEventInputSputnik {
public let shared = SecureEventInputSputnik()
public init() {
oobe()
}
public static func getIORegListResults() -> String? {
// Don't generate results under any of the following situations:
// - Hibernation / LoggedOut / SwitchedOut / ScreenSaver situations.
guard NSWorkspace.activationFlags.isEmpty else { return nil }
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
defer { resultDictionaryCF = nil }
/// Regarding the parameter in IORegistryGetRootEntry:
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
let statusSucceeded = IORegistryEntryCreateCFProperties(
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
)
let dict: CFMutableDictionary? = resultDictionaryCF?.takeRetainedValue()
guard statusSucceeded == KERN_SUCCESS else { return nil }
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
return (dict.description)
}
/// Find all non-system processes using the SecureEventInput.
/// - Parameter abusersOnly: List only non-frontmost processes.
/// - **Reason to Use**: Non-frontmost processes of such are considered abusers of SecureEventInput,
/// hindering 3rd-party input methods from being switched to by the user.
/// They are also hindering users from accessing the menu of all 3rd-party input methods.
/// There are Apple's internal business reasons why macOS always has lack of certain crucial input methods,
/// plus that some some IMEs in macOS have certain bugs / defects for decades and are unlikely to be solved,
/// making the sense that why there are needs of 3rd-party input methods.
/// - **How to Use**: For example, one can use an NSTimer to run this function
/// with `abusersOnly: true` every 15~60 seconds. Once the result dictionary is not empty,
/// you may either warn the users to restart the matched process or directly terminate it.
/// Note that you cannot terminate a process if your app is Sandboxed.
/// - Returns: Matched results as a dictionary in `[Int32: NSRunningApplication]` format. The keys are PIDs.
/// - Remark: The`"com.apple.SecurityAgent"` won't be included in the result since it is a system process.
/// Also, "com.apple.loginwindow" should be excluded as long as the system screen saver engine is running.
public static func getRunningSecureInputApps(abusersOnly: Bool = false) -> [Int32: NSRunningApplication] {
var result = [Int32: NSRunningApplication]()
guard let rawData = getIORegListResults() else { return result }
rawData.enumerateLines { currentLine, _ in
guard currentLine.contains("kCGSSessionSecureInputPID") else { return }
guard let filteredNumStr = Int32(currentLine.filter("0123456789".contains)) else { return }
guard let matchedApp = NSRunningApplication(processIdentifier: filteredNumStr) else { return }
guard matchedApp.bundleIdentifier != "com.apple.SecurityAgent" else { return }
guard !(matchedApp.isLoginWindowWithLockedScreenOrScreenSaver) else { return }
if abusersOnly {
guard !matchedApp.isActive else { return }
}
result[filteredNumStr] = matchedApp
}
return result
}
}
public extension NSWorkspace {
struct ActivationFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
}
static var activationFlags: ActivationFlags = []
}
public extension NSRunningApplication {
var isLoginWindowWithLockedScreenOrScreenSaver: Bool {
guard bundleIdentifier == "com.apple.loginwindow" else { return false }
return !NSWorkspace.activationFlags.isEmpty
}
}
extension SecureEventInputSputnik {
private static var combinePoolCocoa = [any NSObjectProtocol]()
@available(macOS 10.15, *)
private static var combinePool = Set<AnyCancellable>()
func oobe() {
if #available(macOS 10.15, *) {
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screenIsLocked"))
.sink { _ in NSWorkspace.activationFlags.insert(.desktopLocked) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screenIsUnlocked"))
.sink { _ in NSWorkspace.activationFlags.remove(.desktopLocked) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screensaver.didstart"))
.sink { _ in NSWorkspace.activationFlags.insert(.screenSaverRunning) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screensaver.didstop"))
.sink { _ in NSWorkspace.activationFlags.remove(.screenSaverRunning) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.willSleepNotification)
.sink { _ in NSWorkspace.activationFlags.insert(.hibernating) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.didWakeNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.hibernating) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
} else {
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screenIsLocked"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.desktopLocked)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screenIsUnlocked"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.desktopLocked)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screensaver.didstart"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.screenSaverRunning)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screensaver.didstop"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.screenSaverRunning)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.hibernating)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.hibernating)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
}
)
}
}
}