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:
2026-05-13 16:36:33 +03:00
parent fc3c132de0
commit b17b9bb8da
+121 -180
View File
@@ -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")
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:
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
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")