From b17b9bb8da26208f4de78802666f584154f575a4 Mon Sep 17 00:00:00 2001 From: ske087 Date: Wed, 13 May 2026 16:36:33 +0300 Subject: [PATCH] feat: HTTP-based auto-update; rename device_name->work_place; auto-configure from idmasa.txt - Replace SCP/sshpass auto_update_app with HTTP-based _check_and_apply_update() - Downloads zip from /api/wmt/client/version + /api/wmt/client/download - Extracts over WMT dir, skipping data/ to preserve device config - Backs up app.py as app.py.bak. - Restarts via systemctl restart wmt - Startup and periodic (5-min) version check threads - _try_autoconfigure_from_prezenta(): auto-write config.txt from idmasa.txt on first boot - _send_first_registration_log(): POST to update_request after auto-configure - Renamed all APP_CONFIG 'device_name' keys to 'work_place' (12 occurrences) --- app.py | 303 +++++++++++++++++++++++---------------------------------- 1 file changed, 122 insertions(+), 181 deletions(-) diff --git a/app.py b/app.py index 0138be2..eb53e5a 100644 --- a/app.py +++ b/app.py @@ -1236,188 +1236,16 @@ if FLASK_AVAILABLE: @command_app.route('/auto_update', methods=['POST']) def auto_update_app(): """ - Auto-update the application from the central server - Checks version, downloads newer files if available, and restarts the device + Trigger an immediate WMT client update check against the monitoring server. + Delegates to _check_and_apply_update(). """ - try: - # Configuration (read from APP_CONFIG, falling back to hardcoded defaults) - SERVER_HOST = APP_CONFIG.get("update_host", "rpi-ansible") - SERVER_USER = APP_CONFIG.get("update_user", "pi") - SERVER_PASSWORD = "Initial01!" - SERVER_APP_PATH = "/home/pi/Desktop/prezenta/app.py" - SERVER_REPO_PATH = "/home/pi/Desktop/prezenta/Files/reposytory" - - # Dynamically determine local paths based on current script location - current_script_path = os.path.abspath(__file__) - local_base_dir = os.path.dirname(current_script_path) - LOCAL_APP_PATH = current_script_path - LOCAL_REPO_PATH = os.path.join(local_base_dir, "Files", "reposytory") - - log_info_with_server(f"Auto-update process initiated from: {LOCAL_APP_PATH}") - - # Step 1: Get current local version - current_version = None - try: - with open(LOCAL_APP_PATH, 'r') as f: - first_line = f.readline() - if 'version' in first_line.lower(): - # Extract version number (e.g., "2.5" from "#App version 2.5") - import re - version_match = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE) - if version_match: - current_version = float(version_match.group(1)) - log_info_with_server(f"Current local version: {current_version}") - except Exception as e: - log_info_with_server(f"Could not determine local version: {e}") - return jsonify({"error": f"Could not determine local version: {str(e)}"}), 500 - - # Step 2: Get remote version via SCP - temp_dir = "/tmp/app_update" - try: - # Create temporary directory - subprocess.run(['mkdir', '-p', temp_dir], check=True) - - # Download remote app.py to check version - scp_command = [ - 'sshpass', '-p', SERVER_PASSWORD, - 'scp', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', - f'{SERVER_USER}@{SERVER_HOST}:{SERVER_APP_PATH}', - f'{temp_dir}/app.py' - ] - - result = subprocess.run(scp_command, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - log_info_with_server(f"Failed to download remote app.py: {result.stderr}") - return jsonify({"error": f"Failed to connect to server: {result.stderr}"}), 500 - - # Check remote version - remote_version = None - with open(f'{temp_dir}/app.py', 'r') as f: - first_line = f.readline() - if 'version' in first_line.lower(): - import re - version_match = re.search(r'version\s+(\d+\.?\d*)', first_line, re.IGNORECASE) - if version_match: - remote_version = float(version_match.group(1)) - - log_info_with_server(f"Remote version: {remote_version}") - - except subprocess.TimeoutExpired: - return jsonify({"error": "Connection to server timed out"}), 500 - except Exception as e: - log_info_with_server(f"Error checking remote version: {e}") - return jsonify({"error": f"Error checking remote version: {str(e)}"}), 500 - - # Step 3: Compare versions - if remote_version is None: - return jsonify({"error": "Could not determine remote version"}), 500 - - if current_version is None or remote_version <= current_version: - log_info_with_server(f"No update needed. Current: {current_version}, Remote: {remote_version}") - return jsonify({ - "status": "no_update_needed", - "current_version": current_version, - "remote_version": remote_version, - "message": "Application is already up to date" - }), 200 - - # Step 4: Download updated files - log_info_with_server(f"Update available! Downloading version {remote_version}") - - try: - # Create backup of current app - backup_path = f"{LOCAL_APP_PATH}.backup.{current_version}" - subprocess.run(['cp', LOCAL_APP_PATH, backup_path], check=True) - log_info_with_server(f"Backup created: {backup_path}") - - # Download new app.py - subprocess.run(['cp', f'{temp_dir}/app.py', LOCAL_APP_PATH], check=True) - log_info_with_server("New app.py downloaded successfully") - - # Download repository folder - repo_scp_command = [ - 'sshpass', '-p', SERVER_PASSWORD, - 'scp', '-r', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', - f'{SERVER_USER}@{SERVER_HOST}:{SERVER_REPO_PATH}', - f'{LOCAL_REPO_PATH}_new' - ] - - result = subprocess.run(repo_scp_command, capture_output=True, text=True, timeout=60) - if result.returncode == 0: - # Replace old repository with new one - subprocess.run(['rm', '-rf', LOCAL_REPO_PATH], check=True) - subprocess.run(['mv', f'{LOCAL_REPO_PATH}_new', LOCAL_REPO_PATH], check=True) - log_info_with_server("Repository updated successfully") - else: - log_info_with_server(f"Repository update failed: {result.stderr}") - - # Download system packages folder - local_system_packages_path = os.path.join(local_base_dir, 'Files', 'system_packages') - server_system_packages = f'{SERVER_USER}@{SERVER_HOST}:/home/pi/Desktop/prezenta/Files/system_packages' - - system_scp_command = [ - 'sshpass', '-p', SERVER_PASSWORD, - 'scp', '-r', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', - server_system_packages, - f'{local_system_packages_path}_new' - ] - - try: - result = subprocess.run(system_scp_command, capture_output=True, text=True, timeout=60) - if result.returncode == 0: - # Replace old system packages with new ones - if os.path.exists(local_system_packages_path): - subprocess.run(['rm', '-rf', local_system_packages_path], check=True) - subprocess.run(['mv', f'{local_system_packages_path}_new', local_system_packages_path], check=True) - log_info_with_server("System packages updated successfully") - else: - log_info_with_server(f"System packages update failed: {result.stderr}") - except Exception as sys_e: - log_info_with_server(f"System packages update error: {sys_e}") - - except Exception as e: - # Restore backup if something went wrong - try: - subprocess.run(['cp', backup_path, LOCAL_APP_PATH], check=True) - log_info_with_server("Backup restored due to error") - except: - pass - return jsonify({"error": f"Update failed: {str(e)}"}), 500 - - # Step 5: Schedule device restart - log_info_with_server("Update completed successfully. Scheduling restart...") - - # Create a restart script that will run after this response - restart_script = '''#!/bin/bash -sleep 3 -sudo reboot -''' - with open('/tmp/restart_device.sh', 'w') as f: - f.write(restart_script) - subprocess.run(['chmod', '+x', '/tmp/restart_device.sh'], check=True) - - # Schedule the restart in background - subprocess.Popen(['/tmp/restart_device.sh'], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - - return jsonify({ - "status": "success", - "message": f"Updated from version {current_version} to {remote_version}. Device restarting...", - "old_version": current_version, - "new_version": remote_version, - "restart_scheduled": True - }), 200 - - except Exception as e: - log_info_with_server(f"Auto-update error: {str(e)}") - return jsonify({"error": f"Auto-update failed: {str(e)}"}), 500 - finally: - # Cleanup temp directory - try: - subprocess.run(['rm', '-rf', temp_dir], check=True) - except: - pass + 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(): @@ -1654,6 +1482,110 @@ 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) # --------------------------------------------------------------------------- @@ -1687,10 +1619,19 @@ 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")