Skip to content

Commit fbc2961

Browse files
kgoderiscursoragent
andcommitted
Address review: optional JNA, scoped nativeDnssd, drop dead tests.
Make JNA optional, catch LinkageError when it is absent, let callers force JmDNS or pass InetAddress to scope DNS-SD interface/host, deprecate auto(), and replace the non-running live IT with fuller TXT/JmDNS unit coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9c032ca commit fbc2961

11 files changed

Lines changed: 268 additions & 107 deletions

File tree

‎CHANGES.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# HAP-Java 2.0.10
22
* Add optional Mute characteristic to Doorbell service
33
* Add pluggable `HomekitAdvertiser` with native DNS-SD / Bonjour implementation (`DnssdHomekitAdvertiser`) so macOS can advertise `_hap._tcp` without binding UDP 5353 [#212](https://github.com/hap-java/HAP-Java/issues/212) (see [docs/NATIVE_DNSSD.md](docs/NATIVE_DNSSD.md))
4-
* Extract shared HAP TXT helpers (`HapDnsTxt`)
5-
* Add optional runtime dependency on JNA (`net.java.dev.jna:jna`) for native `DNSServiceRegister` (macOS `libSystem` / Avahi `libdns_sd`); JmDNS path unchanged when native DNS-SD is not used
4+
* Extract shared HAP TXT helpers (`HapDnsTxt`) with stable key order; JmDNS and native advertisers publish the same property set
5+
* JNA (`net.java.dev.jna:jna`) is **optional** for `nativeDnssd()`; JmDNS-only consumers do not need it. Applications should choose `jmdns(...)` or `nativeDnssd(InetAddress)` explicitly (`auto(...)` is deprecated)
66

77
# HAP-Java 2.0.9
88
* Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202)

‎README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@ read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html)
2828
Native DNS-SD / Bonjour (macOS)
2929
=========
3030

31-
JmDNS binds UDP **5353**. On macOS that port is owned by `mDNSResponder`, so dedicated JmDNS advertising fails with “Address already in use” and Home never sees `_hap._tcp`.
31+
JmDNS binds UDP **5353**. On macOS that port is owned by `mDNSResponder`, so dedicated JmDNS advertising can fail with “Address already in use” and Home never sees `_hap._tcp`.
3232

33-
This build adds a native advertiser that registers through Bonjour without binding 5353:
33+
This build adds a native advertiser that registers through Bonjour without binding 5353. Choose the advertiser explicitly in your application (e.g. openHAB); declare optional JNA when using the native path:
3434

3535
```java
3636
import io.github.hapjava.server.HomekitAdvertisers;
3737
import io.github.hapjava.server.impl.HomekitServer;
3838

39+
// Scope native registration to the HAP bind address, or use jmdns(sharedJmDNS) to force JmDNS:
3940
HomekitServer server =
40-
new HomekitServer(bindAddress, 9124, HomekitAdvertisers.nativeDnssd());
41+
new HomekitServer(bindAddress, 9124, HomekitAdvertisers.nativeDnssd(bindAddress));
4142
```
4243

43-
Or `HomekitAdvertisers.auto(bindAddress)` to prefer native DNS-SD when available. Full details: [docs/NATIVE_DNSSD.md](docs/NATIVE_DNSSD.md).
44+
Full details: [docs/NATIVE_DNSSD.md](docs/NATIVE_DNSSD.md).
4445

4546
Supported HomeKit Accessories
4647
=========

‎docs/NATIVE_DNSSD.md‎

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Typical symptoms on macOS: empty `dns-sd -B _hap._tcp` results while `lsof -iUDP
1010

1111
## Solution
1212

13-
This fork adds a pluggable **`HomekitAdvertiser`** and a native implementation **`DnssdHomekitAdvertiser`** that calls Apple’s DNS-SD API (`DNSServiceRegister`) via JNA:
13+
This library adds a pluggable **`HomekitAdvertiser`** and a native implementation **`DnssdHomekitAdvertiser`** that calls Apple’s DNS-SD API (`DNSServiceRegister`) via JNA:
1414

1515
| Implementation | Class | Binds UDP 5353? | Typical platform |
1616
| --- | --- | --- | --- |
@@ -19,43 +19,58 @@ This fork adds a pluggable **`HomekitAdvertiser`** and a native implementation *
1919

2020
TXT records (`sf`, `id`, `md`, `sh`, `c#`, `s#`, `ff`, `ci`, `pv`) are shared via `HapDnsTxt` so both advertisers publish the same HAP payload.
2121

22+
**Policy belongs in the application** (e.g. openHAB): choose `jmdns(...)` or `nativeDnssd(...)` explicitly. Do not rely on HAP-Java to auto-pick.
23+
2224
## Quick start
2325

