Files
pi-pm3/app/backend/services/update_service.py
michael a9acdb85ce Build optimization: pre-built PM3 binaries, ARM64 CI, base image caching
Replace PM3 compile-from-source in pi-gen with pre-built tarball extraction
(saves 43-58 min). Merge stagePM3 into stageDangerousPi as 02-pm3-install
substage, renumber all subsequent substages. Switch CI PM3 build to native
ARM64 runner (ubuntu-24.04-arm64) eliminating QEMU overhead. Add weekly
base-image workflow for pre-baking stages 0-2. Support PM3_TARBALL,
BASE_IMAGE, and APT_PROXY env vars in build-image.sh.

Also includes prior Phase 5 work: theme system, design system integration,
component update system, OS updates, CI build pipeline, and test results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:01:01 -08:00

475 lines
16 KiB
Python

"""Update Service Layer.
This service provides software update operations that can be consumed by
multiple interfaces (REST API, BLE GATT, etc.).
"""
from dataclasses import asdict
from typing import Optional, Dict, Any, List
from ..managers.update_manager import (
UpdateManager, UpdateProgress, UpdateStatus, ComponentId,
)
from .pm3_service import PM3ServiceError, PM3ServiceResult
class UpdateService:
"""Update service layer for software update management.
This service can be used by multiple interfaces:
- REST API (FastAPI endpoints)
- BLE GATT (Bluetooth handlers)
- Plugin system (future)
It encapsulates:
- Update checking
- Update downloading
- Update installation
- Progress monitoring
"""
def __init__(self, update_manager: Optional[UpdateManager] = None):
"""Initialize update service.
Args:
update_manager: Update manager instance (creates new if not provided)
"""
from ..managers.update_manager import get_update_manager
self.update_manager = update_manager or get_update_manager()
async def check_for_updates(self) -> PM3ServiceResult:
"""Check for available updates.
Returns:
PM3ServiceResult with update availability and information
"""
try:
result = await self.update_manager.check_for_updates()
return PM3ServiceResult(
success=True,
data=result
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="update_check_error",
message="Failed to check for updates",
details=str(e)
)
)
async def download_update(self) -> PM3ServiceResult:
"""Download the latest available update.
Returns:
PM3ServiceResult indicating download success/failure
"""
try:
success = await self.update_manager.download_update()
if success:
return PM3ServiceResult(
success=True,
data={
"message": "Update downloaded successfully",
"ready_to_install": True
}
)
else:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="download_failed",
message="Update download failed"
)
)
except ValueError as e:
# No update available
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="no_update_available",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="update_download_error",
message="Error downloading update",
details=str(e)
)
)
async def install_update(self) -> PM3ServiceResult:
"""Install the downloaded update.
Returns:
PM3ServiceResult indicating installation success/failure
"""
try:
success = await self.update_manager.install_update()
if success:
return PM3ServiceResult(
success=True,
data={
"message": "Update installed successfully",
"requires_restart": True
}
)
else:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="installation_failed",
message="Update installation failed"
)
)
except ValueError as e:
# No update downloaded
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="no_update_downloaded",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="update_install_error",
message="Error installing update",
details=str(e)
)
)
async def get_progress(self) -> PM3ServiceResult:
"""Get current update progress.
Returns:
PM3ServiceResult with progress information
"""
try:
progress = await self.update_manager.get_progress()
comp_list = []
for comp_id, cp in progress.component_progress.items():
comp_list.append({
"component_id": cp.component_id,
"status": cp.status.value,
"download_progress": cp.download_progress,
"error_message": cp.error_message,
})
return PM3ServiceResult(
success=True,
data={
"status": progress.status.value,
"current_version": progress.current_version,
"available_version": progress.available_version,
"download_progress": progress.download_progress,
"error_message": progress.error_message,
"last_check": progress.last_check,
"active_component": progress.active_component,
"components": comp_list,
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="progress_error",
message="Failed to get update progress",
details=str(e)
)
)
async def get_release_notes(
self,
version: Optional[str] = None
) -> PM3ServiceResult:
"""Get release notes for a specific version or latest.
Args:
version: Version to get notes for (latest if None)
Returns:
PM3ServiceResult with release notes
"""
try:
notes = await self.update_manager.get_release_notes(version)
return PM3ServiceResult(
success=True,
data={
"version": version or "latest",
"release_notes": notes
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="release_notes_error",
message="Failed to get release notes",
details=str(e)
)
)
async def check_and_download_update(self) -> PM3ServiceResult:
"""Combined operation: check for updates and download if available.
Returns:
PM3ServiceResult with check and download status
"""
try:
# First check for updates
check_result = await self.check_for_updates()
if not check_result.success:
return check_result
# If update available, download it
if check_result.data.get("update_available"):
download_result = await self.download_update()
if download_result.success:
return PM3ServiceResult(
success=True,
data={
"message": "Update checked and downloaded",
"update_info": check_result.data,
"ready_to_install": True
}
)
else:
return download_result
else:
return PM3ServiceResult(
success=True,
data={
"message": "System is up to date",
"update_available": False,
"current_version": check_result.data.get("current_version")
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="check_download_error",
message="Error checking and downloading update",
details=str(e)
)
)
async def full_update(self) -> PM3ServiceResult:
"""Full update workflow: check, download, and install.
Returns:
PM3ServiceResult with complete update status
"""
try:
# Check for updates
check_result = await self.check_for_updates()
if not check_result.success:
return check_result
if not check_result.data.get("update_available"):
return PM3ServiceResult(
success=True,
data={
"message": "System is already up to date",
"update_performed": False
}
)
# Download update
download_result = await self.download_update()
if not download_result.success:
return download_result
# Install update
install_result = await self.install_update()
if not install_result.success:
return install_result
return PM3ServiceResult(
success=True,
data={
"message": "Update completed successfully",
"update_performed": True,
"previous_version": check_result.data.get("current_version"),
"new_version": check_result.data.get("latest_version"),
"requires_restart": True
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="full_update_error",
message="Error performing full update",
details=str(e)
)
)
# ------------------------------------------------------------------
# Component-level operations
# ------------------------------------------------------------------
async def get_installed_components(self) -> PM3ServiceResult:
"""Get the installed component manifest.
Returns:
PM3ServiceResult with component manifest data
"""
try:
manifest = self.update_manager.get_manifest()
components = {}
for comp_id, comp in manifest.components.items():
components[comp_id] = asdict(comp)
return PM3ServiceResult(
success=True,
data={
"schema_version": manifest.schema_version,
"system_version": manifest.system_version,
"components": components,
"plugins": manifest.plugins,
"last_updated": manifest.last_updated,
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="manifest_error",
message="Failed to get component manifest",
details=str(e)
)
)
async def download_component(self, component_id: str) -> PM3ServiceResult:
"""Download a specific component update.
Args:
component_id: Component to download (pm3, frontend, backend, theme)
Returns:
PM3ServiceResult indicating download success/failure
"""
try:
success = await self.update_manager.download_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} downloaded successfully",
"component_id": component_id,
"ready_to_install": True,
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_not_available",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_download_error",
message=f"Error downloading {component_id}",
details=str(e)
)
)
async def install_component(self, component_id: str) -> PM3ServiceResult:
"""Install a downloaded component update.
Args:
component_id: Component to install
Returns:
PM3ServiceResult indicating install success/failure
"""
try:
success = await self.update_manager.install_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} installed successfully",
"component_id": component_id,
"requires_restart": component_id in (
ComponentId.BACKEND, ComponentId.PM3
),
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_install_error",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_install_error",
message=f"Error installing {component_id}",
details=str(e)
)
)
async def rollback_component(self, component_id: str) -> PM3ServiceResult:
"""Rollback a component to its previous version.
Args:
component_id: Component to rollback
Returns:
PM3ServiceResult indicating rollback success/failure
"""
try:
success = await self.update_manager.rollback_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} rolled back successfully",
"component_id": component_id,
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="rollback_error",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="rollback_error",
message=f"Error rolling back {component_id}",
details=str(e)
)
)