xP: use the binary protocol for incoming events
And batch event messages together as much as possible. JSON has proven itself to be really slow (for example, encoding/json.Marshaler is a slow interface), and browsers have significant overhead per WS message. Commands are still sent as JSON, sending them in binary would be a laborious rewrite without measurable merits. The xP server now only prints debug output when requested, because that was another source of major slowdowns.
This commit is contained in:
parent
e87cc90b5e
commit
6f39aa6615
223
xC-gen-proto-js.awk
Normal file
223
xC-gen-proto-js.awk
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
# xC-gen-proto-js.awk: Javascript backend for xC-gen-proto.awk.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
#
|
||||||
|
# This backend is currently for decoding the binary format only.
|
||||||
|
# (JSON is way too expensive to process and transfer.)
|
||||||
|
#
|
||||||
|
# Import the resulting script as a Javascript module.
|
||||||
|
|
||||||
|
function define_internal(name) {
|
||||||
|
Types[name] = "internal"
|
||||||
|
}
|
||||||
|
|
||||||
|
function define_sint(size, shortname) {
|
||||||
|
shortname = "i" size
|
||||||
|
define_internal(shortname)
|
||||||
|
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print "\t" shortname "() {"
|
||||||
|
if (size == "64") {
|
||||||
|
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
|
||||||
|
print "\t\tconst " shortname \
|
||||||
|
" = Number(this.getBigInt" size "(this.offset))"
|
||||||
|
} else {
|
||||||
|
print "\t\tconst " shortname " = this.getInt" size "(this.offset)"
|
||||||
|
}
|
||||||
|
print "\t\tthis.offset += " (size / 8)
|
||||||
|
print "\t\treturn " shortname
|
||||||
|
print "\t}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function define_uint(size, shortname) {
|
||||||
|
shortname = "u" size
|
||||||
|
define_internal(shortname)
|
||||||
|
CodegenDeserialize[shortname] = "\t%s = r." shortname "()\n"
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print "\t" shortname "() {"
|
||||||
|
if (size == "64") {
|
||||||
|
# XXX: 2^53 - 1 must be enough for anyone. BigInts are a PITA.
|
||||||
|
print "\t\tconst " shortname \
|
||||||
|
" = Number(this.getBigUint" size "(this.offset))"
|
||||||
|
} else {
|
||||||
|
print "\t\tconst " shortname " = this.getUint" size "(this.offset)"
|
||||||
|
}
|
||||||
|
print "\t\tthis.offset += " (size / 8)
|
||||||
|
print "\t\treturn " shortname
|
||||||
|
print "\t}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_begin() {
|
||||||
|
print "export class Reader extends DataView {"
|
||||||
|
print "\tconstructor() {"
|
||||||
|
print "\t\tsuper(...arguments)"
|
||||||
|
print "\t\tthis.offset = 0"
|
||||||
|
print "\t\tthis.decoder = new TextDecoder('utf-8', {fatal: true})"
|
||||||
|
print "\t}"
|
||||||
|
print ""
|
||||||
|
print "\tget empty() {"
|
||||||
|
print "\t\treturn this.byteLength <= this.offset"
|
||||||
|
print "\t}"
|
||||||
|
print ""
|
||||||
|
print "\trequire(len) {"
|
||||||
|
print "\t\tif (this.byteLength - this.offset < len)"
|
||||||
|
print "\t\t\tthrow `Premature end of data`"
|
||||||
|
print "\t\treturn this.byteOffset + this.offset"
|
||||||
|
print "\t}"
|
||||||
|
|
||||||
|
define_internal("string")
|
||||||
|
CodegenDeserialize["string"] = "\t%s = r.string()\n"
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print "\tstring() {"
|
||||||
|
print "\t\tconst len = this.getUint32(this.offset)"
|
||||||
|
print "\t\tthis.offset += 4"
|
||||||
|
print "\t\tconst array = new Uint8Array("
|
||||||
|
print "\t\t\tthis.buffer, this.require(len), len)"
|
||||||
|
print "\t\tthis.offset += len"
|
||||||
|
print "\t\treturn this.decoder.decode(array)"
|
||||||
|
print "\t}"
|
||||||
|
|
||||||
|
define_internal("bool")
|
||||||
|
CodegenDeserialize["bool"] = "\t%s = r.bool()\n"
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print "\tbool() {"
|
||||||
|
print "\t\tconst u8 = this.getUint8(this.offset)"
|
||||||
|
print "\t\tthis.offset += 1"
|
||||||
|
print "\t\treturn u8 != 0"
|
||||||
|
print "\t}"
|
||||||
|
|
||||||
|
define_sint("8")
|
||||||
|
define_sint("16")
|
||||||
|
define_sint("32")
|
||||||
|
define_sint("64")
|
||||||
|
define_uint("8")
|
||||||
|
define_uint("16")
|
||||||
|
define_uint("32")
|
||||||
|
define_uint("64")
|
||||||
|
|
||||||
|
print "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_constant(name, value) {
|
||||||
|
print ""
|
||||||
|
print "export const " decapitalize(snaketocamel(name)) " = " value
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_enum_value(name, subname, value, cg) {
|
||||||
|
append(cg, "fields", "\t" snaketocamel(subname) ": " value ",\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_enum(name, cg) {
|
||||||
|
print ""
|
||||||
|
print "export const " name " = Object.freeze({"
|
||||||
|
print cg["fields"] "})"
|
||||||
|
|
||||||
|
CodegenDeserialize[name] = "\t%s = r.i8()\n"
|
||||||
|
for (i in cg)
|
||||||
|
delete cg[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_struct_field(d, cg, camel, f, deserialize) {
|
||||||
|
camel = decapitalize(snaketocamel(d["name"]))
|
||||||
|
f = "s." camel
|
||||||
|
append(cg, "fields", "\t" camel "\n")
|
||||||
|
|
||||||
|
deserialize = CodegenDeserialize[d["type"]]
|
||||||
|
if (!d["isarray"]) {
|
||||||
|
append(cg, "deserialize", sprintf(deserialize, f))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
append(cg, "deserialize",
|
||||||
|
"\t{\n" \
|
||||||
|
indent(sprintf(CodegenDeserialize["u32"], "const len")))
|
||||||
|
if (d["type"] == "u8") {
|
||||||
|
append(cg, "deserialize",
|
||||||
|
"\t\t" f " = new Uint8Array(\n" \
|
||||||
|
"\t\t\tr.buffer, r.require(len), len)\n" \
|
||||||
|
"\t\tr.offset += len\n" \
|
||||||
|
"\t}\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (d["type"] == "i8") {
|
||||||
|
append(cg, "deserialize",
|
||||||
|
"\t\t" f " = new Int8Array(\n" \
|
||||||
|
"\t\t\tr.buffer, r.require(len), len)\n" \
|
||||||
|
"\t\tr.offset += len\n" \
|
||||||
|
"\t}\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
append(cg, "deserialize",
|
||||||
|
"\t\t" f " = new Array(len)\n" \
|
||||||
|
"\t}\n" \
|
||||||
|
"\tfor (let i = 0; i < " f ".length; i++)\n" \
|
||||||
|
indent(sprintf(deserialize, f "[i]")))
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_struct_tag(d, cg) {
|
||||||
|
append(cg, "fields", "\t" decapitalize(snaketocamel(d["name"])) "\n")
|
||||||
|
# Do not deserialize here, that is already done by the containing union.
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_struct(name, cg) {
|
||||||
|
print ""
|
||||||
|
print "export class " name " {"
|
||||||
|
print cg["fields"] cg["methods"]
|
||||||
|
print "\tstatic deserialize(r) {"
|
||||||
|
print "\t\tconst s = new " name "()"
|
||||||
|
print indent(cg["deserialize"]) "\t\treturn s"
|
||||||
|
print "\t}"
|
||||||
|
print "}"
|
||||||
|
|
||||||
|
CodegenDeserialize[name] = "\t%s = " name ".deserialize(r)\n"
|
||||||
|
for (i in cg)
|
||||||
|
delete cg[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_union_tag(d, cg) {
|
||||||
|
cg["tagtype"] = d["type"]
|
||||||
|
cg["tagname"] = d["name"]
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_union_struct(name, casename, cg, scg, structname) {
|
||||||
|
append(scg, "methods",
|
||||||
|
"\n" \
|
||||||
|
"\tconstructor() {\n" \
|
||||||
|
"\t\tthis." decapitalize(snaketocamel(cg["tagname"])) \
|
||||||
|
" = " cg["tagtype"] "." snaketocamel(casename) "\n" \
|
||||||
|
"\t}\n")
|
||||||
|
|
||||||
|
# And thus not all generated structs are present in Types.
|
||||||
|
structname = name snaketocamel(casename)
|
||||||
|
codegen_struct(structname, scg)
|
||||||
|
|
||||||
|
append(cg, "deserialize",
|
||||||
|
"\tcase " cg["tagtype"] "." snaketocamel(casename) ":\n" \
|
||||||
|
"\t{\n" \
|
||||||
|
indent(sprintf(CodegenDeserialize[structname], "const s")) \
|
||||||
|
"\t\treturn s\n" \
|
||||||
|
"\t}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
function codegen_union(name, cg, tagvar) {
|
||||||
|
tagvar = decapitalize(snaketocamel(cg["tagname"]))
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print "export function deserialize" name "(r) {"
|
||||||
|
print sprintf(CodegenDeserialize[cg["tagtype"]], "const " tagvar) \
|
||||||
|
"\tswitch (" tagvar ") {"
|
||||||
|
print cg["deserialize"] "\tdefault:"
|
||||||
|
print "\t\tthrow `Unknown " cg["tagtype"] " (${tagvar})`"
|
||||||
|
print "\t}"
|
||||||
|
print "}"
|
||||||
|
|
||||||
|
CodegenDeserialize[name] = "\t%s = deserialize" name "(r)\n"
|
||||||
|
for (i in cg)
|
||||||
|
delete cg[i]
|
||||||
|
}
|
1
xP/.gitignore
vendored
1
xP/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/xP
|
/xP
|
||||||
/proto.go
|
/proto.go
|
||||||
|
/public/proto.js
|
||||||
/public/mithril.js
|
/public/mithril.js
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
.POSIX:
|
.POSIX:
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
|
|
||||||
outputs = xP proto.go public/mithril.js
|
outputs = xP proto.go public/proto.js public/mithril.js
|
||||||
all: $(outputs)
|
all: $(outputs)
|
||||||
|
|
||||||
xP: xP.go proto.go
|
xP: xP.go proto.go
|
||||||
go build -o $@
|
go build -o $@
|
||||||
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
|
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
|
||||||
awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
|
awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
|
||||||
|
public/proto.js: ../xC-gen-proto.awk ../xC-gen-proto-js.awk ../xC-proto
|
||||||
|
awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-js.awk ../xC-proto > $@
|
||||||
public/mithril.js:
|
public/mithril.js:
|
||||||
curl -Lo $@ https://unpkg.com/mithril/mithril.js
|
curl -Lo $@ https://unpkg.com/mithril/mithril.js
|
||||||
clean:
|
clean:
|
||||||
|
202
xP/public/xP.js
202
xP/public/xP.js
@ -1,6 +1,6 @@
|
|||||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
'use strict'
|
import * as Relay from './proto.js'
|
||||||
|
|
||||||
// ---- RPC --------------------------------------------------------------------
|
// ---- RPC --------------------------------------------------------------------
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ class RelayRpc extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_initialize() {
|
_initialize() {
|
||||||
|
this.ws.binaryType = 'arraybuffer'
|
||||||
this.ws.onopen = undefined
|
this.ws.onopen = undefined
|
||||||
this.ws.onmessage = event => {
|
this.ws.onmessage = event => {
|
||||||
this._process(event.data)
|
this._process(event.data)
|
||||||
@ -56,33 +57,30 @@ class RelayRpc extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_process(data) {
|
_process(data) {
|
||||||
if (typeof data !== 'string')
|
if (typeof data === 'string')
|
||||||
throw "Binary messages not supported"
|
throw "JSON messages not supported"
|
||||||
|
|
||||||
let message = JSON.parse(data)
|
const r = new Relay.Reader(data)
|
||||||
if (typeof message !== 'object')
|
while (!r.empty)
|
||||||
throw "Invalid message"
|
this._processOne(Relay.EventMessage.deserialize(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
_processOne(message) {
|
||||||
let e = message.data
|
let e = message.data
|
||||||
if (typeof e !== 'object')
|
|
||||||
throw "Invalid message"
|
|
||||||
|
|
||||||
switch (e.event) {
|
switch (e.event) {
|
||||||
case 'Error':
|
case Relay.Event.Error:
|
||||||
if (this.promised[e.commandSeq] !== undefined)
|
if (this.promised[e.commandSeq] !== undefined)
|
||||||
this.promised[e.commandSeq].reject(e.error)
|
this.promised[e.commandSeq].reject(e.error)
|
||||||
else
|
else
|
||||||
console.error("Unawaited error")
|
console.error("Unawaited error")
|
||||||
break
|
break
|
||||||
case 'Response':
|
case Relay.Event.Response:
|
||||||
if (this.promised[e.commandSeq] !== undefined)
|
if (this.promised[e.commandSeq] !== undefined)
|
||||||
this.promised[e.commandSeq].resolve(e.data)
|
this.promised[e.commandSeq].resolve(e.data)
|
||||||
else
|
else
|
||||||
console.error("Unawaited response")
|
console.error("Unawaited response")
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (typeof e.event !== 'string')
|
|
||||||
throw "Invalid event tag"
|
|
||||||
|
|
||||||
e.eventSeq = message.eventSeq
|
e.eventSeq = message.eventSeq
|
||||||
this.dispatchEvent(new CustomEvent('event', {detail: e}))
|
this.dispatchEvent(new CustomEvent('event', {detail: e}))
|
||||||
return
|
return
|
||||||
@ -115,13 +113,6 @@ class RelayRpc extends EventTarget {
|
|||||||
this.promised[seq] = {resolve, reject}
|
this.promised[seq] = {resolve, reject}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
base64decode(str) {
|
|
||||||
const text = atob(str), bytes = new Uint8Array(text.length)
|
|
||||||
for (let i = 0; i < text.length; i++)
|
|
||||||
bytes[i] = text.charCodeAt(i)
|
|
||||||
return new TextDecoder().decode(bytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Utilities --------------------------------------------------------------
|
// ---- Utilities --------------------------------------------------------------
|
||||||
@ -183,7 +174,7 @@ function updateIcon(highlighted) {
|
|||||||
// ---- Event processing -------------------------------------------------------
|
// ---- Event processing -------------------------------------------------------
|
||||||
|
|
||||||
let rpc = new RelayRpc(proxy)
|
let rpc = new RelayRpc(proxy)
|
||||||
let rpcEventHandlers = {}
|
let rpcEventHandlers = new Map()
|
||||||
|
|
||||||
let buffers = new Map()
|
let buffers = new Map()
|
||||||
let bufferLast = undefined
|
let bufferLast = undefined
|
||||||
@ -221,7 +212,7 @@ function bufferToggleLog() {
|
|||||||
if (bufferCurrent !== name)
|
if (bufferCurrent !== name)
|
||||||
return
|
return
|
||||||
|
|
||||||
bufferLog = rpc.base64decode(resp.log)
|
bufferLog = utf8Decode(resp.log)
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -236,7 +227,7 @@ rpc.connect().then(result => {
|
|||||||
|
|
||||||
servers.clear()
|
servers.clear()
|
||||||
|
|
||||||
rpc.send({command: 'Hello', version: 1})
|
rpc.send({command: 'Hello', version: Relay.version})
|
||||||
connecting = false
|
connecting = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
@ -249,10 +240,11 @@ rpc.addEventListener('close', event => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
rpc.addEventListener('event', event => {
|
rpc.addEventListener('event', event => {
|
||||||
const handler = rpcEventHandlers[event.detail.event]
|
const handler = rpcEventHandlers.get(event.detail.event)
|
||||||
if (handler !== undefined) {
|
if (handler !== undefined) {
|
||||||
handler(event.detail)
|
handler(event.detail)
|
||||||
if (bufferCurrent !== undefined || event.detail.event !== 'BufferLine')
|
if (bufferCurrent !== undefined ||
|
||||||
|
event.detail.event !== Relay.Event.BufferLine)
|
||||||
m.redraw()
|
m.redraw()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -263,7 +255,7 @@ rpcEventHandlers['Ping'] = e => {
|
|||||||
|
|
||||||
// ~~~ Buffer events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// ~~~ Buffer events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
rpcEventHandlers['BufferUpdate'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferUpdate, e => {
|
||||||
let b = buffers.get(e.bufferName)
|
let b = buffers.get(e.bufferName)
|
||||||
if (b === undefined) {
|
if (b === undefined) {
|
||||||
buffers.set(e.bufferName, (b = {
|
buffers.set(e.bufferName, (b = {
|
||||||
@ -277,9 +269,9 @@ rpcEventHandlers['BufferUpdate'] = e => {
|
|||||||
b.hideUnimportant = e.hideUnimportant
|
b.hideUnimportant = e.hideUnimportant
|
||||||
b.kind = e.context.kind
|
b.kind = e.context.kind
|
||||||
b.server = servers.get(e.context.serverName)
|
b.server = servers.get(e.context.serverName)
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferStats'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferStats, e => {
|
||||||
let b = buffers.get(e.bufferName)
|
let b = buffers.get(e.bufferName)
|
||||||
if (b === undefined)
|
if (b === undefined)
|
||||||
return
|
return
|
||||||
@ -287,20 +279,20 @@ rpcEventHandlers['BufferStats'] = e => {
|
|||||||
b.newMessages = e.newMessages,
|
b.newMessages = e.newMessages,
|
||||||
b.newUnimportantMessages = e.newUnimportantMessages
|
b.newUnimportantMessages = e.newUnimportantMessages
|
||||||
b.highlighted = e.highlighted
|
b.highlighted = e.highlighted
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferRename'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferRename, e => {
|
||||||
buffers.set(e.new, buffers.get(e.bufferName))
|
buffers.set(e.new, buffers.get(e.bufferName))
|
||||||
buffers.delete(e.bufferName)
|
buffers.delete(e.bufferName)
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferRemove'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferRemove, e => {
|
||||||
buffers.delete(e.bufferName)
|
buffers.delete(e.bufferName)
|
||||||
if (e.bufferName === bufferLast)
|
if (e.bufferName === bufferLast)
|
||||||
bufferLast = undefined
|
bufferLast = undefined
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferActivate'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferActivate, e => {
|
||||||
let old = buffers.get(bufferCurrent)
|
let old = buffers.get(bufferCurrent)
|
||||||
if (old !== undefined)
|
if (old !== undefined)
|
||||||
bufferResetStats(old)
|
bufferResetStats(old)
|
||||||
@ -333,9 +325,9 @@ rpcEventHandlers['BufferActivate'] = e => {
|
|||||||
textarea.value = b.input
|
textarea.value = b.input
|
||||||
textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection)
|
textarea.setSelectionRange(b.inputStart, b.inputEnd, b.inputDirection)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferLine'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferLine, e => {
|
||||||
let b = buffers.get(e.bufferName), line = {...e}
|
let b = buffers.get(e.bufferName), line = {...e}
|
||||||
delete line.event
|
delete line.event
|
||||||
delete line.eventSeq
|
delete line.eventSeq
|
||||||
@ -372,37 +364,37 @@ rpcEventHandlers['BufferLine'] = e => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.isHighlight ||
|
if (line.isHighlight || (!visible && !line.isUnimportant &&
|
||||||
(!visible && b.kind === 'PrivateMessage' && !line.isUnimportant)) {
|
b.kind === Relay.BufferKind.PrivateMessage)) {
|
||||||
beep()
|
beep()
|
||||||
if (!visible)
|
if (!visible)
|
||||||
b.highlighted = true
|
b.highlighted = true
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['BufferClear'] = e => {
|
rpcEventHandlers.set(Relay.Event.BufferClear, e => {
|
||||||
let b = buffers.get(e.bufferName)
|
let b = buffers.get(e.bufferName)
|
||||||
if (b !== undefined)
|
if (b !== undefined)
|
||||||
b.lines.length = 0
|
b.lines.length = 0
|
||||||
}
|
})
|
||||||
|
|
||||||
// ~~~ Server events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
// ~~~ Server events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
rpcEventHandlers['ServerUpdate'] = e => {
|
rpcEventHandlers.set(Relay.Event.ServerUpdate, e => {
|
||||||
let s = servers.get(e.serverName)
|
let s = servers.get(e.serverName)
|
||||||
if (s === undefined)
|
if (s === undefined)
|
||||||
servers.set(e.serverName, (s = {}))
|
servers.set(e.serverName, (s = {}))
|
||||||
s.state = e.state
|
s.state = e.state
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['ServerRename'] = e => {
|
rpcEventHandlers.set(Relay.Event.ServerRename, e => {
|
||||||
servers.set(e.new, servers.get(e.serverName))
|
servers.set(e.new, servers.get(e.serverName))
|
||||||
servers.delete(e.serverName)
|
servers.delete(e.serverName)
|
||||||
}
|
})
|
||||||
|
|
||||||
rpcEventHandlers['ServerRemove'] = e => {
|
rpcEventHandlers.set(Relay.Event.ServerRemove, e => {
|
||||||
servers.delete(e.serverName)
|
servers.delete(e.serverName)
|
||||||
}
|
})
|
||||||
|
|
||||||
// --- Colours -----------------------------------------------------------------
|
// --- Colours -----------------------------------------------------------------
|
||||||
|
|
||||||
@ -499,18 +491,19 @@ let Content = {
|
|||||||
return a
|
return a
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeMark: line => {
|
||||||
|
switch (line.rendition) {
|
||||||
|
case Relay.Rendition.Indent: return m('span.mark', {}, '')
|
||||||
|
case Relay.Rendition.Status: return m('span.mark', {}, '–')
|
||||||
|
case Relay.Rendition.Error: return m('span.mark.error', {}, '⚠')
|
||||||
|
case Relay.Rendition.Join: return m('span.mark.join', {}, '→')
|
||||||
|
case Relay.Rendition.Part: return m('span.mark.part', {}, '←')
|
||||||
|
case Relay.Rendition.Action: return m('span.mark.action', {}, '✶')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
view: vnode => {
|
view: vnode => {
|
||||||
let line = vnode.children[0]
|
let line = vnode.children[0]
|
||||||
let mark = undefined
|
|
||||||
switch (line.rendition) {
|
|
||||||
case 'Indent': mark = m('span.mark', {}, ''); break
|
|
||||||
case 'Status': mark = m('span.mark', {}, '–'); break
|
|
||||||
case 'Error': mark = m('span.mark.error', {}, '⚠'); break
|
|
||||||
case 'Join': mark = m('span.mark.join', {}, '→'); break
|
|
||||||
case 'Part': mark = m('span.mark.part', {}, '←'); break
|
|
||||||
case 'Action': mark = m('span.mark.action', {}, '✶'); break
|
|
||||||
}
|
|
||||||
|
|
||||||
let classes = new Set()
|
let classes = new Set()
|
||||||
let flip = c => {
|
let flip = c => {
|
||||||
if (classes.has(c))
|
if (classes.has(c))
|
||||||
@ -518,45 +511,49 @@ let Content = {
|
|||||||
else
|
else
|
||||||
classes.add(c)
|
classes.add(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
let fg = -1, bg = -1, inverse = false
|
let fg = -1, bg = -1, inverse = false
|
||||||
return m('.content', vnode.attrs, [mark, line.items.flatMap(item => {
|
return m('.content', vnode.attrs, [
|
||||||
switch (item.kind) {
|
Content.makeMark(line),
|
||||||
case 'Text':
|
line.items.flatMap(item => {
|
||||||
return Content.linkify(item.text, {
|
switch (item.kind) {
|
||||||
class: Array.from(classes.keys()).join(' '),
|
case Relay.Item.Text:
|
||||||
style: Content.applyColor(fg, bg, inverse),
|
return Content.linkify(item.text, {
|
||||||
})
|
class: Array.from(classes.keys()).join(' '),
|
||||||
case 'Reset':
|
style: Content.applyColor(fg, bg, inverse),
|
||||||
classes.clear()
|
})
|
||||||
fg = bg = -1
|
case Relay.Item.Reset:
|
||||||
inverse = false
|
classes.clear()
|
||||||
break
|
fg = bg = -1
|
||||||
case 'FgColor':
|
inverse = false
|
||||||
fg = item.color
|
break
|
||||||
break
|
case Relay.Item.FgColor:
|
||||||
case 'BgColor':
|
fg = item.color
|
||||||
bg = item.color
|
break
|
||||||
break
|
case Relay.Item.BgColor:
|
||||||
case 'FlipInverse':
|
bg = item.color
|
||||||
inverse = !inverse
|
break
|
||||||
break
|
case Relay.Item.FlipInverse:
|
||||||
case 'FlipBold':
|
inverse = !inverse
|
||||||
flip('b')
|
break
|
||||||
break
|
case Relay.Item.FlipBold:
|
||||||
case 'FlipItalic':
|
flip('b')
|
||||||
flip('i')
|
break
|
||||||
break
|
case Relay.Item.FlipItalic:
|
||||||
case 'FlipUnderline':
|
flip('i')
|
||||||
flip('u')
|
break
|
||||||
break
|
case Relay.Item.FlipUnderline:
|
||||||
case 'FlipCrossedOut':
|
flip('u')
|
||||||
flip('s')
|
break
|
||||||
break
|
case Relay.Item.FlipCrossedOut:
|
||||||
case 'FlipMonospace':
|
flip('s')
|
||||||
flip('m')
|
break
|
||||||
break
|
case Relay.Item.FlipMonospace:
|
||||||
}
|
flip('m')
|
||||||
})])
|
break
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,8 +666,17 @@ let Status = {
|
|||||||
let status = `${bufferCurrent}`
|
let status = `${bufferCurrent}`
|
||||||
if (b.hideUnimportant)
|
if (b.hideUnimportant)
|
||||||
status += `<H>`
|
status += `<H>`
|
||||||
if (b.server !== undefined)
|
|
||||||
status += ` (${b.server.state})`
|
// This should be handled differently, so don't mind the lookup.
|
||||||
|
if (b.server !== undefined) {
|
||||||
|
let state = b.server.state
|
||||||
|
for (const s in Relay.ServerState)
|
||||||
|
if (Relay.ServerState[s] == b.server.state) {
|
||||||
|
state = s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
status += ` (${state})`
|
||||||
|
}
|
||||||
return m('.status', {}, status)
|
return m('.status', {}, status)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
122
xP/xP.go
122
xP/xP.go
@ -8,6 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@ -21,6 +22,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
debug = flag.Bool("debug", false, "enable debug output")
|
||||||
|
|
||||||
addressBind string
|
addressBind string
|
||||||
addressConnect string
|
addressConnect string
|
||||||
addressWS string
|
addressWS string
|
||||||
@ -28,7 +31,7 @@ var (
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
func relayReadJSON(r io.Reader) []byte {
|
func relayReadFrame(r io.Reader) []byte {
|
||||||
var length uint32
|
var length uint32
|
||||||
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
log.Println("Event receive failed: " + err.Error())
|
log.Println("Event receive failed: " + err.Error())
|
||||||
@ -40,32 +43,38 @@ func relayReadJSON(r io.Reader) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("<? %v\n", b)
|
if *debug {
|
||||||
|
log.Printf("<? %v\n", b)
|
||||||
|
|
||||||
var m RelayEventMessage
|
var m RelayEventMessage
|
||||||
if after, ok := m.ConsumeFrom(b); !ok {
|
if after, ok := m.ConsumeFrom(b); !ok {
|
||||||
log.Println("Event deserialization failed")
|
log.Println("Event deserialization failed")
|
||||||
return nil
|
return nil
|
||||||
} else if len(after) != 0 {
|
} else if len(after) != 0 {
|
||||||
log.Println("Event deserialization failed: trailing data")
|
log.Println("Event deserialization failed: trailing data")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
j, err := m.MarshalJSON()
|
j, err := m.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Event marshalling failed: " + err.Error())
|
log.Println("Event marshalling failed: " + err.Error())
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("<- %s\n", j)
|
||||||
}
|
}
|
||||||
return j
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte {
|
func relayMakeReceiver(ctx context.Context, conn net.Conn) <-chan []byte {
|
||||||
p := make(chan []byte, 1)
|
// The usual event message rarely gets above 1 kilobyte,
|
||||||
r := bufio.NewReader(conn)
|
// thus this is set to buffer up at most 1 megabyte or so.
|
||||||
|
p := make(chan []byte, 1000)
|
||||||
|
r := bufio.NewReaderSize(conn, 65536)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(p)
|
defer close(p)
|
||||||
for {
|
for {
|
||||||
j := relayReadJSON(r)
|
j := relayReadFrame(r)
|
||||||
if j == nil {
|
if j == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -97,7 +106,9 @@ func relayWriteJSON(conn net.Conn, j []byte) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("-> %v\n", b)
|
if *debug {
|
||||||
|
log.Printf("-> %v\n", b)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,21 +125,23 @@ func clientReadJSON(ctx context.Context, ws *websocket.Conn) []byte {
|
|||||||
"Command receive failed: " + "binary messages are not supported")
|
"Command receive failed: " + "binary messages are not supported")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("?> %s\n", j)
|
|
||||||
|
if *debug {
|
||||||
|
log.Printf("?> %s\n", j)
|
||||||
|
}
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientWriteJSON(ctx context.Context, ws *websocket.Conn, j []byte) bool {
|
func clientWriteBinary(ctx context.Context, ws *websocket.Conn, b []byte) bool {
|
||||||
if err := ws.Write(ctx, websocket.MessageText, j); err != nil {
|
if err := ws.Write(ctx, websocket.MessageBinary, b); err != nil {
|
||||||
log.Println("Event send failed: " + err.Error())
|
log.Println("Event send failed: " + err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
log.Printf("<- %s\n", j)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
|
func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
|
||||||
j, err := (&RelayEventMessage{
|
b, ok := (&RelayEventMessage{
|
||||||
EventSeq: 0,
|
EventSeq: 0,
|
||||||
Data: RelayEventData{
|
Data: RelayEventData{
|
||||||
Interface: RelayEventDataError{
|
Interface: RelayEventDataError{
|
||||||
@ -137,12 +150,12 @@ func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
|
|||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).MarshalJSON()
|
}).AppendTo(nil)
|
||||||
if err != nil {
|
if ok {
|
||||||
log.Println("Event marshalling failed: " + err.Error())
|
log.Println("Event serialization failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return clientWriteJSON(ctx, ws, j)
|
return clientWriteBinary(ctx, ws, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWS(w http.ResponseWriter, r *http.Request) {
|
func handleWS(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -164,15 +177,36 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
conn, err := net.Dial("tcp", addressConnect)
|
conn, err := net.Dial("tcp", addressConnect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Println("Connection failed: " + err.Error())
|
||||||
clientWriteError(ctx, ws, err)
|
clientWriteError(ctx, ws, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
// To decrease latencies, events are received and decoded in parallel
|
||||||
|
// to their sending, and we try to batch them together.
|
||||||
|
relayFrames := relayMakeReceiver(ctx, conn)
|
||||||
|
batchFrames := func() []byte {
|
||||||
|
batch, ok := <-relayFrames
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Batch:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case b, ok := <-relayFrames:
|
||||||
|
if !ok {
|
||||||
|
break Batch
|
||||||
|
}
|
||||||
|
batch = append(batch, b...)
|
||||||
|
default:
|
||||||
|
break Batch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return batch
|
||||||
|
}
|
||||||
|
|
||||||
// We don't need to intervene, so it's just two separate pipes so far.
|
// We don't need to intervene, so it's just two separate pipes so far.
|
||||||
// However, to decrease latencies, events are received and decoded
|
|
||||||
// in parallel to their sending.
|
|
||||||
relayJSON := relayMakeReceiver(ctx, conn)
|
|
||||||
go func() {
|
go func() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
for {
|
for {
|
||||||
@ -186,11 +220,11 @@ func handleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
for {
|
for {
|
||||||
j, ok := <-relayJSON
|
b := batchFrames()
|
||||||
if !ok {
|
if b == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clientWriteJSON(ctx, ws, j)
|
clientWriteBinary(ctx, ws, b)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@ -214,7 +248,7 @@ var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
|
|||||||
<script>
|
<script>
|
||||||
let proxy = '{{ . }}'
|
let proxy = '{{ . }}'
|
||||||
</script>
|
</script>
|
||||||
<script src="xP.js">
|
<script type="module" src="xP.js">
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`))
|
</html>`))
|
||||||
@ -235,13 +269,21 @@ func handleDefault(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 3 || len(os.Args) > 4 {
|
flag.Usage = func() {
|
||||||
log.Fatalf("usage: %s BIND CONNECT [WSURI]\n", os.Args[0])
|
fmt.Fprintf(flag.CommandLine.Output(),
|
||||||
|
"Usage: %s [OPTION...] BIND CONNECT [WSURI]\n\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
addressBind, addressConnect = os.Args[1], os.Args[2]
|
flag.Parse()
|
||||||
if len(os.Args) > 3 {
|
if flag.NArg() < 2 || flag.NArg() > 3 {
|
||||||
addressWS = os.Args[3]
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
addressBind, addressConnect = flag.Arg(0), flag.Arg(1)
|
||||||
|
if flag.NArg() > 2 {
|
||||||
|
addressWS = flag.Arg(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Handle("/ws", http.HandlerFunc(handleWS))
|
http.Handle("/ws", http.HandlerFunc(handleWS))
|
||||||
|
Loading…
Reference in New Issue
Block a user