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>
475 lines
16 KiB
Python
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)
|
|
)
|
|
)
|