Files
fleetd/bridged/src/main/java/dev/ltms/fleet/herdr/UnixSocketHerdrClient.java
T
Dai Ha 9b50dd69d8 CB-632 unit 1: rename the Java package and classes to fleet
Part of #145 (CB-632), under epic #125.

The product is called fleet and the daemon is called fleetd, but the code
still said bridge everywhere. This renames the Java half:

  package dev.ltms.bridged -> dev.ltms.fleet
  Bridged        -> Fleetd          (the main class)
  BridgedConfig  -> FleetConfig
  BridgeMcp      -> FleetMcp
  BridgedApp     -> FleetApp
  BridgedMetrics -> FleetMetrics

The package root is dev.ltms.fleet, not dev.ltms.fleetd. The trailing d
means daemon, which names a process, not a namespace.

What this commit deliberately does NOT change:

  - The module directory stays bridged/, and <finalName> stays bridged.
    The installed launchd plist names bridged/target/bridged.jar and its
    KeepAlive is armed, so renaming the jar on its own strands a restart.
    Both change at the cutover, together with the plist, in one step.
  - The bridge_* MCP tool aliases. CB-622 shipped both names on purpose.
    One test names a local variable viaBridge because it holds the result
    of the deprecated call; the rename collided with it and the compiler
    caught it. That variable is back.
  - BRIDGED_* env var names, and bridged.yaml. Both are operator
    contracts and need a read-both shim, which is a later unit.

Two things a plain search-and-replace would have missed:

  - logback.xml and logback-test.xml name the package twice, once as a
    turboFilter class= attribute. The compiler never checks those.
  - BSD sed does not support \b. The word-boundary expression matched
    nothing and said nothing, while the other ten in the same command
    worked. Checked the leftovers instead of trusting the exit code.

Verified: mvn clean install green, 51 test classes, 878 tests, 0 failures
-- the same count as before the rename.
2026-08-23 06:27:43 +02:00

123 lines
4.7 KiB
Java

package dev.ltms.fleet.herdr;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicLong;
/**
* {@link HerdrClient} over herdr's Unix domain socket using the JDK's
* {@link UnixDomainSocketAddress} + {@link SocketChannel} (no native/JNI dependency).
*
* <p><b>Connection-per-call.</b> The contract test against herdr 0.7.0 established
* that herdr serves <em>one</em> request/response per connection and then closes it —
* a second write on the same socket gets a broken pipe. So every {@link #call} opens a
* fresh connection, writes one frame, reads one line, and closes. A pleasant
* consequence: with no shared socket there is no shared read state, so the client is
* safe to call concurrently from many virtual threads with no locking.
*
* <p>Streaming methods ({@code events.subscribe}) keep their own long-lived connection
* and land in a later ticket; they do not reuse this request/response path.
*/
public final class UnixSocketHerdrClient implements HerdrClient {
private static final Logger log = LoggerFactory.getLogger(UnixSocketHerdrClient.class);
/** herdr's default socket, overridable by {@code HERDR_SOCKET_PATH}. */
public static Path defaultSocketPath() {
String override = System.getenv("HERDR_SOCKET_PATH");
if (override != null && !override.isBlank()) {
return Path.of(override);
}
return Path.of(System.getProperty("user.home"), ".config", "herdr", "herdr.sock");
}
private final Path socketPath;
private final HerdrCodec codec;
private final AtomicLong ids = new AtomicLong(1);
private UnixSocketHerdrClient(Path socketPath, ObjectMapper mapper) {
this.socketPath = socketPath;
this.codec = new HerdrCodec(mapper);
}
/** Client for the default socket with a fresh {@link ObjectMapper}. */
public static UnixSocketHerdrClient connect() {
return connect(defaultSocketPath(), new ObjectMapper());
}
/**
* Client for {@code socketPath}. Does not hold a connection open (herdr is
* one-shot per connection); connectivity surfaces on the first {@link #call}.
*/
public static UnixSocketHerdrClient connect(Path socketPath, ObjectMapper mapper) {
log.debug("herdr client bound to socket {}", socketPath);
return new UnixSocketHerdrClient(socketPath, mapper);
}
@Override
public JsonNode call(String method, Object params) {
String id = Long.toString(ids.getAndIncrement());
byte[] frame = codec.encode(id, method, params);
try (SocketChannel ch = SocketChannel.open(StandardProtocolFamily.UNIX)) {
ch.connect(UnixDomainSocketAddress.of(socketPath));
writeFully(ch, ByteBuffer.wrap(frame));
return codec.decodeResult(readLine(ch));
} catch (IOException e) {
throw new HerdrException("herdr call '" + method + "' failed at transport (socket "
+ socketPath + ", is herdr running?)", e);
}
}
private static void writeFully(SocketChannel ch, ByteBuffer buf) throws IOException {
while (buf.hasRemaining()) {
ch.write(buf);
}
}
/** Read up to and including the first {@code '\n'}, returning the line without it. */
private static String readLine(SocketChannel ch) throws IOException {
ByteBuffer readBuf = ByteBuffer.allocate(64 * 1024);
StringBuilder sb = new StringBuilder();
while (true) {
int nl = indexOfNewline(sb);
if (nl >= 0) {
return sb.substring(0, nl);
}
readBuf.clear();
int n = ch.read(readBuf);
if (n == -1) {
if (sb.length() > 0) {
return sb.toString(); // herdr closed after a complete, unterminated frame
}
throw new IOException("herdr closed the connection with no response");
}
readBuf.flip();
sb.append(StandardCharsets.UTF_8.decode(readBuf));
}
}
private static int indexOfNewline(CharSequence s) {
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '\n') {
return i;
}
}
return -1;
}
/** No persistent resources to release; present for the {@link AutoCloseable} contract. */
@Override
public void close() {
}
}