Script monitoring
This commit is contained in:
@@ -13,7 +13,7 @@ plugins {
|
||||
alias(libs.plugins.buildconfig)
|
||||
}
|
||||
|
||||
version = PhoebenchVersion(1, 1, 2)
|
||||
version = rootProject.version as PhoebenchVersion
|
||||
|
||||
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
|
||||
throw GradleException("UI Clients version must be at least as high as protocol/common version")
|
||||
@@ -142,23 +142,6 @@ compose.desktop {
|
||||
}
|
||||
}
|
||||
|
||||
fun envValue(key: String): String? {
|
||||
val fromEnv = providers.environmentVariable(key).orNull
|
||||
if(fromEnv != null) return fromEnv
|
||||
|
||||
val envFile = rootProject.file(".env")
|
||||
if(!envFile.exists()) return null
|
||||
|
||||
return envFile.useLines { lines ->
|
||||
lines.map { it.trim() }.filter { it.isNotBlank() && !it.startsWith('#') && '=' in it }
|
||||
.map { line ->
|
||||
val (k, v) = line.split('=', limit = 2)
|
||||
k.trim() to v.trim().removeSurrounding("\"").removeSurrounding("'")
|
||||
}
|
||||
.firstOrNull { it.first == key }?.second
|
||||
}
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
generateAtSync = false
|
||||
useKotlinOutput { internalVisibility = false }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,8 +4,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.jaytux.phoebench.common.Either
|
||||
@@ -18,9 +24,10 @@ import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.Padding
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.Font
|
||||
import phoebench.clients.compose.generated.resources.*
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.time.Clock
|
||||
@@ -144,4 +151,16 @@ fun List<Float>.geomean(): Float {
|
||||
var prod = 1.0f
|
||||
for(value in this) prod *= value
|
||||
return prod.pow(1.0f / size.toFloat())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextStyle.makeMonospaced(): TextStyle {
|
||||
val family = FontFamily(
|
||||
Font(Res.font.JetBrainsMono_Regular, weight = FontWeight.Normal, style = FontStyle.Normal),
|
||||
Font(Res.font.JetBrainsMono_Bold, weight = FontWeight.Bold, style = FontStyle.Normal),
|
||||
Font(Res.font.JetBrainsMono_Italic, weight = FontWeight.Normal, style = FontStyle.Italic),
|
||||
Font(Res.font.JetBrainsMono_BoldItalic, weight = FontWeight.Bold, style = FontStyle.Italic)
|
||||
)
|
||||
|
||||
return copy(fontFamily = family)
|
||||
}
|
||||
+5
@@ -38,6 +38,8 @@ interface IProjectRepo {
|
||||
unit: TimeUnit, input: String, hardware: String): Either<ErrorResponse, EntryResponse>
|
||||
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun deleteMonitorLogs(): Either<ErrorResponse, Unit>
|
||||
|
||||
companion object {
|
||||
class Default(private val _client: Client, private val _projectId: Uuid) : IProjectRepo {
|
||||
override suspend fun get(): Either<ErrorResponse, ProjectResponse> =
|
||||
@@ -74,6 +76,9 @@ interface IProjectRepo {
|
||||
|
||||
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
|
||||
|
||||
override suspend fun deleteMonitorLogs(): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Project.rmMonitor, _projectId).ignoreValue()
|
||||
}
|
||||
|
||||
fun default(client: Client, projectId: Uuid) = Default(client, projectId)
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.jaytux.phoebench.common.ErrorResponse
|
||||
import com.jaytux.phoebench.common.HomeEvent
|
||||
import com.jaytux.phoebench.common.ProjectEvent
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.ServerMonitorEvent
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
interface ISSERepo {
|
||||
suspend fun connectHome(onEvent: suspend (Either<ErrorResponse, HomeEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectAdmin(onEvent: suspend (Either<ErrorResponse, AdminEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectProject(id: Uuid, onEvent: suspend (Either<ErrorResponse, ProjectEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectMonitor(id: Uuid, onEvent: suspend (Either<ErrorResponse, ServerMonitorEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
|
||||
companion object {
|
||||
class Default(private val _client: Client) : ISSERepo {
|
||||
@@ -24,6 +26,9 @@ interface ISSERepo {
|
||||
|
||||
override suspend fun connectProject(id: Uuid, onEvent: suspend (Either<ErrorResponse, ProjectEvent>) -> Unit): Either<ErrorResponse, Unit> =
|
||||
_client.callSSE(Routes.SSE.projectSpecific, id, onEvent)
|
||||
|
||||
override suspend fun connectMonitor(id: Uuid, onEvent: suspend (Either<ErrorResponse, ServerMonitorEvent>) -> Unit): Either<ErrorResponse, Unit> =
|
||||
_client.callSSE(Routes.SSE.monitor, id, onEvent)
|
||||
}
|
||||
|
||||
fun default(client: Client) = Default(client)
|
||||
|
||||
+40
-1
@@ -1,5 +1,6 @@
|
||||
package com.jaytux.phoebench.clients.data
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -18,7 +19,9 @@ import com.jaytux.phoebench.common.BenchmarkResponse
|
||||
import com.jaytux.phoebench.common.EntryResponse
|
||||
import com.jaytux.phoebench.common.VersionResponse
|
||||
import com.jaytux.phoebench.common.ProjectEvent
|
||||
import com.jaytux.phoebench.common.ServerMonitorEvent
|
||||
import com.jaytux.phoebench.common.TimeUnit
|
||||
import com.jaytux.phoebench.common.fold
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
@@ -87,6 +90,7 @@ class ProjectVM(
|
||||
private val _currentBenchmark = mutableStateOf(0)
|
||||
private val _inputs = mutableStateOf(setOf<String>())
|
||||
private val _hardware = mutableStateOf(setOf<String>())
|
||||
private val _monitorMessages = mutableStateOf<List<ServerMonitorEvent.ITextEvent>?>(null)
|
||||
|
||||
val name = _name.immutable()
|
||||
val owner = _owner.immutable()
|
||||
@@ -97,8 +101,10 @@ class ProjectVM(
|
||||
val currentBenchmark = _currentBenchmark.immutable()
|
||||
val inputs = _inputs.immutable()
|
||||
val hardware = _hardware.immutable()
|
||||
val monitorMessages = _monitorMessages.immutable()
|
||||
|
||||
private var _job: Job? = null
|
||||
private var _monitorJob: Job? = null
|
||||
|
||||
init {
|
||||
_job = withScope {
|
||||
@@ -221,7 +227,7 @@ class ProjectVM(
|
||||
val (updated, idx) = _benchmarks.value.insortIdx(
|
||||
Benchmark.fromResponse(bench, _labels.value, { i -> _inputs.value += i }, { hw -> _hardware.value += hw })
|
||||
) { it.name }
|
||||
_currentBenchmark.value?.let { curr ->
|
||||
_currentBenchmark.value.let { curr ->
|
||||
if(idx != null) {
|
||||
if(idx < curr) _currentBenchmark.value = curr + 1
|
||||
}
|
||||
@@ -279,4 +285,37 @@ class ProjectVM(
|
||||
fun selectBenchmark(id: Uuid) {
|
||||
_currentBenchmark.value = maxOf(_benchmarks.value.indexOfFirst { it.id == id }, 0)
|
||||
}
|
||||
|
||||
fun openMonitor() {
|
||||
if(_monitorJob != null) return
|
||||
_monitorMessages.value = listOf()
|
||||
_monitorJob = withScope {
|
||||
_sseRepo.connectMonitor(_id) { ev ->
|
||||
ev.snackOr {
|
||||
when(it) {
|
||||
is ServerMonitorEvent.Backlog -> {
|
||||
val res = ArrayList<ServerMonitorEvent.ITextEvent>(it.messages.size + 2)
|
||||
it.start?.let { start -> res += start }
|
||||
res.addAll(it.messages)
|
||||
it.end?.let { end -> res += end }
|
||||
_monitorMessages.value = res
|
||||
}
|
||||
ServerMonitorEvent.Cleared -> _monitorMessages.value = listOf()
|
||||
is ServerMonitorEvent.ApplicationEnd, is ServerMonitorEvent.ApplicationStart, is ServerMonitorEvent.Message ->
|
||||
_monitorMessages.value = (_monitorMessages.value ?: listOf()) + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeMonitor() {
|
||||
_monitorJob?.cancel()
|
||||
_monitorJob = null
|
||||
_monitorMessages.value = null
|
||||
}
|
||||
|
||||
fun clearMonitor() = withScope {
|
||||
_repo.deleteMonitorLogs().snackOnError()
|
||||
}
|
||||
}
|
||||
+92
-9
@@ -18,12 +18,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.composables.icons.lucide.*
|
||||
import com.jaytux.phoebench.clients.*
|
||||
@@ -57,6 +59,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
val versions by vm.versions
|
||||
val benchmarks by vm.benchmarks
|
||||
val currentBenchmark by vm.currentBenchmark
|
||||
val monitor by vm.monitorMessages
|
||||
|
||||
var editing by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
@@ -64,16 +67,20 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
|
||||
Column(Modifier.padding(all = 15.dp)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Project ${name ?: "Unnamed Project"}", style = MaterialTheme.typography.headlineMedium)
|
||||
if(editable) {
|
||||
Spacer(Modifier.width(25.dp))
|
||||
IconButton({ editing = true }) {
|
||||
Icon(Lucide.Pencil, "Edit project details")
|
||||
Text("Project ${name ?: "Unnamed Project"}", style = MaterialTheme.typography.headlineMedium)
|
||||
if (editable) {
|
||||
Spacer(Modifier.width(25.dp))
|
||||
IconButton({ editing = true }) {
|
||||
Icon(Lucide.Pencil, "Edit project details")
|
||||
}
|
||||
IconButton({ deleting = true }) {
|
||||
Icon(Lucide.Trash, "Delete project")
|
||||
}
|
||||
IconButton({ vm.openMonitor() }) {
|
||||
Icon(Lucide.SquareTerminal, "Open monitor")
|
||||
}
|
||||
}
|
||||
IconButton({ deleting = true }) {
|
||||
Icon(Lucide.Trash, "Delete project")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") }
|
||||
Spacer(Modifier.height(15.dp))
|
||||
@@ -112,6 +119,82 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
vm.mkBenchmark(it)
|
||||
addingBenchmark = false
|
||||
}
|
||||
|
||||
monitor?.let { MonitorDialog(name ?: "Unnamed Project", it, vm::closeMonitor, vm::clearMonitor) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonitorMessage(msg: ServerMonitorEvent.ITextEvent) = when(msg) {
|
||||
is ServerMonitorEvent.ApplicationEnd -> Text("[${msg.time.fmt()}] Application finished with exit code ${msg.exitCode}.", fontStyle = FontStyle.Italic)
|
||||
is ServerMonitorEvent.ApplicationStart -> Text("[${msg.time.fmt()}] Application started.", fontStyle = FontStyle.Italic)
|
||||
is ServerMonitorEvent.Message -> {
|
||||
val ansi = ANSI.unpack(msg.options).toSet()
|
||||
|
||||
val weight = if(ANSI.BOLD in ansi) FontWeight.Bold else null
|
||||
val decoration = if(ANSI.UNDERLINE in ansi) TextDecoration.Underline else null
|
||||
val color = when {
|
||||
ANSI.FG_BLACK in ansi -> Color.Black
|
||||
ANSI.FG_RED in ansi -> Color.Red
|
||||
ANSI.FG_GREEN in ansi -> Color.Green
|
||||
ANSI.FG_YELLOW in ansi -> Color.Yellow
|
||||
ANSI.FG_BLUE in ansi -> Color.Blue
|
||||
ANSI.FG_MAGENTA in ansi -> Color.Magenta
|
||||
ANSI.FG_CYAN in ansi -> Color.Cyan
|
||||
else -> LocalContentColor.current
|
||||
}
|
||||
|
||||
val background = when {
|
||||
ANSI.BG_BLACK in ansi -> Color.Black
|
||||
ANSI.BG_RED in ansi -> Color.Red
|
||||
ANSI.BG_GREEN in ansi -> Color.Green
|
||||
ANSI.BG_YELLOW in ansi -> Color.Yellow
|
||||
ANSI.BG_BLUE in ansi -> Color.Blue
|
||||
ANSI.BG_MAGENTA in ansi -> Color.Magenta
|
||||
ANSI.BG_CYAN in ansi -> Color.Cyan
|
||||
else -> null
|
||||
}?.let { Modifier.background(it) } ?: Modifier
|
||||
|
||||
Box(background.fillMaxWidth()) {
|
||||
Text(
|
||||
"[${msg.timeStamp.fmt()}] [${if (msg.stream == Stream.STDOUT) 'O' else 'E'}] ${msg.message}",
|
||||
fontWeight = weight, textDecoration = decoration, color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonitorDialog(name: String, logs: List<ServerMonitorEvent.ITextEvent>, onClose: () -> Unit, onClear: () -> Unit) {
|
||||
Dialog(onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||
Surface(Modifier.padding(15.dp).fillMaxHeight(0.8f).widthIn(min = 300.dp, max = 2000.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text("Monitor for $name", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
if(logs.isEmpty()) {
|
||||
Box(Modifier.weight(1f)) {
|
||||
Box(Modifier.fillMaxHeight(0.33f).fillMaxWidth()) {
|
||||
Text("No monitor logs.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
CompositionLocalProvider(LocalTextStyle provides LocalTextStyle.current.makeMonospaced()) {
|
||||
LazyColumn(Modifier.weight(1f).padding(10.dp).background(MaterialTheme.colorScheme.surfaceDim)) {
|
||||
items(logs) {
|
||||
MonitorMessage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row {
|
||||
Button(onClear, Modifier.weight(0.5f)) { Text("Clear Monitor") }
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Button(onClose, Modifier.weight(0.5f)) { Text("Close Monitor") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-5
@@ -2,11 +2,7 @@ package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferAction
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferable
|
||||
import androidx.compose.ui.draganddrop.awtTransferable
|
||||
import androidx.compose.ui.draganddrop.*
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import com.github.javakeyring.Keyring
|
||||
import com.jaytux.phoebench.clients.ui.DefaultServerSelect
|
||||
|
||||
-2
@@ -13,8 +13,6 @@ import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import org.w3c.dom.DataTransfer
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PhoeBench</title>
|
||||
<link type="text/css" rel="stylesheet" href="styles.css">
|
||||
<link type="text/css" rel="stylesheet" href="styles.css" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Regular.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Bold.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Italic.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-BoldItalic.ttf" />
|
||||
<script type="application/javascript" src="composeApp.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user