26+
Declare the optional JNA dependency if you use the native advertiser (JmDNS-only consumers can omit it):
27+
28+
```xml
29+
<dependency>
30+
<groupId>net.java.dev.jna</groupId>
31+
<artifactId>jna</artifactId>
32+
<version>5.14.0</version>
33+
</dependency>
34+
```
35+
2436
```java
2537
import io.github.hapjava.server.HomekitAdvertisers;
2638
import io.github.hapjava.server.impl.HomekitServer;
2739
import java.net.InetAddress;
2840

2941
InetAddress bind = InetAddress.getByName("192.168.0.6");
42+
43+
// Prefer scoping native registration to the HAP bind address (interface + SRV host):
3044
HomekitServer server =
31-
new HomekitServer(bind, 9124, HomekitAdvertisers.nativeDnssd());
45+
new HomekitServer(bind, 9124, HomekitAdvertisers.nativeDnssd(bind));
3246

33-
// or: prefer native when present, else JmDNS
34-
HomekitServer auto =
35-
new HomekitServer(bind, 9124, HomekitAdvertisers.auto(bind));
47+
// Or share an existing JmDNS (e.g. openHAB's):
48+
// HomekitServer server =
49+
// new HomekitServer(bind, 9124, HomekitAdvertisers.jmdns(sharedJmDNS));
3650
```
3751

38-
Check availability:
52+
Check whether the native library **loads** (does not prove registration will succeed on the LAN):
3953

4054
```java
4155
if (HomekitAdvertisers.isNativeDnssdSupported()) {
42-
// safe to call nativeDnssd()
56+
// safe to construct nativeDnssd(...)
4357
}
4458
```
4559

4660
Existing constructors that take only `InetAddress` / `JmDNS` keep the previous JmDNS behaviour (backward compatible).
4761

48-
## Verifying on macOS
62+
## Verifying on macOS (manual)
4963

50-
After starting a server with `HomekitAdvertisers.nativeDnssd()` (or `auto(...)`):
64+
There is no automated live IT in CI (ubuntu runners lack `libdns_sd` / Bonjour). After starting a server with `HomekitAdvertisers.nativeDnssd(bind)`:
5165

5266
```bash
5367
dns-sd -B _hap._tcp local.
68+
dns-sd -L "<instance name>" _hap._tcp local.
5469
# or
5570
dns-sd -Z _hap._tcp local.
5671
```
5772

58-
You should see the bridge/accessory instance. No Java process should be listening on UDP 5353 (`lsof -iUDP:5353` should only show `mDNSResponder`).
73+
You should see the bridge/accessory instance and HAP TXT keys. No Java process should be listening on UDP 5353 (`lsof -iUDP:5353` should only show `mDNSResponder`).
5974

6075
## API surface
6176

@@ -73,11 +88,12 @@ setConfigurationIndex(int)
7388

7489
| Factory | Behaviour |
7590
| --- | --- |
76-
| `jmdns(JmDNS)` | Wrap an existing JmDNS |
77-
| `jmdns(InetAddress)` | Create JmDNS on that address |
78-
| `nativeDnssd()` | Native Bonjour / dns_sd (throws if unsupported) |
79-
| `auto(InetAddress)` | Native if supported, else JmDNS |
80-
| `isNativeDnssdSupported()` | Probe without constructing |
91+
| `jmdns(JmDNS)` | Wrap an existing JmDNS (force JmDNS / share openHAB’s instance) |
92+
| `jmdns(InetAddress)` | Create dedicated JmDNS on that address |
93+
| `nativeDnssd()` | Native Bonjour on **all** interfaces (`interfaceIndex = 0`, default host) |
94+
| `nativeDnssd(InetAddress)` | Native Bonjour scoped to that address’s interface index + SRV host |
95+
| `isNativeDnssdSupported()` | Whether JNA + dns_sd **load** (not a live register probe) |
96+
| `auto(InetAddress)` | **Deprecated** — prefer an explicit `jmdns` / `nativeDnssd` choice |
8197

8298
### `HomekitServer` constructors
8399

@@ -90,21 +106,20 @@ When an advertiser is supplied, `createBridge` / `createStandaloneAccessory` use
90106

91107
## Implementation notes
92108

