Absturz beim XPC-Rückruf, Menü zurück, Fenster weg von der Notch

Der Absturzbericht war eindeutig:

  Thread 8: com.apple.NSXPCConnection.m-user…onyx.helper
    swift_task_isCurrentExecutorWithFlags
    closure #3 in FanControl.proxy()
    EXC_BREAKPOINT

Die Rückrufe der XPC-Verbindung erben die MainActor-Isolation der Methode,
in der sie stehen. XPC ruft sie aber auf seiner eigenen Warteschlange auf,
Swift 6 prüft das zur Laufzeit und beendet den Prozess. Das
`Task { @MainActor in … }` im Rumpf half nicht: die Prüfung geschieht beim
Betreten des Abschlusses, nicht beim Zugriff.

Alle sieben Rückrufe sind jetzt `@Sendable`. Und weil das eine Fehlerklasse
ist und kein Einzelfall, dieselbe Behandlung für die übrigen Stellen, an
denen ein MainActor-Typ einen Abschluss an eine Systemschnittstelle gibt:
Papierkorb, Vorschaubilder, Adapter-Ende, Darwin-Nachricht.

Dazu drei Dinge aus dem Bericht von eben:

Der Linksklick aufs Menüleistensymbol fuhr das Panel aus — und ging dabei
als Fixieren durch. Danach stand das Panel offen und reagierte auf nichts
mehr. Das war schlechter als das Problem, das es lösen sollte. Ein
Statuselement zeigt bei einem Klick sein Menü; alles andere überrascht.
„Panel öffnen" bleibt draußen.

Ein Klick daneben schließt jetzt auch ein fixiertes Panel. „Klick fixiert"
ist eine gute Regel, aber wer sie nicht kennt, sitzt sonst vor etwas, das
offen steht und nicht reagiert — und sucht den Fehler in der App.

Einstellungs- und Einrichtungsfenster gehen nicht mehr direkt unter der
Notch auf. `center()` setzt oberhalb der Mitte; der Schließknopf landete
damit so weit oben, dass man auf dem Weg dorthin die Notch auslöste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-11 12:54:32 +02:00
parent 70a665f532
commit 667fb61c96
12 changed files with 165 additions and 55 deletions

View File

@@ -117,7 +117,11 @@ struct NotchStateMachineTests {
let effects = machine.handle(.escape)
#expect(machine.phase == .idle)
#expect(effects == [.hide])
// Worauf es ankommt, ist das Schließen. Dass dabei ein womöglich
// laufender Schließtimer abbestellt wird, gehört dazu aus `closing`
// heraus läuft er, und ein verspäteter Zeitgeber hat nichts mehr zu
// melden.
#expect(effects.contains(.hide))
}
@Test("esc schließt auch ein nur geöffnetes Panel")
@@ -127,7 +131,7 @@ struct NotchStateMachineTests {
let effects = machine.handle(.escape)
#expect(machine.phase == .idle)
#expect(effects == [.hide])
#expect(effects.contains(.hide))
}
@Test("esc im Ruhezustand tut nichts")
@@ -336,3 +340,48 @@ struct NotchArmingToleranceTests {
#expect(effects.contains(.cancelCloseTimer))
}
}
// Ein fixiertes Panel ließ sich nur durch einen erneuten Klick in die Notch
// oder mit Escape lösen. Wer nicht weiß, dass ein Klick fixiert, sitzt vor
// einem Panel, das offen steht und auf nichts reagiert und sucht den Fehler
// in der App statt in seiner eigenen Mausbewegung.
@Suite("Klick daneben")
struct NotchOutsideClickTests {
@Test("Ein Klick daneben schließt das fixierte Panel")
func outsideClickReleasesPinned() {
var machine = NotchStateMachine()
machine.forcePhase(.pinned)
let effects = machine.handle(.clickedOutside)
#expect(machine.phase == .idle)
#expect(effects.contains(.hide))
}
@Test("Auch ein offenes Panel schließt damit")
func outsideClickClosesOpen() {
var machine = NotchStateMachine()
machine.forcePhase(.open)
#expect(machine.handle(.clickedOutside).contains(.hide))
#expect(machine.phase == .idle)
}
@Test("Im Ruhezustand passiert nichts")
func outsideClickWhileIdleIsNoop() {
var machine = NotchStateMachine()
#expect(machine.handle(.clickedOutside).isEmpty)
#expect(machine.phase == .idle)
}
@Test("Beim Aufziehen bricht ein Klick daneben ab")
func outsideClickCancelsArming() {
// Sonst fährt das Panel aus, nachdem man längst woanders geklickt hat.
var machine = NotchStateMachine()
_ = machine.handle(.pointerEntered)
let effects = machine.handle(.clickedOutside)
#expect(machine.phase == .idle)
#expect(effects.contains(.cancelArmTimer))
}
}