Show Hysteria stop errors and add live container logs viewer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 02:28:56 +03:00
co-authored by Cursor
parent fda60eaa9a
commit 7323f08c37
8 changed files with 467 additions and 4 deletions
+161 -2
View File
@@ -1705,6 +1705,11 @@ class ProtocolRequest(BaseModel):
protocol: str = 'awg'
class ContainerLogsRequest(BaseModel):
protocol: str = 'hysteria'
tail: Optional[int] = 200
class AddConnectionRequest(BaseModel):
protocol: str = 'awg'
name: str = 'Connection'
@@ -3141,15 +3146,169 @@ async def api_container_toggle(request: Request, server_id: int, req: ProtocolRe
ssh.run_sudo_command(f"docker stop {container}")
action = 'stopped'
else:
ssh.run_sudo_command(f"docker start {container}")
# Hysteria: recreate with host network / current config instead of plain start
if protocol_base(req.protocol) == 'hysteria':
from managers.hysteria_manager import HysteriaManager
mgr = HysteriaManager(ssh, req.protocol)
meta = mgr._read_metadata()
port = int(meta.get('port') or mgr.DEFAULT_PORT)
mgr._write_config_from_clients(port)
mgr._start_container(port)
else:
ssh.run_sudo_command(f"docker start {container}")
action = 'started'
# After stop/start, return diagnostics for UI
error = ''
recent_logs = ''
if protocol_base(req.protocol) == 'hysteria':
from managers.hysteria_manager import HysteriaManager
mgr = HysteriaManager(ssh, req.protocol)
diag = mgr.get_container_diagnostics(req.protocol)
error = diag.get('error_summary') or ''
recent_logs = diag.get('recent_logs') or ''
if action == 'started' and not diag.get('running'):
ssh.disconnect()
return JSONResponse({
'error': error or 'Hysteria failed to start',
'action': action,
'container': container,
'recent_logs': recent_logs,
}, status_code=400)
ssh.disconnect()
return {'status': 'success', 'action': action, 'container': container}
return {
'status': 'success',
'action': action,
'container': container,
'error': error,
'recent_logs': recent_logs,
}
except Exception as e:
logger.exception("Error toggling container")
return JSONResponse({'error': str(e)}, status_code=500)
@app.post('/api/servers/{server_id}/container/logs', tags=["Protocols"])
async def api_container_logs(request: Request, server_id: int, req: ContainerLogsRequest):
"""Fetch recent Docker logs for a protocol container."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
if not is_valid_protocol(req.protocol):
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
server = data['servers'][server_id]
container = protocol_container_name(req.protocol)
ssh = get_ssh(server)
ssh.connect()
if protocol_base(req.protocol) == 'hysteria':
from managers.hysteria_manager import HysteriaManager
mgr = HysteriaManager(ssh, req.protocol)
logs = mgr.get_logs(req.protocol, tail=req.tail or 200)
diag = mgr.get_container_diagnostics(req.protocol)
ssh.disconnect()
return {
'status': 'success',
'protocol': req.protocol,
'container': container,
'logs': logs,
'running': bool(diag.get('running')),
'error': diag.get('error_summary') or '',
'exit_code': diag.get('exit_code'),
'container_status': diag.get('status') or '',
}
# Generic fallback for other protocols
tail = max(20, min(int(req.tail or 200), 2000))
out, err, _ = ssh.run_sudo_command(
f"docker logs --tail {tail} --timestamps {shlex.quote(container)} 2>&1",
timeout=30,
)
running_out, _, _ = ssh.run_sudo_command(
f"docker inspect -f '{{{{.State.Running}}}}' {shlex.quote(container)} 2>/dev/null"
)
ssh.disconnect()
return {
'status': 'success',
'protocol': req.protocol,
'container': container,
'logs': (out or err or '').strip(),
'running': running_out.strip().lower() == 'true',
'error': '',
}
except Exception as e:
logger.exception("Error reading container logs")
return JSONResponse({'error': str(e)}, status_code=500)
@app.get('/api/servers/{server_id}/container/logs/stream', tags=["Protocols"])
async def api_container_logs_stream(request: Request, server_id: int, protocol: str = 'hysteria', tail: int = 100):
"""SSE stream of docker logs (polls every ~1.5s for new lines)."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
if not is_valid_protocol(protocol):
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
server = data['servers'][server_id]
container = protocol_container_name(protocol)
if not container:
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
tail = max(20, min(int(tail or 100), 500))
async def event_gen():
last_text = ''
try:
while True:
if await request.is_disconnected():
break
def _fetch():
ssh = get_ssh(server)
ssh.connect()
try:
out, err, _ = ssh.run_sudo_command(
f"docker logs --tail {tail} --timestamps {shlex.quote(container)} 2>&1",
timeout=25,
)
running_out, _, _ = ssh.run_sudo_command(
f"docker inspect -f '{{{{.State.Status}}}}|{{{{.State.Running}}}}|{{{{.State.ExitCode}}}}|{{{{.State.Error}}}}' {shlex.quote(container)} 2>/dev/null"
)
return (out or err or '').strip(), (running_out or '').strip()
finally:
ssh.disconnect()
logs, state = await asyncio.to_thread(_fetch)
if logs != last_text:
last_text = logs
payload = json.dumps({
'logs': logs,
'state': state,
'container': container,
'ts': time.time(),
}, ensure_ascii=False)
yield f"data: {payload}\n\n"
else:
yield f": ping\n\n"
await asyncio.sleep(1.5)
except asyncio.CancelledError:
return
except Exception as e:
payload = json.dumps({'error': str(e)}, ensure_ascii=False)
yield f"data: {payload}\n\n"
return StreamingResponse(
event_gen(),
media_type='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
},
)
@app.post('/api/servers/{server_id}/server_config', tags=["Protocols"])
async def api_server_config(request: Request, server_id: int, req: ProtocolRequest):
"""Get the raw server-side WireGuard/Xray configuration."""