feat(fleet): preserve operator VESC integration before final driver merge
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""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
|
||||
|
||||
|
||||
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 == "/operation":
|
||||
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))
|
||||
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")
|
||||
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)
|
||||
|
||||
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()
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
stop.set()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user