Skip to content

LiveProbeClient.kt

src/jvmMain/kotlin/afield/app/diagnostics/debug/LiveProbeClient.kt · 95 lines

package afield.app.diagnostics.debug

Field's side of the JDI probe helper: LiveProbeClient starts the helper process, sends it probe requests and routes each response to the listener registered for its request id.

import afield.app.diagnostics.logging.lprintln
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString

class LiveProbeClient

class LiveProbeClient(
    private val helperMainClass: String,
    private val debugHost: String = "127.0.0.1",
    private val debugPort: Int = 5005,
    private val javaBin: String = "java",
    private val classPath: String = System.getProperty("java.class.path")
) : AutoCloseable {

    private val process: Process
    private val writer: PrintWriter
    private val reader: BufferedReader
    private val executor = Executors.newSingleThreadExecutor { r ->
        Thread(r, "live-probe-client-reader").apply { isDaemon = true }
    }

    private val listeners = ConcurrentHashMap<String, (ProbeResponse) -> Unit>()

init

    init {
        val pb = ProcessBuilder(
            javaBin,
            "--add-modules=jdk.jdi",
            "-cp",
            classPath,
            helperMainClass,
            debugHost,
            debugPort.toString()
        )
        pb.redirectErrorStream(true)
        process = pb.start()
        writer = PrintWriter(process.outputStream, true)
        reader = BufferedReader(InputStreamReader(process.inputStream))
        startReader()
    }

fun probe(request: ProbeVariableRequest, onEvent: (ProbeResponse) -> Unit): String {

    fun probe(request: ProbeVariableRequest, onEvent: (ProbeResponse) -> Unit): String {
        listeners[request.requestId] = onEvent

        synchronized(writer) {
            writer.println(ProbeJson.encodeToString<ProbeRequest>(request))
            writer.flush()
        }

        return request.requestId
    }

fun isAlive(): Boolean = process.isAlive

override fun close()

    override fun close() {
        runCatching { writer.close() }
        runCatching { reader.close() }
        runCatching { process.destroy() }
        executor.shutdownNow()
    }

    private fun startReader() {
        executor.submit {
            while (true) {
                val line = reader.readLine() ?: break

                val response = try {
                    ProbeJson.decodeFromString<ProbeResponse>(line)
                } catch (_: Throwable) {
                    if (line.isNotBlank()) {
                        lprintln("warning,debug-probe", "ignoring helper output: $line")
                    }
                    continue
                }

                val listener = listeners[response.requestId]
                listener?.invoke(response)

                if (response is CompleteResponse) {
                    listeners.remove(response.requestId)
                }
            }
        }
    }
}