Der Einblendung zuvorkommen, statt sie abschalten zu wollen
Der Schluss im letzten Commit war falsch. Aus „der Dienst liegt in der
Benutzer-Domäne" hatte ich geschlossen, dass `launchctl bootout` ihn dort
aushängt. Nachgemessen, diesmal ausführend:
Boot-out failed: 150: Operation not permitted while
System Integrity Protection is engaged
SIP schützt den Dienst, nicht nur den Dateipfad; die Domäne spielt keine Rolle.
Ein rein lesender Spike konnte das nicht zeigen — nur der Versuch. `SystemOSD`
fliegt deshalb wieder raus: Code, der behauptet zu wirken und es nicht tut, ist
schlimmer als keiner.
Stattdessen ein `CGEventTap` auf die Lautstärketasten. Onyx regelt selbst und
verbraucht das Ereignis — was das System nie erreicht, blendet es auch nicht
ein. Preis ist die Berechtigung „Bedienungshilfen"; ohne sie bleibt alles wie
vorher, und der Dialog wird angefragt statt stumm zu scheitern.
Vorteil gegenüber dem Aushängen: es verschwindet nur die Einblendung für die
Tasten, die Onyx auch bedient. Die Feststelltaste behält ihre — beim Aushängen
wäre sie mit verschwunden, und dafür hat Onyx keinen Ersatz.
Der Rückruf steht als eigene Funktion und nicht als Verschluss in `start()`: als
Verschluss brachte er Swift 6.3.3 im Durchgang `SendNonSendable` zum Absturz.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a3399d3a96
commit
91b83928ca
@@ -3690,13 +3690,13 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Apples Einblendung unterdrücken"
|
"value": "Lautstärketasten übernehmen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Suppress Apple's own overlay"
|
"value": "Take over the volume keys"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3706,13 +3706,13 @@
|
|||||||
"de": {
|
"de": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Sonst zeigt der Mac bei jedem Lautstärkeschritt zwei Anzeigen. Betrifft alle Einblendungen — auch Helligkeit und Feststelltaste. Beim Beenden von Onyx wird sie wieder eingeschaltet."
|
"value": "Onyx fängt die Tasten ab und regelt selbst — dann erscheint Apples eigene Einblendung nicht mehr, weil der Tastendruck das System nie erreicht. Abschalten lässt sie sich nicht: das verhindert SIP. Braucht die Berechtigung „Bedienungshilfen“."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Otherwise the Mac shows two indicators for every volume step. Affects every overlay — brightness and caps lock too. It is restored when Onyx quits."
|
"value": "Onyx intercepts the keys and sets the volume itself — Apple's own overlay then stays away, because the keypress never reaches the system. Switching it off is not possible: SIP prevents that. Requires the Accessibility permission."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-9
@@ -61,6 +61,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
/// Die Helligkeit des eingebauten Bildschirms. Auf Rechnern ohne lesbare
|
/// Die Helligkeit des eingebauten Bildschirms. Auf Rechnern ohne lesbare
|
||||||
/// Helligkeit bleibt sie leer und meldet nie etwas.
|
/// Helligkeit bleibt sie leer und meldet nie etwas.
|
||||||
private let brightness = DisplayBrightness()
|
private let brightness = DisplayBrightness()
|
||||||
|
/// Fängt die Lautstärketasten ab, damit Apples Einblendung nicht erscheint.
|
||||||
|
private let mediaKeys = MediaKeyTap()
|
||||||
|
|
||||||
/// Der Sperrbildschirm. Steht bereit, sperrt aber nur auf Verlangen.
|
/// Der Sperrbildschirm. Steht bereit, sperrt aber nur auf Verlangen.
|
||||||
private let lockController = LockController()
|
private let lockController = LockController()
|
||||||
/// Der zuletzt gemeldete Stand — der erste meldet nichts, sonst begrüßte
|
/// Der zuletzt gemeldete Stand — der erste meldet nichts, sonst begrüßte
|
||||||
@@ -189,10 +192,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
// Ohne diese Zeile wäre der Schalter in den Einstellungen eine
|
// Ohne diese Zeile wäre der Schalter in den Einstellungen eine
|
||||||
// Behauptung: gespeichert, aber ohne Wirkung.
|
// Behauptung: gespeichert, aber ohne Wirkung.
|
||||||
if model.suppressSystemOSD { SystemOSD.suppress() }
|
applyMediaKeys(model.suppressSystemOSD)
|
||||||
model.onSuppressOSDChanged = { suppress in
|
model.onSuppressOSDChanged = { [weak self] on in self?.applyMediaKeys(on) }
|
||||||
suppress ? SystemOSD.suppress() : SystemOSD.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
lockController.usesFaceUnlock = model.faceUnlockEnabled
|
lockController.usesFaceUnlock = model.faceUnlockEnabled
|
||||||
model.onFaceUnlockChanged = { [weak self] uses in
|
model.onFaceUnlockChanged = { [weak self] uses in
|
||||||
@@ -314,11 +315,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
// Apples Einblendung wieder einhängen. Ohne das säße jemand bis zum
|
mediaKeys.stop()
|
||||||
// nächsten Abmelden ganz ohne Rückmeldung da — auch bei der Helligkeit,
|
|
||||||
// mit der Onyx nichts zu tun hat — und wüsste nicht, warum. Eine
|
|
||||||
// Einstellung darf nicht überleben, was sie eingestellt hat.
|
|
||||||
if model?.suppressSystemOSD == true { SystemOSD.restore() }
|
|
||||||
coordinator?.stop()
|
coordinator?.stop()
|
||||||
menuBar?.stop()
|
menuBar?.stop()
|
||||||
// Der Herzschlag endet damit. Der Helfer stellt die Lüfter binnen fünf
|
// Der Herzschlag endet damit. Der Helfer stellt die Lüfter binnen fünf
|
||||||
@@ -444,6 +441,44 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
@objc private func lockNow() { lockController.lock() }
|
@objc private func lockNow() { lockController.lock() }
|
||||||
|
|
||||||
|
/// Schaltet die Übernahme der Lautstärketasten an oder aus.
|
||||||
|
///
|
||||||
|
/// **Apples Einblendung lässt sich nicht abschalten.** `launchctl bootout`
|
||||||
|
/// scheitert an SIP, egal in welcher Domäne — nachgemessen in
|
||||||
|
/// `docs/spikes/J-osd.md`. Was geht, ist ihr zuvorzukommen: das System
|
||||||
|
/// blendet nichts ein, wenn der Tastendruck es nie erreicht.
|
||||||
|
private func applyMediaKeys(_ on: Bool) {
|
||||||
|
guard on else { mediaKeys.stop(); return }
|
||||||
|
guard MediaKeyTap.isTrusted else {
|
||||||
|
// Ohne Bedienungshilfen tut der Abgriff nichts. Den Dialog anfragen
|
||||||
|
// statt stumm zu scheitern.
|
||||||
|
MediaKeyTap.requestTrust()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mediaKeys.onKey = { [weak self] key in
|
||||||
|
MainActor.assumeIsolated { self?.handleMediaKey(key) ?? false }
|
||||||
|
}
|
||||||
|
mediaKeys.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wie macOS selbst: ein Sechzehntel je Anschlag.
|
||||||
|
private static let volumeStep = 1.0 / 16.0
|
||||||
|
|
||||||
|
private func handleMediaKey(_ key: MediaKeyTap.Key) -> Bool {
|
||||||
|
let current = systemAudio.outputVolume
|
||||||
|
switch key {
|
||||||
|
case .volumeUp:
|
||||||
|
systemAudio.setOutputVolume(min(1, current + Self.volumeStep))
|
||||||
|
case .volumeDown:
|
||||||
|
systemAudio.setOutputVolume(max(0, current - Self.volumeStep))
|
||||||
|
case .mute:
|
||||||
|
systemAudio.setOutputMuted(!systemAudio.outputMuted)
|
||||||
|
}
|
||||||
|
// Die Activity kommt von selbst: `observeVolume()` hört auf genau die
|
||||||
|
// Änderung, die wir hier gerade ausgelöst haben.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/// Meldet jede Änderung der Systemlautstärke als Live Activity an.
|
/// Meldet jede Änderung der Systemlautstärke als Live Activity an.
|
||||||
///
|
///
|
||||||
/// Über `withObservationTracking` und nicht über einen eigenen Listener:
|
/// Über `withObservationTracking` und nicht über einen eigenen Listener:
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import AppKit
|
||||||
|
import os
|
||||||
|
|
||||||
|
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "MediaKeys")
|
||||||
|
|
||||||
|
/// Fängt die Lautstärketasten ab, bevor das System sie sieht.
|
||||||
|
///
|
||||||
|
/// **Warum das der Weg ist.** Apples Einblendung lässt sich nicht abschalten:
|
||||||
|
/// `launchctl bootout` scheitert an SIP, egal in welcher Domäne. Was aber geht,
|
||||||
|
/// ist ihr zuvorzukommen — das System blendet nichts ein, wenn der Tastendruck
|
||||||
|
/// es nie erreicht. Siehe `docs/spikes/J-osd.md`.
|
||||||
|
///
|
||||||
|
/// **Der Preis ist die Berechtigung „Bedienungshilfen".** Ohne sie wird der Tap
|
||||||
|
/// gar nicht erst erzeugt, und dann bleibt alles wie vorher — Apples Kasten
|
||||||
|
/// erscheint, Onyx zeigt seine Activity daneben. Das ist ein hinnehmbarer
|
||||||
|
/// Zustand, aber kein stiller: wer den Schalter umlegt, muss erfahren, dass noch
|
||||||
|
/// etwas fehlt.
|
||||||
|
/// **Nicht akteursgebunden.** Der Rückruf eines `CGEventTap` läuft auf dem
|
||||||
|
/// Faden, dessen Ereignisschleife ihn angemeldet hat — hier der Hauptfaden. Den
|
||||||
|
/// Übersetzer davon zu überzeugen, kostete eine `assumeIsolated`-Umschließung
|
||||||
|
/// im Rückruf, und die brachte Swift 6.3.3 im Durchgang `SendNonSendable` zum
|
||||||
|
/// Absturz. Die Grenze liegt deshalb beim Aufrufer: `onKey` wird auf dem
|
||||||
|
/// Hauptfaden gerufen, und wer dort Akteurszustand anfasst, sagt das selbst.
|
||||||
|
public final class MediaKeyTap {
|
||||||
|
|
||||||
|
/// Welche Taste gedrückt wurde.
|
||||||
|
public enum Key: Sendable {
|
||||||
|
case volumeUp, volumeDown, mute
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wird beim Tastendruck gerufen. Gibt `true` zurück, wenn Onyx die Taste
|
||||||
|
/// übernommen hat — dann wird das Ereignis verbraucht.
|
||||||
|
///
|
||||||
|
/// Wird auf dem **Hauptfaden** gerufen.
|
||||||
|
nonisolated(unsafe) public var onKey: ((Key) -> Bool)?
|
||||||
|
|
||||||
|
nonisolated(unsafe) public private(set) var isRunning = false
|
||||||
|
|
||||||
|
/// Wird auch aus dem Rückruf heraus gelesen, der zwar auf dem Hauptfaden
|
||||||
|
/// läuft, dem Übersetzer gegenüber aber nicht als solcher gilt.
|
||||||
|
nonisolated(unsafe) private var tap: CFMachPort?
|
||||||
|
nonisolated(unsafe) private var source: CFRunLoopSource?
|
||||||
|
|
||||||
|
/// Die Kennungen aus `IOKit/hidsystem/ev_keymap.h`. Sie einzubinden zöge
|
||||||
|
/// den ganzen HID-Header nach; drei Zahlen sind billiger.
|
||||||
|
private enum KeyCode {
|
||||||
|
static let soundUp: Int32 = 0
|
||||||
|
static let soundDown: Int32 = 1
|
||||||
|
static let mute: Int32 = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
/// Ob Onyx die Berechtigung hat.
|
||||||
|
public static var isTrusted: Bool { AXIsProcessTrusted() }
|
||||||
|
|
||||||
|
/// Fragt sie an — öffnet den Systemdialog, falls noch nicht entschieden.
|
||||||
|
public static func requestTrust() {
|
||||||
|
// Der Schlüssel als Zeichenkette statt über die globale Konstante:
|
||||||
|
// letztere ist eine veränderliche Variable und damit für Swifts
|
||||||
|
// Nebenläufigkeitsprüfung nicht sicher — der Wert ist seit jeher
|
||||||
|
// derselbe.
|
||||||
|
let options = ["AXTrustedCheckOptionPrompt": true]
|
||||||
|
_ = AXIsProcessTrustedWithOptions(options as CFDictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func start() {
|
||||||
|
guard !isRunning, Self.isTrusted else { return }
|
||||||
|
|
||||||
|
// `NSSystemDefined` ist der Ereignistyp der Sondertasten.
|
||||||
|
let mask = CGEventMask(1 << 14)
|
||||||
|
guard let tap = CGEvent.tapCreate(tap: .cgSessionEventTap,
|
||||||
|
place: .headInsertEventTap,
|
||||||
|
options: .defaultTap,
|
||||||
|
eventsOfInterest: mask,
|
||||||
|
callback: onyxMediaKeyCallback,
|
||||||
|
userInfo: Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
else {
|
||||||
|
log.notice("Tastenabgriff nicht erzeugbar — Bedienungshilfen fehlen?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tap = tap
|
||||||
|
source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
||||||
|
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes)
|
||||||
|
CGEvent.tapEnable(tap: tap, enable: true)
|
||||||
|
isRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
guard let tap else { return }
|
||||||
|
CGEvent.tapEnable(tap: tap, enable: false)
|
||||||
|
if let source { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes) }
|
||||||
|
self.tap = nil
|
||||||
|
source = nil
|
||||||
|
isRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entscheidet über ein einzelnes Ereignis.
|
||||||
|
fileprivate func handle(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
||||||
|
// Ein Tap, der zu lange braucht, wird vom System abgeschaltet. Dann
|
||||||
|
// wieder einschalten, statt stumm auszufallen.
|
||||||
|
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
||||||
|
if let port = tap { CGEvent.tapEnable(tap: port, enable: true) }
|
||||||
|
return Unmanaged.passUnretained(event)
|
||||||
|
}
|
||||||
|
guard let key = Self.key(from: event), onKey?(key) == true else {
|
||||||
|
return Unmanaged.passUnretained(event)
|
||||||
|
}
|
||||||
|
// Verbraucht: das System sieht den Tastendruck nie und blendet deshalb
|
||||||
|
// auch nichts ein.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Welche Sondertaste in diesem Ereignis steckt — oder keine.
|
||||||
|
private static func key(from event: CGEvent) -> Key? {
|
||||||
|
guard let nsEvent = NSEvent(cgEvent: event), nsEvent.subtype.rawValue == 8 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let data = nsEvent.data1
|
||||||
|
let keyCode = Int32((data & 0xFFFF_0000) >> 16)
|
||||||
|
let keyState = (data & 0x0000_FF00) >> 8
|
||||||
|
// Nur das Drücken, nicht das Loslassen — sonst zählt jeder Anschlag
|
||||||
|
// doppelt.
|
||||||
|
guard keyState == 0x0A else { return nil }
|
||||||
|
|
||||||
|
return switch keyCode {
|
||||||
|
case KeyCode.soundUp: .volumeUp
|
||||||
|
case KeyCode.soundDown: .volumeDown
|
||||||
|
case KeyCode.mute: .mute
|
||||||
|
default: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Der Rückruf des Tastenabgriffs.
|
||||||
|
///
|
||||||
|
/// **Als eigene Funktion und nicht als Verschluss in `start()`.** Als Verschluss
|
||||||
|
/// brachte er Swift 6.3.3 im Durchgang `SendNonSendable` zum Absturz — kein
|
||||||
|
/// Fehler in der Logik, sondern einer im Übersetzer. Hier steht er außerhalb,
|
||||||
|
/// und der Absturz bleibt aus.
|
||||||
|
private func onyxMediaKeyCallback(proxy: CGEventTapProxy,
|
||||||
|
type: CGEventType,
|
||||||
|
event: CGEvent,
|
||||||
|
refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
|
||||||
|
guard let refcon else { return Unmanaged.passUnretained(event) }
|
||||||
|
let tap = Unmanaged<MediaKeyTap>.fromOpaque(refcon).takeUnretainedValue()
|
||||||
|
return tap.handle(type: type, event: event)
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import os
|
|
||||||
|
|
||||||
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "OSD")
|
|
||||||
|
|
||||||
/// Apples eigene Einblendung für Lautstärke, Helligkeit und Feststelltaste.
|
|
||||||
///
|
|
||||||
/// Solange sie erscheint, zeigt der Mac bei jedem Lautstärkeschritt **zwei**
|
|
||||||
/// Anzeigen: Apples Kasten in der Bildschirmmitte und die Live Activity unter
|
|
||||||
/// der Notch. Eine davon ist eine zu viel.
|
|
||||||
///
|
|
||||||
/// **Wie es geht — und warum es ohne Root geht.** `com.apple.OSDUIHelper` ist
|
|
||||||
/// ein LaunchAgent in der GUI-Domäne des angemeldeten Benutzers, nicht im
|
|
||||||
/// System. `launchctl bootout gui/<uid>/…` hängt ihn dort aus. Die Datei selbst
|
|
||||||
/// liegt unter SIP-Schutz und wird **nicht** angefasst; geändert wird nur, was
|
|
||||||
/// in der eigenen Sitzung geladen ist. Siehe `docs/spikes/J-osd.md`.
|
|
||||||
///
|
|
||||||
/// **Und was das kostet.** Es verschwindet nicht nur die Lautstärkeanzeige,
|
|
||||||
/// sondern jede: auch Helligkeit und Feststelltaste. Deshalb steht der Schalter
|
|
||||||
/// standardmäßig aus, und deshalb wird beim Beenden wieder eingehängt — sonst
|
|
||||||
/// säße jemand bis zum nächsten Abmelden ganz ohne Rückmeldung da, ohne zu
|
|
||||||
/// wissen, warum.
|
|
||||||
public enum SystemOSD {
|
|
||||||
|
|
||||||
private static let service = "com.apple.OSDUIHelper"
|
|
||||||
|
|
||||||
private static var domain: String { "gui/\(getuid())/\(service)" }
|
|
||||||
|
|
||||||
/// Ob der Dienst gerade ausgehängt ist.
|
|
||||||
public static var isSuppressed: Bool {
|
|
||||||
// `launchctl print` gibt für einen ausgehängten Dienst einen Fehler
|
|
||||||
// zurück. Kein Fehler heißt: er ist da.
|
|
||||||
run(["print", domain]) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hängt die Einblendung aus. Wirkt bis zum nächsten Abmelden.
|
|
||||||
@discardableResult
|
|
||||||
public static func suppress() -> Bool {
|
|
||||||
guard !isSuppressed else { return true }
|
|
||||||
let status = run(["bootout", domain])
|
|
||||||
if status != 0 {
|
|
||||||
log.notice("OSD-Dienst nicht aushängbar: \(status, privacy: .public)")
|
|
||||||
}
|
|
||||||
return status == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hängt sie wieder ein.
|
|
||||||
///
|
|
||||||
/// Beim Beenden zu rufen. Ohne das bliebe der Mac bis zum nächsten Abmelden
|
|
||||||
/// ohne jede Einblendung — auch für Helligkeit, mit der Onyx nichts zu tun
|
|
||||||
/// hat.
|
|
||||||
@discardableResult
|
|
||||||
public static func restore() -> Bool {
|
|
||||||
guard isSuppressed else { return true }
|
|
||||||
let status = run(["bootstrap", "gui/\(getuid())",
|
|
||||||
"/System/Library/LaunchAgents/\(service).plist"])
|
|
||||||
if status != 0 {
|
|
||||||
log.notice("OSD-Dienst nicht einhängbar: \(status, privacy: .public)")
|
|
||||||
}
|
|
||||||
return status == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func run(_ arguments: [String]) -> Int32 {
|
|
||||||
let task = Process()
|
|
||||||
task.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
|
||||||
task.arguments = arguments
|
|
||||||
task.standardOutput = FileHandle.nullDevice
|
|
||||||
task.standardError = FileHandle.nullDevice
|
|
||||||
do {
|
|
||||||
try task.run()
|
|
||||||
task.waitUntilExit()
|
|
||||||
return task.terminationStatus
|
|
||||||
} catch {
|
|
||||||
log.error("launchctl nicht startbar: \(error.localizedDescription, privacy: .public)")
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+29
-21
@@ -17,31 +17,39 @@ selbst zurück? Was passiert beim Ab- und Anmelden? Steht SIP im Weg?
|
|||||||
| Domäne | **`gui/<uid>`** — die des angemeldeten Benutzers, nicht `system` |
|
| Domäne | **`gui/<uid>`** — die des angemeldeten Benutzers, nicht `system` |
|
||||||
| SIP | aktiviert, `OSDUIHelper.app/Contents` ist `restricted` |
|
| SIP | aktiviert, `OSDUIHelper.app/Contents` ist `restricted` |
|
||||||
|
|
||||||
**Der entscheidende Punkt:** der Dienst ist in der **Benutzer**-Domäne geladen.
|
## Der Schluss daraus war falsch
|
||||||
`launchctl bootout gui/<uid>/com.apple.OSDUIHelper` hängt ihn dort aus — ohne
|
|
||||||
Root und ohne die SIP-geschützte Datei anzufassen. Geändert wird nur, was in der
|
|
||||||
eigenen Sitzung geladen ist.
|
|
||||||
|
|
||||||
## Was das kostet
|
Aus „der Dienst liegt in der Benutzer-Domäne" habe ich geschlossen, dass
|
||||||
|
`launchctl bootout gui/<uid>/…` ihn dort aushängt, ohne Root und ohne SIP zu
|
||||||
|
berühren. **Das stimmt nicht.** Nachgemessen, diesmal ausführend:
|
||||||
|
|
||||||
Es verschwindet **jede** Einblendung, nicht nur die für die Lautstärke: auch
|
```
|
||||||
Helligkeit und Feststelltaste. Onyx zeigt nur die Lautstärke und die Helligkeit
|
$ launchctl bootout gui/501/com.apple.OSDUIHelper
|
||||||
— die Feststelltaste hätte dann gar keine Rückmeldung mehr.
|
Boot-out failed: 150: Operation not permitted while System Integrity Protection is engaged
|
||||||
|
```
|
||||||
|
|
||||||
Deshalb:
|
SIP schützt den **Dienst**, nicht nur den Dateipfad. Die Domäne spielt keine
|
||||||
|
Rolle. Ein rein lesender Spike konnte das nicht zeigen — nur der Versuch.
|
||||||
|
|
||||||
- Der Schalter steht **standardmäßig aus**. Das ist eine Entscheidung, die man
|
**Die Lehre:** Ein Spike, der die entscheidende Handlung nicht ausführt, hat die
|
||||||
treffen soll, nicht eine, die man vorfindet.
|
Frage nicht beantwortet. Er hat nur die Umstände beschrieben, unter denen sie
|
||||||
- Beim Beenden von Onyx wird wieder eingehängt. Ohne das säße jemand bis zum
|
gestellt wird. Wo die Handlung sich zurücknehmen lässt, gehört sie in den
|
||||||
nächsten Abmelden ganz ohne Rückmeldung da und wüsste nicht, warum. Eine
|
Spike — sonst steht am Ende eine Vermutung im Dokument, die wie ein Befund
|
||||||
Einstellung darf nicht überleben, was sie eingestellt hat.
|
aussieht.
|
||||||
|
|
||||||
## Was der Spike **nicht** getan hat
|
## Was stattdessen geht
|
||||||
|
|
||||||
Nichts ausgehängt. An einem Systemdienst zu drehen, um zu sehen was passiert,
|
Nicht die Einblendung abschalten, sondern **ihr zuvorkommen**: die
|
||||||
ist nichts, was ungefragt geschieht — auch dann nicht, wenn es sich zurücknehmen
|
Lautstärketasten mit einem `CGEventTap` abfangen, die Lautstärke selbst setzen
|
||||||
lässt. Geprüft wurde nur, wo der Dienst liegt und wem er gehört.
|
und das Ereignis verbrauchen. Was das System nie erreicht, blendet es auch nicht
|
||||||
|
ein.
|
||||||
|
|
||||||
**Damit ist auch nicht gemessen**, ob er von selbst zurückkommt, wenn etwas ihn
|
Preis: das braucht die Berechtigung „Bedienungshilfen". Ohne sie tut der Tap
|
||||||
anfordert. Der Verdacht besteht: er ist bedarfsgesteuert. Falls ja, muss das
|
nichts — dann erscheint Apples Einblendung wie gehabt, und das muss die
|
||||||
Aushängen wiederholt werden — das zeigt sich beim ersten Einschalten.
|
Oberfläche sagen, statt es stumm hinzunehmen.
|
||||||
|
|
||||||
|
## Der Vorteil des Abfangens
|
||||||
|
|
||||||
|
Es verschwindet nur die Einblendung für die Tasten, die Onyx selbst bedient.
|
||||||
|
Feststelltaste und alles andere behalten ihre — beim Aushängen des Dienstes
|
||||||
|
wären sie mit verschwunden, und für die Feststelltaste hat Onyx keinen Ersatz.
|
||||||
|
|||||||
Reference in New Issue
Block a user