feat: support Linux headless HWPOD node
This commit is contained in:
+50
-7
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPEC: PJ2026-010104 AI网关; PJ2026-010405 云端控制台。
|
||||
# Implementation reference: draft-2026-07-13-p0-cloud-console.
|
||||
"""面向 Windows HWPOD 的单文件 HWLAB 图形节点。
|
||||
"""跨 Windows GUI 与 Linux headless 的单文件 HWLAB HWPOD 节点。
|
||||
|
||||
用户入口不接受命令行参数,也不依赖 HWLAB_* 环境变量。
|
||||
持久配置统一存放在 ~/.hwlab/,并由设置界面或受控部署管理。
|
||||
@@ -24,6 +24,7 @@ import queue
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
@@ -54,15 +55,16 @@ except Exception: # pragma: no cover - import failure is reported in main.
|
||||
|
||||
|
||||
APP_VERSION = "0.1.3"
|
||||
NODE_VERSION = f"{APP_VERSION}-python-gui"
|
||||
NODE_VERSION = f"{APP_VERSION}-python"
|
||||
CONTRACT_VERSION = "hwpod-node-ops-v1"
|
||||
APP_NAME = "HWLAB Node"
|
||||
CONFIG_DIR = Path.home() / ".hwlab"
|
||||
LOG_DIR = CONFIG_DIR / "logs"
|
||||
STATE_DIR = CONFIG_DIR / "state"
|
||||
UPDATE_DIR = CONFIG_DIR / "update"
|
||||
CONFIG_PATH = CONFIG_DIR / "config.json"
|
||||
STATE_PATH = CONFIG_DIR / "state.json"
|
||||
OPERATION_LEDGER_PATH = CONFIG_DIR / "hwpod-operations.json"
|
||||
STATE_PATH = STATE_DIR / "node.json"
|
||||
OPERATION_LEDGER_PATH = STATE_DIR / "hwpod-operations.json"
|
||||
LOG_PATH = LOG_DIR / "hwlab-node.log"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -101,6 +103,7 @@ SECRET_PATTERNS = [
|
||||
def ensure_dirs() -> None:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
UPDATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -1003,9 +1006,9 @@ class WebSocketNodeClient:
|
||||
"capabilities": self.executor.capabilities(),
|
||||
"maxInFlight": 1,
|
||||
"runtime": {
|
||||
"kind": "python-tkinter-gui",
|
||||
"kind": "python-tkinter-gui" if sys.platform == "win32" else "python-headless",
|
||||
"installed": True,
|
||||
"desktopVisible": True,
|
||||
"desktopVisible": sys.platform == "win32",
|
||||
"currentVersion": APP_VERSION,
|
||||
"updateChannel": str(self.config.get("update", {}).get("channel") or "stable"),
|
||||
},
|
||||
@@ -1807,12 +1810,52 @@ class HwlabNodeApp:
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
class HeadlessHwlabNodeApp:
|
||||
def __init__(self):
|
||||
ensure_dirs()
|
||||
self.store = ConfigStore()
|
||||
self.config = self.store.config
|
||||
self.logger = setup_logger()
|
||||
self.bus = UiEventBus()
|
||||
install_exception_hooks(self.logger, self.bus)
|
||||
self.executor = NodeOpsExecutor(self.config, self.logger)
|
||||
self.client = WebSocketNodeClient(self.config, self.executor, self.logger, self.bus)
|
||||
self.stop_event = threading.Event()
|
||||
|
||||
def stop(self, *_args) -> None:
|
||||
self.stop_event.set()
|
||||
self.client.stop()
|
||||
|
||||
def run(self) -> None:
|
||||
if self.config.get("autoConnect") is not True:
|
||||
raise RuntimeError("Linux headless mode requires autoConnect=true")
|
||||
for name in ("SIGINT", "SIGTERM"):
|
||||
signum = getattr(signal, name, None)
|
||||
if signum is not None:
|
||||
signal.signal(signum, self.stop)
|
||||
self.logger.info("headless node starting: nodeId=%s config=%s", self.executor.node_id, CONFIG_PATH)
|
||||
self.client.start()
|
||||
while not self.stop_event.wait(0.25):
|
||||
while True:
|
||||
try:
|
||||
kind, payload = self.bus.queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
if kind == "log":
|
||||
self.logger.info("%s", redact(payload))
|
||||
elif kind == "connection":
|
||||
self.logger.info("connection state: %s", redact(payload))
|
||||
if self.client.thread and self.client.thread.is_alive():
|
||||
self.client.thread.join(timeout=5)
|
||||
atomic_write_json(STATE_PATH, {"lastExitAt": utc_now(), "connectionState": "stopped", "runtimeKind": "python-headless"})
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ensure_dirs()
|
||||
logger = setup_logger()
|
||||
install_exception_hooks(logger)
|
||||
try:
|
||||
app = HwlabNodeApp()
|
||||
app = HwlabNodeApp() if sys.platform == "win32" else HeadlessHwlabNodeApp()
|
||||
app.run()
|
||||
return 0
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user