Skip to content

fix(macos): close() races AppKit work already running on the main queue #14

Description

@Kitty-Hivens

AppKitTrayImpl.close() runs synchronously on whichever thread the consumer calls it from. It flips the open flag, sends removeStatusItem: and then objc_releases the last reference to the NSStatusItem.

Mutating calls take the opposite route: runOnMainQueue marshals them onto the Cocoa main queue via dispatch_async_f. Since 0.1.3 the queued body re-checks the open flag before it starts, which covers an action that has not begun by the time close() lands. It does not cover an action that is already past that check: the main queue can be inside applyMenu or applyIcon, get preempted, and resume after close() has released the status item, at which point setMenu: or setImage: goes to a deallocated object.

There is a second, related problem in the same method. close() messages NSStatusBar and NSStatusItem from an arbitrary thread, while create() explicitly refuses to run anywhere but the Cocoa main thread and logs a pointer at -XstartOnFirstThread when it does. Teardown holding itself to a weaker rule than construction is inconsistent at best.

Both point at the same fix: marshal close() onto the main queue too, with a bounded wait so a consumer shutting down cannot hang on a main queue nobody is draining, and keep the synchronous fallback for the case where no run loop is running. The current comment on close() argues against marshalling because a queued teardown could run after the method returns; that reasoning holds for a fire-and-forget async dispatch, not for a bounded synchronous one.

Needs a macOS host to reproduce and verify.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions