mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-07-04 17:01:34 +02:00
feat: Add remote update capability from web GUI
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>
This commit is contained in:
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# mc-webui Update Webhook Installer
|
||||
#
|
||||
# This script installs the update webhook service that allows
|
||||
# remote updates from the mc-webui GUI.
|
||||
#
|
||||
# Usage: sudo ./install.sh [--uninstall]
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "Please run as root: sudo $0"
|
||||
fi
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MCWEBUI_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
|
||||
# Detect the user who owns mc-webui directory
|
||||
MCWEBUI_USER=$(stat -c '%U' "$MCWEBUI_DIR")
|
||||
MCWEBUI_GROUP=$(stat -c '%G' "$MCWEBUI_DIR")
|
||||
|
||||
SERVICE_NAME="mc-webui-updater"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
# Uninstall
|
||||
if [ "$1" == "--uninstall" ]; then
|
||||
info "Uninstalling ${SERVICE_NAME}..."
|
||||
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
systemctl stop "$SERVICE_NAME"
|
||||
info "Service stopped"
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then
|
||||
systemctl disable "$SERVICE_NAME"
|
||||
info "Service disabled"
|
||||
fi
|
||||
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
rm "$SERVICE_FILE"
|
||||
systemctl daemon-reload
|
||||
info "Service file removed"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Uninstallation complete!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install
|
||||
info "Installing ${SERVICE_NAME}..."
|
||||
info " mc-webui directory: $MCWEBUI_DIR"
|
||||
info " mc-webui user: $MCWEBUI_USER"
|
||||
|
||||
# Check if updater.py exists
|
||||
if [ ! -f "$SCRIPT_DIR/updater.py" ]; then
|
||||
error "updater.py not found in $SCRIPT_DIR"
|
||||
fi
|
||||
|
||||
# Check if update.sh exists
|
||||
if [ ! -f "$MCWEBUI_DIR/scripts/update.sh" ]; then
|
||||
error "update.sh not found in $MCWEBUI_DIR/scripts/"
|
||||
fi
|
||||
|
||||
# Create service file with correct paths
|
||||
info "Creating systemd service file..."
|
||||
cat > "$SERVICE_FILE" << EOF
|
||||
[Unit]
|
||||
Description=mc-webui Update Webhook Server
|
||||
Documentation=https://github.com/MarekWo/mc-webui
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=MCWEBUI_DIR=${MCWEBUI_DIR}
|
||||
Environment=UPDATER_TOKEN=
|
||||
ExecStart=/usr/bin/python3 ${SCRIPT_DIR}/updater.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
info "Reloading systemd..."
|
||||
systemctl daemon-reload
|
||||
|
||||
info "Enabling service..."
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
|
||||
info "Starting service..."
|
||||
systemctl start "$SERVICE_NAME"
|
||||
|
||||
# Wait a moment for service to start
|
||||
sleep 2
|
||||
|
||||
# Check if service is running
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
info "Service is running!"
|
||||
|
||||
# Test health endpoint
|
||||
if command -v curl &> /dev/null; then
|
||||
HEALTH=$(curl -s http://127.0.0.1:5050/health 2>/dev/null || echo "")
|
||||
if echo "$HEALTH" | grep -q '"status":"ok"'; then
|
||||
info "Health check passed!"
|
||||
else
|
||||
warn "Health check failed - service may still be starting"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
error "Service failed to start. Check: journalctl -u $SERVICE_NAME"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Installation complete!${NC}"
|
||||
echo ""
|
||||
echo "The update webhook is now running on port 5050."
|
||||
echo "You can now use the 'Update' button in mc-webui GUI."
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " systemctl status $SERVICE_NAME # Check status"
|
||||
echo " journalctl -u $SERVICE_NAME -f # View logs"
|
||||
echo " sudo $0 --uninstall # Uninstall"
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=mc-webui Update Webhook Server
|
||||
Documentation=https://github.com/MarekWo/mc-webui
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Environment=MCWEBUI_DIR=/home/marek/mc-webui
|
||||
Environment=UPDATER_TOKEN=
|
||||
ExecStart=/usr/bin/python3 /home/marek/mc-webui/scripts/updater/updater.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=false
|
||||
ProtectHome=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user