// Docker/Podman auf entfernten Hosts über die bestehende SSH-Verbindung (CLI per exec). // Kein Zugriff auf den Docker-Socket nötig: MrTerm führt `docker ps`, `docker start` … aus. // Fehlt die Berechtigung (Benutzer nicht in der Gruppe "docker"), wird sudo mit dem gespeicherten Host-Passwort versucht. const i18n = require('../i18n'); const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; const ACTIONS = { start: 'start', stop: 'stop', restart: 'restart', remove: 'rm -f' }; const LIST_FMT = "'{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.State}}\\t{{.Status}}\\t{{.Ports}}'"; const STATS_FMT = "'{{.ID}}\\t{{.CPUPerc}}\\t{{.MemUsage}}'"; const lastLine = (s) => String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(-1)[0] || ''; const denied = (s) => /permission denied|connect to the docker daemon socket/i.test(s); function execOn(conn, cmd, stdin) { return new Promise((resolve, reject) => { conn.exec(cmd, (err, stream) => { if (err) return reject(err); let out = '', errOut = ''; stream.on('data', (d) => { out += d; }); stream.stderr.on('data', (d) => { errOut += d; }); stream.on('close', (code) => resolve({ code: code ?? 0, out, err: errOut })); stream.end(stdin ?? ''); }); }); } class DockerManager { constructor(ssh) { this.ssh = ssh; this.sessions = new Map(); // id -> { conn, jumps, host, runtime, sudo } } async open(id, host, onClose) { const { conn, jumps } = await this.ssh.connect(host, id); this.sessions.set(id, { conn, jumps, host, runtime: null, sudo: false }); conn.on('close', () => { if (this.sessions.has(id)) { this.close(id); onClose(); } }); conn.on('error', () => {}); return this.list(id); } get(id) { const s = this.sessions.get(id); if (!s) throw new Error(i18n.t('Docker session not found')); return s; } run(s, args) { const sudo = s.sudo ? "sudo -S -p '' " : ''; return execOn(s.conn, `${sudo}${s.runtime} ${args}`, s.sudo ? `${s.host.password || ''}\n` : ''); } async list(id) { const s = this.get(id); if (!s.runtime) { const r = await execOn(s.conn, 'for c in docker podman; do if command -v $c >/dev/null 2>&1; then echo $c; exit 0; fi; done; exit 127'); if (r.code) throw new Error(i18n.t('Neither Docker nor Podman was found on this host.')); s.runtime = r.out.trim(); } let r = await this.run(s, `ps -a --format ${LIST_FMT}`); if (r.code && denied(r.err) && !s.sudo) { s.sudo = true; r = await this.run(s, `ps -a --format ${LIST_FMT}`); if (r.code) s.sudo = false; } if (r.code) { if (denied(r.err) || /sudo/i.test(r.err)) { throw new Error(i18n.t('No permission to use {runtime}. Add the user to the "docker" group (sudo usermod -aG docker {user}) or save the host password so MrTerm can use sudo.', { runtime: s.runtime, user: s.host.username || '$USER' })); } throw new Error(lastLine(r.err) || i18n.t('{runtime} exited with code {code}', { runtime: s.runtime, code: r.code })); } const containers = r.out.split('\n').filter(Boolean).map((l) => { const [cid, name, image, state, status, ports] = l.split('\t'); return { id: cid.slice(0, 12), name, image, state: (state || '').toLowerCase(), status, ports }; }); return { runtime: s.runtime, sudo: s.sudo, containers }; } // CPU/RAM der laufenden Container (dauert ~2 s) async stats(id) { const s = this.get(id); const r = await this.run(s, `stats --no-stream --format ${STATS_FMT}`); if (r.code) return {}; return Object.fromEntries(r.out.split('\n').filter(Boolean).map((l) => { const [cid, cpu, mem] = l.split('\t'); return [cid.slice(0, 12), { cpu, mem }]; })); } async action(id, action, cid) { const s = this.get(id); if (!ACTIONS[action] || !SAFE_ID.test(cid)) throw new Error('Invalid action'); const r = await this.run(s, `${ACTIONS[action]} ${cid}`); if (r.code) throw new Error(lastLine(r.err) || i18n.t('{runtime} exited with code {code}', { runtime: s.runtime, code: r.code })); return true; } // Befehl für einen Terminal-Tab (läuft mit PTY; sudo fragt dort ggf. selbst nach dem Passwort) command(id, kind, cid) { const s = this.get(id); if (!SAFE_ID.test(cid)) throw new Error('Invalid container'); const pre = `${s.sudo ? 'sudo ' : ''}${s.runtime}`; if (kind === 'logs') return `${pre} logs -f --tail 500 ${cid}`; return `${pre} exec -it ${cid} sh -c 'if command -v bash >/dev/null 2>&1; then exec bash; else exec sh; fi'`; } close(id) { const s = this.sessions.get(id); if (!s) return; this.sessions.delete(id); try { s.conn.end(); } catch {} s.jumps?.forEach((c) => { try { c.end(); } catch {} }); } } module.exports = { DockerManager, execOn };