updated WMT client
This commit is contained in:
@@ -14,24 +14,35 @@ CONFIGURATION_MODE = False
|
||||
|
||||
# Run dependency check before importing anything else
|
||||
try:
|
||||
# ABI-specific wheels are available for both Bookworm (cp311) and Trixie (cp313).
|
||||
# pip will pick the correct file for the running interpreter automatically.
|
||||
_pyver = f"cp{sys.version_info.major}{sys.version_info.minor}"
|
||||
_abi_wheels = {
|
||||
'aiohttp': f'aiohttp-3.13.5-{_pyver}-{_pyver}-linux_armv7l.whl',
|
||||
'frozenlist': f'frozenlist-1.8.0-{_pyver}-{_pyver}-linux_armv7l.whl',
|
||||
'multidict': f'multidict-6.7.1-{_pyver}-{_pyver}-linux_armv7l.whl',
|
||||
'propcache': f'propcache-0.5.2-{_pyver}-{_pyver}-linux_armv7l.whl',
|
||||
'yarl': f'yarl-1.24.2-{_pyver}-{_pyver}-linux_armv7l.whl',
|
||||
}
|
||||
check_and_install_dependencies({
|
||||
'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl',
|
||||
'gpiozero': None,
|
||||
'requests': 'requests-2.32.3-py3-none-any.whl',
|
||||
'aiohttp': 'aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl',
|
||||
'flask': None,
|
||||
'urllib3': 'urllib3-2.3.0-py3-none-any.whl',
|
||||
'certifi': 'certifi-2025.1.31-py3-none-any.whl',
|
||||
'rdm6300': 'rdm6300-0.1.1-py3-none-any.whl',
|
||||
'serial': 'pyserial-3.5-py2.py3-none-any.whl',
|
||||
'gpiozero': None,
|
||||
'requests': 'requests-2.32.3-py3-none-any.whl',
|
||||
'aiohttp': _abi_wheels['aiohttp'],
|
||||
'flask': None,
|
||||
'urllib3': 'urllib3-2.3.0-py3-none-any.whl',
|
||||
'certifi': 'certifi-2025.1.31-py3-none-any.whl',
|
||||
'charset_normalizer': 'charset_normalizer-3.4.1-py3-none-any.whl',
|
||||
'idna': 'idna-3.10-py3-none-any.whl',
|
||||
'multidict': 'multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl',
|
||||
'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl',
|
||||
'frozenlist': 'frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl',
|
||||
'attrs': 'attrs-25.3.0-py3-none-any.whl',
|
||||
'yarl': 'yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl',
|
||||
'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl',
|
||||
'propcache': 'propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl'
|
||||
}, "./Files/reposytory")
|
||||
'idna': 'idna-3.10-py3-none-any.whl',
|
||||
'multidict': _abi_wheels['multidict'],
|
||||
'aiosignal': 'aiosignal-1.3.2-py2.py3-none-any.whl',
|
||||
'frozenlist': _abi_wheels['frozenlist'],
|
||||
'attrs': 'attrs-25.3.0-py3-none-any.whl',
|
||||
'yarl': _abi_wheels['yarl'],
|
||||
'aiohappyeyeballs': 'aiohappyeyeballs-2.6.1-py3-none-any.whl',
|
||||
'propcache': _abi_wheels['propcache'],
|
||||
}, "./Files/repository")
|
||||
except Exception as e:
|
||||
print(f"Warning: Dependency check failed: {e}")
|
||||
print("Continuing with existing packages...")
|
||||
@@ -103,32 +114,6 @@ except ImportError as e:
|
||||
print("Async functionality may be limited")
|
||||
import asyncio
|
||||
|
||||
# Import Flask for command server
|
||||
try:
|
||||
from flask import Flask, request, jsonify
|
||||
print("✓ Flask imported successfully")
|
||||
FLASK_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import Flask: {e}")
|
||||
print("Command server functionality will be disabled")
|
||||
FLASK_AVAILABLE = False
|
||||
# Create dummy Flask classes
|
||||
class Flask:
|
||||
def __init__(self, name):
|
||||
pass
|
||||
def route(self, *args, **kwargs):
|
||||
def decorator(f):
|
||||
return f
|
||||
return decorator
|
||||
def run(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def request():
|
||||
pass
|
||||
|
||||
def jsonify(data):
|
||||
return data
|
||||
|
||||
import configparser
|
||||
import json
|
||||
|
||||
@@ -646,58 +631,8 @@ def check_system_requirements():
|
||||
"""
|
||||
print("Checking system requirements...")
|
||||
|
||||
# 1. Check and install required system packages
|
||||
system_packages = {
|
||||
'sshpass': 'sshpass_1.09-1_armhf.deb' # Required for auto-update functionality
|
||||
}
|
||||
|
||||
for package, deb_file in system_packages.items():
|
||||
try:
|
||||
result = subprocess.run(['which', package], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(f"✓ {package} is installed")
|
||||
else:
|
||||
print(f"Installing {package}...")
|
||||
|
||||
# Try online installation first
|
||||
try:
|
||||
install_result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True, text=True, timeout=120)
|
||||
install_result = subprocess.run(['sudo', 'apt', 'install', '-y', package],
|
||||
capture_output=True, text=True, timeout=300)
|
||||
if install_result.returncode == 0:
|
||||
print(f"✓ {package} installed successfully (online)")
|
||||
continue
|
||||
else:
|
||||
print(f"Online installation failed, trying offline...")
|
||||
except Exception as online_error:
|
||||
print(f"Online installation failed: {online_error}, trying offline...")
|
||||
|
||||
# Try offline installation from local .deb file
|
||||
deb_path = f"./Files/system_packages/{deb_file}"
|
||||
if os.path.exists(deb_path):
|
||||
try:
|
||||
print(f"Installing {package} from local package: {deb_path}")
|
||||
offline_result = subprocess.run(['sudo', 'dpkg', '-i', deb_path],
|
||||
capture_output=True, text=True, timeout=120)
|
||||
if offline_result.returncode == 0:
|
||||
print(f"✓ {package} installed successfully (offline)")
|
||||
else:
|
||||
print(f"✗ Offline installation failed: {offline_result.stderr}")
|
||||
# Try to fix dependencies
|
||||
print("Attempting to fix dependencies...")
|
||||
subprocess.run(['sudo', 'apt', '--fix-broken', 'install', '-y'],
|
||||
capture_output=True, text=True, timeout=300)
|
||||
except Exception as offline_error:
|
||||
print(f"✗ Offline installation error: {offline_error}")
|
||||
else:
|
||||
print(f"✗ Local package not found: {deb_path}")
|
||||
print(f" To add offline support, download with: apt download {package}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not check/install {package}: {e}")
|
||||
|
||||
# 2. Check and create required directories
|
||||
required_dirs = ['./data', './Files', './Files/reposytory', './Files/system_packages']
|
||||
# 1. Check and create required directories
|
||||
required_dirs = ['./data', './Files', './Files/repository', './Files/system_packages']
|
||||
for dir_path in required_dirs:
|
||||
try:
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
@@ -736,65 +671,6 @@ def check_system_requirements():
|
||||
|
||||
return True
|
||||
|
||||
def check_port_capabilities():
|
||||
"""
|
||||
Check if the application can bind to port 80 and set up capabilities if needed
|
||||
"""
|
||||
print("Checking port 80 capabilities...")
|
||||
|
||||
try:
|
||||
# Check if we're running as root
|
||||
if os.geteuid() == 0:
|
||||
print("✓ Running as root - port 80 access available")
|
||||
return True
|
||||
|
||||
# Check if capabilities are set
|
||||
python_path = sys.executable
|
||||
result = subprocess.run(['getcap', python_path], capture_output=True, text=True)
|
||||
|
||||
if 'cap_net_bind_service=ep' in result.stdout:
|
||||
print("✓ Port binding capabilities already set")
|
||||
return True
|
||||
|
||||
# Try to set capabilities
|
||||
print("Setting up port 80 binding capabilities...")
|
||||
setup_script = './setup_port_capability.sh'
|
||||
|
||||
if os.path.exists(setup_script):
|
||||
result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print("✓ Port capabilities set successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Failed to set capabilities: {result.stderr}")
|
||||
else:
|
||||
# Create the setup script if it doesn't exist
|
||||
script_content = f'''#!/bin/bash
|
||||
# Set port binding capability for Python to allow port 80 access
|
||||
echo "Setting port binding capability for Python..."
|
||||
sudo setcap cap_net_bind_service=ep {python_path}
|
||||
echo "Capability set successfully"
|
||||
'''
|
||||
try:
|
||||
with open(setup_script, 'w') as f:
|
||||
f.write(script_content)
|
||||
os.chmod(setup_script, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH)
|
||||
|
||||
result = subprocess.run(['sudo', 'bash', setup_script], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print("✓ Port capabilities set successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ Failed to set capabilities: {result.stderr}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to create setup script: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not check port capabilities: {e}")
|
||||
|
||||
print("Warning: Port 80 may not be accessible. App will try to run on default port.")
|
||||
return False
|
||||
|
||||
def check_hardware_interfaces():
|
||||
"""
|
||||
Check hardware interfaces (UART/Serial) required for RFID reader
|
||||
@@ -920,7 +796,6 @@ def perform_system_initialization():
|
||||
|
||||
initialization_steps = [
|
||||
("System Requirements", check_system_requirements),
|
||||
("Port Capabilities", check_port_capabilities),
|
||||
("Hardware Interfaces", check_hardware_interfaces),
|
||||
("GPIO Permissions", initialize_gpio_permissions),
|
||||
("Network Connectivity", check_network_connectivity)
|
||||
@@ -1125,269 +1000,6 @@ def log_info_with_server(message):
|
||||
# CONFIGURATION_MODE not defined yet (during initialization)
|
||||
send_log_to_server(message, n_masa, hostname, device_ip) # Send the original message to the server
|
||||
|
||||
# Function to execute system commands with proper security
|
||||
def execute_system_command(command):
|
||||
"""
|
||||
Execute system commands with proper logging and security checks
|
||||
"""
|
||||
# Define allowed commands for security
|
||||
allowed_commands = [
|
||||
"sudo apt update",
|
||||
"sudo apt upgrade -y",
|
||||
"sudo apt update && sudo apt upgrade -y", # Combined update and upgrade
|
||||
"sudo apt autoremove -y",
|
||||
"sudo apt autoclean",
|
||||
"sudo reboot",
|
||||
"sudo shutdown -h now",
|
||||
"df -h",
|
||||
"free -m",
|
||||
"uptime",
|
||||
"systemctl status",
|
||||
"sudo systemctl restart networking",
|
||||
"sudo systemctl restart ssh"
|
||||
]
|
||||
|
||||
try:
|
||||
# Check if command is allowed
|
||||
if command not in allowed_commands:
|
||||
log_info_with_server(f"Command '{command}' is not allowed for security reasons")
|
||||
return {"status": "error", "message": f"Command '{command}' is not allowed", "output": ""}
|
||||
|
||||
log_info_with_server(f"Executing command: {command}")
|
||||
|
||||
# Execute the command
|
||||
result = subprocess.run(
|
||||
command.split(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 minute timeout
|
||||
)
|
||||
|
||||
output = result.stdout + result.stderr
|
||||
|
||||
if result.returncode == 0:
|
||||
log_info_with_server(f"Command '{command}' executed successfully")
|
||||
return {"status": "success", "message": "Command executed successfully", "output": output}
|
||||
else:
|
||||
log_info_with_server(f"Command '{command}' failed with return code {result.returncode}")
|
||||
return {"status": "error", "message": f"Command failed with return code {result.returncode}", "output": output}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
log_info_with_server(f"Command '{command}' timed out")
|
||||
return {"status": "error", "message": "Command timed out", "output": ""}
|
||||
except Exception as e:
|
||||
log_info_with_server(f"Error executing command '{command}': {str(e)}")
|
||||
return {"status": "error", "message": f"Error: {str(e)}", "output": ""}
|
||||
|
||||
# Flask app for receiving commands (only if Flask is available)
|
||||
if FLASK_AVAILABLE:
|
||||
command_app = Flask(__name__)
|
||||
|
||||
@command_app.route('/execute_command', methods=['POST'])
|
||||
def handle_command_execution():
|
||||
"""
|
||||
Endpoint to receive and execute system commands
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
if not data or 'command' not in data:
|
||||
return jsonify({"error": "Invalid request. 'command' field is required"}), 400
|
||||
|
||||
command = data.get('command')
|
||||
|
||||
# Execute the command
|
||||
result = execute_system_command(command)
|
||||
|
||||
return jsonify(result), 200 if result['status'] == 'success' else 400
|
||||
|
||||
except Exception as e:
|
||||
log_info_with_server(f"Error handling command execution request: {str(e)}")
|
||||
return jsonify({"error": f"Server error: {str(e)}"}), 500
|
||||
|
||||
@command_app.route('/status', methods=['GET'])
|
||||
def get_device_status():
|
||||
"""
|
||||
Endpoint to get device status information
|
||||
"""
|
||||
try:
|
||||
n_masa = read_name_from_file()
|
||||
|
||||
# Get system information
|
||||
uptime_result = subprocess.run(['uptime'], capture_output=True, text=True)
|
||||
df_result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True)
|
||||
free_result = subprocess.run(['free', '-m'], capture_output=True, text=True)
|
||||
|
||||
status_info = {
|
||||
"hostname": hostname,
|
||||
"device_ip": device_ip,
|
||||
"nume_masa": n_masa,
|
||||
"timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"uptime": uptime_result.stdout.strip() if uptime_result.returncode == 0 else "N/A",
|
||||
"disk_usage": df_result.stdout.strip() if df_result.returncode == 0 else "N/A",
|
||||
"memory_usage": free_result.stdout.strip() if free_result.returncode == 0 else "N/A"
|
||||
}
|
||||
|
||||
return jsonify(status_info), 200
|
||||
|
||||
except Exception as e:
|
||||
log_info_with_server(f"Error getting device status: {str(e)}")
|
||||
return jsonify({"error": f"Error getting status: {str(e)}"}), 500
|
||||
|
||||
@command_app.route('/auto_update', methods=['POST'])
|
||||
def auto_update_app():
|
||||
"""
|
||||
Trigger an immediate WMT client update check against the monitoring server.
|
||||
Delegates to _check_and_apply_update().
|
||||
"""
|
||||
result = _check_and_apply_update()
|
||||
if result.get('updated'):
|
||||
return jsonify({"status": "success", "message": result['message']}), 200
|
||||
elif result.get('error'):
|
||||
return jsonify({"error": result['message']}), 500
|
||||
else:
|
||||
return jsonify({"status": "no_update", "message": result['message']}), 200
|
||||
|
||||
@command_app.route('/update_config', methods=['POST'])
|
||||
def update_config_endpoint():
|
||||
"""
|
||||
Update configuration from Server_Monitorizare_v2.
|
||||
Accepts a JSON body with sections matching config.txt structure.
|
||||
Example body: {"chrome": {"chrome_url": "http://..."}}
|
||||
"""
|
||||
global APP_CONFIG
|
||||
|
||||
ALLOWED_SECTIONS = {
|
||||
"chrome": ["chrome_url", "chrome_local_url", "chrome_insecure_origin"],
|
||||
"card_api": ["base_url"],
|
||||
"server": ["log_url", "update_host", "update_user", "internet_check_host"],
|
||||
"device": ["name", "hostname", "ip"],
|
||||
}
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"error": "JSON body required"}), 400
|
||||
|
||||
config_path = "./data/config.txt"
|
||||
parser = configparser.ConfigParser()
|
||||
if os.path.exists(config_path):
|
||||
parser.read(config_path)
|
||||
|
||||
updated_keys = []
|
||||
for section, allowed_keys in ALLOWED_SECTIONS.items():
|
||||
if section in data and isinstance(data[section], dict):
|
||||
if not parser.has_section(section):
|
||||
parser.add_section(section)
|
||||
for key in allowed_keys:
|
||||
if key in data[section]:
|
||||
parser.set(section, key, str(data[section][key]))
|
||||
updated_keys.append(f"{section}.{key}")
|
||||
|
||||
os.makedirs("./data", exist_ok=True)
|
||||
with open(config_path, "w") as f:
|
||||
f.write("# WMT Application Configuration\n")
|
||||
f.write(f"# Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
parser.write(f)
|
||||
|
||||
APP_CONFIG = load_config()
|
||||
log_info_with_server(f"Configuration updated via API: {updated_keys}")
|
||||
return jsonify({"status": "success", "updated_keys": updated_keys}), 200
|
||||
|
||||
except Exception as e:
|
||||
log_info_with_server(f"Error updating config: {str(e)}")
|
||||
return jsonify({"error": f"Failed to update configuration: {str(e)}"}), 500
|
||||
|
||||
@command_app.route('/reload_config', methods=['POST'])
|
||||
def reload_config_endpoint():
|
||||
"""
|
||||
Reload configuration from data/config.txt into memory without restarting.
|
||||
"""
|
||||
global APP_CONFIG
|
||||
try:
|
||||
APP_CONFIG = load_config()
|
||||
log_info_with_server("Configuration reloaded via API")
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": "Configuration reloaded",
|
||||
"chrome_url": APP_CONFIG.get("chrome_url"),
|
||||
"card_post_base_url": APP_CONFIG.get("card_post_base_url"),
|
||||
"server_log_url": APP_CONFIG.get("server_log_url"),
|
||||
"device_name": APP_CONFIG.get("work_place"),
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Failed to reload config: {str(e)}"}), 500
|
||||
|
||||
def start_command_server():
|
||||
"""
|
||||
Start the Flask server with enhanced port handling and fallback
|
||||
"""
|
||||
# Try different ports in order of preference
|
||||
preferred_ports = [
|
||||
int(os.environ.get('FLASK_PORT', 80)), # Use environment variable or default to 80
|
||||
80, # Standard HTTP port
|
||||
5000, # Flask default
|
||||
8080, # Alternative HTTP port
|
||||
3000 # Development port
|
||||
]
|
||||
|
||||
for port in preferred_ports:
|
||||
try:
|
||||
print(f"Attempting to start command server on port {port}...")
|
||||
|
||||
# Test if port is available
|
||||
import socket
|
||||
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
test_socket.bind(('0.0.0.0', port))
|
||||
test_socket.close()
|
||||
|
||||
# Port is available, start Flask server
|
||||
print(f"Port {port} is available. Starting command server...")
|
||||
command_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
|
||||
return # Success, exit function
|
||||
|
||||
except PermissionError:
|
||||
print(f"✗ Permission denied for port {port}")
|
||||
if port == 80:
|
||||
print(" Hint: Port 80 requires root privileges or capabilities")
|
||||
print(" Try running: sudo setcap cap_net_bind_service=ep $(which python3)")
|
||||
continue
|
||||
except OSError as e:
|
||||
if "Address already in use" in str(e):
|
||||
print(f"✗ Port {port} is already in use")
|
||||
else:
|
||||
print(f"✗ Port {port} error: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to start on port {port}: {e}")
|
||||
continue
|
||||
|
||||
# If we get here, all ports failed
|
||||
log_info_with_server("Error: Could not start command server on any port")
|
||||
print("✗ Could not start command server on any available port")
|
||||
|
||||
# Start command server in a separate process with enhanced error handling
|
||||
try:
|
||||
print("Initializing command server...")
|
||||
command_server_process = Process(target=start_command_server)
|
||||
command_server_process.daemon = True # Ensure it dies with main process
|
||||
command_server_process.start()
|
||||
|
||||
# Give the server a moment to start and check if it's running
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
if command_server_process.is_alive():
|
||||
port = int(os.environ.get('FLASK_PORT', 80))
|
||||
print(f"✓ Command server started successfully on port {port}")
|
||||
else:
|
||||
print("Warning: Command server process stopped unexpectedly")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not start command server: {e}")
|
||||
log_info_with_server(f"Command server startup error: {str(e)}")
|
||||
else:
|
||||
print("Warning: Flask not available - Command server disabled")
|
||||
# Call the function to delete old logs
|
||||
delete_old_logs()
|
||||
def config():
|
||||
@@ -1482,110 +1094,6 @@ if not CONFIGURATION_MODE:
|
||||
else:
|
||||
print("🔧 Configuration mode: Internet connectivity monitoring DISABLED")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP-based client auto-update helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_local_version():
|
||||
"""Return local app version as float by reading the first line of this file."""
|
||||
try:
|
||||
with open(os.path.abspath(__file__), 'r') as f:
|
||||
first_line = f.readline()
|
||||
m = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE)
|
||||
if m:
|
||||
return float(m.group(1))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _check_and_apply_update():
|
||||
"""
|
||||
Query the monitoring server for the latest WMT release.
|
||||
If a newer version is available, download the zip, back up app.py,
|
||||
extract into the WMT directory, and schedule a systemd service restart.
|
||||
Returns a dict: {updated, message, error}.
|
||||
"""
|
||||
server_host = APP_CONFIG.get("server_host", "")
|
||||
server_port = APP_CONFIG.get("server_port", "5000")
|
||||
if not server_host:
|
||||
return {"updated": False, "error": False, "message": "server_host not configured – skipping update check"}
|
||||
|
||||
base_url = f"http://{server_host}:{server_port}"
|
||||
local_version = _get_local_version()
|
||||
|
||||
try:
|
||||
resp = requests.get(f"{base_url}/api/wmt/client/version", timeout=10)
|
||||
if resp.status_code != 200:
|
||||
return {"updated": False, "error": True, "message": f"Version endpoint returned {resp.status_code}"}
|
||||
meta = resp.json()
|
||||
server_version = float(meta.get("version", 0))
|
||||
except Exception as e:
|
||||
return {"updated": False, "error": True, "message": f"Could not reach version endpoint: {e}"}
|
||||
|
||||
if local_version is not None and server_version <= local_version:
|
||||
return {"updated": False, "error": False, "message": f"Already on latest version {local_version}"}
|
||||
|
||||
log_info_with_server(f"WMT update available: local={local_version} server={server_version} – downloading …")
|
||||
|
||||
wmt_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
tmp_zip = "/tmp/wmt_update.zip"
|
||||
app_py = os.path.join(wmt_dir, "app.py")
|
||||
|
||||
try:
|
||||
# Download the zip
|
||||
dl = requests.get(f"{base_url}/api/wmt/client/download", timeout=60, stream=True)
|
||||
if dl.status_code != 200:
|
||||
return {"updated": False, "error": True, "message": f"Download endpoint returned {dl.status_code}"}
|
||||
with open(tmp_zip, 'wb') as f:
|
||||
for chunk in dl.iter_content(chunk_size=65536):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
|
||||
# Validate the zip
|
||||
import zipfile
|
||||
if not zipfile.is_zipfile(tmp_zip):
|
||||
return {"updated": False, "error": True, "message": "Downloaded file is not a valid zip"}
|
||||
|
||||
# Backup current app.py
|
||||
bak = f"{app_py}.bak.{local_version or 'old'}"
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(app_py, bak)
|
||||
except Exception as e:
|
||||
log_info_with_server(f"Warning: could not back up app.py: {e}")
|
||||
|
||||
# Extract into WMT directory – skip anything inside data/ to preserve device config
|
||||
with zipfile.ZipFile(tmp_zip, 'r') as zf:
|
||||
for member in zf.infolist():
|
||||
# Normalise path separators and skip the data folder
|
||||
member_path = member.filename.replace('\\', '/')
|
||||
if member_path.startswith('data/') or member_path == 'data':
|
||||
continue
|
||||
zf.extract(member, wmt_dir)
|
||||
|
||||
log_info_with_server(f"WMT updated to version {server_version} – scheduling service restart")
|
||||
|
||||
# Schedule restart via systemd (non-blocking)
|
||||
subprocess.Popen(
|
||||
["bash", "-c", "sleep 3 && sudo systemctl restart wmt"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
stdin=subprocess.DEVNULL, start_new_session=True
|
||||
)
|
||||
|
||||
return {"updated": True, "error": False,
|
||||
"message": f"Updated from {local_version} to {server_version}. Restarting service …"}
|
||||
|
||||
except Exception as e:
|
||||
log_info_with_server(f"WMT auto-update failed: {e}")
|
||||
return {"updated": False, "error": True, "message": str(e)}
|
||||
finally:
|
||||
try:
|
||||
os.remove(tmp_zip)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Periodic server config sync (background thread)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1619,19 +1127,10 @@ def _periodic_config_sync():
|
||||
except Exception as e:
|
||||
print(f"Periodic config sync error: {e}")
|
||||
|
||||
# Check for WMT client update every cycle (non-fatal)
|
||||
try:
|
||||
_check_and_apply_update()
|
||||
except Exception as e:
|
||||
print(f"Periodic update check error: {e}")
|
||||
|
||||
if not CONFIGURATION_MODE:
|
||||
_sync_thread = threading.Thread(target=_periodic_config_sync, daemon=True, name="config-sync")
|
||||
_sync_thread.start()
|
||||
print("✅ Periodic server config sync started (every 5 min)")
|
||||
# Startup version check (non-blocking)
|
||||
_update_thread = threading.Thread(target=_check_and_apply_update, daemon=True, name="startup-update-check")
|
||||
_update_thread.start()
|
||||
else:
|
||||
print("🔧 Configuration mode: Periodic config sync DISABLED")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user