Files
NODEDC_MISSION_CORE/plugins/vesc/runtime/server.py
T

140 lines
5.1 KiB
Python

"""Private Unix socket endpoint; peer UID must be the installed Node service."""
import json
import os
from pathlib import Path
import pwd
import re
import socket
import socketserver
import struct
import threading
from http.server import BaseHTTPRequestHandler
from urllib.parse import urlsplit, parse_qs
from .service import Service
from .remote_control import RemoteControl
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
self.dispatch()
def do_POST(self):
self.dispatch()
def dispatch(self):
self.connection.settimeout(10)
status = 200
try:
node = self.headers.get("X-Node-Id", "")
if not re.fullmatch(r"[a-zA-Z0-9_.:-]{1,128}", node) or self.headers.get("Transfer-Encoding"):
raise ValueError("Invalid request")
if self.command == "GET" and self.path == "/inventory":
result = self.server.service.inventory(node)
elif self.command == "GET" and self.path.startswith("/archives/"):
url = urlsplit(self.path)
parts = url.path.split("/")
if len(parts) not in (3, 4) or not re.fullmatch(r"vesc_[0-9a-f]{32}", parts[2]):
raise ValueError("Invalid archive target")
archive = self.server.service.archive
if len(parts) == 4:
result = archive.read("local", parts[2], parts[3])
else:
before = int(parse_qs(url.query).get("before", ["0"])[0])
result = archive.listing("local", parts[2], before)
elif self.command == "GET" and self.path.startswith("/archive-export?"):
after = int(parse_qs(urlsplit(self.path).query).get("after", ["0"])[0])
result = self.server.service.archive.export("local", after)
elif self.command == "POST" and self.path in ("/operation", "/remote"):
size = int(self.headers.get("Content-Length", "0"))
if not 0 < size <= 65536 or self.headers.get("Content-Type") != "application/json":
raise ValueError("Invalid command")
raw = self.rfile.read(size)
if len(raw) != size:
raise ValueError("Truncated command")
result = (self.server.service.execute(json.loads(raw)) if self.path == "/operation"
else self.server.service.remote.feed(json.loads(raw)))
else:
raise ValueError("Unknown route")
except (ValueError, KeyError, TypeError, OSError):
status, result = 400, {"error": "Запрос VESC отклонён. Обновите сведения об устройстве."}
data = json.dumps(result, ensure_ascii=False, allow_nan=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(data)
class Server(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
daemon_threads = True
def __init__(self, path, handler):
self.slots = threading.BoundedSemaphore(8)
super().__init__(path, handler)
def process_request(self, request, address):
if not self.slots.acquire(blocking=False):
self.shutdown_request(request)
return
try:
super().process_request(request, address)
except BaseException:
self.slots.release()
raise
def process_request_thread(self, request, address):
try:
super().process_request_thread(request, address)
finally:
self.slots.release()
def verify_request(self, request, address):
_, uid, _ = struct.unpack("3i", request.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))
return uid == self.node_uid
def main():
if os.geteuid() == 0:
raise RuntimeError("VESC must run as its own unprivileged user")
os.umask(0o007)
service = Service("/var/lib/mission-core-vesc")
service.remote = RemoteControl(service)
stop = threading.Event()
def scan():
while not stop.is_set():
try:
service.scan()
except OSError:
# A transient sysfs race must not silently kill discovery.
pass
stop.wait(2)
def observe():
while not stop.is_set():
service.remote.observe()
stop.wait(.2)
path = Path("/run/mission-core-vesc/driver.sock")
path.unlink(missing_ok=True)
with Server(str(path), Handler) as server:
server.node_uid = pwd.getpwnam("mission-core-node").pw_uid
server.service = service
thread = threading.Thread(target=scan, daemon=True)
thread.start()
threading.Thread(target=observe, daemon=True, name="vesc-observer").start()
try:
server.serve_forever()
finally:
stop.set()
if __name__ == "__main__":
main()