mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01: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:
37
README.md
37
README.md
@@ -194,6 +194,43 @@ git checkout main
|
||||
./scripts/update.sh
|
||||
```
|
||||
|
||||
### Remote updates from web GUI (optional)
|
||||
|
||||
You can enable one-click updates directly from the mc-webui menu. This requires installing a small webhook service on the host machine.
|
||||
|
||||
**Install the updater service:**
|
||||
|
||||
```bash
|
||||
cd ~/mc-webui
|
||||
sudo ./scripts/updater/install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- Create a systemd service `mc-webui-updater`
|
||||
- Start a webhook server on port 5050 (localhost only)
|
||||
- Enable automatic startup on boot
|
||||
|
||||
**Usage:**
|
||||
1. Click the refresh button (↻) next to the version in the menu
|
||||
2. If an update is available, an "Update" button appears
|
||||
3. Click "Update" to trigger the update remotely
|
||||
4. The app will automatically reload when the update completes
|
||||
|
||||
**Useful commands:**
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
systemctl status mc-webui-updater
|
||||
|
||||
# View logs
|
||||
journalctl -u mc-webui-updater -f
|
||||
|
||||
# Uninstall
|
||||
sudo ~/mc-webui/scripts/updater/install.sh --uninstall
|
||||
```
|
||||
|
||||
**Security note:** The webhook listens only on localhost. The Docker container connects to it via the Docker bridge network.
|
||||
|
||||
---
|
||||
|
||||
## Gallery
|
||||
|
||||
@@ -2125,6 +2125,131 @@ def check_update():
|
||||
}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Remote Update (via host webhook)
|
||||
# =============================================================================
|
||||
|
||||
# Updater webhook URL - tries multiple addresses for Docker compatibility
|
||||
UPDATER_URLS = [
|
||||
'http://host.docker.internal:5050', # Docker Desktop (Mac/Windows)
|
||||
'http://172.17.0.1:5050', # Docker default bridge gateway (Linux)
|
||||
'http://127.0.0.1:5050', # Localhost fallback
|
||||
]
|
||||
|
||||
|
||||
def get_updater_url():
|
||||
"""Find working updater webhook URL."""
|
||||
for url in UPDATER_URLS:
|
||||
try:
|
||||
response = requests.get(f"{url}/health", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return url
|
||||
except requests.RequestException:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@api_bp.route('/updater/status', methods=['GET'])
|
||||
def updater_status():
|
||||
"""
|
||||
Check if the update webhook is available on the host.
|
||||
|
||||
Returns:
|
||||
JSON with updater status:
|
||||
{
|
||||
"success": true,
|
||||
"available": true,
|
||||
"url": "http://172.17.0.1:5050",
|
||||
"update_in_progress": false
|
||||
}
|
||||
"""
|
||||
try:
|
||||
url = get_updater_url()
|
||||
|
||||
if not url:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'available': False,
|
||||
'message': 'Update webhook not installed or not running'
|
||||
}), 200
|
||||
|
||||
# Get detailed status from webhook
|
||||
response = requests.get(f"{url}/health", timeout=5)
|
||||
data = response.json()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'available': True,
|
||||
'url': url,
|
||||
'update_in_progress': data.get('update_in_progress', False),
|
||||
'mcwebui_dir': data.get('mcwebui_dir', '')
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking updater status: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'available': False,
|
||||
'error': str(e)
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/updater/trigger', methods=['POST'])
|
||||
def updater_trigger():
|
||||
"""
|
||||
Trigger remote update via host webhook.
|
||||
|
||||
This will:
|
||||
1. Call the webhook to start update.sh
|
||||
2. The server will restart (containers rebuilt)
|
||||
3. Frontend should poll /api/version to detect completion
|
||||
|
||||
Returns:
|
||||
JSON with result:
|
||||
{
|
||||
"success": true,
|
||||
"message": "Update started"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
url = get_updater_url()
|
||||
|
||||
if not url:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Update webhook not available. Install it first.'
|
||||
}), 503
|
||||
|
||||
# Trigger update
|
||||
response = requests.post(f"{url}/update", timeout=10)
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 200 and data.get('success'):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Update started. Server will restart shortly.'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': data.get('error', 'Unknown error')
|
||||
}), response.status_code
|
||||
|
||||
except requests.Timeout:
|
||||
# Timeout might mean the update started and server is restarting
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Update may have started (request timed out)'
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering update: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/read_status/mark_read', methods=['POST'])
|
||||
def mark_read_api():
|
||||
"""
|
||||
|
||||
@@ -1349,8 +1349,16 @@ async function checkForAppUpdates() {
|
||||
|
||||
if (data.success) {
|
||||
if (data.update_available) {
|
||||
// Update available - show green with link
|
||||
versionText.innerHTML = `${data.current_version} <a href="${data.github_url}" target="_blank" class="text-success" title="Update available: ${data.latest_date}+${data.latest_commit}"><i class="bi bi-arrow-up-circle-fill"></i> Update available</a>`;
|
||||
// Check if remote update is available
|
||||
const updaterStatus = await fetch('/api/updater/status').then(r => r.json()).catch(() => ({ available: false }));
|
||||
|
||||
if (updaterStatus.available) {
|
||||
// Show "Update Now" link that opens modal
|
||||
versionText.innerHTML = `${data.current_version} <a href="#" onclick="openUpdateModal('${data.latest_date}+${data.latest_commit}'); return false;" class="text-success" title="Click to update"><i class="bi bi-arrow-up-circle-fill"></i> Update now</a>`;
|
||||
} else {
|
||||
// Show link to GitHub (no remote update available)
|
||||
versionText.innerHTML = `${data.current_version} <a href="${data.github_url}" target="_blank" class="text-success" title="Update available: ${data.latest_date}+${data.latest_commit}"><i class="bi bi-arrow-up-circle-fill"></i> Update available</a>`;
|
||||
}
|
||||
icon.className = 'bi bi-check-circle-fill text-success';
|
||||
showNotification(`Update available: ${data.latest_date}+${data.latest_commit}`, 'success');
|
||||
} else {
|
||||
@@ -1382,6 +1390,137 @@ async function checkForAppUpdates() {
|
||||
}
|
||||
}
|
||||
|
||||
// Store update info for modal
|
||||
let pendingUpdateVersion = null;
|
||||
|
||||
/**
|
||||
* Open update modal and prepare for remote update
|
||||
*/
|
||||
function openUpdateModal(newVersion) {
|
||||
pendingUpdateVersion = newVersion;
|
||||
|
||||
// Close offcanvas menu
|
||||
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('mainMenu'));
|
||||
if (offcanvas) offcanvas.hide();
|
||||
|
||||
// Reset modal state
|
||||
document.getElementById('updateStatus').classList.remove('d-none');
|
||||
document.getElementById('updateProgress').classList.add('d-none');
|
||||
document.getElementById('updateResult').classList.add('d-none');
|
||||
document.getElementById('updateCancelBtn').classList.remove('d-none');
|
||||
document.getElementById('updateConfirmBtn').classList.remove('d-none');
|
||||
document.getElementById('updateReloadBtn').classList.add('d-none');
|
||||
document.getElementById('updateMessage').textContent = `New version available: ${newVersion}`;
|
||||
|
||||
// Hide spinner, show message
|
||||
document.querySelector('#updateStatus .spinner-border').classList.add('d-none');
|
||||
|
||||
// Setup confirm button
|
||||
document.getElementById('updateConfirmBtn').onclick = performRemoteUpdate;
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform remote update via webhook
|
||||
*/
|
||||
async function performRemoteUpdate() {
|
||||
const currentVersion = document.getElementById('versionText')?.textContent?.split(' ')[0] || '';
|
||||
|
||||
// Show progress state
|
||||
document.getElementById('updateStatus').classList.add('d-none');
|
||||
document.getElementById('updateProgress').classList.remove('d-none');
|
||||
document.getElementById('updateCancelBtn').classList.add('d-none');
|
||||
document.getElementById('updateConfirmBtn').classList.add('d-none');
|
||||
document.getElementById('updateProgressMessage').textContent = 'Starting update...';
|
||||
|
||||
try {
|
||||
// Trigger update
|
||||
const response = await fetch('/api/updater/trigger', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
showUpdateResult(false, data.error || 'Failed to start update');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('updateProgressMessage').textContent = 'Update started. Waiting for server to restart...';
|
||||
|
||||
// Poll for server to come back up with new version
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 2 minutes max
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
|
||||
const pollForCompletion = async () => {
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const versionResponse = await fetch('/api/version', {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' }
|
||||
});
|
||||
|
||||
if (versionResponse.ok) {
|
||||
const versionData = await versionResponse.json();
|
||||
const newVersion = versionData.version;
|
||||
|
||||
// Check if version changed
|
||||
if (newVersion !== currentVersion) {
|
||||
showUpdateResult(true, `Updated to ${newVersion}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Server not responding yet - this is expected during restart
|
||||
document.getElementById('updateProgressMessage').textContent =
|
||||
`Rebuilding containers... (${attempts}/${maxAttempts})`;
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(pollForCompletion, pollInterval);
|
||||
} else {
|
||||
showUpdateResult(false, 'Update timed out. Please check server manually.');
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after a short delay
|
||||
setTimeout(pollForCompletion, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Update error:', error);
|
||||
showUpdateResult(false, 'Network error during update');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show update result in modal
|
||||
*/
|
||||
function showUpdateResult(success, message) {
|
||||
document.getElementById('updateProgress').classList.add('d-none');
|
||||
document.getElementById('updateResult').classList.remove('d-none');
|
||||
|
||||
const icon = document.getElementById('updateResultIcon');
|
||||
const msg = document.getElementById('updateResultMessage');
|
||||
|
||||
if (success) {
|
||||
icon.className = 'bi bi-check-circle-fill text-success fs-1 mb-3 d-block';
|
||||
msg.className = 'mb-0 text-success';
|
||||
document.getElementById('updateReloadBtn').classList.remove('d-none');
|
||||
} else {
|
||||
icon.className = 'bi bi-x-circle-fill text-danger fs-1 mb-3 d-block';
|
||||
msg.className = 'mb-0 text-danger';
|
||||
document.getElementById('updateCancelBtn').classList.remove('d-none');
|
||||
document.getElementById('updateCancelBtn').textContent = 'Close';
|
||||
}
|
||||
|
||||
msg.textContent = message;
|
||||
}
|
||||
|
||||
// Make openUpdateModal globally accessible
|
||||
window.openUpdateModal = openUpdateModal;
|
||||
|
||||
/**
|
||||
* Scroll to bottom of messages
|
||||
*/
|
||||
|
||||
@@ -330,6 +330,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Modal -->
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-cloud-download"></i> Update mc-webui</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center py-4">
|
||||
<div id="updateStatus">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p id="updateMessage" class="mb-0">Checking for updates...</p>
|
||||
</div>
|
||||
<div id="updateProgress" class="d-none">
|
||||
<div class="progress mb-3" style="height: 8px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 100%"></div>
|
||||
</div>
|
||||
<p id="updateProgressMessage" class="text-muted mb-0">Updating...</p>
|
||||
</div>
|
||||
<div id="updateResult" class="d-none">
|
||||
<i id="updateResultIcon" class="bi fs-1 mb-3 d-block"></i>
|
||||
<p id="updateResultMessage" class="mb-0"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center" id="updateFooter">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="updateCancelBtn">Cancel</button>
|
||||
<button type="button" class="btn btn-primary d-none" id="updateConfirmBtn">Update Now</button>
|
||||
<button type="button" class="btn btn-primary d-none" id="updateReloadBtn" onclick="location.reload()">Reload Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<div class="toast-container position-fixed top-0 start-0 p-3">
|
||||
<div id="notificationToast" class="toast" role="alert">
|
||||
|
||||
137
scripts/updater/install.sh
Executable file
137
scripts/updater/install.sh
Executable file
@@ -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"
|
||||
21
scripts/updater/mc-webui-updater.service
Normal file
21
scripts/updater/mc-webui-updater.service
Normal file
@@ -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
|
||||
228
scripts/updater/updater.py
Executable file
228
scripts/updater/updater.py
Executable file
@@ -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