mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
Adds webhook-based update system that allows triggering updates directly from the mc-webui menu. Includes: - Webhook server (updater.py) on port 5050 - Systemd service and install script - API proxy endpoints for container-to-host communication - Update modal with progress tracking and auto-reload Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
6.8 KiB
Python
Executable File
229 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
mc-webui Update Webhook Server
|
|
|
|
A simple HTTP server that listens for update requests and executes
|
|
the update script. Designed to run as a systemd service on the host.
|
|
|
|
Security:
|
|
- Listens only on localhost (127.0.0.1)
|
|
- Simple token-based authentication (optional)
|
|
|
|
Endpoints:
|
|
- GET /health - Check if webhook is running
|
|
- POST /update - Trigger update (returns immediately, runs in background)
|
|
- GET /status - Check if update is in progress
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
# Configuration
|
|
HOST = '0.0.0.0' # Listen on all interfaces (Docker needs this)
|
|
PORT = 5050
|
|
MCWEBUI_DIR = os.environ.get('MCWEBUI_DIR', os.path.expanduser('~/mc-webui'))
|
|
UPDATE_SCRIPT = os.path.join(MCWEBUI_DIR, 'scripts', 'update.sh')
|
|
AUTH_TOKEN = os.environ.get('UPDATER_TOKEN', '') # Optional token
|
|
|
|
# Global state
|
|
update_in_progress = False
|
|
last_update_result = None
|
|
last_update_time = None
|
|
|
|
|
|
class UpdateHandler(BaseHTTPRequestHandler):
|
|
"""HTTP request handler for update webhook."""
|
|
|
|
def log_message(self, format, *args):
|
|
"""Override to use custom logging format."""
|
|
print(f"[{self.log_date_time_string()}] {args[0]}")
|
|
|
|
def send_json(self, data, status=200):
|
|
"""Send JSON response."""
|
|
self.send_response(status)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(data).encode())
|
|
|
|
def check_auth(self):
|
|
"""Check authorization token if configured."""
|
|
if not AUTH_TOKEN:
|
|
return True
|
|
|
|
auth_header = self.headers.get('Authorization', '')
|
|
if auth_header.startswith('Bearer '):
|
|
token = auth_header[7:]
|
|
return token == AUTH_TOKEN
|
|
|
|
# Also check query parameter
|
|
parsed = urlparse(self.path)
|
|
params = parse_qs(parsed.query)
|
|
token = params.get('token', [''])[0]
|
|
return token == AUTH_TOKEN
|
|
|
|
def do_OPTIONS(self):
|
|
"""Handle CORS preflight."""
|
|
self.send_response(200)
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
"""Handle GET requests."""
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path
|
|
|
|
if path == '/health':
|
|
self.handle_health()
|
|
elif path == '/status':
|
|
self.handle_status()
|
|
else:
|
|
self.send_json({'error': 'Not found'}, 404)
|
|
|
|
def do_POST(self):
|
|
"""Handle POST requests."""
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path
|
|
|
|
if path == '/update':
|
|
self.handle_update()
|
|
else:
|
|
self.send_json({'error': 'Not found'}, 404)
|
|
|
|
def handle_health(self):
|
|
"""Health check endpoint."""
|
|
self.send_json({
|
|
'status': 'ok',
|
|
'service': 'mc-webui-updater',
|
|
'update_in_progress': update_in_progress,
|
|
'mcwebui_dir': MCWEBUI_DIR
|
|
})
|
|
|
|
def handle_status(self):
|
|
"""Get update status."""
|
|
self.send_json({
|
|
'update_in_progress': update_in_progress,
|
|
'last_update_result': last_update_result,
|
|
'last_update_time': last_update_time
|
|
})
|
|
|
|
def handle_update(self):
|
|
"""Trigger update."""
|
|
global update_in_progress
|
|
|
|
if not self.check_auth():
|
|
self.send_json({'error': 'Unauthorized'}, 401)
|
|
return
|
|
|
|
if update_in_progress:
|
|
self.send_json({
|
|
'success': False,
|
|
'error': 'Update already in progress'
|
|
}, 409)
|
|
return
|
|
|
|
if not os.path.exists(UPDATE_SCRIPT):
|
|
self.send_json({
|
|
'success': False,
|
|
'error': f'Update script not found: {UPDATE_SCRIPT}'
|
|
}, 500)
|
|
return
|
|
|
|
# Start update in background thread
|
|
update_in_progress = True
|
|
thread = threading.Thread(target=run_update, daemon=True)
|
|
thread.start()
|
|
|
|
self.send_json({
|
|
'success': True,
|
|
'message': 'Update started',
|
|
'note': 'Server will restart. Poll /health to detect completion.'
|
|
})
|
|
|
|
|
|
def run_update():
|
|
"""Run update script in background."""
|
|
global update_in_progress, last_update_result, last_update_time
|
|
|
|
try:
|
|
print(f"[UPDATE] Starting update from {UPDATE_SCRIPT}")
|
|
|
|
# Run the update script
|
|
result = subprocess.run(
|
|
['/bin/bash', UPDATE_SCRIPT],
|
|
cwd=MCWEBUI_DIR,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300 # 5 minute timeout
|
|
)
|
|
|
|
last_update_result = {
|
|
'success': result.returncode == 0,
|
|
'returncode': result.returncode,
|
|
'stdout': result.stdout[-2000:] if result.stdout else '', # Last 2000 chars
|
|
'stderr': result.stderr[-500:] if result.stderr else ''
|
|
}
|
|
last_update_time = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
if result.returncode == 0:
|
|
print(f"[UPDATE] Update completed successfully")
|
|
else:
|
|
print(f"[UPDATE] Update failed with code {result.returncode}")
|
|
print(f"[UPDATE] stderr: {result.stderr}")
|
|
|
|
except subprocess.TimeoutExpired:
|
|
last_update_result = {
|
|
'success': False,
|
|
'error': 'Update timed out after 5 minutes'
|
|
}
|
|
last_update_time = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
print("[UPDATE] Update timed out")
|
|
|
|
except Exception as e:
|
|
last_update_result = {
|
|
'success': False,
|
|
'error': str(e)
|
|
}
|
|
last_update_time = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
print(f"[UPDATE] Update error: {e}")
|
|
|
|
finally:
|
|
update_in_progress = False
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
print(f"mc-webui Update Webhook Server")
|
|
print(f" Listening on: {HOST}:{PORT}")
|
|
print(f" mc-webui dir: {MCWEBUI_DIR}")
|
|
print(f" Update script: {UPDATE_SCRIPT}")
|
|
print(f" Auth token: {'configured' if AUTH_TOKEN else 'disabled'}")
|
|
print()
|
|
|
|
if not os.path.exists(MCWEBUI_DIR):
|
|
print(f"WARNING: mc-webui directory not found: {MCWEBUI_DIR}")
|
|
|
|
if not os.path.exists(UPDATE_SCRIPT):
|
|
print(f"WARNING: Update script not found: {UPDATE_SCRIPT}")
|
|
|
|
server = HTTPServer((HOST, PORT), UpdateHandler)
|
|
|
|
try:
|
|
print(f"Server started. Press Ctrl+C to stop.")
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
server.shutdown()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|