vChewing-macOS/Packages/vChewing_OSFrameworkImpl/Sources/OSFrameworkImpl/SecureEventInputSputnik.swift

184 lines
8.5 KiB
Swift

// (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? {
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
/// 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)
)
guard statusSucceeded == KERN_SUCCESS else { return nil }
let dict = resultDictionaryCF?.takeRetainedValue()
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 sesssionSwitchedOut = 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(.sesssionSwitchedOut) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.sesssionSwitchedOut) }
.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(.sesssionSwitchedOut)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.sesssionSwitchedOut)
}
)
}
}
}