Skip to content

Commit

Permalink
simulate hard reset (hacky)
Browse files Browse the repository at this point in the history
  • Loading branch information
froks committed Apr 23, 2024
1 parent 4ec5925 commit 2f95f6b
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 63 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ plugins {
apply<NexusReleasePlugin>()

group = "io.github.doip-sim-ecu"
version = "0.11.1"
version = "0.12.0-SNAPSHOT"

repositories {
gradlePluginPortal()
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/SimDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,14 @@ public open class ResponseData<T>(
public val pendingForCallback: () -> Unit
get() = _pendingForCallback

public val hardResetEntityFor: Duration?
get() = _hardResetEntityFor

private var _response: ByteArray = ByteArray(0)
private var _continueMatching: Boolean = false
private var _pendingFor: Duration? = null
private var _pendingForCallback: () -> Unit = {}
private var _hardResetEntityFor: Duration? = null

/**
* See [SimEcu.addOrReplaceTimer]
Expand Down Expand Up @@ -249,10 +253,22 @@ public open class ResponseData<T>(
_continueMatching = continueMatching
}

/**
* Sends a busy wait (NRC 0x78, received but pending) for duration, before calling
* callback, and sending the reply
*/
public fun pendingFor(duration: Duration, callback: () -> Unit = {}) {
_pendingFor = duration
_pendingForCallback = callback
}

/**
* Pretend to hard-reset entity by disconnecting the current, nd not being reachable for duration,
* and sending a vam after being reachable again
*/
public fun hardResetEntityFor(duration: Duration) {
_hardResetEntityFor = duration
}
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/main/kotlin/SimEcu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ public class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()
try {
matcher.responseHandler.invoke(responseData)
handlePending(request, responseData)

if (responseData.continueMatching) {
logger.logForRequest(matcher) { "Request for $name: '${request.message.toHexString(limit = 10, limitExceededByteCount = true)}' matched '$matcher' -> Continue matching" }
return@findMessageAndHandle false
Expand All @@ -233,11 +232,16 @@ public class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()
} else {
logger.logForRequest(matcher) { "Request for $name: '${request.message.toHexString(limit = 10, limitExceededByteCount = true)}' matched '$matcher' -> No response" }
}

throwDoipEntityExceptionsIfNecessary(matcher, request, responseData)
} catch (e: NrcException) {
handlePending(request, responseData)
val response = byteArrayOf(0x7F, request.message[0], e.code)
logger.logForRequest(matcher) { "Request for $name: '${request.message.toHexString(limit = 10, limitExceededByteCount = true)}' matched '$matcher' -> Send NRC response '${response.toHexString(limit = 10)}'" }
sendResponse(request, response)
} catch (e: DoipEntityHardResetException) {
// handled outside
throw e
} catch (e: Exception) {
logger.errorIf(e) { "An error occurred while processing a request for $name: '${request.message.toHexString(limit = 10, limitExceededByteCount = true)}' -> Sending NRC" }
sendResponse(request, byteArrayOf(0x7F, request.message[0], NrcError.GeneralProgrammingFailure))
Expand All @@ -255,6 +259,19 @@ public class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()
}
}

protected fun throwDoipEntityExceptionsIfNecessary(
matcher: RequestMatcher,
request: UdsMessage,
responseData: ResponseData<RequestMatcher>
) {
if (responseData.hardResetEntityFor != null) {
logger.logForRequest(matcher) { "Simulating hard reset for ${matcher.name}" }
val duration = responseData.hardResetEntityFor!!
throw DoipEntityHardResetException(this, duration, "Simulating Hard Reset for ${duration.inWholeMilliseconds} ms")
}

}

/**
* Adds an interceptor to the ecu.
*
Expand Down
156 changes: 95 additions & 61 deletions src/main/kotlin/library/DoipEntity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
public val ecus: List<T>
get() = _ecus

private lateinit var udpServerSocket: BoundDatagramSocket

protected abstract fun createEcu(config: EcuConfig): T

protected open fun createDoipUdpMessageHandler(): DoipUdpMessageHandler =
Expand Down Expand Up @@ -198,7 +200,7 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
public open fun findEcuByName(name: String, ignoreCase: Boolean = true): T? =
this.ecus.firstOrNull { name.equals(it.name, ignoreCase = ignoreCase) }

protected open fun CoroutineScope.handleTcpSocket(socket: DoipTcpSocket) {
protected open fun CoroutineScope.handleTcpSocket(socket: DoipTcpSocket, disableServerSocketCallback: (kotlin.time.Duration) -> Unit) {
launch {
logger.debugIf { "New incoming data connection from ${socket.remoteAddress}" }
val tcpMessageHandler = createDoipTcpMessageHandler(socket)
Expand Down Expand Up @@ -234,6 +236,12 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
socket.runCatching { this.close() }
}
}
} catch (e: DoipEntityHardResetException) {
logger.warn("Simulating Hard Reset on ${this@DoipEntity.name} for ${e.duration.inWholeMilliseconds} ms")
output.flush()
socket.close()

disableServerSocketCallback(e.duration)
} catch (e: Exception) {
if (!socket.isClosed) {
logger.error("Unknown error parsing/handling message, sending negative acknowledgment", e)
Expand Down Expand Up @@ -297,76 +305,37 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
}
}

public fun start() {
this._ecus.addAll(this.config.ecuConfigList.map { createEcu(it) })

targetEcusByLogical = this.ecus.associateBy { it.config.logicalAddress }
targetEcusByFunctional = _ecus.groupByTo(mutableMapOf()) { it.config.functionalAddress }
private val serverSockets: MutableList<ServerSocket> = mutableListOf()

_ecus.forEach {
it.simStarted()
}

thread(name = "UDP") {
runBlocking {
val serverSocket =
aSocket(ActorSelectorManager(Dispatchers.IO))
.udp()
.bind(localAddress = InetSocketAddress(config.localAddress, 13400)) {
broadcast = true
reuseAddress = true
// reusePort = true // not supported on windows
typeOfService = TypeOfService.IPTOS_RELIABILITY
// socket.joinGroup(multicastAddress)
}
logger.info("Listening on udp: ${serverSocket.localAddress}")
startVamTimer(serverSocket)
val udpMessageHandler = createDoipUdpMessageHandler()

if (config.localAddress != "0.0.0.0" && config.bindOnAnyForUdpAdditional) {
logger.info("Also listening on udp 0.0.0.0 for broadcasts")
val localAddress = InetSocketAddress("0.0.0.0", 13400)
val anyServerSocket =
aSocket(ActorSelectorManager(Dispatchers.IO))
.udp()
.bind(localAddress = localAddress) {
broadcast = true
reuseAddress = true
// reusePort = true // not supported on windows
typeOfService = TypeOfService.IPTOS_RELIABILITY
}
thread(start = true, isDaemon = true) {
runBlocking {
while (!anyServerSocket.isClosed) {
val datagram = anyServerSocket.receive()
if (datagram.address is InetSocketAddress) {
if (datagram.address == localAddress) {
continue
}
}
handleUdpMessage(udpMessageHandler, datagram, anyServerSocket)
}
}
}
}

while (!serverSocket.isClosed) {
val datagram = serverSocket.receive()
handleUdpMessage(udpMessageHandler, datagram, serverSocket)
}
public fun pauseTcpServerSockets(duration: kotlin.time.Duration) {
logger.warn("Closing serversockets")
serverSockets.forEach { try { it.close() } catch (ignored: Exception) {} }
serverSockets.clear()
logger.warn("Pausing server sockets for ${duration.inWholeMilliseconds} ms")
Thread.sleep(duration.inWholeMilliseconds)
logger.warn("Restarting server sockets after ${duration.inWholeMilliseconds} ms")
runBlocking {
launch {
startVamTimer(udpServerSocket)
}
launch {
startTcpServerSockets()
}
}
}

public fun startTcpServerSockets() {
thread(name = "TCP") {
runBlocking {
val serverSocket =
aSocket(ActorSelectorManager(Dispatchers.IO))
.tcp()
.bind(InetSocketAddress(config.localAddress, config.localPort))
serverSockets.add(serverSocket)
logger.info("Listening on tcp: ${serverSocket.localAddress}")
while (!serverSocket.isClosed) {
val socket = serverSocket.accept()
handleTcpSocket(DelegatedKtorSocket(socket))
handleTcpSocket(DelegatedKtorSocket(socket), ::pauseTcpServerSockets)
}
}
}
Expand Down Expand Up @@ -402,13 +371,15 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
.withTrustMaterial(trustMaterial)
.build()

val tlsServerSocket = withContext(Dispatchers.IO) {
val serverSocket = withContext(Dispatchers.IO) {
(sslFactory.sslServerSocketFactory.createServerSocket(
config.tlsPort,
50,
InetAddress.getByName(config.localAddress)
) as SSLServerSocket)
))
}
serverSockets.add(serverSocket as ServerSocket)
val tlsServerSocket = serverSocket as SSLServerSocket
logger.info("Listening on tls: ${tlsServerSocket.localSocketAddress}")

if (tlsOptions.tlsProtocols != null) {
Expand All @@ -431,11 +402,74 @@ public abstract class DoipEntity<out T : SimulatedEcu>(
while (!tlsServerSocket.isClosed) {
withContext(Dispatchers.IO) {
val socket = tlsServerSocket.accept() as SSLSocket
handleTcpSocket(SSLDoipTcpSocket(socket))
handleTcpSocket(SSLDoipTcpSocket(socket), ::pauseTcpServerSockets)
}
}
}
}
}
}

public fun start() {
this._ecus.addAll(this.config.ecuConfigList.map { createEcu(it) })

targetEcusByLogical = this.ecus.associateBy { it.config.logicalAddress }
targetEcusByFunctional = _ecus.groupByTo(mutableMapOf()) { it.config.functionalAddress }

_ecus.forEach {
it.simStarted()
}

thread(name = "UDP") {
runBlocking {
udpServerSocket =
aSocket(ActorSelectorManager(Dispatchers.IO))
.udp()
.bind(localAddress = InetSocketAddress(config.localAddress, 13400)) {
broadcast = true
reuseAddress = true
// reusePort = true // not supported on windows
typeOfService = TypeOfService.IPTOS_RELIABILITY
// socket.joinGroup(multicastAddress)
}
logger.info("Listening on udp: ${udpServerSocket.localAddress}")
startVamTimer(udpServerSocket)
val udpMessageHandler = createDoipUdpMessageHandler()

if (config.localAddress != "0.0.0.0" && config.bindOnAnyForUdpAdditional) {
logger.info("Also listening on udp 0.0.0.0 for broadcasts")
val localAddress = InetSocketAddress("0.0.0.0", 13400)
val anyServerSocket =
aSocket(ActorSelectorManager(Dispatchers.IO))
.udp()
.bind(localAddress = localAddress) {
broadcast = true
reuseAddress = true
// reusePort = true // not supported on windows
typeOfService = TypeOfService.IPTOS_RELIABILITY
}
thread(start = true, isDaemon = true) {
runBlocking {
while (!anyServerSocket.isClosed) {
val datagram = anyServerSocket.receive()
if (datagram.address is InetSocketAddress) {
if (datagram.address == localAddress) {
continue
}
}
handleUdpMessage(udpMessageHandler, datagram, anyServerSocket)
}
}
}
}

while (!udpServerSocket.isClosed) {
val datagram = udpServerSocket.receive()
handleUdpMessage(udpMessageHandler, datagram, udpServerSocket)
}
}
}

startTcpServerSockets()
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/library/Exceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package library

import SimEcu
import kotlin.time.Duration

public abstract class DoipEntityHandledException(message: String) : RuntimeException(message)

public class DoipEntityHardResetException(
public val ecu: SimEcu,
public val duration: Duration, message: String
) : DoipEntityHandledException(message)

public class DisableServerSocketException(
public val duration: Duration
) : DoipEntityHandledException("Disabling server socket for ${duration.inWholeMilliseconds} ms")
19 changes: 19 additions & 0 deletions src/test/kotlin/SimEcuTest.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import assertk.assertThat
import assertk.assertions.*
import io.ktor.utils.io.*
import library.DoipEntityHardResetException
import library.UdsMessage
import library.decodeHex
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito
import org.mockito.kotlin.*
import java.lang.Thread.sleep
import kotlin.concurrent.thread
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class SimEcuTest {
@Test
Expand Down Expand Up @@ -463,6 +466,22 @@ class SimEcuTest {
assertThat(invokeAfterCalled).isTrue()
}

@Test
fun `test hard reset`() {
val ecu = spy(SimEcu(ecuData(
name = "TEST",
requests = listOf(
RequestMatcher("TEST", byteArrayOf(0x3E, 0x00)) {
hardResetEntityFor(5.seconds)
ack()
},
)
)))
assertThrows<DoipEntityHardResetException> {
ecu.handleRequest(req(byteArrayOf(0x3e, 0x00)))
}
}

private fun ecuData(
name: String,
logicalAddress: Short = 0x0001,
Expand Down
Loading

0 comments on commit 2f95f6b

Please sign in to comment.