From bb7dacea91bfb64e82335715396dfb3688d5a57f Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 30 Nov 2025 16:02:44 +0100 Subject: [PATCH] Update AppImage --- AppImage/scripts/build_appimage.sh | 1 + AppImage/scripts/flask_script_runner.py | 211 ++++++++++++++++++++++++ AppImage/scripts/flask_server.py | 77 +++++++++ 3 files changed, 289 insertions(+) create mode 100644 AppImage/scripts/flask_script_runner.py diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh index a107b9d..b2c450e 100644 --- a/AppImage/scripts/build_appimage.sh +++ b/AppImage/scripts/build_appimage.sh @@ -88,6 +88,7 @@ cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || ec cp "$SCRIPT_DIR/flask_terminal_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_terminal_routes.py not found" cp "$SCRIPT_DIR/hardware_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ hardware_monitor.py not found" cp "$SCRIPT_DIR/proxmox_storage_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ proxmox_storage_monitor.py not found" +cp "$SCRIPT_DIR/flask_script_runner.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_script_runner.py not found" echo "📋 Adding translation support..." cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF' diff --git a/AppImage/scripts/flask_script_runner.py b/AppImage/scripts/flask_script_runner.py new file mode 100644 index 0000000..802a1ec --- /dev/null +++ b/AppImage/scripts/flask_script_runner.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Script Runner System for ProxMenux +Executes bash scripts and provides real-time log streaming with interactive menu support +""" + +import os +import sys +import json +import subprocess +import threading +import time +from datetime import datetime +from pathlib import Path +import uuid + +class ScriptRunner: + """Manages script execution with real-time log streaming and menu interactions""" + + def __init__(self): + self.active_sessions = {} + self.log_dir = Path("/var/log/proxmenux/scripts") + self.log_dir.mkdir(parents=True, exist_ok=True) + self.interaction_handlers = {} + + def create_session(self, script_name): + """Create a new script execution session""" + session_id = str(uuid.uuid4())[:8] + log_file = self.log_dir / f"{script_name}_{session_id}_{int(time.time())}.log" + + self.active_sessions[session_id] = { + 'script_name': script_name, + 'log_file': str(log_file), + 'start_time': datetime.now().isoformat(), + 'status': 'initializing', + 'process': None, + 'exit_code': None, + 'pending_interaction': None + } + + return session_id + + def execute_script(self, script_path, session_id, env_vars=None): + """Execute a script in web mode with logging""" + if session_id not in self.active_sessions: + return {'success': False, 'error': 'Invalid session ID'} + + session = self.active_sessions[session_id] + log_file = session['log_file'] + + # Prepare environment + env = os.environ.copy() + env['EXECUTION_MODE'] = 'web' + env['LOG_FILE'] = log_file + + if env_vars: + env.update(env_vars) + + # Initialize log file + with open(log_file, 'w') as f: + f.write(json.dumps({ + 'type': 'init', + 'session_id': session_id, + 'script': script_path, + 'timestamp': int(time.time()) + }) + '\n') + + try: + # Execute script + session['status'] = 'running' + process = subprocess.Popen( + ['/bin/bash', script_path], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + + session['process'] = process + + # Monitor output for interactions + def monitor_output(): + for line in process.stdout: + with open(log_file, 'a') as f: + f.write(line) + + # Check for interaction requests + try: + if line.strip().startswith('{'): + data = json.loads(line.strip()) + if data.get('type') == 'interaction_request': + session['pending_interaction'] = data + except json.JSONDecodeError: + pass + + monitor_thread = threading.Thread(target=monitor_output, daemon=True) + monitor_thread.start() + + # Wait for completion + process.wait() + monitor_thread.join(timeout=5) + + session['exit_code'] = process.returncode + session['status'] = 'completed' if process.returncode == 0 else 'failed' + session['end_time'] = datetime.now().isoformat() + + return { + 'success': True, + 'session_id': session_id, + 'exit_code': process.returncode, + 'log_file': log_file + } + + except Exception as e: + session['status'] = 'error' + session['error'] = str(e) + return { + 'success': False, + 'error': str(e) + } + + def get_session_status(self, session_id): + """Get current status of a script execution session""" + if session_id not in self.active_sessions: + return {'success': False, 'error': 'Session not found'} + + session = self.active_sessions[session_id] + return { + 'success': True, + 'session_id': session_id, + 'status': session['status'], + 'start_time': session['start_time'], + 'script_name': session['script_name'], + 'exit_code': session['exit_code'], + 'pending_interaction': session.get('pending_interaction') + } + + def respond_to_interaction(self, session_id, interaction_id, value): + """Respond to a script interaction request""" + if session_id not in self.active_sessions: + return {'success': False, 'error': 'Session not found'} + + session = self.active_sessions[session_id] + + # Write response to file that script is waiting for + response_file = f"/tmp/nvidia_response_{interaction_id}.json" + with open(response_file, 'w') as f: + json.dump({ + 'interaction_id': interaction_id, + 'value': value, + 'timestamp': int(time.time()) + }, f) + + # Clear pending interaction + session['pending_interaction'] = None + + return {'success': True} + + def stream_logs(self, session_id): + """Generator that yields log entries as they are written""" + if session_id not in self.active_sessions: + yield json.dumps({'type': 'error', 'message': 'Invalid session ID'}) + return + + session = self.active_sessions[session_id] + log_file = session['log_file'] + + # Wait for log file to be created + timeout = 10 + start = time.time() + while not os.path.exists(log_file) and (time.time() - start) < timeout: + time.sleep(0.1) + + if not os.path.exists(log_file): + yield json.dumps({'type': 'error', 'message': 'Log file not created'}) + return + + # Stream log file + with open(log_file, 'r') as f: + # Start from beginning + f.seek(0) + + while session['status'] in ['initializing', 'running']: + line = f.readline() + if line: + # Try to parse as JSON, yield as-is if not JSON + try: + log_entry = json.loads(line.strip()) + yield json.dumps(log_entry) + except json.JSONDecodeError: + yield json.dumps({'type': 'raw', 'message': line.strip()}) + else: + time.sleep(0.1) + + # Read any remaining lines after completion + for line in f: + try: + log_entry = json.loads(line.strip()) + yield json.dumps(log_entry) + except json.JSONDecodeError: + yield json.dumps({'type': 'raw', 'message': line.strip()}) + + def cleanup_session(self, session_id): + """Clean up a completed session""" + if session_id in self.active_sessions: + del self.active_sessions[session_id] + return {'success': True} + return {'success': False, 'error': 'Session not found'} + +# Global instance +script_runner = ScriptRunner() diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index be695d6..cc85c6a 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -36,6 +36,8 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) +from flask_script_runner import script_runner +import threading from proxmox_storage_monitor import proxmox_storage_monitor from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402 from flask_health_routes import health_bp # noqa: E402 @@ -6342,6 +6344,81 @@ def api_vm_config_update(vmid): pass return jsonify({'error': str(e)}), 500 + +@app.route('/api/scripts/execute', methods=['POST']) +def execute_script(): + """Execute a script with real-time logging""" + try: + data = request.json + script_name = data.get('script_name') + script_params = data.get('params', {}) + + # Map script names to file paths + script_map = { + 'nvidia_installer': '/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh', + } + + if script_name not in script_map: + return jsonify({'success': False, 'error': 'Unknown script'}), 400 + + script_path = script_map[script_name] + + if not os.path.exists(script_path): + return jsonify({'success': False, 'error': 'Script file not found'}), 404 + + # Create session and start execution in background thread + session_id = script_runner.create_session(script_name) + + def run_script(): + script_runner.execute_script(script_path, session_id, script_params) + + thread = threading.Thread(target=run_script, daemon=True) + thread.start() + + return jsonify({ + 'success': True, + 'session_id': session_id + }) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/scripts/status/', methods=['GET']) +def get_script_status(session_id): + """Get status of a running script""" + try: + status = script_runner.get_session_status(session_id) + return jsonify(status) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/scripts/respond', methods=['POST']) +def respond_to_script(): + """Respond to script interaction""" + try: + data = request.json + session_id = data.get('session_id') + interaction_id = data.get('interaction_id') + value = data.get('value') + + result = script_runner.respond_to_interaction(session_id, interaction_id, value) + return jsonify(result) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/scripts/logs/', methods=['GET']) +def stream_script_logs(session_id): + """Stream logs from a running script""" + try: + def generate(): + for log_entry in script_runner.stream_logs(session_id): + yield f"data: {log_entry}\n\n" + + return Response(generate(), mimetype='text/event-stream') + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + if __name__ == '__main__': # API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics