- Rename package/module from toki_socket to proto_socket in Dart, Kotlin, Python - Update crosstest implementations to use renamed packages - Add new proto_socket skill, deprecate add-toki-socket-crosstest-language skill - Update domain rules for all languages - Update documentation (README, PORTING_GUIDE, PROTOCOL, VERSIONING)
224 lines
7.6 KiB
Kotlin
224 lines
7.6 KiB
Kotlin
@file:JvmName("PythonKotlinClientKt")
|
|
|
|
package com.tokilabs.proto_socket.crosstest
|
|
|
|
import com.tokilabs.proto_socket.DialTcp
|
|
import com.tokilabs.proto_socket.DialTcpTls
|
|
import com.tokilabs.proto_socket.DialWs
|
|
import com.tokilabs.proto_socket.DialWss
|
|
import com.tokilabs.proto_socket.ParserMap
|
|
import com.tokilabs.proto_socket.WsClient
|
|
import com.tokilabs.proto_socket.TcpClient
|
|
import com.tokilabs.proto_socket.addListenerTyped
|
|
import com.tokilabs.proto_socket.sendRequestTyped
|
|
import com.tokilabs.proto_socket.typeNameOf
|
|
import com.tokilabs.proto_socket.packets.TestData
|
|
import kotlinx.coroutines.CompletableDeferred
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.coroutineScope
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.coroutines.withTimeout
|
|
import kotlin.system.exitProcess
|
|
|
|
private const val HOST = "127.0.0.1"
|
|
private const val WS_PATH = "/"
|
|
private const val CONNECT_WINDOW_MS = 3_000L
|
|
private const val REQUEST_WINDOW_MS = 2_000L
|
|
|
|
private interface PythonClientHandle {
|
|
suspend fun send(data: TestData)
|
|
suspend fun close()
|
|
val communicator: com.tokilabs.proto_socket.Communicator
|
|
}
|
|
|
|
private class PythonTcpHandle(
|
|
private val client: TcpClient,
|
|
) : PythonClientHandle {
|
|
override val communicator = client.communicator
|
|
override suspend fun send(data: TestData) { client.send(data) }
|
|
override suspend fun close() { client.close() }
|
|
}
|
|
|
|
private class PythonWsHandle(
|
|
private val client: WsClient,
|
|
) : PythonClientHandle {
|
|
override val communicator = client.communicator
|
|
override suspend fun send(data: TestData) { client.send(data) }
|
|
override suspend fun close() { client.close() }
|
|
}
|
|
|
|
fun main(args: Array<String>) = runBlocking {
|
|
val mode = argValue(args, "mode") ?: "tcp"
|
|
val phase = argValue(args, "phase") ?: "send-push"
|
|
val port = argValue(args, "port")?.toIntOrNull()
|
|
val cert = argValue(args, "cert")
|
|
|
|
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
|
|
|
|
if (port == null) {
|
|
fail("setup", "port is required")
|
|
exitProcess(1)
|
|
}
|
|
|
|
val client = try {
|
|
connectWithRetry(mode, port, cert)
|
|
} catch (error: Throwable) {
|
|
fail("setup", error.message ?: error.toString())
|
|
exitProcess(1)
|
|
}
|
|
|
|
val ok: Boolean = try {
|
|
when (phase) {
|
|
"send-push" -> runSendPush(client)
|
|
"requests" -> runRequests(client)
|
|
else -> {
|
|
fail("setup", "unknown phase $phase")
|
|
false
|
|
}
|
|
}
|
|
} finally {
|
|
client.close()
|
|
}
|
|
|
|
if (!ok) exitProcess(1)
|
|
}
|
|
|
|
private suspend fun connectWithRetry(mode: String, port: Int, cert: String?): PythonClientHandle {
|
|
val deadline = System.nanoTime() + CONNECT_WINDOW_MS * 1_000_000L
|
|
var lastError: Throwable? = null
|
|
while (System.nanoTime() < deadline) {
|
|
try {
|
|
return when (mode) {
|
|
"tcp" -> PythonTcpHandle(DialTcp(HOST, port, 0, 0, parserMap()))
|
|
"ws" -> PythonWsHandle(DialWs(HOST, port, WS_PATH, 0, 0, parserMap()))
|
|
"tls" -> {
|
|
val sslCtx = buildClientSslContext(cert ?: error("--cert required for tls"))
|
|
PythonTcpHandle(DialTcpTls(HOST, port, sslCtx, 0, 0, parserMap()))
|
|
}
|
|
"wss" -> {
|
|
val sslCtx = buildClientSslContext(cert ?: error("--cert required for wss"))
|
|
PythonWsHandle(DialWss(HOST, port, WS_PATH, sslCtx, 0, 0, parserMap()))
|
|
}
|
|
else -> error("unknown mode $mode")
|
|
}
|
|
} catch (error: Throwable) {
|
|
lastError = error
|
|
delay(100)
|
|
}
|
|
}
|
|
error("connect $mode:$port timed out: $lastError")
|
|
}
|
|
|
|
private fun buildClientSslContext(certPath: String): javax.net.ssl.SSLContext {
|
|
val certFactory = java.security.cert.CertificateFactory.getInstance("X.509")
|
|
val cert = java.io.File(certPath).inputStream().use {
|
|
certFactory.generateCertificate(it) as java.security.cert.X509Certificate
|
|
}
|
|
val trustStore = java.security.KeyStore.getInstance("PKCS12")
|
|
trustStore.load(null, null)
|
|
trustStore.setCertificateEntry("toki", cert)
|
|
val trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(
|
|
javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm(),
|
|
)
|
|
trustManagerFactory.init(trustStore)
|
|
val ctx = javax.net.ssl.SSLContext.getInstance("TLS")
|
|
ctx.init(null, trustManagerFactory.trustManagers, null)
|
|
return ctx
|
|
}
|
|
|
|
private suspend fun runSendPush(client: PythonClientHandle): Boolean {
|
|
val push = CompletableDeferred<TestData>()
|
|
addListenerTyped<TestData>(client.communicator) {
|
|
push.complete(it)
|
|
}
|
|
|
|
return try {
|
|
client.send(
|
|
TestData.newBuilder()
|
|
.setIndex(101)
|
|
.setMessage("fire from kotlin client")
|
|
.build(),
|
|
)
|
|
pass("1", "fire-and-forget sent")
|
|
|
|
val msg = withTimeout(REQUEST_WINDOW_MS) { push.await() }
|
|
if (msg.index != 200 || msg.message != "push from python server") {
|
|
fail("2", "unexpected push index=${msg.index} message=${msg.message}")
|
|
false
|
|
} else {
|
|
pass("2", "received push from python server")
|
|
true
|
|
}
|
|
} catch (error: Throwable) {
|
|
fail("2", error.message ?: error.toString())
|
|
false
|
|
}
|
|
}
|
|
|
|
private suspend fun runRequests(client: PythonClientHandle): Boolean =
|
|
try {
|
|
val single = sendRequestTyped<TestData, TestData>(
|
|
client.communicator,
|
|
TestData.newBuilder()
|
|
.setIndex(21)
|
|
.setMessage("single request from kotlin")
|
|
.build(),
|
|
REQUEST_WINDOW_MS,
|
|
)
|
|
if (single.index != 42 || single.message != "echo: single request from kotlin") {
|
|
fail("3", "unexpected response index=${single.index} message=${single.message}")
|
|
false
|
|
} else {
|
|
pass("3", "single request response matched")
|
|
runConcurrentRequests(client)
|
|
}
|
|
} catch (error: Throwable) {
|
|
fail("3", error.message ?: error.toString())
|
|
false
|
|
}
|
|
|
|
private suspend fun runConcurrentRequests(client: PythonClientHandle): Boolean =
|
|
coroutineScope {
|
|
val jobs = (0 until 5).map { i ->
|
|
async {
|
|
val index = 30 + i
|
|
val message = "multi request $i from kotlin"
|
|
val res = sendRequestTyped<TestData, TestData>(
|
|
client.communicator,
|
|
TestData.newBuilder()
|
|
.setIndex(index)
|
|
.setMessage(message)
|
|
.build(),
|
|
REQUEST_WINDOW_MS,
|
|
)
|
|
check(res.index == index * 2 && res.message == "echo: $message") {
|
|
"request $i got index=${res.index} message=${res.message}"
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
jobs.forEach { it.await() }
|
|
pass("4", "concurrent request responses matched")
|
|
true
|
|
} catch (error: Throwable) {
|
|
fail("4", error.message ?: error.toString())
|
|
false
|
|
}
|
|
}
|
|
|
|
private fun parserMap(): ParserMap =
|
|
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
|
|
|
|
private fun argValue(args: Array<String>, name: String): String? {
|
|
val prefix = "--$name="
|
|
return args.firstOrNull { it.startsWith(prefix) }?.substring(prefix.length)
|
|
}
|
|
|
|
private fun pass(scenario: String, detail: String) {
|
|
println("PASS scenario=$scenario detail=$detail")
|
|
}
|
|
|
|
private fun fail(scenario: String, error: String) {
|
|
println("FAIL scenario=$scenario error=$error")
|
|
}
|