feature/app_update (#18)
implement slim update functinallity
This commit was merged in pull request #18.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -899,6 +899,176 @@ 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:
|
||||
# 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(
|
||||
"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:
|
||||
# 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(
|
||||
"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:
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
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 (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_path, "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__))
|
||||
|
||||
Reference in New Issue
Block a user