WorkingSetLease.java
package network.ike.plugin.ws;
import network.ike.lease.core.LeaseProtocol;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
/**
* The {@code ws:} goals' bridge to the working-set lease protocol —
* in-process on {@code ike-lease-core} since the protocol port
* (IKE-Network/ike-issues#1067), which is what retired this class's
* original shape: a Maven-published plugin shelling out to a
* {@code $HOME} script. The single-implementation rule survives intact —
* the very same {@code LeaseProtocol} the {@code lease.sh} wrapper execs
* and the IDE plugin embeds runs here, so the fencing system's halves
* cannot drift.
*
* <p>A working set — any project root under {@code ~/ike-dev} — has one
* writer at a time across the machines that share that folder. A
* {@code ws:} goal that rewrites branches, versions and history is
* emphatically a writer, so it confirms it holds the lease before it
* starts; a finish that lands in the <em>parent</em> working set
* short-holds that lease too ({@link ParentLeaseHold}). Design:
* {@code dev-working-set-lease} in ike-lab-documents,
* IKE-Network/ike-issues#1002; this half is #1005.
*
* <p><strong>Inert outside that setup.</strong> No
* {@code ~/.ike-machine-id}, no development folder, or a path that does
* not resolve to a working set, and the goal proceeds exactly as it did
* before — this plugin is published and run by people with no
* Syncthing-paired fleet, for whom the lease protocol does not exist. It
* fails open for the same reason the Claude fence does: a coordination
* aid that can wedge a build is worse than no coordination aid.
*
* <p>System properties {@code ike.lease.home}, {@code ike.lease.ikeDev}
* and {@code ike.lease.settleSeconds} override the environment
* ({@code HOME}, {@code IKE_DEV}, {@code IKE_LEASE_SETTLE_SECONDS}) —
* the seam sandboxed tests set, since a JVM cannot re-point its
* environment per test.
*/
final class WorkingSetLease {
private WorkingSetLease() { }
/**
* The outcome of asking the lease protocol whether this machine may write.
*
* @param verdict what the protocol decided
* @param detail the protocol's own explanation, for the operator; empty
* when there is nothing to say
*/
record Decision(Verdict verdict, String detail) { }
/** What {@link #confirm} concluded. */
enum Verdict {
/** This machine holds the lease; proceed. */
HELD,
/** No lease machinery, or not a working set. Proceed. */
NOT_APPLICABLE,
/** Another machine holds it, or won the race. Do not write. */
FENCED
}
/**
* A read-only look at a working set's lease, for draft goals and
* listings that must not acquire anything.
*
* @param liveElsewhere {@code true} when another machine holds the
* lease live — the one state that refuses a write
* @param line the protocol's own one-line description
*/
record Status(boolean liveElsewhere, String line) { }
/**
* Confirms this machine holds the working set's lease, waiting out the
* sync layer's propagation window and reading the record back — the
* #1005 confirmed acquisition, for consequential steps.
*
* @param workingSetRoot the directory the goal is about to write to
* @return the decision; never {@code null}
*/
static Decision confirm(Path workingSetRoot) {
Optional<Context> context = Context.of(workingSetRoot);
if (context.isEmpty()) {
return new Decision(Verdict.NOT_APPLICABLE, "");
}
LeaseProtocol.Outcome outcome =
context.get().protocol().ensure(context.get().workingSet(), true);
return switch (outcome.exitCode()) {
case 0 -> new Decision(Verdict.HELD, context.get().workingSet());
case 1 -> new Decision(Verdict.FENCED, outcome.stdout().trim());
default -> new Decision(Verdict.NOT_APPLICABLE, "");
};
}
/**
* Reads a working set's lease state without changing it.
*
* @param workingSetRoot the working set's directory
* @return the status, or empty when the lease machinery is absent or
* the path is not a working set — the not-applicable cases
*/
static Optional<Status> status(Path workingSetRoot) {
return Context.of(workingSetRoot).map(context -> {
LeaseProtocol.Outcome outcome =
context.protocol().status(context.workingSet());
return new Status(outcome.exitCode() == 1,
outcome.stdout().trim());
});
}
/**
* Releases a lease this machine holds — the closing half of a
* short-hold. The protocol refuses politely when the lease is not
* held here; anything but a clean release is the caller's to log,
* never to fail on.
*
* @param workingSetRoot the working set's directory
* @return {@code true} when the lease was released (or no record
* existed); {@code false} otherwise
*/
static boolean release(Path workingSetRoot) {
return Context.of(workingSetRoot)
.map(context -> context.protocol()
.release(context.workingSet()).exitCode() == 0)
.orElse(true);
}
/**
* The resolved protocol instance and working-set name for a path, or
* empty in every not-applicable case.
*
* @param protocol the in-process protocol
* @param workingSet the resolved working-set name
*/
private record Context(LeaseProtocol protocol, String workingSet) {
static Optional<Context> of(Path workingSetRoot) {
if (workingSetRoot == null) {
return Optional.empty();
}
String home = firstNonBlank(
System.getProperty("ike.lease.home"),
System.getenv("HOME"),
System.getProperty("user.home"));
String ikeDev = firstNonBlank(
System.getProperty("ike.lease.ikeDev"),
System.getenv("IKE_DEV"),
home + "/ike-dev");
if (!Files.isRegularFile(Path.of(home, ".ike-machine-id"))
|| !Files.isDirectory(Path.of(ikeDev))) {
return Optional.empty();
}
long settle = parseLong(firstNonBlank(
System.getProperty("ike.lease.settleSeconds"),
System.getenv("IKE_LEASE_SETTLE_SECONDS"),
"25"), 25L);
String ttl = firstNonBlank(System.getenv("IKE_LEASE_TTL"),
"PT10M");
LeaseProtocol protocol = new LeaseProtocol(Path.of(ikeDev),
Path.of(home), Path.of(System.getProperty("user.dir")),
ttl, settle);
LeaseProtocol.Outcome resolved =
protocol.resolve(workingSetRoot.toString());
if (resolved.exitCode() != 0) {
return Optional.empty();
}
return Optional.of(new Context(protocol,
resolved.stdout().trim()));
}
private static String firstNonBlank(String... values) {
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return "";
}
private static long parseLong(String value, long fallback) {
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException e) {
return fallback;
}
}
}
}