Production deb 1.0.891 (the post-#164 release) still couldn’t reach krill-pi4j. The new defensive logging from the #164 follow-up surfaced the real cause this time:
1
2
3
Failed to initialize krill-pi4j gRPC client on localhost:50051. ...
java.lang.IllegalArgumentException: Address types of NameResolver 'unix' for 'localhost:50051' not supported by transport
WARN - getAllPins: krill-pi4j client unavailable, returning empty header
/header still returned [], all GPIO operations were no-ops.
shadowJar’s default duplicatesStrategy = EXCLUDE silently drops the second
copy of any duplicated file path before it reaches a configured
transformer. META-INF/services/io.grpc.NameResolverProvider exists in two
artifacts on the runtime classpath:
grpc-core → io.grpc.internal.DnsNameResolverProvidergrpc-netty-shaded → io.grpc.netty.shaded.io.grpc.netty.UdsNameResolverProviderWhichever one Gradle’s copy task processed first won; the other was excluded.
The shipped jar therefore registered only the unix-domain resolver. When
Pi4jClient called ManagedChannelBuilder.forAddress("localhost", 50051),
gRPC picked unix: as the only available scheme and the Netty TCP transport
rejected the resulting DomainSocketAddress. lateinit var client never got
assigned, the post-#164 short-circuit kicked in for every method, and the
hardware bridge remained silent.
The same trap applies to every other ServiceLoader-using library on the
classpath (LoadBalancerProvider, ServerProvider, JDBC, kotlinx-serialization,
…). #164 fixed the class-stripping axis of this problem; #168 fixes the
service-file-merge axis.
server/build.gradle.kts — add mergeServiceFiles() to the shadowJar
configuration and set duplicatesStrategy = DuplicatesStrategy.INCLUDE.
shadow plugin’s docs explicitly call out that mergeServiceFiles() requires
a non-EXCLUDE duplicates strategy because the transformer never sees files
the copy task already excluded; setting INCLUDE lets every copy reach the
transformer, which then concatenates them into a single output entry.doLast smoke test (added in #164 for
ManagedChannelProvider class presence) to also parse
META-INF/services/io.grpc.ManagedChannelProvider and
META-INF/services/io.grpc.NameResolverProvider and fail the build if
any required provider line is missing. Class presence + service-file
contents now both guard the gRPC bootstrap path.The build-time assertion is the load-bearing prevention. Two distinct
packaging regressions (classes pruned by minimize, service files dropped
by duplicatesStrategy) presented identically as /header returning []
and silent GPIO; the assertion now catches both. New runtime dependencies
that bring service files don’t need any further config — the INCLUDE +
mergeServiceFiles combination handles them automatically — but if a future
gRPC version adds a new required provider line, the assertion list at the
top of the doLast block needs to grow with it.