Files
MeshCore/tools/hil/ethernet_serial_rpc.py
T

97 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Private SSH-stdio transport for one explicitly selected lab USB CLI port.
Stdout is exclusively bounded JSON-lines. The SSH connection owns the serial
session; EOF or a malformed/incomplete command closes it, without rebooting.
"""
from __future__ import annotations
import argparse
import contextlib
import json
import math
import sys
MAX_LINE = 65536
MAX_COMMAND = 512
PROTOCOL = "meshcore-serial-rpc-1"
def validate_command(command, timeout):
if not isinstance(command, str) or not command or len(command) > MAX_COMMAND \
or any(character in command for character in "\r\n\0"):
raise ValueError("a bounded single-line command is required")
command.encode("ascii")
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) \
or not math.isfinite(timeout) or not 0 < timeout <= 30:
raise ValueError("command timeout must be finite and in (0,30]")
def encode_message(message):
data = (json.dumps(message, ensure_ascii=True, allow_nan=False) + "\n").encode("ascii")
if len(data) > MAX_LINE:
raise ValueError("RPC message exceeds the line limit")
return data
def decode_message(line):
if not line or len(line) > MAX_LINE or not line.endswith(b"\n"):
raise ValueError("RPC requires bounded, complete JSON lines")
message = json.loads(line.decode("utf-8"), parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value)))
if not isinstance(message, dict):
raise ValueError("RPC message must be an object")
return message
def serve(node, source, destination):
def send(message):
destination.write(encode_message(message))
destination.flush()
try:
send(dict(type="ready", protocol=PROTOCOL, identity=node.identity,
hello=getattr(node, "hello", "")))
while True:
line = source.readline(MAX_LINE + 1)
if not line:
return 0
request_id = None
try:
request = decode_message(line)
if set(request) != {"id", "command", "timeout"}:
raise ValueError("unexpected RPC request fields")
request_id = request["id"]
if type(request_id) is not int or request_id <= 0:
raise ValueError("positive integer request id required")
validate_command(request["command"], request["timeout"])
# No incidental diagnostic output may corrupt the protocol.
with contextlib.redirect_stdout(sys.stderr):
reply = node.command(request["command"], timeout=request["timeout"])
if not isinstance(reply, str):
raise ValueError("serial command did not return a text reply")
send(dict(id=request_id, completed=True, reply=reply))
except Exception as error:
send(dict(id=request_id, completed=False, error=str(error)[:1024]))
return 1 # Never attribute a late serial reply to another command.
finally:
with contextlib.redirect_stdout(sys.stderr):
node.close()
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", required=True)
args = parser.parse_args(argv)
try:
from ethernet_primary_probe import SerialNode
with contextlib.redirect_stdout(sys.stderr):
node = SerialNode(args.port)
return serve(node, sys.stdin.buffer, sys.stdout.buffer)
except Exception as error:
print("Serial RPC stopped: " + str(error), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())