Ubuntu GPU sessions: spawn/destroy full desktop with 4080 attached via noVNC proxy. Fixed branding (was 'My Google AI Studio App').

This commit is contained in:
drjones
2026-08-24 21:33:40 -07:00
parent d0f81b7432
commit 214f97b580
5 changed files with 334 additions and 7 deletions

View File

@@ -7,6 +7,7 @@ import { execFile } from "child_process";
import { promisify } from "util";
import { createServer as createViteServer } from "vite";
import { DatabaseSync } from "node:sqlite";
import { createProxyMiddleware } from "http-proxy-middleware";
const exec = promisify(execFile);
@@ -68,7 +69,7 @@ type GpuNode = {
type GpuJob = {
id: string;
hostname: string;
kind: "shell" | "hashcat" | "comfyui";
kind: "shell" | "hashcat" | "comfyui" | "provision_ubuntu" | "destroy_ubuntu";
command: string;
payload: Record<string, unknown>;
status: "pending" | "running" | "done" | "failed";
@@ -119,6 +120,13 @@ db.exec(`
btcpay_invoice_id TEXT, checkout_link TEXT,
status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, settled_at INTEGER
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
instance_id TEXT NOT NULL UNIQUE, node_hostname TEXT NOT NULL,
node_ip TEXT NOT NULL, port INTEGER NOT NULL, password TEXT NOT NULL,
resolution TEXT, state TEXT NOT NULL DEFAULT 'provisioning',
created_at INTEGER NOT NULL
);
`);
// ---- Migrations (add columns that predate password/unlimited) ----
@@ -202,6 +210,21 @@ function allocatePort(): number {
return p;
}
// Allocate a session (noVNC) port from a dedicated range, distinct from VM ports.
function allocateSessionPort(): number {
const used = all<{ port: number }>("SELECT port FROM sessions WHERE port IS NOT NULL").map((r) => r.port);
let p = 6090;
while (used.includes(p) && p < 6190) p++;
return p;
}
// Enqueue a GPU job for a node to pick up on its next poll.
function dispatchJob(hostname: string, kind: GpuJob["kind"], command: string, payload: Record<string, unknown>): GpuJob {
const job: GpuJob = { id: "job_" + crypto.randomBytes(6).toString("hex"), hostname, kind, command, payload, status: "pending", result: "", createdAt: Date.now(), completedAt: null };
jobs.push(job); persistJobs();
return job;
}
// ---- auth helpers ----
function nodeAuthorized(req: express.Request) { return req.headers["x-node-secret"] === NODE_SECRET; }
function adminAuthorized(req: express.Request) { return (req.headers["authorization"] || "") === `Bearer ${ADMIN_TOKEN}`; }
@@ -380,6 +403,46 @@ async function startServer() {
res.json({ received: true });
});
// ===== UBUNTU GPU SESSIONS (spawn in-browser desktop with the 4080 attached) =====
app.post("/api/session/spawn", (req, res) => {
const { userId, resolution } = req.body || {};
const user = one<any>("SELECT * FROM users WHERE id=?", str(userId, ""));
if (!user) return res.status(400).json({ error: "invalid user" });
// Target the Linux GPU node (nightmare) that runs the Ubuntu-session agent.
const hostname = "nightmare";
const node = nodes[hostname];
if (!node || Date.now() - node.lastSeen > 30_000) {
return res.status(503).json({ error: "GPU node offline — try again shortly" });
}
const instanceId = "sess_" + crypto.randomBytes(4).toString("hex");
const port = allocateSessionPort();
const password = "Ub" + crypto.randomBytes(6).toString("hex") + "!";
const reso = str(resolution, "1440x900");
const id = "ses_" + crypto.randomBytes(8).toString("hex");
q("INSERT INTO sessions (id,user_id,instance_id,node_hostname,node_ip,port,password,resolution,state,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
id, user.id, instanceId, hostname, node.ip, port, password, reso, "provisioning", Date.now());
dispatchJob(hostname, "provision_ubuntu", "", { instanceId, port, password, resolution: reso });
res.json({ id, instanceId, port, password, resolution: reso, state: "provisioning", url: `/session/${instanceId}/` });
});
app.get("/api/sessions", (req, res) => {
const userId = str(req.query.userId, "");
if (!userId) return res.status(400).json({ error: "userId required" });
res.json(all<any>("SELECT * FROM sessions WHERE user_id=? ORDER BY created_at DESC", userId));
});
app.post("/api/session/destroy", (req, res) => {
const sess = one<any>("SELECT * FROM sessions WHERE id=? AND user_id=?", str(req.body?.sessionId, ""), str(req.body?.userId, ""));
if (!sess) return res.status(404).json({ error: "not found" });
q("UPDATE sessions SET state='stopping' WHERE id=?", sess.id);
dispatchJob(sess.node_hostname, "destroy_ubuntu", "", { instanceId: sess.instance_id });
res.json({ ok: true });
});
// ===== GPU NODE LAYER =====
app.post("/api/node/register", (req, res) => {
if (!nodeAuthorized(req)) return res.status(401).json({ error: "unauthorized" });
@@ -417,6 +480,13 @@ async function startServer() {
job.status = req.body?.ok ? "done" : "failed";
job.result = String(req.body?.result ?? "");
job.completedAt = Date.now();
// Reflect provisioning result onto the session row.
const p = job.payload as any;
if (job.kind === "provision_ubuntu" && p?.instanceId) {
q("UPDATE sessions SET state=? WHERE instance_id=?", req.body?.ok ? "running" : "failed", p.instanceId);
} else if (job.kind === "destroy_ubuntu" && p?.instanceId) {
q("UPDATE sessions SET state='stopped' WHERE instance_id=?", p.instanceId);
}
persistJobs();
res.json({ ok: true });
});
@@ -475,6 +545,22 @@ async function startServer() {
} catch (e) { console.error("[billing]", e); }
}, 60_000);
// ===== SESSION noVNC PROXY (WebSocket-capable) =====
const sessionProxy = createProxyMiddleware({
target: "http://127.0.0.1:1",
changeOrigin: true,
ws: true,
pathFilter: "/session/**",
router: (req) => {
const m = (req.url || "").match(/^\/session\/([^/]+)/);
if (!m) return "http://127.0.0.1:1";
const sess = one<any>("SELECT * FROM sessions WHERE instance_id=?", m[1]);
return sess ? `http://${sess.node_ip}:${sess.port}` : "http://127.0.0.1:1";
},
pathRewrite: (path) => path.replace(/^\/session\/[^/]+/, "") || "/",
});
app.use(sessionProxy);
// ===== STATIC =====
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" });
@@ -488,11 +574,12 @@ async function startServer() {
});
}
app.listen(PORT, "0.0.0.0", () => {
const server = app.listen(PORT, "0.0.0.0", () => {
console.log(`[VortexGPU] rent-a-PC gateway on :${PORT}`);
console.log(`[VortexGPU] Proxmox ${PVE_HOST} | win tpl ${PVE_TEMPLATE_WIN} | linux tpl ${PVE_TEMPLATE_LINUX}`);
console.log(`[VortexGPU] GPU SKU: ${GPU_SKU}`);
});
server.on("upgrade", sessionProxy.upgrade);
}
startServer().catch((e) => { console.error(e); process.exit(1); });