Fix mDNS registration for multi-provider daemons#30
Merged
Conversation
Two issues prevented both daemons (codex on 9595, claude on 9596) from
advertising via mDNS simultaneously:
1. Both registered with the same instance name `codexmeter._http._tcp.local.`
so the second registration would collide. Renamed to
`codexmeter-{PROVIDER}._http._tcp.local.` and added `provider` to TXT
properties.
2. The real cause of the "mDNS advertise failed during register:" empty
errors was EventLoopBlocked, not the name collision. Calling sync
`Zeroconf.register_service` from inside `asyncio.run(main())`
deadlocks on Python 3.14 + zeroconf 0.148. Switched to `AsyncZeroconf`
with `await azc.async_register_service(info)`. Made `start_http_server`
async and await it from main. Cleanup uses the underlying sync
`Zeroconf` at atexit time (safe once the asyncio loop has exited).
Also improved error logging to include exception type/repr so future
failures aren't silent.
Verified: `dns-sd -B _http._tcp local.` shows both `codexmeter-codex`
and `codexmeter-claude`; both /status endpoints return 200. iOS
discovery is unaffected because MDNSBrowser matches on
`name.contains("codexmeter")`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 97d35c221b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The atexit handler called sync unregister_service after asyncio.run() had already closed the loop, which raised "RuntimeError: Event loop is closed" and emitted "coroutine 'async_unregister_service' was never awaited" (reproducible with --transport none --once). The goodbye/ unregister packet was not reliably sent on shutdown. - Replace atexit + sync cleanup with async _async_cleanup_zeroconf() that awaits async_unregister_service + async_close while the loop is still alive. - Invoke it from main()'s finally block so it runs on normal exit, KeyboardInterrupt, or any unwind. - Install a SIGTERM signal handler that sets an asyncio.Event; run_http_loop now races against that event so launchctl-initiated shutdowns reach the finally block. Verified --transport none --once no longer emits the RuntimeWarning, and SIGTERM to a running daemon now logs "Unregistered mDNS service and closed Zeroconf". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When both codex (port 9595) and claude (port 9596) daemons ran, the second one's
register_servicewould fail with an empty error message:Two distinct bugs were at play:
Name collision. Both daemons registered as
codexmeter._http._tcp.local.. Renamed tocodexmeter-{PROVIDER}._http._tcp.local.(socodexmeter-codex/codexmeter-claude) and addedproviderto TXT properties.The actual cause of the failures:
EventLoopBlocked. Improved error logging revealed the emptystr(exc)was hidingEventLoopBlocked(). Calling syncZeroconf.register_servicefrom insideasyncio.run(main())deadlocks on Python 3.14 + zeroconf 0.148. Reproduced in isolation, then switched toAsyncZeroconfwithawait azc.async_register_service(info).start_http_serveris nowasyncand awaited frommain. atexit cleanup uses the underlying syncZeroconf(safe because asyncio.run has terminated by then).Test plan
launchctl kickstartmDNS advertisement active: codexmeter-codex._http._tcp.local. at http://...:9595andcodexmeter-claude…:9596dns-sd -B _http._tcp local.lists bothcodexmeter-codexandcodexmeter-claudeas separate Instance Namescurl http://localhost:9595/statusand:9596/statusreturn 200MDNSBrowser.swiftmatches onservice.name.lowercased().contains("codexmeter"), which catches both new namesNotes for reviewer
processDiscoveryinMeterViewModel.swiftcurrently assigns the first discovered URL tocodexServerURLregardless of provider. With two daemons now visible this is worth revisiting (route byproviderTXT record orcodexmeter-{provider}suffix), but it's outside this fix's scope.🤖 Generated with Claude Code