mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
fix(dashboard): marshal tray menu updates onto the macOS main thread
SerenaDashboardTrayManager._update_menu() called pystray's Icon.update_menu() on whatever thread reached it. Four call sites are off the main thread: the Flask handlers for /register, /update_project and /unregister, and _alive_check_loop. pystray does no marshalling. Icon.update_menu() calls the backend directly and pystray/_darwin.py goes straight to NSStatusItem.setMenu_(), which AppKit requires on the main thread. On macOS versions that enforce it the tray manager traps with SIGTRAP inside the request handler, so the agent logs "Failed to register with tray manager: Remote end closed connection without response" and the tray icon never becomes usable. Menu refreshes now go through PyObjCTools.AppHelper.callAfter on Darwin, which is asynchronous so no Flask handler blocks on the main run loop. Other platforms call through unchanged. The helper is separate from _update_menu because _open_dashboard and _run_viewer run on the same Flask threads and will need it too. No new dependency: PyObjCTools comes from pyobjc-core, already required on macOS via pystray -> pyobjc-framework-Quartz -> pyobjc-core. The crash itself could not be reproduced locally (macOS 26.6.2; the reporter is on 27.0, everything else matching). Verified instead that the AppKit call moves from a Flask worker thread to the main thread: NSThread.isMainThread() across the three HTTP routes reads [False, False] before and [True, True, True] after.
This commit is contained in:
1 parent
602837b6e0
commit
f8f53b77f0
2 files changed
+28
-1
No files matched your search
@@ -64,6 +64,14 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
successful Serena call. Add a `serena-hooks reset` command and a `PostToolUse` example matched to
|
||||
Serena's own tools to close the gap (#1852)
|
||||
|
||||
* Dashboard:
|
||||
- Fix: On macOS, the tray manager refreshed the tray menu straight from the Flask request handlers
|
||||
for `/register`, `/update_project` and `/unregister` and from the alive-check thread. That reaches
|
||||
`NSStatusItem.setMenu_()` off the main thread, which AppKit forbids and which recent macOS
|
||||
versions punish with SIGTRAP, so the tray-manager process died within seconds of every agent
|
||||
start and the tray icon never became usable. Menu refreshes are now marshalled onto the main
|
||||
thread (#2038)
|
||||
|
||||
* Language Servers:
|
||||
- Fix: Godot's GDScript parser can report a symbol's end column one column past the
|
||||
line-end convention every other language server follows (closing a node's range from
|
||||
|
||||
+20
-1
@@ -1030,9 +1030,28 @@ class SerenaDashboardTrayManager:
|
||||
log.info("Unregistered instance on port %d", port)
|
||||
return {"status": "unregistered"}
|
||||
|
||||
@staticmethod
|
||||
def _run_in_ui_thread(fn: Callable[[], None]) -> None:
|
||||
"""
|
||||
Runs a UI mutation in the thread in which the platform's UI toolkit requires it to run.
|
||||
|
||||
On macOS, AppKit demands that mutations of the status item happen on the main thread, and
|
||||
recent macOS versions terminate the process with SIGTRAP when they do not. The tray manager
|
||||
reaches such mutations from Flask request handlers and from the alive-check thread, so the
|
||||
call has to be marshalled. On other platforms it is made directly.
|
||||
|
||||
:param fn: the UI mutation to run
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
from PyObjCTools import AppHelper
|
||||
|
||||
AppHelper.callAfter(fn)
|
||||
else:
|
||||
fn()
|
||||
|
||||
def _update_menu(self) -> None:
|
||||
if self._tray_icon:
|
||||
self._tray_icon.update_menu()
|
||||
self._run_in_ui_thread(self._tray_icon.update_menu)
|
||||
|
||||
def _build_menu_items(self) -> tuple[Any, ...]:
|
||||
"""
|
||||
|
||||
Reference in new issue
Block a user