93-
* **JNA** loads `System` on macOS/Darwin and `dns_sd` elsewhere (Avahi Bonjour compatibility).
109+
* **JNA** is an **optional** dependency. It loads `System` on macOS/Darwin and `dns_sd` elsewhere (Avahi Bonjour compatibility). Missing JNA surfaces as `LinkageError` and `isNativeDnssdSupported()` returns `false`.
94110
* `DNSServiceRegister` receives the TCP port in **network byte order**.
95-
* TXT is RFC 6763 length-prefixed `key=value` bytes (`HapDnsTxt.encodeTxt`).
111+
* Default `nativeDnssd()` uses `interfaceIndex = 0` (all interfaces) and `host = null` (system hostname). Prefer `nativeDnssd(InetAddress)` when the HAP TCP listener is bound to one LAN address.
112+
* TXT is RFC 6763 length-prefixed `key=value` bytes (`HapDnsTxt.encodeTxt`). Key insertion order is stable (`LinkedHashMap`); RFC 6763 does not require a particular pair order.
96113
* A daemon thread runs `DNSServiceProcessResult` until `DNSServiceRefDeallocate` on `stop()` / re-register.
97114
* Re-registration (discoverability, MAC, `c#`) deallocates and registers again — same pattern as the JmDNS advertiser.
98115

99116
## Tests
100117

101118
| Test | Purpose |
102119
| --- | --- |
103-
| `HapDnsTxtTest` | Required TXT keys + wire encoding |
104-
| `DnssdHomekitAdvertiserTest` | Mocked JNA register / re-register / stop / port endianness |
105-
| `JmdnsHomekitAdvertiserTest` | Existing JmDNS behaviour (unchanged) |
106-
| `HomekitAdvertisersTest` | Factory / support probe |
107-
| `DnssdLiveAdvertiserIT` | Real register+stop when native DNS-SD is present |
120+
| `HapDnsTxtTest` | Full TXT key set, stable order, wire encoding |
121+
| `DnssdHomekitAdvertiserTest` | Mocked JNA register / interface+host / re-register / stop / port endianness |
122+
| `JmdnsHomekitAdvertiserTest` | JmDNS register including full HAP property set |
108123

