Feature, not a bug: users need to stop a server from auto-joining the swarm. Building it surfaced a latent defect — the server’s beacon listener could not actually be stopped.
ServerMetaData.beaconsEnabled (SDK 0.0.52) gives the setting a home, but the
JVM Multicast.receiveBeacons launched its receive loop in a detached
SupervisorJob scope. Cancelling the supervisor’s listener job never reached
that scope, so the socket stayed bound and kept responding — there was no way to
“shut down all beacon processes.” Separately, ServerServerProcessor started
beacons unconditionally at startup and nothing reacted to a live beaconsEnabled
edit (meta-only CRUD updates don’t wake processors).
shared/.../io/http/Beacon.kt: receiveBeacons now runs structured in the
caller’s coroutine (withContext(Dispatchers.IO) + use {}), so cancelling
the launching job closes the socket and leaves the multicast group. Also fixes
the same leak on the desktop client.ServerBeaconSupervisor.stopBeaconProcess() cancels the listener + reannounce
jobs (idempotent; a same-job guard on invokeOnCompletion makes stop→start
safe). It is a concrete method, not added to the SDK BeaconSupervisor
interface, so no cross-repo SDK release is needed.ServerServerProcessor gates start/stop on the self node’s beaconsEnabled
at startup; the setting persists because ServerIdentity.getSelfWithInfo()
preserves it through its copy.BeaconSettingsTask (a ServerTask on nodeUpdates) applies live toggles
by re-asserting the desired start/stop state on each self-node update.composeApp/.../krillapp/server/ServerView.kt: a “Network beacon” Switch in
EditServer bound to meta.beaconsEnabled, persisting through the existing
LaunchedEffect(meta) -> nodeManager.edited path, with explanatory helper text.A background loop that owns an OS resource (socket, file handle) must run
structured under a cancellable job — never a detached SupervisorJob — or it
can never be torn down. New env-dependent server units (ServerServerProcessor,
BeaconSettingsTask) take a selfId: () -> String seam instead of calling
installId(), so they are unit-testable without the real ~/.krill/install_id.
Socket-level behavior stays out of unit tests (the port-bind/network ban); it is
covered by the supervisor job-lifecycle test plus QA on real hardware.