noVNC bridge: per-device live screens in dashboard (websockify + TokenFile + VNC button)
This commit is contained in:
12
android-vnc.service
Normal file
12
android-vnc.service
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Android Fleet — noVNC bridge (per-device live screens)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/bin/websockify --web=/usr/share/novnc --token-plugin=TokenFile --token-source=/opt/android-fleet/vnc_tokens.json 0.0.0.0:8081
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
41
app.py
41
app.py
@@ -691,6 +691,45 @@ def api_shot_latest(mac):
|
|||||||
return jsonify({"error": "no screenshot yet"}), 404
|
return jsonify({"error": "no screenshot yet"}), 404
|
||||||
return send_file(path, mimetype="image/png")
|
return send_file(path, mimetype="image/png")
|
||||||
|
|
||||||
|
VNC_TOKEN_FILE = "/opt/android-fleet/vnc_tokens.json"
|
||||||
|
|
||||||
|
def regen_vnc_tokens():
|
||||||
|
"""Map each online device MAC -> its QEMU VNC port for websockify TokenFile."""
|
||||||
|
try:
|
||||||
|
con = db()
|
||||||
|
rows = con.execute("SELECT mac, vmid FROM devices WHERE vmid IS NOT NULL").fetchall()
|
||||||
|
con.close()
|
||||||
|
lines = []
|
||||||
|
for r in rows:
|
||||||
|
got = pve("GET", f"/nodes/pve/qemu/{r['vmid']}/config") or {}
|
||||||
|
cfg = (got.get("data") or {}) or {}
|
||||||
|
args = cfg.get("args", "") or ""
|
||||||
|
m = re.search(r"-vnc 0\.0\.0\.0:(\d{4})", args)
|
||||||
|
if m:
|
||||||
|
port = int(m.group(1)) + 5900 # QEMU: -vnc :N listens on N+5900
|
||||||
|
lines.append(f"{r['mac']}: 10.30.20.85:{port}")
|
||||||
|
with open(VNC_TOKEN_FILE, "w") as f:
|
||||||
|
f.write("\n".join(lines) + ("\n" if lines else ""))
|
||||||
|
except Exception as e:
|
||||||
|
log("WARN", f"vnc token regen: {e}")
|
||||||
|
|
||||||
|
def vnc_loop():
|
||||||
|
while True:
|
||||||
|
regen_vnc_tokens()
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
@app.route("/api/device/<mac>/vnc")
|
||||||
|
def api_vnc(mac):
|
||||||
|
regen_vnc_tokens()
|
||||||
|
try:
|
||||||
|
tokens = open(VNC_TOKEN_FILE).read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
tokens = ""
|
||||||
|
if mac not in tokens:
|
||||||
|
return jsonify({"error": "device has no VNC mapping (needs vmid + -vnc args)"}), 404
|
||||||
|
return jsonify({"url": (f"http://10.30.20.31:8081/vnc.html?autoconnect=true"
|
||||||
|
f"&reconnect=true&resize=scale&path=websockify?token={mac}")})
|
||||||
|
|
||||||
@app.route("/api/device/<mac>", methods=["DELETE"])
|
@app.route("/api/device/<mac>", methods=["DELETE"])
|
||||||
def api_delete_device(mac):
|
def api_delete_device(mac):
|
||||||
"""Remove a device row (and release its proxy)."""
|
"""Remove a device row (and release its proxy)."""
|
||||||
@@ -841,7 +880,7 @@ if __name__ == "__main__":
|
|||||||
subprocess.run(["adb", "start-server"], capture_output=True, timeout=10)
|
subprocess.run(["adb", "start-server"], capture_output=True, timeout=10)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for fn in (discovery_loop, adb_loop, proxy_loop, ws_broadcast_loop, screenshot_loop):
|
for fn in (discovery_loop, adb_loop, proxy_loop, ws_broadcast_loop, screenshot_loop, vnc_loop):
|
||||||
threading.Thread(target=fn, daemon=True).start()
|
threading.Thread(target=fn, daemon=True).start()
|
||||||
log("STARTUP", f"godhead up — template {TEMPLATE}, LAN {LAN}")
|
log("STARTUP", f"godhead up — template {TEMPLATE}, LAN {LAN}")
|
||||||
app.run(host="0.0.0.0", port=8080, threaded=True)
|
app.run(host="0.0.0.0", port=8080, threaded=True)
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ function renderFleet() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1.5 flex-wrap">
|
<div class="flex gap-1.5 flex-wrap">
|
||||||
<button onclick="doTerm('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/40">TERMINAL</button>
|
<button onclick="doTerm('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/40">TERMINAL</button>
|
||||||
|
<button onclick="doVnc('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-cyan-500/20 text-cyan-300 hover:bg-cyan-500/40">VNC</button>
|
||||||
<button onclick="doOnboard('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-fuchsia-500/20 text-fuchsia-300 hover:bg-fuchsia-500/40">ONBOARD</button>
|
<button onclick="doOnboard('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-fuchsia-500/20 text-fuchsia-300 hover:bg-fuchsia-500/40">ONBOARD</button>
|
||||||
<button onclick="doShell('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-zinc-700/50 hover:bg-zinc-600">SHELL</button>
|
<button onclick="doShell('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-zinc-700/50 hover:bg-zinc-600">SHELL</button>
|
||||||
<button onclick="doShot('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-zinc-700/50 hover:bg-zinc-600">SCREENSHOT</button>
|
<button onclick="doShot('${d.mac}')" class="text-[10px] px-2 py-1 rounded bg-zinc-700/50 hover:bg-zinc-600">SCREENSHOT</button>
|
||||||
@@ -396,6 +397,16 @@ function hoverOff() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- interactive terminal ----------
|
// ---------- interactive terminal ----------
|
||||||
|
function doVnc(mac) {
|
||||||
|
fetch("/api/device/" + mac + "/vnc").then(r => r.json()).then(d => {
|
||||||
|
if (d.url) {
|
||||||
|
openModal("LIVE SCREEN — " + mac.slice(0, 14), `<iframe src="${d.url}" class="w-full rounded" style="height:75vh;background:#000"></iframe>`);
|
||||||
|
} else {
|
||||||
|
openModal("LIVE SCREEN", `<div class="text-red-400">${d.error || "no VNC mapping"}</div>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function doTerm(mac) {
|
function doTerm(mac) {
|
||||||
openModal("TERMINAL — " + mac.slice(0, 14),
|
openModal("TERMINAL — " + mac.slice(0, 14),
|
||||||
`<div id="termHost" class="bg-black rounded border border-zinc-700 p-2" style="height:420px"></div>
|
`<div id="termHost" class="bg-black rounded border border-zinc-700 p-2" style="height:420px"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user