|
|
from fastapi import FastAPI, HTTPException |
|
|
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse |
|
|
from fastapi.staticfiles import StaticFiles |
|
|
import subprocess |
|
|
import os |
|
|
import json |
|
|
from pathlib import Path |
|
|
import asyncio |
|
|
import time |
|
|
from datetime import datetime |
|
|
import re |
|
|
|
|
|
app = FastAPI(title="Windows XP RDP") |
|
|
|
|
|
|
|
|
WINDOWS_CONTAINER_NAME = "windows-rdp" |
|
|
WINDOWS_IMAGE = "dockurr/windows:latest" |
|
|
|
|
|
def get_installation_progress(): |
|
|
"""Tenta detectar o progresso da instalação do Windows""" |
|
|
progress = { |
|
|
"stage": "unknown", |
|
|
"percentage": 0, |
|
|
"message": "Aguardando inicialização..." |
|
|
} |
|
|
|
|
|
|
|
|
storage_path = Path("/storage") |
|
|
if storage_path.exists(): |
|
|
|
|
|
disk_file = storage_path / "disk.qcow2" |
|
|
if disk_file.exists(): |
|
|
size_mb = disk_file.stat().st_size / (1024 * 1024) |
|
|
if size_mb < 100: |
|
|
progress = { |
|
|
"stage": "downloading", |
|
|
"percentage": min(int(size_mb / 10), 10), |
|
|
"message": f"Baixando ISO do Windows XP... ({size_mb:.1f} MB)" |
|
|
} |
|
|
elif size_mb < 2000: |
|
|
progress = { |
|
|
"stage": "creating_disk", |
|
|
"percentage": min(10 + int((size_mb - 100) / 20), 30), |
|
|
"message": f"Criando disco virtual... ({size_mb:.1f} MB)" |
|
|
} |
|
|
else: |
|
|
progress = { |
|
|
"stage": "installing", |
|
|
"percentage": min(30 + int((size_mb - 2000) / 100), 90), |
|
|
"message": f"Instalando Windows XP... ({size_mb:.1f} MB)" |
|
|
} |
|
|
|
|
|
|
|
|
try: |
|
|
import socket |
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
sock.settimeout(1) |
|
|
result = sock.connect_ex(('localhost', 8006)) |
|
|
sock.close() |
|
|
if result == 0: |
|
|
progress = { |
|
|
"stage": "ready", |
|
|
"percentage": 100, |
|
|
"message": "Windows XP está pronto!" |
|
|
} |
|
|
except: |
|
|
pass |
|
|
|
|
|
return progress |
|
|
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
|
async def root(): |
|
|
"""Página inicial com logs em tempo real""" |
|
|
html_content = """ |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Windows XP RDP</title> |
|
|
<meta charset="UTF-8"> |
|
|
<meta http-equiv="refresh" content="5"> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: #333; |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
} |
|
|
.container { |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
border-radius: 10px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
} |
|
|
h1 { |
|
|
color: #667eea; |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
.progress-container { |
|
|
background: #f5f5f5; |
|
|
border-radius: 10px; |
|
|
padding: 20px; |
|
|
margin: 20px 0; |
|
|
} |
|
|
.progress-bar { |
|
|
width: 100%; |
|
|
height: 30px; |
|
|
background: #e0e0e0; |
|
|
border-radius: 15px; |
|
|
overflow: hidden; |
|
|
margin: 10px 0; |
|
|
position: relative; |
|
|
} |
|
|
.progress-fill { |
|
|
height: 100%; |
|
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); |
|
|
transition: width 0.5s ease; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: white; |
|
|
font-weight: bold; |
|
|
} |
|
|
.progress-text { |
|
|
text-align: center; |
|
|
font-size: 18px; |
|
|
margin: 10px 0; |
|
|
font-weight: 500; |
|
|
} |
|
|
.stage-badge { |
|
|
display: inline-block; |
|
|
padding: 5px 15px; |
|
|
border-radius: 20px; |
|
|
font-size: 14px; |
|
|
font-weight: bold; |
|
|
margin: 10px 0; |
|
|
} |
|
|
.stage-downloading { background: #ffc107; color: #000; } |
|
|
.stage-creating_disk { background: #17a2b8; color: #fff; } |
|
|
.stage-installing { background: #28a745; color: #fff; } |
|
|
.stage-ready { background: #28a745; color: #fff; } |
|
|
.stage-unknown { background: #6c757d; color: #fff; } |
|
|
|
|
|
.logs-container { |
|
|
background: #1e1e1e; |
|
|
color: #d4d4d4; |
|
|
border-radius: 10px; |
|
|
padding: 20px; |
|
|
margin: 20px 0; |
|
|
max-height: 500px; |
|
|
overflow-y: auto; |
|
|
font-family: 'Courier New', monospace; |
|
|
font-size: 12px; |
|
|
} |
|
|
.log-line { |
|
|
margin: 2px 0; |
|
|
padding: 2px 0; |
|
|
} |
|
|
.log-time { |
|
|
color: #858585; |
|
|
} |
|
|
.log-info { color: #4ec9b0; } |
|
|
.log-success { color: #4ec9b0; } |
|
|
.log-error { color: #f48771; } |
|
|
.log-warning { color: #dcdcaa; } |
|
|
|
|
|
.info-box { |
|
|
background: #f5f5f5; |
|
|
border-left: 4px solid #667eea; |
|
|
padding: 15px; |
|
|
margin: 20px 0; |
|
|
border-radius: 5px; |
|
|
} |
|
|
.button { |
|
|
display: inline-block; |
|
|
padding: 12px 24px; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
text-decoration: none; |
|
|
border-radius: 5px; |
|
|
margin: 10px 5px; |
|
|
transition: background 0.3s; |
|
|
} |
|
|
.button:hover { |
|
|
background: #5568d3; |
|
|
} |
|
|
.status-indicator { |
|
|
display: inline-block; |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
border-radius: 50%; |
|
|
margin-right: 8px; |
|
|
} |
|
|
.status-online { background: #28a745; } |
|
|
.status-offline { background: #dc3545; } |
|
|
.status-loading { background: #ffc107; animation: pulse 1s infinite; } |
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.5; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>🖥️ Windows XP RDP</h1> |
|
|
|
|
|
<div class="progress-container"> |
|
|
<div class="progress-text" id="progress-message">Aguardando inicialização...</div> |
|
|
<div class="progress-bar"> |
|
|
<div class="progress-fill" id="progress-fill" style="width: 0%"> |
|
|
<span id="progress-percent">0%</span> |
|
|
</div> |
|
|
</div> |
|
|
<div id="stage-badge" class="stage-badge stage-unknown">Aguardando</div> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2>📋 Informações de Conexão</h2> |
|
|
<p><strong>Versão:</strong> Windows XP</p> |
|
|
<p><strong>Usuário RDP:</strong> <code>Docker</code></p> |
|
|
<p><strong>Senha RDP:</strong> <code>admin</code></p> |
|
|
<p><strong>Porta RDP:</strong> <code>3389</code></p> |
|
|
<p><strong>Web Viewer:</strong> <a href="/web-viewer" class="button">Abrir Web Viewer</a></p> |
|
|
<p><strong>Status:</strong> <span id="status-indicator" class="status-indicator status-loading"></span><span id="status-text">Verificando...</span></p> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2>📊 Logs de Instalação</h2> |
|
|
<div class="logs-container" id="logs-container"> |
|
|
<div class="log-line"><span class="log-time">[Aguardando logs...]</span></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="info-box"> |
|
|
<h2>ℹ️ Informações</h2> |
|
|
<ul> |
|
|
<li>A instalação do Windows XP pode levar <strong>20-40 minutos</strong></li> |
|
|
<li>Você pode acompanhar o progresso acima</li> |
|
|
<li>O Web Viewer estará disponível quando o Windows iniciar</li> |
|
|
<li>Esta página atualiza automaticamente a cada 5 segundos</li> |
|
|
</ul> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let lastLogTime = null; |
|
|
|
|
|
function updateProgress() { |
|
|
fetch('/progress') |
|
|
.then(r => r.json()) |
|
|
.then(data => { |
|
|
const progressFill = document.getElementById('progress-fill'); |
|
|
const progressPercent = document.getElementById('progress-percent'); |
|
|
const progressMessage = document.getElementById('progress-message'); |
|
|
const stageBadge = document.getElementById('stage-badge'); |
|
|
|
|
|
progressFill.style.width = data.percentage + '%'; |
|
|
progressPercent.textContent = data.percentage + '%'; |
|
|
progressMessage.textContent = data.message; |
|
|
|
|
|
// Atualizar badge de estágio |
|
|
stageBadge.className = 'stage-badge stage-' + data.stage; |
|
|
const stageNames = { |
|
|
'downloading': 'Baixando ISO', |
|
|
'creating_disk': 'Criando Disco', |
|
|
'installing': 'Instalando', |
|
|
'ready': 'Pronto!', |
|
|
'unknown': 'Aguardando' |
|
|
}; |
|
|
stageBadge.textContent = stageNames[data.stage] || 'Aguardando'; |
|
|
}) |
|
|
.catch(err => console.error('Erro ao atualizar progresso:', err)); |
|
|
} |
|
|
|
|
|
function updateStatus() { |
|
|
fetch('/status') |
|
|
.then(r => r.json()) |
|
|
.then(data => { |
|
|
const statusIndicator = document.getElementById('status-indicator'); |
|
|
const statusText = document.getElementById('status-text'); |
|
|
|
|
|
if (data.running || data.web_viewer_available) { |
|
|
statusIndicator.className = 'status-indicator status-online'; |
|
|
statusText.textContent = 'Windows XP está rodando'; |
|
|
} else { |
|
|
statusIndicator.className = 'status-indicator status-loading'; |
|
|
statusText.textContent = 'Windows XP está inicializando...'; |
|
|
} |
|
|
}) |
|
|
.catch(err => console.error('Erro ao atualizar status:', err)); |
|
|
} |
|
|
|
|
|
function addLog(message, type = 'info') { |
|
|
const logsContainer = document.getElementById('logs-container'); |
|
|
const time = new Date().toLocaleTimeString(); |
|
|
const logLine = document.createElement('div'); |
|
|
logLine.className = 'log-line log-' + type; |
|
|
logLine.innerHTML = '<span class="log-time">[' + time + ']</span> ' + message; |
|
|
logsContainer.appendChild(logLine); |
|
|
logsContainer.scrollTop = logsContainer.scrollHeight; |
|
|
} |
|
|
|
|
|
function updateLogs() { |
|
|
fetch('/logs-stream') |
|
|
.then(r => r.text()) |
|
|
.then(text => { |
|
|
if (text && text.trim()) { |
|
|
const lines = text.split('\n').filter(l => l.trim()); |
|
|
lines.forEach(line => { |
|
|
if (line && !line.includes('[Aguardando logs...]')) { |
|
|
let type = 'info'; |
|
|
if (line.toLowerCase().includes('error') || line.toLowerCase().includes('fail')) { |
|
|
type = 'error'; |
|
|
} else if (line.toLowerCase().includes('success') || line.toLowerCase().includes('ready')) { |
|
|
type = 'success'; |
|
|
} else if (line.toLowerCase().includes('warn')) { |
|
|
type = 'warning'; |
|
|
} |
|
|
addLog(line, type); |
|
|
} |
|
|
}); |
|
|
} |
|
|
}) |
|
|
.catch(err => { |
|
|
// Silenciar erros de logs |
|
|
}); |
|
|
} |
|
|
|
|
|
// Atualizar a cada 2 segundos |
|
|
setInterval(() => { |
|
|
updateProgress(); |
|
|
updateStatus(); |
|
|
}, 2000); |
|
|
|
|
|
// Atualizar logs a cada 5 segundos |
|
|
setInterval(updateLogs, 5000); |
|
|
|
|
|
// Atualizar imediatamente |
|
|
updateProgress(); |
|
|
updateStatus(); |
|
|
updateLogs(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
return html_content |
|
|
|
|
|
@app.get("/progress") |
|
|
async def get_progress(): |
|
|
"""Retorna o progresso da instalação""" |
|
|
progress = get_installation_progress() |
|
|
return progress |
|
|
|
|
|
@app.get("/status") |
|
|
async def get_status(): |
|
|
"""Verifica o status do Windows (rodando no mesmo container)""" |
|
|
try: |
|
|
import socket |
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
sock.settimeout(2) |
|
|
result = sock.connect_ex(('localhost', 8006)) |
|
|
sock.close() |
|
|
web_viewer_available = result == 0 |
|
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
sock.settimeout(2) |
|
|
result = sock.connect_ex(('localhost', 3389)) |
|
|
sock.close() |
|
|
rdp_available = result == 0 |
|
|
|
|
|
running = web_viewer_available or rdp_available |
|
|
|
|
|
return { |
|
|
"running": running, |
|
|
"web_viewer_available": web_viewer_available, |
|
|
"rdp_available": rdp_available, |
|
|
"mode": "direct" |
|
|
} |
|
|
except Exception as e: |
|
|
return { |
|
|
"running": True, |
|
|
"web_viewer_available": False, |
|
|
"rdp_available": False, |
|
|
"mode": "direct", |
|
|
"note": "Status não pôde ser verificado, mas o container está ativo" |
|
|
} |
|
|
|
|
|
@app.get("/logs-stream") |
|
|
async def get_logs_stream(): |
|
|
"""Retorna logs do Windows em formato de texto""" |
|
|
logs = [] |
|
|
|
|
|
|
|
|
try: |
|
|
fastapi_log = Path("/tmp/fastapi.log") |
|
|
if fastapi_log.exists(): |
|
|
with open(fastapi_log, 'r', encoding='utf-8', errors='ignore') as f: |
|
|
lines = f.readlines() |
|
|
logs.extend(lines[-50:]) |
|
|
except: |
|
|
pass |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
result = subprocess.run( |
|
|
["ps", "aux"], |
|
|
capture_output=True, |
|
|
text=True, |
|
|
timeout=2 |
|
|
) |
|
|
if "qemu" in result.stdout.lower() or "windows" in result.stdout.lower(): |
|
|
logs.append("Windows/QEMU está rodando\n") |
|
|
except: |
|
|
pass |
|
|
|
|
|
|
|
|
progress = get_installation_progress() |
|
|
logs.append(f"Status: {progress['message']} ({progress['percentage']}%)\n") |
|
|
|
|
|
if not logs: |
|
|
logs.append("Aguardando logs do Windows...\n") |
|
|
|
|
|
return "".join(logs) |
|
|
|
|
|
@app.get("/web-viewer") |
|
|
async def web_viewer(): |
|
|
"""Redireciona para o web viewer do Windows""" |
|
|
import socket |
|
|
try: |
|
|
hostname = socket.gethostname() |
|
|
except: |
|
|
hostname = "localhost" |
|
|
|
|
|
return HTMLResponse(f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Windows XP Web Viewer</title> |
|
|
<meta charset="UTF-8"> |
|
|
<style> |
|
|
body {{ |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
background: #000; |
|
|
}} |
|
|
iframe {{ |
|
|
width: 100vw; |
|
|
height: 100vh; |
|
|
border: none; |
|
|
}} |
|
|
.error {{ |
|
|
color: white; |
|
|
text-align: center; |
|
|
padding: 50px; |
|
|
font-family: Arial, sans-serif; |
|
|
}} |
|
|
.error a {{ |
|
|
color: #667eea; |
|
|
text-decoration: none; |
|
|
}} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<iframe id="viewer" src="http://localhost:8006" allowfullscreen></iframe> |
|
|
<script> |
|
|
const currentHost = window.location.hostname; |
|
|
const currentProtocol = window.location.protocol; |
|
|
const viewerUrl = currentHost === 'localhost' || currentHost === '127.0.0.1' |
|
|
? 'http://localhost:8006' |
|
|
: currentProtocol + '//' + currentHost + ':8006'; |
|
|
|
|
|
const iframe = document.getElementById('viewer'); |
|
|
iframe.src = viewerUrl; |
|
|
|
|
|
let loadTimeout = setTimeout(() => {{ |
|
|
iframe.style.display = 'none'; |
|
|
document.body.innerHTML = '<div class="error"><h1>Web Viewer não disponível</h1><p>O Windows XP pode ainda estar inicializando.</p><p>Aguarde alguns minutos e tente novamente.</p><p>Verifique o progresso em <a href="/">página inicial</a></p><p><small>Tentando conectar em: ' + viewerUrl + '</small></p></div>'; |
|
|
}}, 10000); |
|
|
|
|
|
iframe.onload = function() {{ |
|
|
clearTimeout(loadTimeout); |
|
|
}}; |
|
|
|
|
|
iframe.onerror = function() {{ |
|
|
clearTimeout(loadTimeout); |
|
|
iframe.style.display = 'none'; |
|
|
document.body.innerHTML = '<div class="error"><h1>Erro ao carregar Web Viewer</h1><p>O Windows XP pode ainda estar inicializando ou a porta 8006 não está acessível.</p><p>Verifique o progresso em <a href="/">página inicial</a></p><p><small>URL tentada: ' + viewerUrl + '</small></p></div>'; |
|
|
}}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""") |
|
|
|
|
|
@app.get("/info") |
|
|
async def get_info(): |
|
|
"""Retorna informações sobre o ambiente""" |
|
|
import socket |
|
|
try: |
|
|
hostname = socket.gethostname() |
|
|
except: |
|
|
hostname = "localhost" |
|
|
|
|
|
status = await get_status() |
|
|
progress = get_installation_progress() |
|
|
|
|
|
hf_space_host = os.environ.get("SPACE_HOST", None) |
|
|
|
|
|
return { |
|
|
"hostname": hostname, |
|
|
"hf_space_host": hf_space_host, |
|
|
"status": status, |
|
|
"progress": progress, |
|
|
"mode": "direct", |
|
|
"version": "Windows XP", |
|
|
"rdp_info": { |
|
|
"username": "Docker", |
|
|
"password": "admin", |
|
|
"port": 3389, |
|
|
"host": "localhost" |
|
|
}, |
|
|
"web_viewer": { |
|
|
"url": "http://localhost:8006", |
|
|
"note": "No HuggingFace Spaces, use o hostname do Space se localhost não funcionar" |
|
|
} |
|
|
} |
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
uvicorn.run(app, host="0.0.0.0", port=7860) |