Files
Stend_mont/rdp-proxy/entrypoint.sh
T
ruslan d0ff949828 fix: remove stale Xvfb lock file on container restart
On restart, /tmp/.X1-lock remains from previous run causing Xvfb to fail
with 'Server is already active for display 1', which then breaks xfreerdp
and x11vnc. Clean up lock and socket before starting Xvfb.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 06:50:56 +00:00

220 lines
7.8 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
RDP_HOST="${RDP_HOST:?RDP_HOST is required}"
RDP_PORT="${RDP_PORT:-3389}"
RDP_USER="${RDP_USER:-}"
RDP_PASSWORD="${RDP_PASSWORD:-}"
RDP_DOMAIN="${RDP_DOMAIN:-}"
RDP_SECURITY="${RDP_SECURITY:-}"
SESSION_ID="${SESSION_ID:-unknown}"
IDLE_TIMEOUT="${IDLE_TIMEOUT:-1800}"
ENABLE_HEARTBEAT="${ENABLE_HEARTBEAT:-1}"
TOUCH_PATH="${TOUCH_PATH:-/api/sessions/${SESSION_ID}/touch}"
SCREEN_GEOMETRY="${SCREEN_GEOMETRY:-1920x1080x24}"
DISPLAY_NUM="${DISPLAY_NUM:-:1}"
mkdir -p /opt/portal
cp -r /usr/share/novnc/* /opt/portal/
cat > /opt/portal/index.html <<HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RDP Session</title>
<style>
html,body,#screen{margin:0;height:100%;background:#111}
.status{
position:fixed;left:12px;top:12px;z-index:50;padding:8px 10px;border-radius:8px;
background:rgba(16,22,32,.86);border:1px solid rgba(255,255,255,.18);
color:#dce8f5;font:600 13px/1.25 sans-serif;max-width:min(92vw,560px);
}
.status.error{background:rgba(85,20,20,.9);border-color:rgba(255,130,130,.36);color:#ffe3e3}
.status.hidden{display:none}
.spinner{display:inline-block;width:12px;height:12px;border:2px solid rgba(220,232,245,.3);
border-top-color:#dce8f5;border-radius:50%;animation:spin .8s linear infinite;margin-right:7px;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
.nav-panel{
position:fixed;left:16px;top:64px;z-index:99;display:flex;gap:10px;
background:linear-gradient(180deg,rgba(15,24,36,.78),rgba(9,14,22,.86));
border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(5px);
box-shadow:0 10px 28px rgba(0,0,0,.36);padding:9px 10px;border-radius:14px
}
.nav-btn{
border:1px solid rgba(255,255,255,.26);border-radius:999px;padding:9px 14px;cursor:pointer;
background:linear-gradient(180deg,#2a8cd6,#1668a6);color:#fff;font:700 13px/1 sans-serif;
box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 5px 12px rgba(10,46,78,.45)
}
.nav-btn:hover{filter:brightness(1.08)}
.nav-btn:active{transform:translateY(1px)}
</style>
</head>
<body>
<div id="screen"></div>
<div id="status" class="status"><span class="spinner"></span>Ожидайте...</div>
<div class="nav-panel">
<button class="nav-btn" id="btn-back" type="button" title="Назад">←</button>
<button class="nav-btn" id="btn-home" type="button" title="Главная">⌂</button>
</div>
<script type="module">
import RFB from './core/rfb.js';
const wsBase = location.pathname.replace(/\/+$/, '');
const wsUrl = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + wsBase + '/websockify';
const statusEl = document.getElementById('status');
const XK_ALT_L = 0xffe9;
const XK_LEFT = 0xff51;
let rfb = null;
let connected = false;
let reconnectTimer = null;
let reconnectAttempts = 0;
const MAX_RECONNECT = 12;
const DELAYS = [1000,2000,3000,5000,8000];
let manualDisconnect = false;
function showStatus(text, isError) {
const spinner = isError ? '' : '<span class="spinner"></span>';
statusEl.innerHTML = spinner + text;
statusEl.className = 'status' + (isError ? ' error' : '');
}
function hideStatus() { statusEl.className = 'status hidden'; }
function scheduleReconnect(reason) {
if (manualDisconnect) return;
if (reconnectAttempts >= MAX_RECONNECT) {
showStatus('Соединение потеряно. Переподключение не удалось. Откройте сервис заново.', true);
return;
}
const n = ++reconnectAttempts;
const delay = DELAYS[Math.min(n-1, DELAYS.length-1)];
showStatus(\`\${reason} Повтор \${n}/\${MAX_RECONNECT} через \${Math.ceil(delay/1000)} сек.\`, true);
reconnectTimer = setTimeout(connect, delay);
}
function connect() {
if (manualDisconnect) return;
connected = false;
showStatus('Ожидайте...');
if (rfb) { try { rfb.disconnect(); } catch(e){} }
rfb = new RFB(document.getElementById('screen'), wsUrl);
rfb.viewOnly = false;
rfb.scaleViewport = true;
rfb.resizeSession = true;
rfb.addEventListener('connect', () => {
connected = true;
reconnectAttempts = 0;
clearTimeout(reconnectTimer);
showStatus('Устанавливается соединение с рабочим столом...');
setTimeout(hideStatus, 6000);
});
rfb.addEventListener('disconnect', () => {
connected = false;
if (!manualDisconnect) scheduleReconnect('Соединение потеряно.');
});
}
const enableHeartbeat = "${ENABLE_HEARTBEAT}" === "1";
const TOUCH_PATH = '${TOUCH_PATH}';
const CLOSE_PATH = TOUCH_PATH.replace(/\/touch$/, '/close');
const SESSION_CLOSED_URL = '/?session_closed=idle';
function goSessionClosed(reason) {
const r = reason === 'limit' ? 'limit' : 'idle';
try {
if (window.top && window.top !== window) { window.top.location.href = '/?session_closed=' + r; return; }
} catch(e) {}
window.location.href = '/?session_closed=' + r;
}
async function touch() {
try {
const res = await fetch(TOUCH_PATH, {method:'POST', credentials:'include'});
if (!res.ok) {
let reason = 'idle';
try { const p = await res.json(); if (p && typeof p.reason === 'string') reason = p.reason; } catch(e) {}
goSessionClosed(reason);
}
} catch(e) {}
}
let closing = false;
async function closeSessionNow() {
if (closing) return;
closing = true;
manualDisconnect = true;
clearTimeout(reconnectTimer);
try { if (rfb) rfb.disconnect(); } catch(e) {}
try { await fetch(CLOSE_PATH, {method:'POST', credentials:'include', keepalive:true}); } catch(e) {}
}
if (enableHeartbeat) {
setInterval(touch, 15000);
touch();
window.addEventListener('pagehide', closeSessionNow);
window.addEventListener('beforeunload', closeSessionNow);
}
function chord(mod, key, modCode, keyCode) {
if (!rfb) return;
rfb.sendKey(mod, modCode, true);
rfb.sendKey(key, keyCode, true);
rfb.sendKey(key, keyCode, false);
rfb.sendKey(mod, modCode, false);
}
function goHome() {
manualDisconnect = true;
clearTimeout(reconnectTimer);
try { if (window.top && window.top !== window) { window.top.location.href = '/'; return; } } catch(e) {}
window.location.href = '/';
}
document.getElementById('btn-back').addEventListener('click', () => chord(XK_ALT_L, XK_LEFT, 'AltLeft', 'ArrowLeft'));
document.getElementById('btn-home').addEventListener('click', goHome);
document.addEventListener('contextmenu', (e) => e.preventDefault());
connect();
</script>
</body>
</html>
HTML
export DISPLAY="$DISPLAY_NUM"
DISPLAY_N="${DISPLAY_NUM#:}"
rm -f "/tmp/.X${DISPLAY_N}-lock" "/tmp/.X11-unix/X${DISPLAY_N}" 2>/dev/null || true
Xvfb "$DISPLAY_NUM" -screen 0 "$SCREEN_GEOMETRY" >/tmp/xvfb.log 2>&1 &
sleep 1
RDP_ARGS=(
"/v:${RDP_HOST}:${RDP_PORT}"
"/cert:ignore"
"/f"
"/dynamic-resolution"
"/gfx-h264:avc444"
"/network:auto"
"+clipboard"
)
if [ -n "$RDP_SECURITY" ]; then
RDP_ARGS+=("/sec:${RDP_SECURITY}")
fi
if [ -n "$RDP_USER" ]; then
RDP_ARGS+=("/u:${RDP_USER}")
fi
if [ -n "$RDP_PASSWORD" ]; then
RDP_ARGS+=("/p:${RDP_PASSWORD}")
fi
if [ -n "$RDP_DOMAIN" ]; then
RDP_ARGS+=("/d:${RDP_DOMAIN}")
fi
xfreerdp "${RDP_ARGS[@]}" >/tmp/xfreerdp.log 2>&1 &
x11vnc -display "$DISPLAY_NUM" -rfbport 5900 -forever -shared -nopw -noxdamage >/tmp/x11vnc.log 2>&1 &
exec websockify --verbose --idle-timeout="$IDLE_TIMEOUT" --web=/opt/portal 6080 localhost:5900