109124
```bash
110125
mvn test
@@ -114,10 +129,10 @@ mvn test
114129

115130
| Symptom | Likely cause |
116131
| --- | --- |
117-
| `Address already in use` / “no suitable mDNS” | Still using JmDNS against mDNSResponder — switch to `nativeDnssd()` |
118-
| `UnsupportedOperationException: Native DNS-SD` | No `libSystem`/`libdns_sd` on this JVM/OS |
132+
| `Address already in use` / “no suitable mDNS” | Still using dedicated JmDNS against mDNSResponder — switch to `nativeDnssd(bind)` or share an existing JmDNS |
133+
| `UnsupportedOperationException: Native DNS-SD` | No JNA on classpath, or no `libSystem`/`libdns_sd` on this JVM/OS |
119134
| `DNSServiceRegister failed with error code …` | Bad name/type, permissions, or daemon unavailable — check Console / `mDNSResponder` |
120-
| Browse empty after register | Wrong interface, firewall, or advertiser stopped; confirm with `dns-sd -B _hap._tcp` |
135+
| Browse empty after register / too many interfaces | Prefer `nativeDnssd(localAddress)` instead of all-interfaces `nativeDnssd()` |
121136

122137
## License
123138

‎pom.xml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,14 @@
145145
<version>3.5.6</version>
146146
</dependency>
147147

148-
<!-- Native Bonjour / DNS-SD (macOS libSystem, Linux libdns_sd) -->
148+
<!-- Native Bonjour / DNS-SD (macOS libSystem, Linux libdns_sd). Optional:
149+
consumers that only use JmDNS need not pull JNA; nativeDnssd() users
150+
must declare JNA themselves (openHAB already embeds it). -->
149151
<dependency>
150152
<groupId>net.java.dev.jna</groupId>
151153
<artifactId>jna</artifactId>
152154
<version>5.14.0</version>
155+
<optional>true</optional>
153156
</dependency>
154157

155158
<dependency>

‎src/main/java/io/github/hapjava/server/HomekitAdvertisers.java‎

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@
99
/**
1010
* Factories for {@link HomekitAdvertiser} implementations.
1111
*
12-
* <p>Typical macOS usage:
12+
* <p>Callers (e.g. openHAB) should choose the advertiser explicitly: use {@link #jmdns(JmDNS)} to
13+
* share an existing JmDNS instance, {@link #jmdns(InetAddress)} for a dedicated JmDNS socket, or
14+
* {@link #nativeDnssd()} / {@link #nativeDnssd(InetAddress)} for Bonjour without binding UDP 5353.
15+
* Do not rely on HAP-Java to pick a policy automatically.
1316
*
1417
* <pre>{@code
15-
* HomekitServer server =
16-
* new HomekitServer(InetAddress.getByName("192.168.0.6"), 9124, HomekitAdvertisers.nativeDnssd());
17-
* }</pre>
18+
* // Force JmDNS (e.g. share openHAB's instance):
19+
* HomekitAdvertiser adv = HomekitAdvertisers.jmdns(sharedJmDNS);
1820
*
19-
* That listens on HAP TCP without binding UDP 5353; Bonjour/mDNSResponder publishes {@code
20-
* _hap._tcp}.
21+
* // Native DNS-SD limited to one LAN address / interface:
22+
* HomekitAdvertiser adv = HomekitAdvertisers.nativeDnssd(InetAddress.getByName("192.168.0.6"));
23+
* }</pre>
2124
*
2225
* @author Karel Goderis
2326
*/
@@ -47,11 +50,14 @@ public static HomekitAdvertiser jmdns(InetAddress localAddress) throws IOExcepti
4750
}
4851

4952
/**
50-
* Native DNS-SD / Bonjour advertiser (macOS {@code mDNSResponder}, or Avahi's dns_sd compat
51-
* layer). Does not bind UDP 5353. Requires the JNA runtime dependency.
53+
* Native DNS-SD / Bonjour advertiser on <strong>all</strong> interfaces ({@code interfaceIndex =
54+
* 0}, default host). Prefer {@link #nativeDnssd(InetAddress)} when the HAP TCP listener is bound
55+
* to a single LAN address.
56+
*
57+
* <p>Requires the optional JNA dependency on the classpath.
5258
*
5359
* @return a native DNS-SD advertiser
54-
* @throws UnsupportedOperationException if the native library cannot be loaded
60+
* @throws UnsupportedOperationException if JNA or the native library cannot be loaded
5561
*/
5662
public static HomekitAdvertiser nativeDnssd() {
5763
if (!DnssdHomekitAdvertiser.isSupported()) {
@@ -62,22 +68,49 @@ public static HomekitAdvertiser nativeDnssd() {
6268
}
6369

6470
/**
65-
* Prefer native DNS-SD when available (macOS); otherwise create a dedicated JmDNS instance on
66-
* {@code localAddress}.
71+
* Native DNS-SD / Bonjour advertiser scoped to {@code localAddress}: SRV host is that address and
72+
* registration uses the matching network interface index when resolvable.
6773
*
68-
* @param localAddress address used if falling back to JmDNS
69-
* @return a native DNS-SD advertiser when supported, otherwise a JmDNS-backed advertiser
74+
* @param localAddress LAN address the HAP TCP server listens on
75+
* @return a native DNS-SD advertiser
76+
* @throws UnsupportedOperationException if JNA or the native library cannot be loaded
77+
*/
78+
public static HomekitAdvertiser nativeDnssd(InetAddress localAddress) {
79+
if (!DnssdHomekitAdvertiser.isSupported()) {
80+
throw new UnsupportedOperationException(
81+
"Native DNS-SD (dns_sd / Bonjour) is not available on this platform/JVM");
82+
}
83+
return new DnssdHomekitAdvertiser(localAddress);
84+
}
85+
86+
/**
87+
* Convenience: prefer native DNS-SD when the library loads, otherwise JmDNS on {@code
88+
* localAddress}.
89+
*
90+
* <p><strong>Applications should prefer an explicit {@link #jmdns(JmDNS)} / {@link
91+
* #nativeDnssd(InetAddress)} choice</strong> (policy belongs in openHAB / the integrator). This
92+
* helper only probes whether the native library <em>loads</em>; it does not verify that
93+
* registration will succeed on the network.
94+
*
95+
* @param localAddress address used for native scoping or JmDNS fallback
96+
* @return a native DNS-SD advertiser when the library loads, otherwise a JmDNS-backed advertiser
7097
* @throws IOException if native DNS-SD is unavailable and JmDNS cannot be created
98+
* @deprecated Prefer {@link #jmdns(JmDNS)}, {@link #jmdns(InetAddress)}, or {@link
99+
* #nativeDnssd(InetAddress)} so the application chooses the advertiser.
71100
*/
101+
@Deprecated
72102
public static HomekitAdvertiser auto(InetAddress localAddress) throws IOException {
73103
if (DnssdHomekitAdvertiser.isSupported()) {
74-
return new DnssdHomekitAdvertiser();
104+
return new DnssdHomekitAdvertiser(localAddress);
75105
}
76106
return new JmdnsHomekitAdvertiser(localAddress);
77107
}
78108

79109
/**
80-
* Whether {@link #nativeDnssd()} can be used on this JVM/OS.
110+
* Whether {@link #nativeDnssd()} can be constructed on this JVM/OS.
111+
*
112+
* <p>Only checks that JNA and the platform DNS-SD library <em>load</em>. It does not register a
113+
* service or prove mDNS is reachable on the LAN.
81114
*
82115
* @return {@code true} if the platform DNS-SD library loaded successfully
83116
*/

0 commit comments

Comments
 (0)