Coverage Summary for Class: LanDiscoveryKt (it.unibo.tuprolog.argumentation.parallel.federation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| LanDiscoveryKt |
100%
(1/1)
|
50%
(1/2)
|
|
100%
(1/1)
|
100%
(14/14)
|
package it.unibo.tuprolog.argumentation.parallel.federation
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.StandardProtocolFamily
import java.net.StandardSocketOptions
import java.nio.ByteBuffer
import java.nio.channels.ClosedChannelException
import java.nio.channels.DatagramChannel
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
/**
* The sites of the local network found and linked to [site] (see [Beacons]): its beacons sent over UDP multicast,
* to [group] on [beaconPort], every [interval] milliseconds, with the port of the WebSocket server of [links], [wsPort].
* The group is joined, and the beacons sent, on every interface able to (the loopback one as well, for the sites of
* the same machine), over IPv4 alone: a socket of both IPv4 and IPv6 may not reach an IPv4 group (on macOS).
*
* The beacons are heard on a thread of their own, and sent on another; the links to the sites heard are opened on
* threads of their own, as one may wait for a while. A beacon whose handling fails (a callback of the site raising,
* say) is reported and skipped.
*/
class LanDiscovery(
site: Site,
private val links: WebSocketLinks,
wsPort: Int,
private val beaconPort: Int = Beacons.PORT,
group: String = Beacons.GROUP,
private val interval: Long = 2_000,
) : AutoCloseable {
private val address = InetAddress.getByName(group)
private val executor = Executors.newScheduledThreadPool(2, daemons("arg2p-discovery"))
private val connecting = Executors.newCachedThreadPool(daemons("arg2p-discovery-link"))
// The addresses of this machine
private val local =
NetworkInterface
.networkInterfaces()
.toList()
.flatMap { runCatching { it.inetAddresses().toList() }.getOrDefault(emptyList()) }
.map { it.hostAddress }
.toSet()
// Whether [host] is this machine, whose beacons reach it from each of its addresses
private fun isLocal(host: String) = host in local || runCatching { InetAddress.getByName(host).isLoopbackAddress }.getOrDefault(false)
// a site gone since it was heard is no failure: heard again, it is linked to again
private val beacons =
Beacons(site, wsPort, interval, ::isLocal) { url, code, done ->
connecting.execute { runCatching { links.connect(url, code = code) }.also { done() } }
}
private val channel =
DatagramChannel.open(StandardProtocolFamily.INET).apply {
setOption(StandardSocketOptions.SO_REUSEADDR, true)
bind(InetSocketAddress(beaconPort))
setOption(StandardSocketOptions.IP_MULTICAST_TTL, 1)
setOption(StandardSocketOptions.IP_MULTICAST_LOOP, true)
}
// The interfaces up, with an IPv4 address, that joined the group
private val interfaces =
NetworkInterface
.networkInterfaces()
.toList()
.filter {
runCatching {
it.isUp &&
it.supportsMulticast() &&
it.inetAddresses().anyMatch { a ->
a is Inet4Address
}
}.getOrDefault(false)
}.filter { runCatching { channel.join(address, it) }.isSuccess }
init {
executor.execute(::listen)
executor.scheduleAtFixedRate(::announce, 0, interval, TimeUnit.MILLISECONDS)
}
/** Stops announcing the site and finding the others: the links opened stay */
override fun close() {
executor.shutdownNow()
connecting.shutdownNow()
channel.close()
}
// the beacon sent on every interface: one failing (down since, say) skipped
private fun announce() =
handled {
val bytes = beacons.beacon.encodeToByteArray()
val target = InetSocketAddress(address, beaconPort)
interfaces.forEach { networkInterface ->
runCatching {
channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface)
channel.send(ByteBuffer.wrap(bytes), target)
}
}
}
private fun listen() {
val buffer = ByteBuffer.allocate(1024)
while (channel.isOpen) {
buffer.clear()
val sender =
try {
channel.receive(buffer) as InetSocketAddress
} catch (_: ClosedChannelException) {
return
}
buffer.flip()
val text = ByteArray(buffer.remaining()).also { buffer.get(it) }.decodeToString()
handled { beacons.heard(text, sender.address.hostAddress, System.currentTimeMillis()) }
}
}
}
// The threads of an executor named [name], daemons: they do not keep the JVM running
private fun daemons(name: String) = ThreadFactory { task -> Thread(task, name).apply { isDaemon = true } }