|
5 | 5 | import json |
6 | 6 | import sys |
7 | 7 | import re |
8 | | -import os |
9 | | -import shlex |
10 | 8 | import shutil |
11 | 9 | import subprocess |
12 | 10 | from pathlib import Path |
13 | 11 | from typing import Any, Dict, List, Optional |
14 | 12 |
|
15 | | -from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, HTTPException |
| 13 | +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException |
16 | 14 | from fastapi.middleware.cors import CORSMiddleware |
17 | 15 | from fastapi.staticfiles import StaticFiles |
18 | | -from fastapi import HTTPException |
19 | 16 | from pydantic import BaseModel |
20 | 17 | from paths import ensure_user_file |
21 | 18 | from auto_setup import ensure_can_environment, log_env_summary |
22 | 19 |
|
23 | 20 | # Local modules |
24 | | -from bus import BusManager, Frame |
| 21 | +from bus import BusManager |
25 | 22 | from decoder import decode_frame, safe_hex |
26 | 23 | from j1939_maps import PGN_NAME_MAP |
27 | 24 | from models import ConnectRequest, SendRequest, LogStartRequest |
|
41 | 38 |
|
42 | 39 | # ---------------- Privileged runner and link helpers ---------------- |
43 | 40 |
|
| 41 | +# backend/app.py (replace _run_priv) |
44 | 42 | def _run_priv(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: |
45 | 43 | """ |
46 | | - Run a privileged netlink command. We try directly first (works if the Linux |
47 | | - binary has cap_net_admin/cap_net_raw). If that fails for permissions and |
48 | | - pkexec is available, we transparently retry with pkexec (GUI password). |
| 44 | + Run a privileged netlink command. |
| 45 | +
|
| 46 | + Behavior: |
| 47 | + - Try normally first. |
| 48 | + - If it fails (non-zero exit), and it *looks* like a permission problem, |
| 49 | + retry via pkexec (GUI password) when available. |
| 50 | + - If check=True, raise CalledProcessError for a non-zero final result. |
49 | 51 | """ |
50 | | - try: |
51 | | - return subprocess.run(cmd, text=True, capture_output=True, check=check) |
52 | | - except subprocess.CalledProcessError as e: |
53 | | - # If permission problem, try pkexec if present |
54 | | - if ("Operation not permitted" in (e.stderr or "") or e.returncode in (1, 126)) and shutil.which("pkexec"): |
55 | | - return subprocess.run(["pkexec", *cmd], text=True, capture_output=True, check=check) |
56 | | - raise |
57 | | - except PermissionError: |
58 | | - if shutil.which("pkexec"): |
59 | | - return subprocess.run(["pkexec", *cmd], text=True, capture_output=True, check=check) |
60 | | - raise |
| 52 | + proc = subprocess.run(cmd, text=True, capture_output=True) |
| 53 | + if proc.returncode != 0: |
| 54 | + stderr = (proc.stderr or "") |
| 55 | + looks_perm = ( |
| 56 | + "Operation not permitted" in stderr |
| 57 | + or "permission denied" in stderr.lower() |
| 58 | + or proc.returncode in (1, 126, 127) |
| 59 | + ) |
| 60 | + if looks_perm and shutil.which("pkexec"): |
| 61 | + proc = subprocess.run(["pkexec", *cmd], text=True, capture_output=True) |
| 62 | + |
| 63 | + if check and proc.returncode != 0: |
| 64 | + raise subprocess.CalledProcessError(proc.returncode, cmd, output=proc.stdout, stderr=proc.stderr) |
| 65 | + |
| 66 | + return proc |
61 | 67 |
|
62 | 68 | def _ip_exists(iface: str) -> bool: |
63 | 69 | r = subprocess.run(["ip", "-br", "link", "show", iface], text=True, capture_output=True) |
@@ -107,26 +113,12 @@ def bus_health_snapshot_safe() -> Dict[str, Any]: |
107 | 113 | # ----------------------------------------------------------------------------- |
108 | 114 | # Helpers for bring-up |
109 | 115 | # ----------------------------------------------------------------------------- |
110 | | -def _which_any(*names: str) -> str: |
111 | | - """Return first existing absolute path for a binary name; raise if none.""" |
112 | | - for n in names: |
113 | | - p = shutil.which(n) |
114 | | - if p: |
115 | | - return p |
116 | | - raise FileNotFoundError(f"Missing required tool: {', '.join(names)}") |
117 | | - |
118 | | -def _safe_bitrate(bps: int) -> int: |
119 | | - allowed = {125000, 250000, 500000, 1000000} |
120 | | - if bps in allowed: |
121 | | - return bps |
122 | | - raise HTTPException(status_code=400, detail=f"Unsupported bitrate {bps}") |
123 | | - |
124 | 116 | def _safe_iface(name: str) -> str: |
125 | | - import re |
126 | 117 | if re.fullmatch(r"(v?can)\d{1,3}", name): |
127 | 118 | return name |
128 | 119 | raise HTTPException(status_code=400, detail=f"Bad interface name {name}") |
129 | 120 |
|
| 121 | + |
130 | 122 | # ----------------------------------------------------------------------------- |
131 | 123 | # API routes |
132 | 124 | # ----------------------------------------------------------------------------- |
@@ -223,7 +215,11 @@ def api_can_bringup(req: BringUpReq): |
223 | 215 |
|
224 | 216 | # Physical SocketCAN device: DOWN -> type can bitrate -> UP |
225 | 217 | # Bring it down first (ignore error if it's already down) |
226 | | - _run_priv(["ip", "link", "set", iface, "down"], check=False) |
| 218 | + try: |
| 219 | + _run_priv(["ip", "link", "set", iface, "down"], check=True) |
| 220 | + except subprocess.CalledProcessError: |
| 221 | + # Ignore errors like "Cannot find device" — the next steps will clarify state |
| 222 | + pass |
227 | 223 | # Configure bitrate/type (this is what fails if you try it on vcan or while UP) |
228 | 224 | _run_priv(["ip", "link", "set", iface, "type", "can", "bitrate", str(bitrate)], check=True) |
229 | 225 | # Bring it up |
@@ -294,7 +290,6 @@ async def api_platform(): |
294 | 290 | Report the server's platform: 'linux', 'win32', 'darwin', etc. |
295 | 291 | Frontend uses this to hide Bring Up on Windows (not needed for Kvaser). |
296 | 292 | """ |
297 | | - import sys |
298 | 293 | return {"platform": sys.platform} |
299 | 294 |
|
300 | 295 | # ----------------------------- Logging control ------------------------------- |
|
0 commit comments