/** * @file member-tmux.js * @description Server mode, one Linux user per account (S3): each member's * own tmux server as the Users page shows it — the version it runs next to * the installed one (`TMUX_BIN`), how many sessions it holds and whether an * agent of theirs is working — and an admin's restart of it. A tmux server * keeps the version it started with until its last session closes, so after * a tmux upgrade an old server lingers; `tmux-member` (install-system) keeps * every call on it matched to its own binary, and this is how an admin moves * a member onto the new version on purpose. A restart ends every pane of * theirs (their Claude sessions resume from the workspace). * @author Nguyễn Ngọc Trí Vĩ */ const { execFileSync } = require("node:child_process"); const { db } = require("../db"); const linux = require("./linux-accounts"); const tmux = require("./tmux"); const err = (code, message) => Object.assign(new Error(message), { code }); // The installed tmux's version (`3.8`), re-read at most once a minute. let installed = { at: 0, bin: null, version: null }; function installedVersion() { const bin = linux.serverConf()?.tmuxInstalled || null; if (installed.bin === bin && Date.now() - installed.at < 60_000) return installed.version; let version = null; if (bin) { try { version = String(execFileSync(bin, ["-V"], { encoding: "utf8", timeout: 3000, stdio: "pipe" })) .trim() .replace(/^tmux\s+/, "") || null; } catch { /* missing or broken: unknown */ } } installed = { at: Date.now(), bin, version }; return version; } /** How many of `userId`'s workspaces have an agent working right now. */ function busyCount(userId) { return db .prepare("SELECT COUNT(*) AS n FROM lanes WHERE owner_id = ? AND status = 'running'") .get(userId).n; } /** One user's row; null for an account with no Linux side. */ function rowFor(user, installedV) { const acct = linux.accountFor(user.username); if (!acct) return null; const info = tmux.memberServerInfo(acct); return { id: user.id, username: user.username, ...info, busy: busyCount(user.id), outdated: !!(info.running && info.version && installedV && info.version !== installedV), }; } /** `{available, installed, matched, users: [...]}` for the Users page. */ function overview(users) { if (!linux.s3Enabled()) return { available: false, users: [] }; const conf = linux.serverConf(); const v = installedVersion(); return { available: true, installed: v, // Whether calls on an older server already use its own binary. matched: conf?.bins?.tmux !== conf?.tmuxInstalled, users: users.map((u) => rowFor(u, v)).filter(Boolean), }; } /** * Restart `user`'s tmux server. `{restarted: false}` when none ran; throws * ETMUXSTUCK (with the pid) when it would not stop — root has to kill it. */ function restart(user) { const acct = linux.accountFor(user.username); if (!acct) throw err("ENOS3", "this account has no Linux user (S3 is not installed)"); const before = tmux.memberServerInfo(acct); if (!before.running) return { restarted: false, before }; try { tmux.killMemberServer(acct); } catch { /* judged by what is left below */ } const after = tmux.memberServerInfo(acct); if (after.running) { const pid = before.pid || after.pid; throw err( "ETMUXSTUCK", `${user.username}'s tmux server did not stop — run \`sudo kill -9 ${pid || ""}\` on the server` ); } return { restarted: true, before }; } module.exports = { overview, restart, installedVersion };