Shell peer: send and read the 0.32 kinds, and a bot mode
Test harness only. Cards, events and answers, games (replayed with the games engine), photo stories with edits and take-downs, stickers and directory shares, all built with the engine's own encoders. The bot polls, logs every envelope it opens, and runs commands dropped in a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
067e6fcf51
commit
1d6de8ba72
@@ -14,6 +14,8 @@ repositories { mavenCentral() }
|
||||
dependencies {
|
||||
implementation(project(":engine"))
|
||||
implementation(project(":client"))
|
||||
// The shell peer replays game logs with the app's own rules engine.
|
||||
implementation(project(":games"))
|
||||
implementation("org.json:json:20240303")
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ object Peer {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
if (args.size < 2) {
|
||||
System.err.println("usage: peer <state.json> <create|add|send|poll|whoami> [...]")
|
||||
System.err.println("usage: peer <state.json> <create|add|send|poll|whoami|claim|inspect|do|bot> [...]")
|
||||
kotlin.system.exitProcess(2)
|
||||
}
|
||||
val stateFile = File(args[0])
|
||||
@@ -70,8 +70,31 @@ object Peer {
|
||||
val conversations =
|
||||
Conversations(identity, vault, relay)
|
||||
.also { it.myUserId = state.getLong("userId") }
|
||||
// The 0.32 kinds, and the long-running correspondent (test harness).
|
||||
val kinds = PeerKinds(stateFile, state, relay, api, conversations)
|
||||
|
||||
when (command) {
|
||||
"claim" -> {
|
||||
println(kinds.claim())
|
||||
}
|
||||
|
||||
/** One 0.32 command, as JSON: `peer state.json do '{"cmd":"card",...}'`. */
|
||||
"do" -> {
|
||||
println(kinds.run(JSONObject(args.drop(2).joinToString(" "))))
|
||||
}
|
||||
|
||||
/** Poll, describing every envelope in full. */
|
||||
"inspect" -> {
|
||||
val out = org.json.JSONArray()
|
||||
conversations.poll().forEach { out.put(kinds.describe(it)) }
|
||||
println(JSONObject().put("messages", out))
|
||||
}
|
||||
|
||||
/** `peer state.json bot <command-folder> [seconds]` */
|
||||
"bot" -> {
|
||||
kinds.bot(File(args[2]), args.getOrNull(3)?.toIntOrNull() ?: 3600)
|
||||
}
|
||||
|
||||
"whoami" -> {
|
||||
println(
|
||||
JSONObject()
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
package io.flamenet.interop
|
||||
|
||||
import io.flamenet.client.AccountIdentity
|
||||
import io.flamenet.client.Conversations
|
||||
import io.flamenet.client.MessengerApi
|
||||
import io.flamenet.client.RelayClient
|
||||
import io.flamenet.e2e.Content
|
||||
import io.flamenet.games.GameType
|
||||
import io.flamenet.games.Games
|
||||
import io.flamenet.games.Outcome
|
||||
import io.flamenet.games.ReplayResult
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* The 0.32 kinds for the shell peer: cards, events and answers, games, photo
|
||||
* stories with their edits and take-downs, stickers and directory shares.
|
||||
*
|
||||
* Test harness only. Every envelope is built by the engine's own encoders
|
||||
* (`Content.card`, `Content.event`, ...), so what the other app receives is
|
||||
* the Android app's bytes, not a copy of them written here.
|
||||
*
|
||||
* `bot` is a long-running correspondent: it polls, writes one line per
|
||||
* envelope it opens, and carries out commands dropped into a folder as
|
||||
* `<name>.cmd.json`, answering each with `<name>.done.json`. That lets a UI
|
||||
* test on the other platform say "send the sealed card now" without a second
|
||||
* device.
|
||||
*/
|
||||
internal class PeerKinds(
|
||||
private val stateFile: File,
|
||||
private val state: JSONObject,
|
||||
private val relay: RelayClient,
|
||||
private val api: MessengerApi,
|
||||
private val conversations: Conversations,
|
||||
) {
|
||||
private val myUserId = state.getLong("userId")
|
||||
|
||||
/** What this peer knows about each game: its type, the side it plays, the log. */
|
||||
private data class Track(
|
||||
val type: String,
|
||||
val mine: String,
|
||||
var log: List<String>,
|
||||
var ending: String? = null,
|
||||
var endedBy: String? = null,
|
||||
)
|
||||
|
||||
private val games = HashMap<String, Track>()
|
||||
|
||||
/** Events this peer organised, so a revision can carry the whole event. */
|
||||
private val events = HashMap<String, Content.Event>()
|
||||
|
||||
/** The last `n` sent for each event (DESIGN 7). */
|
||||
private val answered = HashMap<String, Long>()
|
||||
|
||||
private val handles = HashMap<String, Long>()
|
||||
|
||||
/** Every envelope opened by `bot`, in order, as `describe` wrote it. */
|
||||
private val received = mutableListOf<JSONObject>()
|
||||
|
||||
/** Where the last `mark` left the received list. */
|
||||
private var lastMark = 0
|
||||
|
||||
/** The newest event, game and story ids seen, for `"latest"`. */
|
||||
private var lastEventId: String? = null
|
||||
private var lastGameId: String? = null
|
||||
private var lastPostedStory: String? = null
|
||||
|
||||
// ---- Who ------------------------------------------------------------------
|
||||
|
||||
private fun userIdFor(to: String): Long {
|
||||
to.toLongOrNull()?.let { return it }
|
||||
handles[to]?.let { return it }
|
||||
val found =
|
||||
api.buddies().firstOrNull { it.handle == to || it.displayName == to }?.userId
|
||||
?: api.lookup(to)?.let { me -> me.userId }
|
||||
?: error("nobody called '$to' on this relay")
|
||||
handles[to] = found
|
||||
return found
|
||||
}
|
||||
|
||||
/** Claim an account address (fnid) with a fresh seed, as the app does at sign-in. */
|
||||
fun claim(): JSONObject {
|
||||
state.optString("fnid").takeIf { it.isNotEmpty() }?.let { fnid ->
|
||||
return JSONObject().put("fnid", fnid).put("address", address(fnid))
|
||||
}
|
||||
val seed = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||
val fnid = AccountIdentity.claim(relay, seed, myUserId)
|
||||
state.put("fnid", fnid)
|
||||
state.put("accountSeed", java.util.Base64.getEncoder().encodeToString(seed))
|
||||
stateFile.writeText(state.toString(2))
|
||||
return JSONObject().put("fnid", fnid).put("address", address(fnid))
|
||||
}
|
||||
|
||||
private fun address(fnid: String): String {
|
||||
val host = java.net.URI(state.getString("relay")).host
|
||||
return "$fnid@$host"
|
||||
}
|
||||
|
||||
// ---- Reading --------------------------------------------------------------
|
||||
|
||||
/** One opened envelope, as plainly as it can be written down. */
|
||||
fun describe(r: Conversations.Received): JSONObject {
|
||||
val c = r.content
|
||||
val o =
|
||||
JSONObject()
|
||||
.put("at", System.currentTimeMillis())
|
||||
.put("id", r.id)
|
||||
.put("from_user", r.fromUser)
|
||||
.put("from_device", r.fromDevice.take(8))
|
||||
.put("sealed", r.sealed)
|
||||
.put("proven", r.senderProven)
|
||||
.put("kind", c.kind)
|
||||
.put("text", c.text)
|
||||
c.messageId?.let { o.put("mid", it) }
|
||||
c.groupId?.let { o.put("gid", it) }
|
||||
c.raw?.let { o.put("raw", it) }
|
||||
c.sticker?.let { o.put("sticker", JSONObject().put("pack", it.pack).put("id", it.id)) }
|
||||
c.link?.let { o.put("link", it.toJson()) }
|
||||
c.card?.let { card ->
|
||||
o.put(
|
||||
"card",
|
||||
JSONObject(card.toJson())
|
||||
.put("sealedNow", card.isSealed(false, System.currentTimeMillis(), System.currentTimeMillis())),
|
||||
)
|
||||
}
|
||||
c.event?.let {
|
||||
o.put("event", JSONObject(it.toJson()))
|
||||
lastEventId = it.id
|
||||
}
|
||||
c.rsvp?.let { o.put("rsvp", JSONObject(it.toJson())) }
|
||||
c.game?.let { g ->
|
||||
lastGameId = g.id
|
||||
o.put("game", JSONObject(g.toJson()))
|
||||
o.put("gameAfter", gameView(g))
|
||||
}
|
||||
c.unpost?.let { u ->
|
||||
o.put("unpost", JSONObject().put("sid", u.storyId ?: JSONObject.NULL).put("set", u.setId ?: JSONObject.NULL))
|
||||
}
|
||||
c.editOf?.let { o.put("editOf", it).put("editsStory", c.editsStory) }
|
||||
c.attachment?.let { a ->
|
||||
o.put(
|
||||
"attachment",
|
||||
JSONObject()
|
||||
.put("kind", a.kind)
|
||||
.put("id", a.id)
|
||||
.put("mime", a.mime)
|
||||
.put("w", a.width)
|
||||
.put("h", a.height)
|
||||
.put("bytes", a.bytes),
|
||||
)
|
||||
}
|
||||
c.storySet?.let { o.put("storySet", it.toJson()) }
|
||||
o.put("controlKind", c.kind in setOf(Content.Kind.GAME_MOVE, Content.Kind.GAME_END, Content.Kind.RSVP, Content.Kind.UNPOST))
|
||||
return o
|
||||
}
|
||||
|
||||
/** Fold a game envelope from the other side into what this peer knows. */
|
||||
private fun gameView(g: Content.Game): JSONObject {
|
||||
val theirs = g.mine
|
||||
val mineSide = if (theirs == Content.Game.FIRST) Content.Game.SECOND else Content.Game.FIRST
|
||||
val track = games.getOrPut(g.id) { Track(g.type, mineSide, emptyList()) }
|
||||
val incoming = g.log
|
||||
val note =
|
||||
when {
|
||||
incoming == null -> "start"
|
||||
incoming.size > track.log.size && incoming.subList(0, track.log.size) == track.log -> {
|
||||
track.log = incoming
|
||||
"advanced"
|
||||
}
|
||||
incoming == track.log -> "same"
|
||||
else -> "OUT OF STEP: known ${track.log} got $incoming"
|
||||
}
|
||||
if (g.ending != null) {
|
||||
track.ending = g.ending
|
||||
track.endedBy = theirs
|
||||
}
|
||||
return outcome(g.id).put("fold", note)
|
||||
}
|
||||
|
||||
/** The engine's verdict on what this peer holds for one game. */
|
||||
private fun outcome(gameId: String): JSONObject {
|
||||
val track = games[gameId] ?: return JSONObject().put("error", "unknown game")
|
||||
val type = GameType.fromWire(track.type) ?: return JSONObject().put("error", "unknown type")
|
||||
val out =
|
||||
JSONObject()
|
||||
.put("game", gameId)
|
||||
.put("t", track.type)
|
||||
.put("peerPlays", track.mine)
|
||||
.put("log", JSONArray(track.log))
|
||||
track.ending?.let { out.put("r", it).put("rBy", track.endedBy) }
|
||||
when (val replay = Games.replay(type, track.log)) {
|
||||
is ReplayResult.Replayed -> {
|
||||
when (val result = Games.outcome(replay.position)) {
|
||||
is Outcome.Ongoing -> out.put("outcome", "ongoing")
|
||||
is Outcome.Win -> out.put("outcome", "win:${result.side.wire}:${result.reason.wire}")
|
||||
is Outcome.Draw -> out.put("outcome", "draw:${result.reason.wire}")
|
||||
}
|
||||
out.put("legalNext", JSONArray(Games.legalMoves(replay.position)))
|
||||
}
|
||||
is ReplayResult.TooLong -> out.put("outcome", "too-long")
|
||||
is ReplayResult.Failed -> out.put("outcome", "replay-failed-at-${replay.ply}:${replay.error.wire}")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- Sending --------------------------------------------------------------
|
||||
|
||||
private fun send(
|
||||
to: Long,
|
||||
body: String,
|
||||
): JSONObject {
|
||||
val sent = conversations.send(to, body)
|
||||
return JSONObject()
|
||||
.put("to", to)
|
||||
.put("body", body)
|
||||
.put("delivered", sent.delivered.size)
|
||||
.put("failed", JSONObject(sent.failed as Map<*, *>))
|
||||
.put("sealed", sent.sealed)
|
||||
}
|
||||
|
||||
private fun mid() = UUID.randomUUID().toString()
|
||||
|
||||
private fun now() = System.currentTimeMillis() / 1000
|
||||
|
||||
fun run(cmd: JSONObject): JSONObject {
|
||||
val to = cmd.optString("to").takeIf { it.isNotEmpty() }?.let(::userIdFor)
|
||||
fun target() = to ?: error("'${cmd.optString("cmd")}' needs \"to\"")
|
||||
return when (val name = cmd.getString("cmd")) {
|
||||
"add" -> {
|
||||
api.addBuddy(cmd.getString("handle"))
|
||||
JSONObject().put("added", cmd.getString("handle"))
|
||||
}
|
||||
|
||||
"claim" -> claim()
|
||||
|
||||
"text" -> send(target(), Content.text(cmd.getString("body"), messageId = mid()))
|
||||
|
||||
"rawtext" -> send(target(), cmd.getString("body"))
|
||||
|
||||
"sticker" -> send(target(), Content.sticker(cmd.getString("pack"), cmd.getString("id")))
|
||||
|
||||
"card" -> {
|
||||
val open =
|
||||
when {
|
||||
cmd.has("open") -> cmd.getLong("open")
|
||||
cmd.has("openIn") -> now() + cmd.getLong("openIn")
|
||||
else -> null
|
||||
}
|
||||
val card =
|
||||
Content.Card(
|
||||
design = cmd.optString("d", "hello"),
|
||||
msg = cmd.optString("msg"),
|
||||
to = cmd.optString("toLine").takeIf { it.isNotEmpty() },
|
||||
from = cmd.optString("from").takeIf { it.isNotEmpty() },
|
||||
open = open,
|
||||
)
|
||||
send(target(), Content.card(card, mid())).put("open", open ?: JSONObject.NULL)
|
||||
}
|
||||
|
||||
"event" -> {
|
||||
val id = cmd.optString("id").ifEmpty { UUID.randomUUID().toString() }
|
||||
val before = events[id]
|
||||
val event =
|
||||
Content.Event(
|
||||
id = id,
|
||||
rev = cmd.optInt("rev", (before?.rev ?: 0) + 1),
|
||||
title = cmd.optString("title").ifEmpty { before?.title ?: error("an event needs a title") },
|
||||
start = if (cmd.has("start")) cmd.getLong("start") else if (cmd.has("startIn")) now() + cmd.getLong("startIn") else before?.start,
|
||||
end = if (cmd.has("end")) cmd.getLong("end") else if (cmd.has("endIn")) now() + cmd.getLong("endIn") else before?.end,
|
||||
day = cmd.optString("day").takeIf { it.isNotEmpty() } ?: before?.day,
|
||||
days = if (cmd.has("days")) cmd.getInt("days") else before?.days,
|
||||
tz = cmd.optString("tz").takeIf { it.isNotEmpty() } ?: before?.tz,
|
||||
place = cmd.optString("place").takeIf { it.isNotEmpty() } ?: before?.place,
|
||||
note = cmd.optString("note").takeIf { it.isNotEmpty() } ?: before?.note,
|
||||
cancelled = cmd.optBoolean("cancel", false),
|
||||
)
|
||||
events[id] = event
|
||||
send(target(), Content.event(event, mid())).put("eventId", id).put("rev", event.rev)
|
||||
}
|
||||
|
||||
"rsvp" -> {
|
||||
val eid = cmd.getString("eid").let { if (it == "latest") lastEventId ?: error("no event yet") else it }
|
||||
val n = maxOf(now(), (answered[eid] ?: 0L) + 1)
|
||||
answered[eid] = n
|
||||
send(target(), Content.rsvp(Content.Rsvp(eid, cmd.getString("r"), n))).put("n", n)
|
||||
}
|
||||
|
||||
"game" -> {
|
||||
val id = UUID.randomUUID().toString()
|
||||
val type = cmd.getString("t")
|
||||
val mine = cmd.getString("mine")
|
||||
games[id] = Track(type, mine, emptyList())
|
||||
lastGameId = id
|
||||
send(target(), Content.game(Content.Game(id, type, mine), mid())).put("game", id)
|
||||
}
|
||||
|
||||
"gmove" -> {
|
||||
val id = gameId(cmd)
|
||||
val track = games[id] ?: error("no game $id here")
|
||||
val next = track.log + cmd.getString("move")
|
||||
val type = GameType.fromWire(track.type) ?: error("unknown type ${track.type}")
|
||||
val replay = Games.replay(type, next)
|
||||
require(replay is ReplayResult.Replayed) { "the engine refuses $next: $replay" }
|
||||
require(type.sideOfPly(next.size - 1).wire == track.mine) { "not this peer's move: ${track.log}" }
|
||||
val body = Content.gameMove(Content.Game(id, track.type, track.mine, next))
|
||||
val sent = send(target(), body)
|
||||
track.log = next
|
||||
sent.put("after", outcome(id))
|
||||
}
|
||||
|
||||
"gend" -> {
|
||||
val id = gameId(cmd)
|
||||
val track = games[id] ?: error("no game $id here")
|
||||
val body = Content.gameEnd(Content.Game(id, track.type, track.mine, track.log, cmd.getString("r")))
|
||||
val sent = send(target(), body)
|
||||
track.ending = cmd.getString("r")
|
||||
track.endedBy = track.mine
|
||||
sent.put("after", outcome(id))
|
||||
}
|
||||
|
||||
"gstate" -> {
|
||||
outcome(gameId(cmd))
|
||||
}
|
||||
|
||||
"games" -> {
|
||||
val out = JSONObject()
|
||||
games.keys.forEach { out.put(it, outcome(it)) }
|
||||
out
|
||||
}
|
||||
|
||||
"story" -> {
|
||||
val bytes = File(cmd.getString("path")).readBytes()
|
||||
val posted =
|
||||
conversations.postStory(
|
||||
data = bytes,
|
||||
mime = cmd.optString("mime", "image/jpeg"),
|
||||
width = cmd.getInt("w"),
|
||||
height = cmd.getInt("h"),
|
||||
caption = cmd.optString("caption").takeIf { it.isNotEmpty() },
|
||||
toUserIds = listOf(target()),
|
||||
)
|
||||
lastPostedStory = posted.ref.id
|
||||
JSONObject()
|
||||
.put("story", posted.ref.id)
|
||||
.put("reached", posted.reached)
|
||||
.put("missed", posted.missed)
|
||||
}
|
||||
|
||||
"storyedit" -> send(target(), Content.storyEdit(storyId(cmd), cmd.getString("caption")))
|
||||
|
||||
"unpost" -> send(target(), Content.unpostStory(storyId(cmd)))
|
||||
|
||||
"mark" -> {
|
||||
lastMark = received.size
|
||||
JSONObject().put("seq", lastMark)
|
||||
}
|
||||
|
||||
"received" -> {
|
||||
val from = cmd.optInt("after", 0)
|
||||
JSONObject().put("messages", JSONArray(received.drop(from)))
|
||||
}
|
||||
|
||||
"dirshare" -> {
|
||||
val url = cmd.getString("url")
|
||||
val preview = Content.LinkPreview.of(url, cmd.getString("title"), "")
|
||||
send(target(), Content.text(url, messageId = mid(), link = preview))
|
||||
}
|
||||
|
||||
"whoami" -> {
|
||||
JSONObject()
|
||||
.put("handle", state.getString("username"))
|
||||
.put("user_id", myUserId)
|
||||
.put("fnid", state.optString("fnid"))
|
||||
}
|
||||
|
||||
else -> error("unknown command '$name'")
|
||||
}
|
||||
}
|
||||
|
||||
private fun gameId(cmd: JSONObject): String =
|
||||
cmd.optString("game").let { if (it.isEmpty() || it == "latest") lastGameId ?: error("no game yet") else it }
|
||||
|
||||
private fun storyId(cmd: JSONObject): String =
|
||||
cmd.optString("sid").let { if (it.isEmpty() || it == "latest") lastPostedStory ?: error("no story posted yet") else it }
|
||||
|
||||
/** A value at a dotted path such as `event.rev`, as text, or null. */
|
||||
private fun at(
|
||||
o: JSONObject,
|
||||
path: String,
|
||||
): String? {
|
||||
var node: Any? = o
|
||||
for (part in path.split('.')) {
|
||||
node = (node as? JSONObject)?.opt(part) ?: return null
|
||||
}
|
||||
return node?.toString()
|
||||
}
|
||||
|
||||
/** The first envelope from index [after] on that has [kind] and every value in [where]. */
|
||||
private fun match(
|
||||
kind: String,
|
||||
where: JSONObject?,
|
||||
after: Int,
|
||||
): JSONObject? =
|
||||
received.drop(after).firstOrNull { o ->
|
||||
o.optString("kind") == kind &&
|
||||
(where == null || where.keys().asSequence().all { key -> at(o, key) == where.get(key).toString() })
|
||||
}
|
||||
|
||||
private data class Pending(
|
||||
val name: String,
|
||||
val cmd: JSONObject,
|
||||
val after: Int,
|
||||
val deadline: Long,
|
||||
)
|
||||
|
||||
// ---- The long-running correspondent ---------------------------------------
|
||||
|
||||
fun bot(
|
||||
commands: File,
|
||||
seconds: Int,
|
||||
) {
|
||||
commands.mkdirs()
|
||||
val deadline = System.currentTimeMillis() + seconds * 1000L
|
||||
log("START", JSONObject().put("user_id", myUserId).put("handle", state.getString("username")))
|
||||
var stop = false
|
||||
val pending = mutableListOf<Pending>()
|
||||
while (!stop && System.currentTimeMillis() < deadline) {
|
||||
val opened =
|
||||
try {
|
||||
conversations.poll()
|
||||
} catch (e: Exception) {
|
||||
log("POLL-ERROR", JSONObject().put("error", e.toString()))
|
||||
emptyList()
|
||||
}
|
||||
for (r in opened) {
|
||||
val o = runCatching { describe(r) }.getOrElse { JSONObject().put("describeError", it.toString()) }
|
||||
o.put("seq", received.size)
|
||||
received += o
|
||||
log("RECV", o)
|
||||
}
|
||||
val settled = mutableListOf<Pending>()
|
||||
for (p in pending) {
|
||||
val hit = match(p.cmd.getString("kind"), p.cmd.optJSONObject("where"), p.after)
|
||||
val result =
|
||||
when {
|
||||
hit != null -> JSONObject().put("ok", true).put("message", hit)
|
||||
System.currentTimeMillis() > p.deadline ->
|
||||
JSONObject().put("ok", false).put("error", "nothing matched ${p.cmd} after #${p.after}")
|
||||
else -> null
|
||||
} ?: continue
|
||||
settled += p
|
||||
finish(commands, p.name, p.cmd, result)
|
||||
}
|
||||
pending.removeAll(settled)
|
||||
val waiting =
|
||||
commands
|
||||
.listFiles { f -> f.name.endsWith(".cmd.json") }
|
||||
.orEmpty()
|
||||
.sortedBy { it.name }
|
||||
for (file in waiting) {
|
||||
val name = file.name.removeSuffix(".cmd.json")
|
||||
val taken = File(commands, "$name.taken")
|
||||
if (!file.renameTo(taken)) continue
|
||||
val cmd = runCatching { JSONObject(taken.readText()) }.getOrNull()
|
||||
if (cmd?.optString("cmd") == "expect") {
|
||||
val after = if (cmd.has("after")) cmd.getInt("after") else lastMark
|
||||
val seconds = cmd.optInt("timeout", 60)
|
||||
pending += Pending(name, cmd, after, System.currentTimeMillis() + seconds * 1000L)
|
||||
log("EXPECT $name", cmd)
|
||||
continue
|
||||
}
|
||||
val result =
|
||||
if (cmd == null) {
|
||||
JSONObject().put("ok", false).put("error", "not JSON")
|
||||
} else if (cmd.optString("cmd") == "stop") {
|
||||
stop = true
|
||||
JSONObject().put("ok", true)
|
||||
} else {
|
||||
try {
|
||||
run(cmd).put("ok", true)
|
||||
} catch (e: Exception) {
|
||||
JSONObject().put("ok", false).put("error", e.toString())
|
||||
}
|
||||
}
|
||||
finish(commands, name, cmd, result)
|
||||
}
|
||||
Thread.sleep(1200)
|
||||
}
|
||||
log("STOP", JSONObject())
|
||||
}
|
||||
|
||||
private fun finish(
|
||||
commands: File,
|
||||
name: String,
|
||||
cmd: JSONObject?,
|
||||
result: JSONObject,
|
||||
) {
|
||||
log("DONE $name", JSONObject().put("cmd", cmd ?: JSONObject.NULL).put("result", result))
|
||||
val done = File(commands, "$name.done.json.part")
|
||||
done.writeText(result.toString())
|
||||
done.renameTo(File(commands, "$name.done.json"))
|
||||
}
|
||||
|
||||
private fun log(
|
||||
what: String,
|
||||
o: JSONObject,
|
||||
) {
|
||||
println("${java.time.Instant.now()} $what $o")
|
||||
System.out.flush()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user