Script monitoring

This commit is contained in:
2026-09-01 11:47:10 +02:00
parent 8da391b6ce
commit 85902620da
37 changed files with 803 additions and 71 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ val generatePartials = tasks.register<Exec>("generatePartials") {
description = "Generate Partial classes (requests with all-nullable fields)"
val scriptFile = project.file("partialize.main.kts")
val targets = fileTree(requestsDirectory) {
include("**/*.kt")
include("Requests.kt")
}
val lst = targets.map { it.absolutePath }
@@ -0,0 +1,62 @@
package com.jaytux.phoebench.common
import io.ktor.client.plugins.websocket.*
import io.ktor.http.*
import io.ktor.util.reflect.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import kotlin.uuid.Uuid
sealed class CSERoute<TParams, TEvent: Any>(val path: String, val elevation: Elevation, private val _eventType: TypeInfo) {
open val pattern = path
protected open fun buildUrl(params: TParams): String = path
abstract fun extractParams(reqParams: Parameters): TParams?
suspend fun call(client: IClient, params: TParams, body: suspend (sender: suspend (TEvent) -> Unit) -> Unit): Either<ErrorResponse, Unit> {
val server = client.serverUrl.replace("https", "ws").replace("http", "ws")
var error: ErrorResponse? = null
try {
client.client.webSocket("$server${buildUrl(params)}", {}) {
try {
body { sendSerialized(it, _eventType) }
}
catch(e: CancellationException) {
closeReason.await()?.let {
if(it.code != CloseReason.Codes.NORMAL.code) error = ErrorResponse(it.message)
}
throw e
}
}
return Unit.value()
}
catch(e: CancellationException) {
return (error ?: ErrorResponse(e.message ?: "Unknown websocket error")).error()
}
catch(e: WebSocketException) {
return ErrorResponse("Could not set up websocket stream: ${e.message}").error()
}
catch(e: Exception) {
return ErrorResponse("WebSocket connection failed: ${e.message}").error()
}
}
class CSERoute1<T1, TEvent: Any>(path: String, elevation: Elevation, eventType: TypeInfo, val urlEncode: (T1) -> String, val urlDecode: (String?) -> T1?)
: CSERoute<T1, TEvent>(path, elevation, eventType)
{
override val pattern: String = "$path/{param}"
override fun buildUrl(params: T1): String = "$path/${urlEncode(params)}"
override fun extractParams(reqParams: Parameters): T1? = urlDecode(reqParams["param"])
}
companion object {
inline fun <T1, reified TEvent: Any> single(path: String, elevation: Elevation,
noinline urlEncode: (T1) -> String = { it.toString() }, noinline urlDecode: (String?) -> T1?
) = CSERoute1<T1, TEvent>(path, elevation, typeInfo<TEvent>(), urlEncode, urlDecode)
inline fun <reified TEvent: Any> uuid(path: String, elevation: Elevation) = single<Uuid, TEvent>(path, elevation) {
it?.let { p -> Uuid.parseOrNull(p) }
}
}
}
@@ -0,0 +1,8 @@
package com.jaytux.phoebench.common
enum class CloseReasons(val code: Short) {
NOT_AUTHORIZED(4001),
INVALID_REQUEST(4002),
CONFLICT(4003),
NOT_FOUND(4004)
}
@@ -1,6 +1,8 @@
package com.jaytux.phoebench.common
import kotlinx.serialization.Serializable
import kotlin.collections.fold
import kotlin.time.Instant
import kotlin.uuid.Uuid
@Serializable
@@ -65,4 +67,83 @@ sealed class ProjectEvent {
@Serializable
data class EntryDeleted(val id: Uuid, val benchmarkId: Uuid) : ProjectEvent()
}
@Serializable
enum class Stream {
STDOUT, STDERR
}
@Serializable
enum class ANSI(val bitIdx: Int, val ansiCode: Int) {
BOLD(0, 1), UNDERLINE(1, 4),
FG_BLACK(2, 30), FG_RED(3, 31), FG_GREEN(4, 32),
FG_YELLOW(5, 33), FG_BLUE(6, 34), FG_MAGENTA(7, 35),
FG_CYAN(8, 36),
BG_BLACK(9, 40), BG_RED(10, 41), BG_GREEN(11, 42),
BG_YELLOW(12, 43), BG_BLUE(13, 44), BG_MAGENTA(14, 45),
BG_CYAN(15, 46);
companion object {
private val _mapping: Map<Int, ANSI>
val ansiMapping: Map<Int, ANSI>
init {
if(entries.map { it.bitIdx }.toSet().size != entries.size)
throw IllegalStateException("ANSI bit indices contain duplicates")
if(entries.map { it.ansiCode }.toSet().size != entries.size)
throw IllegalStateException("ANSI escape codes contain duplicates")
_mapping = entries.associateBy { ansi -> ansi.bitIdx }
ansiMapping = entries.associateBy { ansi -> ansi.ansiCode }
}
fun pack(vararg options: ANSI): UShort = options.fold(0u) { acc, ansi ->
acc or (1u shl ansi.bitIdx).toUShort()
}
fun unpack(packed: UShort): List<ANSI> {
val res = mutableListOf<ANSI>()
var remaining = packed
for(i in 0..15) {
if((remaining and 1u) != 0.toUShort()) {
_mapping[i]?.let { res += it }
}
remaining = (remaining.toUInt() shr 1).toUShort()
}
return res
}
}
}
@Serializable
sealed class ClientMonitorEvent {
@Serializable
data class Message(val msg: String, val stream: Stream, val options: UShort) : ClientMonitorEvent() {
constructor(msg: String, stream: Stream, vararg options: ANSI) : this(msg, stream, ANSI.pack(*options))
constructor(msg: String, stream: Stream, options: List<ANSI>) : this(msg, stream, ANSI.pack(*options.toTypedArray()))
}
@Serializable
data class ApplicationFinished(val exitCode: Int) : ClientMonitorEvent()
}
@Serializable
sealed class ServerMonitorEvent {
sealed interface ITextEvent
@Serializable
object Cleared : ServerMonitorEvent()
@Serializable
data class ApplicationStart(val time: Instant) : ServerMonitorEvent(), ITextEvent
@Serializable
data class Message(val timeStamp: Instant, val message: String, val options: UShort, val stream: Stream) : ServerMonitorEvent(), ITextEvent
@Serializable
data class Backlog(val start: ApplicationStart?, val messages: List<Message>, val end: ApplicationEnd?) : ServerMonitorEvent()
@Serializable
data class ApplicationEnd(val time: Instant, val exitCode: Int) : ServerMonitorEvent(), ITextEvent
}
@@ -29,6 +29,7 @@ object Routes {
val get = ApiRoute.getUuid<ProjectResponse>("/project", Elevation.AUTH)
val update = ApiRoute.patchUuidNoRes<PartialProjectRequest>("/project", Elevation.AUTH)
val delete = ApiRoute.deleteUuidNoRes("/project", Elevation.AUTH)
val rmMonitor = ApiRoute.deleteUuidNoRes("/project/monitor", Elevation.AUTH)
}
object Benchmark {
@@ -52,5 +53,10 @@ object Routes {
val home = SSERoute.noArgs<HomeEvent>("/rt/home", Elevation.AUTH)
val admin = SSERoute.noArgs<AdminEvent>("/rt/admin", Elevation.ADMIN)
val projectSpecific = SSERoute.uuid<ProjectEvent>("/rt/project", Elevation.AUTH)
val monitor = SSERoute.uuid<ServerMonitorEvent>("/rt/monitor", Elevation.AUTH)
}
object CSE {
val monitor = CSERoute.uuid<ClientMonitorEvent>("/stream/monitor", Elevation.AUTH)
}
}