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.
AppKitTrayImpl.close()runs synchronously on whichever thread the consumer calls it from. It flips the open flag, sendsremoveStatusItem:and thenobjc_releases the last reference to theNSStatusItem.Mutating calls take the opposite route:
runOnMainQueuemarshals them onto the Cocoa main queue viadispatch_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 timeclose()lands. It does not cover an action that is already past that check: the main queue can be insideapplyMenuorapplyIcon, get preempted, and resume afterclose()has released the status item, at which pointsetMenu:orsetImage:goes to a deallocated object.There is a second, related problem in the same method.
close()messagesNSStatusBarandNSStatusItemfrom an arbitrary thread, whilecreate()explicitly refuses to run anywhere but the Cocoa main thread and logs a pointer at-XstartOnFirstThreadwhen 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 onclose()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.