From cc4b6ed68bcd77235d79c92483210b45eff3f01b Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 5 Jan 2026 15:55:32 +0100 Subject: [PATCH 1/4] implement slim update functinallity --- src/auracast/server/multicast_frontend.py | 63 +++++++++ src/auracast/server/multicast_server.py | 158 ++++++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index c847a24..fde2288 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -916,6 +916,69 @@ with st.expander("System control", expanded=False): except Exception as e: st.error(f"Failed to update password: {e}") + st.subheader("Software Version") + # Show current version + try: + ver_resp = requests.get(f"{BACKEND_URL}/version", timeout=2) + if ver_resp.ok: + ver_data = ver_resp.json() + current_version = ver_data.get('version', 'unknown') + ver_type = ver_data.get('type', '') + ver_label = current_version if ver_type == 'tag' else f"{current_version} (dev)" + st.write(f"**Current version:** {ver_label}") + else: + st.write("**Current version:** unknown") + current_version = "unknown" + except Exception: + st.write("**Current version:** unknown") + current_version = "unknown" + + # Initialize session state for update check + if 'available_update' not in st.session_state: + st.session_state['available_update'] = None + + col_check, col_status = st.columns([1, 2]) + with col_check: + if st.button("Check for updates"): + try: + check_resp = requests.get(f"{BACKEND_URL}/check_update", timeout=30) + if check_resp.ok: + check_data = check_resp.json() + if check_data.get('error'): + st.session_state['available_update'] = {'error': check_data['error']} + else: + st.session_state['available_update'] = check_data + else: + st.session_state['available_update'] = {'error': f"Failed: {check_resp.status_code}"} + except Exception as e: + st.session_state['available_update'] = {'error': str(e)} + st.rerun() + + with col_status: + if st.session_state['available_update']: + upd = st.session_state['available_update'] + if upd.get('error'): + st.warning(f"Check failed: {upd['error']}") + elif upd.get('update_available'): + st.info(f"Update available: **{upd['available']}**") + else: + st.success("You are on the latest version.") + + # Update button (only show if update is available) + if st.session_state['available_update'] and st.session_state['available_update'].get('update_available'): + if st.button("Update now"): + try: + r = requests.post(f"{BACKEND_URL}/system_update", timeout=120) + if r.ok: + result = r.json() + tag = result.get('tag', 'unknown') + st.success(f"Update to {tag} initiated. The UI will restart shortly.") + st.session_state['available_update'] = None + else: + st.error(f"Failed to update: {r.status_code} {r.text}") + except Exception as e: + st.error(f"Error calling update: {e}") + st.subheader("Reboot") if st.button("Reboot now", type="primary"): try: diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index 20a3823..ce424b3 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -899,6 +899,164 @@ async def system_reboot(): raise HTTPException(status_code=500, detail=str(e)) +@app.get("/version") +async def get_version(): + """Get the current software version (git tag or commit).""" + try: + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + + # Try to get the current tag + proc = await asyncio.create_subprocess_exec( + "git", "describe", "--tags", "--exact-match", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode == 0: + return {"version": stdout.decode().strip(), "type": "tag"} + + # Fallback: get the current commit short hash + proc = await asyncio.create_subprocess_exec( + "git", "rev-parse", "--short", "HEAD", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode == 0: + return {"version": stdout.decode().strip(), "type": "commit"} + + return {"version": "unknown", "type": "unknown"} + except Exception as e: + log.error("Exception in /version: %s", e, exc_info=True) + return {"version": "unknown", "type": "error"} + + +@app.get("/check_update") +async def check_update(): + """Check for available updates by comparing current version with latest release tag.""" + try: + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + + # Fetch latest from release branch + proc = await asyncio.create_subprocess_exec( + "git", "fetch", "--tags", "origin", "release", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + await proc.communicate() + + # Get the latest tag on the release branch + proc = await asyncio.create_subprocess_exec( + "git", "describe", "--tags", "--abbrev=0", "origin/release", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + return {"available": None, "error": "Could not determine latest release tag"} + latest_tag = stdout.decode().strip() + + # Get current version + current_version = (await get_version()).get("version", "unknown") + + update_available = latest_tag != current_version + return { + "current": current_version, + "available": latest_tag, + "update_available": update_available + } + except Exception as e: + log.error("Exception in /check_update: %s", e, exc_info=True) + return {"available": None, "error": str(e)} + + +@app.post("/system_update") +async def system_update(): + """Update application: git pull release branch (latest tag), poetry install, restart services.""" + try: + # Best-effort: stop any active streaming cleanly + try: + await _stop_all() + except Exception: + pass + + # Project root is 4 levels up from this file: server -> auracast -> src -> bumble-auracast + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + + # 1. Fetch and checkout the latest tag from the release branch + # First fetch all tags and the release branch + proc = await asyncio.create_subprocess_exec( + "git", "fetch", "--tags", "origin", "release", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + log.error("git fetch failed: %s", stderr.decode()) + raise HTTPException(status_code=500, detail=f"git fetch failed: {stderr.decode()}") + + # Get the latest tag on the release branch + proc = await asyncio.create_subprocess_exec( + "git", "describe", "--tags", "--abbrev=0", "origin/release", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + log.error("git describe failed: %s", stderr.decode()) + raise HTTPException(status_code=500, detail=f"Failed to get latest tag: {stderr.decode()}") + latest_tag = stdout.decode().strip() + log.info("Updating to tag: %s", latest_tag) + + # Checkout the latest tag + proc = await asyncio.create_subprocess_exec( + "git", "checkout", latest_tag, + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + log.error("git checkout failed: %s", stderr.decode()) + raise HTTPException(status_code=500, detail=f"git checkout failed: {stderr.decode()}") + + # 2. Run poetry install + proc = await asyncio.create_subprocess_exec( + "poetry", "install", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + log.error("poetry install failed: %s", stderr.decode()) + raise HTTPException(status_code=500, detail=f"poetry install failed: {stderr.decode()}") + + # 3. Restart services via the update script + update_script = os.path.join(project_root, 'src', 'service', 'update_and_run_server_and_frontend.sh') + proc = await asyncio.create_subprocess_exec( + "bash", update_script, + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + # Don't wait for completion as we'll be restarted + await asyncio.sleep(0.5) + + return {"status": "updating", "tag": latest_tag} + except HTTPException: + raise + except Exception as e: + log.error("Exception in /system_update: %s", e, exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + if __name__ == '__main__': import os os.chdir(os.path.dirname(__file__)) -- 2.52.0 From 8549691e675a1bdeb349b7f6462530459c247695 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 5 Jan 2026 15:59:15 +0100 Subject: [PATCH 2/4] Improve update check to use version-sorted tags instead of release branch - Fetch all tags and refs from origin instead of only release branch - Sort tags by semantic version to find latest release - Add error handling for git fetch failures - Filter empty tag strings and validate tag list exists - Use version:refname sorting to properly handle semantic versioning --- src/auracast/server/multicast_server.py | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index ce424b3..d0b0614 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -939,26 +939,35 @@ async def check_update(): try: project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) - # Fetch latest from release branch + # Fetch all tags and refs from origin proc = await asyncio.create_subprocess_exec( - "git", "fetch", "--tags", "origin", "release", - cwd=project_root, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await proc.communicate() - - # Get the latest tag on the release branch - proc = await asyncio.create_subprocess_exec( - "git", "describe", "--tags", "--abbrev=0", "origin/release", + "git", "fetch", "--tags", "--force", "origin", cwd=project_root, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode != 0: - return {"available": None, "error": "Could not determine latest release tag"} - latest_tag = stdout.decode().strip() + log.warning("git fetch failed: %s", stderr.decode()) + + # Get the latest tag sorted by version (semver-like sorting) + # This gets all tags, sorts them by version, and takes the last one + proc = await asyncio.create_subprocess_exec( + "git", "tag", "--sort=version:refname", + cwd=project_root, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + return {"available": None, "error": f"Could not list tags: {stderr.decode()}"} + + tags = stdout.decode().strip().split('\n') + tags = [t for t in tags if t] # Filter empty strings + if not tags: + return {"available": None, "error": "No tags found in repository"} + + latest_tag = tags[-1] # Last tag after version sorting # Get current version current_version = (await get_version()).get("version", "unknown") -- 2.52.0 From a61d7b086872a5ecc372e6b9efc9ed89f658fde9 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 5 Jan 2026 16:02:24 +0100 Subject: [PATCH 3/4] Fix project root path calculation in version and update functions - Correct path traversal from 4 to 3 levels up (server -> auracast -> src -> bumble-auracast) - Add clarifying comments for path calculation - Apply fix consistently across get_version(), check_update(), and system_update() functions --- src/auracast/server/multicast_server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index d0b0614..8f8f157 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -903,7 +903,8 @@ async def system_reboot(): async def get_version(): """Get the current software version (git tag or commit).""" try: - project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + # server -> auracast -> src -> bumble-auracast (3 levels up) + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) # Try to get the current tag proc = await asyncio.create_subprocess_exec( @@ -937,7 +938,8 @@ async def get_version(): async def check_update(): """Check for available updates by comparing current version with latest release tag.""" try: - project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + # server -> auracast -> src -> bumble-auracast (3 levels up) + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) # Fetch all tags and refs from origin proc = await asyncio.create_subprocess_exec( @@ -993,8 +995,8 @@ async def system_update(): except Exception: pass - # Project root is 4 levels up from this file: server -> auracast -> src -> bumble-auracast - project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) + # server -> auracast -> src -> bumble-auracast (3 levels up) + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) # 1. Fetch and checkout the latest tag from the release branch # First fetch all tags and the release branch -- 2.52.0 From 663be554df74229249b7994a2880f53fc45ed516 Mon Sep 17 00:00:00 2001 From: pstruebi Date: Mon, 5 Jan 2026 16:07:42 +0100 Subject: [PATCH 4/4] Use full path for poetry command in system update to ensure binary is found - Change poetry command to use absolute path ~/.local/bin/poetry - Add comment explaining poetry location in user's local bin directory - Prevents command not found errors when poetry is not in system PATH --- src/auracast/server/multicast_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auracast/server/multicast_server.py b/src/auracast/server/multicast_server.py index 8f8f157..0b97357 100644 --- a/src/auracast/server/multicast_server.py +++ b/src/auracast/server/multicast_server.py @@ -1037,9 +1037,10 @@ async def system_update(): log.error("git checkout failed: %s", stderr.decode()) raise HTTPException(status_code=500, detail=f"git checkout failed: {stderr.decode()}") - # 2. Run poetry install + # 2. Run poetry install (use full path as poetry is in user's ~/.local/bin) + poetry_path = os.path.expanduser("~/.local/bin/poetry") proc = await asyncio.create_subprocess_exec( - "poetry", "install", + poetry_path, "install", cwd=project_root, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE -- 2.52.0