AppHouseKitchen / AppHouseKitchen/AlDente-Battery_Care_and_Monitoring
Deadlock on "Deactivate License" — Combine/SwiftUI lock inversion in Paddle completion block (1.38.1)
- Linguagem predominante
- Swift
- Estrelas
- 9.2k
- Forks
- 332
- Métricas de merge de PRs
- Nenhum PR com merge em 30d
Descrição
**MacBook (please complete the following information):**
- MacBook Model: MacBook Pro (M3 Pro), Apple Silicon / ARM64
- macOS Version: macOS 26.3.1 (25D2128)
- Charger Model: (not relevant to this bug — see Additional context)
- Charging Cable: (same)
- AlDente Pro or AlDente Free: **Pro** (`com.apphousekitchen.aldente-pro`)
- AlDente Version: **1.38.1 (98)**
- List or add screenshots of all enabled/disabled settings in AlDente:
- Debug file: The app is fully deadlocked when this occurs and the in-app debug generator may itself hang (see note on `libtrace` below). Attaching `sample AlDente` output instead — the relevant excerpt is inline below, full capture available on request.
- External Monitor connected? no
- MacBook used in Clamshell Mode (with the lid closed)? no
**Describe the bug**
Clicking "Deactivate License" in Settings → Manage License hangs AlDente permanently. The menu bar item stops responding, the popover never opens, and only Force Quit recovers the app. CPU is idle — this is a hard deadlock, not a spin.
The deactivation itself **succeeded**: the license was deactivated server-side. The Paddle network call completed and its response was received; the hang happens in the UI update that runs afterward.
`sample` shows all 2476 samples in an identical state — a lock inversion (ABBA deadlock) between Combine's `ObservableObjectPublisher` lock and SwiftUI's main-thread graph lock.
**Thread A — main thread**, blocked *acquiring* the publisher lock:
```
_dispatch_main_queue_drain
AlDente + 0x7fe5c → AlDente + 0x17e184
ObservableObjectPublisher.send() + 472
ObservableObjectPublisher.Inner.send() + 176
_os_unfair_lock_lock_slow → __ulock_wait2
```
**Thread B — `com.apple.NSURLSession-delegate` queue**, holding the publisher lock and waiting on the main-thread lock:
```
-[PADProduct deactivateWithCompletion:]_block_invoke_2 (Paddle 4.5.1)
AlDente + 0x104bb4 → 0x1049f4 → 0x17a394
ObservableObjectPublisher.send() + 472
ObservableObjectPublisher.Inner.send() + 212
SubscriptionLifetime.Connection.receive(_:)
protocol witness for Subscriber.receive(_:) in conformance
AttributeInvalidatingSubscriber
AttributeInvalidatingSubscriber.invalidateAttribute()
GraphHost.asyncTransaction → flushTransactions → runTransaction
AG::Subgraph::update → AG::Graph::UpdateStack::update
GeometryReader.Child.updateValue()
_MovableLockSyncMain → _pthread_cond_wait → __psynch_cvwait
```
The offsets are the key detail: `Inner.send() + 176` (thread A, still at the lock acquire) vs `+ 212` (thread B, past it, inside the critical section). So thread B owns the Combine publisher lock and wants SwiftUI's graph lock, while thread A owns the main thread and wants the publisher lock. Neither can proceed, and neither ever will.
**Suspected root cause**
An `@Published` mutation or `objectWillChange.send()` is being performed directly inside the Paddle deactivation completion block. Paddle invokes that block on the `NSURLSession` delegate queue, not the main queue. Publishing off the main thread is unsafe in this specific shape because SwiftUI's `AttributeInvalidatingSubscriber` synchronously reaches for the main-thread-affine graph lock (`_MovableLockSyncMain`) while the Combine lock is still held — and the main thread is meanwhile publishing on the same object.
A likely fix is to hop to the main queue *before* publishing rather than after:
```swift
paddleProduct.deactivate { [weak self] success, error in
DispatchQueue.main.async {
self?.isActivated = false
}
}
```
Marking the owning model `@MainActor`, or inserting `.receive(on: DispatchQueue.main)` between the publisher and its SwiftUI subscriber, would work equally well. Any of the three breaks the cycle, since the graph lock is then only ever requested by the thread that already owns the main thread.
**To Reproduce**
1. Open AlDente Pro with a valid license activated.
2. Go to Settings → Manage License.
3. Click "Deactivate License".
4. The app hangs immediately and permanently.
Observed once. I have not re-tested — re-activating and repeating the deactivation would cost a license activation slot, so I'd rather not unless you want confirmation that it's deterministic. A VPN was active at the time, but since the Paddle response had already been received when the deadlock occurred, latency shouldn't be a factor.
**Expected Behavior**
Deactivating the license updates the UI and leaves the app responsive.
**Screenshots and Screen Recordings**
N/A — the failure is a silent hang with no visual artifact beyond an unresponsive menu bar item and a frozen Settings window.
**Additional context**
- The app stayed deadlocked for roughly a day before I force-quit it. This does not resolve on its own.
- `Physical footprint: 114.6M` (peak 219.2M) — no memory pressure involved.
- `Thread_9544901` (`com.apple.libtrace.state.block-list`) is also stuck, in `_dispatch_sync_f_slow` under `___os_state_request_for_self_block_invoke`. That's collateral damage from the diagnostic state handler waiting on a queue the deadlock is holding — not a separate bug, but it does mean `os_state`-based diagnostics (possibly including the in-app debug file) may hang or come back empty while the app is wedged.
- Nothing in the stacks touches SMC or charge control. This is entirely in the licensing/UI path, so the charger and cable fields above are almost certainly irrelevant.
- Paddle framework version in the binary images: `com.paddle.Paddle 4.5.1`.
Guia de contribuição
Nenhum guia de contribuição indexado para este repositório
Direção de pesquisa
Locate the Paddle deactivation completion used by Settings → Manage License and inspect where the license state is published; the issue provides no source file or test path. First trace or reproduce the completion queue behavior, then verify that deactivation updates the UI without freezing the menu bar app.
Escrita pelo modelo de indexação a partir do texto da issue.
Avaliação
- Stack de tecnologia
- swift
- Domínio
- desktop, payments
- Tipo de issue
- Bug
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Status de atividade
- Ativa
- Clareza
- Razoavelmente clara
- Facilidade para iniciantes
- 55/100