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:
91
server.ts
91
server.ts
@@ -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); });
|
||||
|
||||
Reference in New Issue
Block a user