CLI client: more robust

This commit is contained in:
2026-08-13 15:22:53 +02:00
parent 8494a1a9bb
commit 462c6da965
4 changed files with 64 additions and 17 deletions
@@ -80,9 +80,10 @@ object Client {
val res = route.call(client, body) val res = route.call(client, body)
res res
} catch (e: CancellationException) { } catch (e: CancellationException) {
println("Coroutine calling ${route.verb} ${route.pattern} was cancelled.")
ErrorResponse("Coroutine calling ${route.verb} ${route.pattern} was cancelled.").error() ErrorResponse("Coroutine calling ${route.verb} ${route.pattern} was cancelled.").error()
} catch (e: Exception) { } catch (e: Exception) {
// println("Call to ${route.pattern} ran into an exception") println("Call to ${route.pattern} ran into an exception: ${e.message} (${e::class.simpleName})")
ErrorResponse(e.message ?: "Unknown error.").error() ErrorResponse(e.message ?: "Unknown error.").error()
} }
} }
@@ -1,7 +1,10 @@
package com.jaytux.phoebench.clients.cli package com.jaytux.phoebench.clients.cli
import kotlin.system.exitProcess
fun main(args: Array<String>) { fun main(args: Array<String>) {
Client.forceGloballyInitialized() Client.forceGloballyInitialized()
CLI.main(args) CLI.main(args)
Client.shutdown() Client.shutdown()
exitProcess(0) // because D-Bus threads...
} }
@@ -1,14 +1,63 @@
package com.jaytux.phoebench.clients.cli package com.jaytux.phoebench.clients.cli
import com.github.javakeyring.BackendNotSupportedException
import com.github.javakeyring.Keyring import com.github.javakeyring.Keyring
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import java.util.prefs.Preferences
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
object PersistentStorage { object PersistentStorage {
private val json = Json private val json = Json
const val SERVICE = "com.jaytux.phoebench.cli" const val SERVICE = "com.jaytux.phoebench.cli"
private var _backend: IBackend = KeyringBackend
interface IBackend {
fun read(key: String): String?
fun write(key: String, value: String)
fun erase(key: String)
}
private object KeyringBackend : IBackend {
override fun read(key: String): String? = try {
Keyring.create().use { it.getPassword(SERVICE, key) }
} catch(e: Exception) {
_backend = PreferencesBackend
_backend.read(key)
}
override fun write(key: String, value: String) {
try {
Keyring.create().use { it.setPassword(SERVICE, key, value) }
} catch(e: Exception) {
_backend = PreferencesBackend
_backend.write(key, value)
}
}
override fun erase(key: String) {
try {
Keyring.create().use { it.deletePassword(SERVICE, key) }
} catch (e: Exception) {
_backend = PreferencesBackend
_backend.erase(key)
}
}
}
private object PreferencesBackend : IBackend {
private val prefs = Preferences.userNodeForPackage(this::class.java)
override fun read(key: String): String? = prefs.get(key, null)
override fun write(key: String, value: String) {
prefs.put(key, value)
}
override fun erase(key: String) {
prefs.remove(key)
}
}
class StoredProperty<T>( class StoredProperty<T>(
private val _key: String, private val _key: String,
@@ -20,21 +69,15 @@ object PersistentStorage {
) )
fun load(): T? = runCatching { fun load(): T? = runCatching {
Keyring.create().use { _backend.read(_key)
it.getPassword(SERVICE, _key)
}
}.getOrNull()?.let { _fromString(it) } }.getOrNull()?.let { _fromString(it) }
fun save(value: T) = runCatching { fun save(value: T) = runCatching {
val keyring = Keyring.create().use { _backend.write(_key, _toString(value))
it.setPassword(SERVICE, _key, _toString(value))
}
}.onFailure { println("Failed to write to OS keyring: ${it.message}") }.ignore() }.onFailure { println("Failed to write to OS keyring: ${it.message}") }.ignore()
fun erase() = runCatching { fun erase() = runCatching {
val keyring = Keyring.create().use { _backend.erase(_key)
it.deletePassword(SERVICE, _key)
}
}.ignore() }.ignore()
} }
@@ -67,15 +67,15 @@ object ProjectHandlers {
System.err.println("Failed to get project details: ${it.msg}") System.err.println("Failed to get project details: ${it.msg}")
}) { }) {
println("Project ${it.owner.name}/${it.name} [${it.id}]:") println("Project ${it.owner.name}/${it.name} [${it.id}]:")
println("${it.usedLabels.size} labels:") println("${it.usedLabels.size} versions:")
it.usedLabels.forEach { l -> it.usedLabels.forEach { v ->
println(" - [${l.id}] ${l.name} (with color ${l.color})") println(" - [${v.id}] VERSION ${v.name} (with color ${v.color}); metadata: ${v.meta}")
} }
val map = it.usedLabels.associateBy { l -> l.id } val map = it.usedLabels.associateBy { l -> l.id }
println("\n${it.entries.size} entries:") println("\n${it.entries.size} entries:")
it.entries.forEach { e -> it.entries.forEach { e ->
println(" - [${e.id}] Entry labeled ${map[e.labelId]?.name ?: "<invalid label>"} at ${e.timestamp.fmt()} " + println(" - [${e.id}] ENTRY versioned ${map[e.labelId]?.name ?: "<invalid version>"} at ${e.timestamp.fmt()} " +
"(${e.warmups.size} warmup data points, ${e.measurements.size} measurement data points; in ${e.unit.disp})") "(${e.warmups.size} warmup data points, ${e.measurements.size} measurement data points; in ${e.unit.disp})")
} }
} }
@@ -94,7 +94,7 @@ object ProjectHandlers {
fun newVersion(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?, version: String?) { fun newVersion(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?, version: String?) {
val id = ensureProjectIdentification(project) val id = ensureProjectIdentification(project)
val useName = name.maybePrompt("label name") { it } val useName = name.maybePrompt("version name") { it }
val useColor = color.maybePrompt("color") { val useColor = color.maybePrompt("color") {
val check = it.length == 7 && it[0] == '#' && it.substring(1, it.length).all { c -> c.isDigit() || c in "ABCDEF" } val check = it.length == 7 && it[0] == '#' && it.substring(1, it.length).all { c -> c.isDigit() || c in "ABCDEF" }
if(check) it if(check) it
@@ -110,7 +110,7 @@ object ProjectHandlers {
Client.callRoute(Routes.Version.new, VersionRequest(useName, useColor, useVersion, projectId)) Client.callRoute(Routes.Version.new, VersionRequest(useName, useColor, useVersion, projectId))
} }
}.fold({ }.fold({
System.err.println("Could not create label: ${it.msg}") System.err.println("Could not create version: ${it.msg}")
}) { }) {
println("Label ${it.name} (${it.color}) [${it.id}] created.") println("Label ${it.name} (${it.color}) [${it.id}] created.")
} }
@@ -164,7 +164,7 @@ object ProjectHandlers {
is CLI.Commands.Project.ID -> versionId.id.value() is CLI.Commands.Project.ID -> versionId.id.value()
is CLI.Commands.Project.VersionName -> Client.callRoute(Routes.Project.get, pId).bind { is CLI.Commands.Project.VersionName -> Client.callRoute(Routes.Project.get, pId).bind {
it.usedLabels.firstOrNull { l -> l.name == versionId.name }?.id?.value() it.usedLabels.firstOrNull { l -> l.name == versionId.name }?.id?.value()
?: ErrorResponse("Label ${versionId.name} does not exist in this project.").error() ?: ErrorResponse("Version ${versionId.name} does not exist in this project.").error()
} }
}.map { pId to it } }.map { pId to it }
}.bind { (pId, vId) -> }.bind { (pId, vId) ->