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.<version> - 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)
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user