126 lines
3.7 KiB
GDScript
126 lines
3.7 KiB
GDScript
@tool
|
|
extends Node
|
|
|
|
# Line-delimited JSON-RPC over TCP on localhost.
|
|
# One request per line: {"id": <any>, "method": "<name>", "params": {...}}
|
|
# One response per line: {"id": <echoed>, "result": <data>} OR {"id": ..., "error": {"code": int, "message": str, "data"?: any}}
|
|
|
|
const MAX_MESSAGE_BYTES := 1_048_576 # 1 MiB per line — safety cap
|
|
|
|
var _tcp: TCPServer
|
|
var _clients: Array = [] # Array of {peer: StreamPeerTCP, buf: PackedByteArray}
|
|
var _registry
|
|
|
|
|
|
func _init(registry) -> void:
|
|
_registry = registry
|
|
|
|
|
|
func start(port: int, interface: String = "127.0.0.1") -> bool:
|
|
_tcp = TCPServer.new()
|
|
var err := _tcp.listen(port, interface)
|
|
if err != OK:
|
|
_tcp = null
|
|
return false
|
|
set_process(true)
|
|
return true
|
|
|
|
|
|
func stop() -> void:
|
|
set_process(false)
|
|
for c in _clients:
|
|
(c.peer as StreamPeerTCP).disconnect_from_host()
|
|
_clients.clear()
|
|
if _tcp:
|
|
_tcp.stop()
|
|
_tcp = null
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _tcp == null:
|
|
return
|
|
|
|
while _tcp.is_connection_available():
|
|
var peer := _tcp.take_connection()
|
|
_clients.append({"peer": peer, "buf": PackedByteArray()})
|
|
|
|
var still_alive: Array = []
|
|
for c in _clients:
|
|
var peer: StreamPeerTCP = c.peer
|
|
peer.poll()
|
|
if peer.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
continue
|
|
var available := peer.get_available_bytes()
|
|
if available > 0:
|
|
var chunk := peer.get_data(available)
|
|
if chunk[0] == OK:
|
|
c.buf.append_array(chunk[1])
|
|
_drain_messages(c)
|
|
if (c.buf as PackedByteArray).size() > MAX_MESSAGE_BYTES:
|
|
_send_error(peer, null, -32700, "message exceeds %d bytes" % MAX_MESSAGE_BYTES)
|
|
peer.disconnect_from_host()
|
|
continue
|
|
still_alive.append(c)
|
|
_clients = still_alive
|
|
|
|
|
|
func _drain_messages(c: Dictionary) -> void:
|
|
while true:
|
|
var buf: PackedByteArray = c.buf
|
|
var nl := buf.find(0x0A) # \n
|
|
if nl < 0:
|
|
return
|
|
var line_bytes := buf.slice(0, nl)
|
|
c.buf = buf.slice(nl + 1)
|
|
var line := line_bytes.get_string_from_utf8().strip_edges()
|
|
if line == "":
|
|
continue
|
|
_handle_line(c.peer, line)
|
|
|
|
|
|
func _handle_line(peer: StreamPeerTCP, line: String) -> void:
|
|
var parsed = JSON.parse_string(line)
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
_send_error(peer, null, -32700, "parse error — expected JSON object")
|
|
return
|
|
var id = parsed.get("id")
|
|
var method: String = parsed.get("method", "")
|
|
var params: Dictionary = parsed.get("params", {})
|
|
if method == "":
|
|
_send_error(peer, id, -32600, "missing 'method'")
|
|
return
|
|
|
|
var result: Dictionary = _registry.dispatch(method, params)
|
|
# Guard against silent tool-module failures: when a tool's .gd file has a parse
|
|
# error, preload() returns null, calls on it produce no output, and dispatch
|
|
# returns its default {}. Surface that instead of sending {"result": null}.
|
|
if result.is_empty():
|
|
_send_error(peer, id, -32000,
|
|
"tool returned empty response — likely a parse error in the tool module. Check Godot's Output panel for the actual error.",
|
|
{"method": method})
|
|
elif result.has("error"):
|
|
var e: Dictionary = result.error
|
|
_send_error(peer, id, e.get("code", -32000), e.get("message", ""), e.get("data"))
|
|
elif result.has("data"):
|
|
_send_result(peer, id, result.data)
|
|
else:
|
|
_send_error(peer, id, -32000,
|
|
"tool returned malformed response (missing both 'data' and 'error')",
|
|
{"method": method})
|
|
|
|
|
|
func _send_result(peer: StreamPeerTCP, id, data) -> void:
|
|
_write(peer, {"id": id, "result": data})
|
|
|
|
|
|
func _send_error(peer: StreamPeerTCP, id, code: int, msg: String, data = null) -> void:
|
|
var err := {"code": code, "message": msg}
|
|
if data != null:
|
|
err["data"] = data
|
|
_write(peer, {"id": id, "error": err})
|
|
|
|
|
|
func _write(peer: StreamPeerTCP, obj: Dictionary) -> void:
|
|
var line := JSON.stringify(obj) + "\n"
|
|
peer.put_data(line.to_utf8_buffer())
|