diff --git a/frontend/src/msw/handlers.ts b/frontend/src/msw/handlers.ts index b7a9e6c2..e098cd0e 100644 --- a/frontend/src/msw/handlers.ts +++ b/frontend/src/msw/handlers.ts @@ -39,6 +39,9 @@ export default [ rest.get("/further-url", (_, res, ctx) => { return res(ctx.json({ url: "https://further.pi-top.com" })); }), + rest.get("/available-space", (_, res, ctx) => { + return res(ctx.body("20378521600")); + }), rest.get("/rover-controller-status", (_, res, ctx) => { return res(ctx.json({ status: "inactive" })); }), @@ -48,6 +51,9 @@ export default [ rest.post("/rover-controller-stop", (_, res, ctx) => { return res(ctx.body("OK")); }), + rest.post("/restart-web-portal-service", (_, res, ctx) => { + return res(ctx.body("OK")); + }), rest.get("/is-connected", (_, res, ctx) => { return res(ctx.json({ connected: false })); }), diff --git a/frontend/src/pages/upgradePage/UpgradePage.tsx b/frontend/src/pages/upgradePage/UpgradePage.tsx index bf0e8882..e5e20911 100644 --- a/frontend/src/pages/upgradePage/UpgradePage.tsx +++ b/frontend/src/pages/upgradePage/UpgradePage.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react"; import { Line as ProgressBar } from "rc-progress"; import prettyBytes from "pretty-bytes"; -import CheckBox from "../../components/atoms/checkBox/CheckBox"; import Layout from "../../components/layout/Layout"; import Spinner from "../../components/atoms/spinner/Spinner"; @@ -21,7 +20,7 @@ import UpgradeHistoryTextArea from "../../components/upgradeHistoryTextArea/Upgr export enum ErrorMessage { NoSpaceAvailable = "There's not enough space on the device to install updates. Please, free up space and try updating again.", - GenericError = "There was a problem during system update.\nIf this is the first time, please try again using the recommended method.\nIf you're experiencing repeated issues, try another method.", + GenericError = "There was a problem during system update.\nPlease try again later.\nIf you're experiencing repeated issues, contact pi-top support.", CloseOtherWindow = "The OS Updater application is already running in another window.", } @@ -43,7 +42,7 @@ export type Props = { onSkipClick?: () => void; onBackClick?: () => void; onStartUpgradeClick: () => void; - onRetry: (defaultBackend: boolean) => void; + onRetry: () => void; isCompleted?: boolean; message?: OSUpdaterMessage; updateState: UpdateState; @@ -70,9 +69,10 @@ export default ({ error, }: Props) => { const [isNewOsDialogActive, setIsNewOsDialogActive] = useState(false); - const [isUsingDefaultBackend, setIsUsingDefaultBackend] = useState(true); const [isRetrying, setIsRetrying] = useState(false); + const displayProgressBar = false; + useEffect(() => { setIsNewOsDialogActive(requireBurn || shouldBurn); }, [requireBurn, shouldBurn]); @@ -206,7 +206,7 @@ export default ({ const onNextButtonClick = () => { if (hasError()) { setIsRetrying(true); - onRetry(isUsingDefaultBackend); + onRetry(); } else if (updateState === UpdateState.WaitingForUserInput) { onStartUpgradeClick(); } else { @@ -260,18 +260,6 @@ export default ({ ); })} - - {error !== ErrorType.UpdaterAlreadyRunning && ( - - setIsUsingDefaultBackend(!isUsingDefaultBackend) - } - className={styles.checkbox} - /> - )} )} @@ -306,7 +294,7 @@ export default ({ message?.type === OSUpdaterMessageType.UpdateSources) && updateState !== UpdateState.WaitingForServer && !hasError() && - isUsingDefaultBackend && ( + displayProgressBar && (
{ - socket.send(defaultBackend ? SocketMessage.USE_DEFAULT_UPDATER : SocketMessage.USE_LEGACY_UPDATER); + () => { setError(ErrorType.None); setUpdateSize({downloadSize: 0, requiredSpace: 0}); checkingWebPortalRef.current = true; setState(UpdateState.UpdatingSources); }, - [socket], + [setError, setUpdateSize, setState], ) useEffect(() => { @@ -284,7 +281,6 @@ export default ({ goToNextPage, goToPreviousPage, hideSkip, isCompleted, setEnab if (message.payload.busy) { setState(UpdateState.Reattaching); } else { - socket.send(SocketMessage.USE_DEFAULT_UPDATER); setState(UpdateState.UpdatingSources); } } @@ -361,18 +357,9 @@ export default ({ goToNextPage, goToPreviousPage, hideSkip, isCompleted, setEnab return ( { - setEnableDisconnectedFromApDialog && setEnableDisconnectedFromApDialog(true); - goToNextPage && goToNextPage() - }} - onSkipClick={() => { - setEnableDisconnectedFromApDialog && setEnableDisconnectedFromApDialog(true); - goToNextPage && goToNextPage() - }} - onBackClick={() => { - setEnableDisconnectedFromApDialog && setEnableDisconnectedFromApDialog(true); - goToPreviousPage && goToPreviousPage() - }} + onNextClick={goToNextPage} + onSkipClick={goToNextPage} + onBackClick={goToPreviousPage} hideSkip={hideSkip} onStartUpgradeClick={() => { if (isOpen) { @@ -381,9 +368,7 @@ export default ({ goToNextPage, goToPreviousPage, hideSkip, isCompleted, setEnab } setError(ErrorType.GenericError); }} - onRetry={(useDefaultBackend: boolean) => { - doRetry(useDefaultBackend) - }} + onRetry={doRetry} isCompleted={isCompleted} message={message} updateState={state} diff --git a/frontend/src/pages/upgradePage/__tests__/UpgradePageContainer.test.tsx b/frontend/src/pages/upgradePage/__tests__/UpgradePageContainer.test.tsx index 334ec4c3..a4354610 100644 --- a/frontend/src/pages/upgradePage/__tests__/UpgradePageContainer.test.tsx +++ b/frontend/src/pages/upgradePage/__tests__/UpgradePageContainer.test.tsx @@ -237,12 +237,10 @@ describe("UpgradePageContainer", () => { expect(textAreaElement).toMatchSnapshot(); }); - it("renders progress bar correctly", async () => { - const { findByTestId, queryByTestId } = mount(); + it("doesn't render progress bar", async () => { + const { container: upgradePage } = mount(); - await findByTestId("progress"); - const progressBar = queryByTestId("progress"); - expect(progressBar).toMatchSnapshot(); + expect(upgradePage.querySelector(".progress")).not.toBeInTheDocument(); }); it("doesn't render the Skip button", async () => { @@ -1011,14 +1009,12 @@ describe("UpgradePageContainer", () => { await waitForInstallingPackages(); }); - it("renders progress bar correctly", async () => { + it("doesn't render the progress bar", async () => { const { getByText, waitForPreparation, container: upgradePage } = mount(); await waitForPreparation(); fireEvent.click(getByText("Update")); - await waitForElement(() => upgradePage.querySelector(".progress")); - const progressBar = upgradePage.querySelector(".progress"); - expect(progressBar).toMatchSnapshot(); + expect(upgradePage.querySelector(".progress")).not.toBeInTheDocument(); }); it("renders the textarea component", async () => { @@ -1295,13 +1291,13 @@ describe("UpgradePageContainer", () => { await waitForUpgradeFinish(); }); - it("renders progress bar correctly", async () => { + it("doesn't render the progress bar", async () => { const { getByText, waitForPreparation, waitForUpgradeFinish, container: upgradePage } = mount(); await waitForPreparation(); fireEvent.click(getByText("Update")); await waitForUpgradeFinish(); - expect(upgradePage.querySelector(".progress")).toMatchSnapshot(); + expect(upgradePage.querySelector(".progress")).not.toBeInTheDocument(); }); it("renders the textarea component", async () => { diff --git a/frontend/src/pages/upgradePage/__tests__/__snapshots__/UpgradePageContainer.test.tsx.snap b/frontend/src/pages/upgradePage/__tests__/__snapshots__/UpgradePageContainer.test.tsx.snap index 01129b25..588ff6a1 100644 --- a/frontend/src/pages/upgradePage/__tests__/__snapshots__/UpgradePageContainer.test.tsx.snap +++ b/frontend/src/pages/upgradePage/__tests__/__snapshots__/UpgradePageContainer.test.tsx.snap @@ -58,39 +58,6 @@ dpkg-exec: Running dpkg `; -exports[`UpgradePageContainer when the system is being updated renders progress bar correctly 1`] = ` -
- - - - -
-`; - exports[`UpgradePageContainer when the system is being updated renders prompt correctly 1`] = `

`; -exports[`UpgradePageContainer when the upgrade finishes renders progress bar correctly 1`] = ` -
- - - - -
-`; - exports[`UpgradePageContainer when the upgrade finishes renders prompt correctly 1`] = `

`; -exports[`UpgradePageContainer while updating sources renders progress bar correctly 1`] = ` -
- - - - -
-`; - exports[`UpgradePageContainer while updating sources renders prompt correctly 1`] = `

None: - self.name = package_name - self.is_upgradable = False - - def mark_upgrade(self) -> None: - pass - - -class CacheMock: - required_download = 2155000000 - install_count = 1 - required_space = 99300000 - sleep_time = 1 - _dummy_upgrade_messages = [ - ["dpkg-exec", 0.0, "Running dpkg"], - [ - "gnome-control-center-data", - 25.0, - "Preparing gnome-control-center-data (armhf)", - ], - [ - "gnome-control-center-data", - 50.6923, - "Unpacking gnome-control-center-data (armhf)", - ], - [ - "gnome-control-center-data", - 95.3333, - "Installing gnome-control-center-data (armhf)", - ], - ] - _sources_to_update = 4 - packages = { - "pt-os-web-portal": PackageMock("pt-os-web-portal"), - "python3-pitop": PackageMock("python3-pitop"), - "python3-pitop-full": PackageMock("python3-pitop-full"), - } - - def get(self, package_name): - return self.packages.get(package_name) - - def update(self, progress=None): - progress.total_items = self._sources_to_update - if hasattr(progress, "pulse"): - for idx in range(self._sources_to_update): - progress.current_items = idx - progress.pulse(None) - if self.sleep_time: - print(f"CacheMock.update: Sleeping for {self.sleep_time}s") - sleep(self.sleep_time) - - def open(self, opt=None): - pass - - def upgrade(self, dist_upgrade=None): - if self.sleep_time: - print(f"CacheMock.upgrade: Sleeping for {self.sleep_time}s") - sleep(self.sleep_time) - - def commit(self, fetch_progress, install_progress): - if self.sleep_time: - print(f"CacheMock.commit: Sleeping for {self.sleep_time}s") - sleep(self.sleep_time) - - if hasattr(fetch_progress, "pulse"): - fetch_progress.pulse(None) - - if hasattr(install_progress, "status_change"): - for pkg_name, percent, status in self._dummy_upgrade_messages: - install_progress.status_change( - pkg=pkg_name, percent=percent, status=status - ) - if self.sleep_time: - print(f"CacheMock.commit: Sleeping for {self.sleep_time}s") - sleep(self.sleep_time) - - def keys(self): - return {} - - -class FilterMock: - pass - - -class AptCacheMock: - Filter = FilterMock - - -class AptMock: - Cache = CacheMock - cache = AptCacheMock - Package = PackageMock - - -class ProgressMock: - current_items = 0 - total_items = 1 - current_cps = 0 - current_bytes = 0 - total_bytes = 0 - - def pulse(self, owner): - pass - - def status_change(self, pkg, percent, status): - pass - - def update_interface(self): - pass - - -class AptProgressClassesMock: - AcquireProgress = ProgressMock - InstallProgress = ProgressMock - - -class AptProgressMock: - base = AptProgressClassesMock() - - -class AptPkgMock: - def size_to_str(self, size): - return f"{size} " if size < 1e6 else f"{int(size*1e-6)} k" - - def init_config(self): - pass diff --git a/pt_os_web_portal/backend/helpers/modules.py b/pt_os_web_portal/backend/helpers/modules.py index 7e03920d..fa124816 100644 --- a/pt_os_web_portal/backend/helpers/modules.py +++ b/pt_os_web_portal/backend/helpers/modules.py @@ -10,16 +10,3 @@ def get_pywifi(): from .vendor import pywifi return pywifi - - -def get_apt(): - if use_test_path(): - from .mocks.apt_mock import AptMock, AptPkgMock, AptProgressMock - - return AptMock(), AptProgressMock(), AptPkgMock() - - import apt - import apt.progress - import apt_pkg - - return apt, apt.progress, apt_pkg diff --git a/pt_os_web_portal/backend/routes.py b/pt_os_web_portal/backend/routes.py index 1d4fab70..55e2a271 100644 --- a/pt_os_web_portal/backend/routes.py +++ b/pt_os_web_portal/backend/routes.py @@ -308,8 +308,6 @@ def os_upgrade(ws): "prepare_web_portal": get_os_updater().stage_web_portal, "start": get_os_updater().start_os_upgrade, "size": get_os_updater().upgrade_size, - "legacy-updater-backend": get_os_updater().use_legacy_backend, - "default-updater-backend": get_os_updater().use_default_backend, "state": get_os_updater().state, } diff --git a/pt_os_web_portal/os_updater/backend.py b/pt_os_web_portal/os_updater/backend.py new file mode 100644 index 00000000..0c42d591 --- /dev/null +++ b/pt_os_web_portal/os_updater/backend.py @@ -0,0 +1,196 @@ +import logging +from subprocess import PIPE, CalledProcessError, Popen +from typing import Callable, List + +logger = logging.getLogger(__name__) + + +def str_to_float(text): + dot_pos = text.rfind(".") + comma_pos = text.rfind(",") + if comma_pos > dot_pos: + text = text.replace(".", "") + text = text.replace(",", ".") + else: + text = text.replace(",", "") + return float(text) + + +def size_str_to_bytes(size): + units = {"B": 1, "KB": 1e3, "kB": 1e3, "MB": 1e6, "GB": 1e9, "TB": 1e12} + number, unit = [string.strip() for string in size.split()] + return int(float(number) * units[unit]) + + +class AptCommands: + @classmethod + def update(cls): + return ["apt-get", "update"] + + @classmethod + def dist_upgrade(cls): + return [ + "apt-get", + '-o Dpkg::Options::="--force-confdef"', + '-o Dpkg::Options::="--force-confold"', + "-o APT::Get::Upgrade-Allow-New=true", + "dist-upgrade", + "--quiet", + "--yes", + ] + + @classmethod + def update_size(cls): + return ["apt-get", "dist-upgrade", "--assume-no"] + + @classmethod + def install_packages(cls, packages): + return [ + "apt-get", + '-o Dpkg::Options::="--force-confdef"', + '-o Dpkg::Options::="--force-confold"', + "-o APT::Get::Upgrade-Allow-New=true", + "install", + *packages, + "--quiet", + "--yes", + ] + + @classmethod + def install_size(cls, packages): + return ["apt-get", "install", "--assume-no", *packages] + + +def run_command(cmd: List, callback: Callable, check: bool = True): + logger.info(f"run_command: executing '{cmd}'") + with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p: + for line in p.stdout: + line = line.strip() + if callable(callback): + callback(line) + logger.info(f"run_command: {line}") + if check and p.returncode != 0: + raise CalledProcessError(p.returncode, p.args) + + +class OsUpdaterBackend: + def __init__(self) -> None: + self.lock = False + self._download_size = 0 + self.download_size_str = "" + self._required_space = 0 + self.required_space_str = "" + self._install_count = 0 + self.packages = [] + + def download_size(self): + return self._download_size + + def required_space(self): + return self._required_space + + def install_count(self): + return self._install_count + + def update(self, callback) -> None: + logger.info("OsUpdaterBackend: Updating APT sources") + if self.lock: + raise Exception("OsUpdaterBackend is locked") + self.lock = True + + try: + self._do_update(callback) + finally: + self.lock = False + + def stage_upgrade(self, packages=[]) -> None: + logger.info("OsUpdaterBackend: Staging packages for upgrade") + if self.lock: + raise Exception("OsUpdaterBackend is locked") + self.lock = True + + try: + self._do_stage_upgrade(packages) + finally: + self.lock = False + + def upgrade(self, callback): + logger.info("OsUpdaterBackend: starting upgrade") + if self.lock: + raise Exception("OsUpdaterBackend is locked") + self.lock = True + + try: + if len(self.packages) > 0: + self._do_install(callback) + else: + self._do_upgrade(callback) + finally: + self.lock = False + + logger.info("OsUpdaterBackend: finished upgrade") + + def _do_update(self, callback): + run_command(AptCommands.update(), callback) + + def _do_install(self, callback): + run_command(AptCommands.install_packages(self.packages), callback) + + def _do_upgrade(self, callback): + run_command(AptCommands.dist_upgrade(), callback) + + def _do_get_install_size(self): + if len(self.packages) > 0: + cmd = AptCommands.install_size(self.packages) + else: + cmd = AptCommands.update_size() + run_command(cmd, self._parse_install_size_and_packages, check=False) + + def _parse_install_size_and_packages(self, line): + # parse lines from 'apt' command. + # on error, assume there's something to install + line_arr = line.split() + if "disk space" in line: + try: + # Sometimes partial downloads are required when a network error prevents from completing a previous upgrade. + # In this case, we need to get the first number before the '/' + # eg: 'Need to get 11.4 kB/1,861 kB of archives.' + self.required_space_str = ( + f"{str_to_float(line_arr[3])} {line_arr[4].split('/')[0]}" + ) + self._required_space = size_str_to_bytes(self.required_space_str) + except Exception: + self._required_space = 0 + self.required_space_str = f"{self._required_space} M" + elif "Need to get" in line: + try: + self.download_size_str = ( + f"{str_to_float(line_arr[3])} {line_arr[4].split('/')[0]}" + ) + self._download_size = size_str_to_bytes(self.download_size_str) + except Exception: + self._download_size = 0 + self.download_size_str = f"{self._download_size} M" + elif "newly installed" in line: + try: + self._install_count = int(line_arr[0]) + int(line_arr[2]) + except Exception: + self._install_count = 0 + + def _do_stage_upgrade(self, packages): + self._download_size = 0 + self.download_size_str = "" + self._required_space = 0 + self.required_space_str = "" + self._install_count = 0 + self.packages = packages + + self._do_get_install_size() + + logger.info( + f"OsUpdaterBackend: Will upgrade/install {self._install_count} packages" + ) + logger.info(f"OsUpdaterBackend: Need to download {self.download_size_str}") + logger.info( + f"OsUpdaterBackend: After this operation, {self.required_space_str} of additional disk space will be used." + ) diff --git a/pt_os_web_portal/os_updater/legacy.py b/pt_os_web_portal/os_updater/legacy.py deleted file mode 100644 index 4fb14c41..00000000 --- a/pt_os_web_portal/os_updater/legacy.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging -from inspect import signature -from subprocess import PIPE, CalledProcessError, Popen - -from .types import MessageType - -logger = logging.getLogger(__name__) - - -class LegacyOSUpdateManager: - def __init__(self) -> None: - self.lock = False - self._download_size = 0 - self.download_size_str = "" - self._required_space = 0 - self.required_space_str = "" - self.install_count = 0 - - def __run(self, cmd, callback=None, check=True): - with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p: - for line in p.stdout: - line = line.strip() - if callable(callback): - callback_signature = signature(callback) - if len(callback_signature.parameters) == 1: - callback(line) - elif len(callback_signature.parameters) == 3: - callback( # lgtm [py/call/wrong-arguments] - MessageType.STATUS, line, 0.0 - ) - logger.info(line) - - if check and p.returncode != 0: - raise CalledProcessError(p.returncode, p.args) - - def update(self, callback) -> None: - logger.info("LegacyOSUpdaterManager: Updating APT sources") - if self.lock: - callback(MessageType.ERROR, "LegacyOSUpdaterManager is locked", 0.0) - return - self.lock = True - - try: - self.__run(["apt-get", "update"], callback) - except Exception as e: - logger.error(f"LegacyOSUpdaterManager Error: {e}") - raise - finally: - self.lock = False - - def stage_upgrade(self, callback, packages=[]) -> None: - logger.info("LegacyOSUpdaterManager: Staging packages for upgrade") - if self.lock: - callback(MessageType.ERROR, "LegacyOSUpdaterManager is locked", 0.0) - return - self.lock = True - - self._download_size = 0 - self.download_size_str = "" - self._required_space = 0 - self.required_space_str = "" - self.install_count = 0 - self.packages = packages - - try: - cmd = ["apt-get", "dist-upgrade", "--assume-no"] - if len(packages) > 0: - cmd = ["apt-get", "install", *packages, "--assume-no"] - - def str_to_float(text): - dot_pos = text.rfind(".") - comma_pos = text.rfind(",") - if comma_pos > dot_pos: - text = text.replace(".", "") - text = text.replace(",", ".") - else: - text = text.replace(",", "") - return float(text) - - def get_update_info(line): - # parse lines from 'apt' command. - # on error, assume there's something to install - line_arr = line.split() - if "disk space" in line: - try: - self._required_space = str_to_float(line_arr[3]) - self.required_space_str = ( - f"{self._required_space} {line_arr[4].split('/')[0]}" - ) - except Exception: - self._required_space = 1 - self.required_space_str = f"{self._required_space} M" - elif "Need to get" in line: - try: - self._download_size = str_to_float(line_arr[3]) - self.download_size_str = ( - f"{self._download_size} {line_arr[4].split('/')[0]}" - ) - except Exception: - self._download_size = 1 - self.download_size_str = f"{self._download_size} M" - elif "newly installed" in line: - try: - self.install_count = int(line_arr[0]) + int(line_arr[2]) - except Exception: - self.install_count = 1 - - self.__run(cmd, get_update_info, check=False) - logger.info( - f"LegacyOSUpdateManager: Will upgrade/install {self.install_count} packages" - ) - logger.info( - f"LegacyOSUpdateManager: Need to download {self.download_size_str}" - ) - logger.info( - f"LegacyOSUpdateManager: After this operation, {self.required_space_str} of additional disk space will be used." - ) - except Exception as e: - logger.error(f"{e}") - raise e - finally: - self.lock = False - - def download_size(self): - return self._download_size - - def required_space(self): - return self._required_space - - def upgrade(self, callback): - logger.info("LegacyOSUpdaterManager: starting upgrade") - if self.lock: - callback(MessageType.ERROR, "LegacyOSUpdaterManager is locked", 0.0) - return - self.lock = True - - cmd = [ - "apt-get", - '-o Dpkg::Options::="--force-confdef"', - '-o Dpkg::Options::="--force-confold"', - "-o APT::Get::Upgrade-Allow-New=true", - "dist-upgrade", - "--quiet", - "--yes", - ] - if len(self.packages) > 0: - cmd = [ - "apt-get", - '-o Dpkg::Options::="--force-confdef"', - '-o Dpkg::Options::="--force-confold"', - "-o APT::Get::Upgrade-Allow-New=true", - "install", - *self.packages, - "--quiet", - "--yes", - ] - try: - callback(MessageType.START, "Starting install & upgrade process", 0.0) - self.__run(cmd, callback) - callback(MessageType.FINISH, "Finished upgrade", 100.0) - except Exception as e: - raise e - finally: - self.lock = False - - logger.info("LegacyOSUpdaterManager: finished upgrade") diff --git a/pt_os_web_portal/os_updater/manager.py b/pt_os_web_portal/os_updater/manager.py deleted file mode 100644 index 691c5984..00000000 --- a/pt_os_web_portal/os_updater/manager.py +++ /dev/null @@ -1,195 +0,0 @@ -import logging -from subprocess import run -from typing import Dict, List - -from ..backend.helpers.modules import get_apt -from .progress import FetchProgress, InstallProgress -from .types import MessageType - -logger = logging.getLogger(__name__) - -(apt, apt.progress, apt_pkg) = get_apt() - - -class APTUpgradeException(Exception): - def __init__(self, packages_arr: List): - formatted_packages = "\\n - ".join(packages_arr) - super().__init__( - f"Errors were encountered while processing:\\n - {formatted_packages}" - ) - - -class OSUpdateManager: - def __init__(self) -> None: - # Load apt system configuration - apt_pkg.init_config() - - self.cache = apt.Cache() - self.lock = False - - @property - def install_count(self): - return self.cache.install_count - - def update(self, callback) -> None: - logger.info("OsUpdateManager: Updating APT sources") - if self.lock: - callback(MessageType.ERROR, "OsUpdateManager is locked", 0.0) - return - self.lock = True - fetch_sources_progress = FetchProgress(callback) - - try: - self.cache.update(fetch_sources_progress) - self.cache.open(None) - except Exception as e: - logger.error(f"OsUpdateManager Error: {e}") - raise - finally: - self.lock = False - - def get_upgrade_dependencies( - self, package: apt.Package, dependency_dict: Dict # type: ignore - ) -> Dict: - """ - Returns a dictionary with the dependencies and the versions required to - upgrade the given package. - - This is not a straightforward task since multiple entries of a - dependency might appear in the dependency array if the package requires - a specific version or range of versions of a dependency. - - e.g.: if package A depends on package B (>1.0, <1.5), the dependency - array of A will be [B(>1.0), B(<1.5)] - """ - - logger.debug("Generating list of pi-top packages...") - - pi_top_packages = str( - run( - ["aptitude", "search", "?origin (pi-top)", "-F", "\\%p"], - capture_output=True, - ).stdout, - "utf8", - ) - - for package_dependencies in package.candidate.dependencies: - for dependency in package_dependencies: - if len(dependency.target_versions) == 0: - continue - - if dependency.name not in dependency_dict: - dependency_dict[dependency.name] = set(dependency.target_versions) - - # Only recurse pi-top package dependencies to ensure that the latest are included - if dependency.name not in pi_top_packages: - continue - - if dependency.name not in self.cache: - continue - - self.get_upgrade_dependencies( - self.cache[dependency.name], dependency_dict - ) - else: - # store only the versions that comply with previous and new constraints - dependency_dict[dependency.name] = set( - dependency.target_versions - ) & set(dependency_dict[dependency.name]) - return dependency_dict - - def stage_package(self, package_name: str) -> None: - package = self.cache.get(package_name) - if package is None: - logger.info(f"OS Updater: invalid package '{package_name}' - skipping") - return - if not package.is_upgradable: - logger.info( - f"OS Updater: package '{package_name}' has no updates - skipping" - ) - return - logger.info(f"OS Updater: staging package '{package_name}' to be updated") - - package.mark_upgrade() - dependency_dict = self.get_upgrade_dependencies(package, {}) - for pkg_name, versions in dependency_dict.items(): - if len(versions) == 0: - # There are no versions of the package available - # TODO: This means it will fail to install? - continue - - pkg = self.cache.get(pkg_name) - if pkg: - pkg.candidate = sorted([*versions], reverse=True)[0] - if pkg.is_upgradable: - logger.info( - f"OS Updater: staging upgrade for package '{pkg}' to version '{pkg.candidate.version}'" - ) - pkg.mark_upgrade() - - def stage_upgrade(self, callback, packages=[]) -> None: - logger.info("OsUpdateManager: Staging packages for upgrade") - if self.lock: - callback(MessageType.ERROR, "OsUpdateManager is locked", 0.0) - return - self.lock = True - - try: - if len(packages) == 0: - logger.info("OsUpdateManager: Staging all packages to be upgraded") - self.cache.upgrade() - self.cache.upgrade(True) - else: - for package_name in packages: - self.stage_package(package_name) - - logger.info( - f"OsUpdateManager: Will upgrade/install {self.cache.install_count} packages" - ) - logger.info( - f"OsUpdateManager: Need to download {apt_pkg.size_to_str(self.cache.required_download)}" - ) - logger.info( - f"OsUpdateManager: After this operation, {apt_pkg.size_to_str(self.cache.required_space)} of additional disk space will be used." - ) - except Exception as e: - logger.error(f"OsUpdateManager Error: {e}") - raise - finally: - self.lock = False - - def download_size(self): - size = self.cache.required_download if self.cache else 0 - logger.info( - f"OsUpdateManager download_size: Need to download {apt_pkg.size_to_str(size)} - ({size} B)" - ) - return size - - def required_space(self): - size = self.cache.required_space if self.cache else 0 - logger.info( - f"OsUpdateManager required_space: {apt_pkg.size_to_str(size)} - ({size} B) needed for upgrade" - ) - return size - - def upgrade(self, callback): - logger.info("OsUpdateManager: starting upgrade") - if self.lock: - callback(MessageType.ERROR, "OsUpdateManager is locked", 0.0) - return - self.lock = True - - fetch_packages_progress = FetchProgress(callback) - install_progress = InstallProgress(callback) - try: - callback(MessageType.START, "Starting install & upgrade process", 0.0) - self.cache.commit(fetch_packages_progress, install_progress) - callback(MessageType.FINISH, "Finished upgrade", 100.0) - except Exception as e: - if len(install_progress.packages_with_errors) > 0: - raise APTUpgradeException(install_progress.packages_with_errors) - raise e - finally: - self.lock = False - - logger.info("OsUpdateManager: finished upgrade") diff --git a/pt_os_web_portal/os_updater/message_handler.py b/pt_os_web_portal/os_updater/message_handler.py index 6519bc27..28f97c2b 100644 --- a/pt_os_web_portal/os_updater/message_handler.py +++ b/pt_os_web_portal/os_updater/message_handler.py @@ -5,13 +5,10 @@ from geventwebsocket.exceptions import WebSocketError from geventwebsocket.websocket import WebSocket -from ..backend.helpers.modules import get_apt from .types import EventNames, MessageType logger = logging.getLogger(__name__) -(apt, apt.progress, apt_pkg) = get_apt() - class OSUpdaterFrontendMessageHandler: ws_clients: List[WebSocket] = [] diff --git a/pt_os_web_portal/os_updater/progress.py b/pt_os_web_portal/os_updater/progress.py deleted file mode 100644 index b56ed4ba..00000000 --- a/pt_os_web_portal/os_updater/progress.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging - -from ..backend.helpers.modules import get_apt -from .types import MessageType - -logger = logging.getLogger(__name__) - -(apt, apt.progress, apt_pkg) = get_apt() - - -class FetchProgress(apt.progress.base.AcquireProgress): # type: ignore - def __init__(self, callback): - apt.progress.base.AcquireProgress.__init__(self) - self._callback = callback - - @property - def callback(self): - if callable(self._callback): - return self._callback - - def pulse(self, owner): - current_item = self.current_items + 1 - if current_item > self.total_items: - current_item = self.total_items - - text = f"Downloading file {current_item} of {self.total_items}" - if self.current_cps > 0: - text = text + f" at {apt_pkg.size_to_str(self.current_cps)}/s" - - progress = ( - (self.current_bytes + self.current_items) - / float(self.total_bytes + self.total_items) - ) * 100.0 - self.callback(MessageType.STATUS, text, round(progress, 1)) - return apt.progress.base.AcquireProgress.pulse(self, owner) - - -class InstallProgress(apt.progress.base.InstallProgress): # type: ignore - def __init__(self, callback): - apt.progress.base.InstallProgress.__init__(self) - self.callback = callback - self.packages_with_errors = list() - - def status_change(self, pkg, percent, status): - logger.debug(f"Progress: {percent}% - {pkg}: {status}") - self.callback(MessageType.STATUS, f"{pkg}: {status}", percent) - - def update_interface(self): - apt.progress.base.InstallProgress.update_interface(self) - - def error(self, pkg, errormsg): - logger.error(f"InstallProgress {pkg}: {errormsg}") - self.packages_with_errors.append(pkg) - # sent as MessageType.STATUS instead of MessageType.ERROR to avoid confusions, - # since several other messages are sent after this one - self.callback(MessageType.STATUS, f"ERROR - {pkg}: {errormsg}", 0) - super().error(pkg, errormsg) diff --git a/pt_os_web_portal/os_updater/updater.py b/pt_os_web_portal/os_updater/updater.py index 3caf3dbe..a768895a 100644 --- a/pt_os_web_portal/os_updater/updater.py +++ b/pt_os_web_portal/os_updater/updater.py @@ -1,10 +1,8 @@ import logging -from enum import Enum, auto from time import sleep from ..event import AppEvents, post_event -from .legacy import LegacyOSUpdateManager -from .manager import OSUpdateManager +from .backend import OsUpdaterBackend from .message_handler import OSUpdaterFrontendMessageHandler from .system_clock import is_system_clock_synchronized, synchronize_system_clock from .types import MessageType @@ -12,27 +10,17 @@ logger = logging.getLogger(__name__) -class UpdaterBackend(Enum): - PY_APT = auto() - LEGACY = auto() - - class OSUpdater: def __init__(self): - self.backends = { - UpdaterBackend.PY_APT: OSUpdateManager(), - UpdaterBackend.LEGACY: LegacyOSUpdateManager(), - } - self.active_backend = self.backends[UpdaterBackend.PY_APT] + self.backend = OsUpdaterBackend() self.message_handler = OSUpdaterFrontendMessageHandler() def start(self): pass def stop(self): - while self.active_backend.lock: - # TODO: lower to debug - logger.info("Waiting: OS updater backend lock") + while self.backend.lock: + logger.debug("Waiting: OS updater backend lock") sleep(0.2) logger.info("Stopped: OS updater") @@ -40,7 +28,7 @@ def stop(self): def updates_available(self): self.update_sources() self.stage_packages() - return self.active_backend.install_count > 0 + return self.backend.install_count > 0 def update_sources(self, ws=None): if not is_system_clock_synchronized(): @@ -48,27 +36,36 @@ def update_sources(self, ws=None): post_event(AppEvents.OS_UPDATE_SOURCES, "started") callback = self.message_handler.create_emit_update_sources_message(ws) + + def on_state_update(status_message): + return callback( + message_type=MessageType.STATUS, + percent=0.0, + status_message=status_message, + ) + try: callback(MessageType.START, "Updating sources", 0.0) - self.active_backend.update(callback) + self.backend.update(on_state_update) callback(MessageType.FINISH, "Finished updating sources", 100.0) post_event(AppEvents.OS_UPDATE_SOURCES, "success") except Exception as e: + logger.error(f"OSUpdater.update_sources: {e}") post_event(AppEvents.OS_UPDATE_SOURCES, "failed") - callback(MessageType.ERROR, f"{e}", 0.0) + callback(message_type=MessageType.ERROR, percent=0.0, status_message=f"{e}") def stage_packages(self, ws=None, packages=[]): post_event(AppEvents.OS_UPDATER_PREPARE, "started") callback = self.message_handler.create_emit_os_prepare_upgrade_message(ws) try: callback(MessageType.START, "Preparing OS upgrade", 0.0) - self.active_backend.stage_upgrade(callback, packages) - + self.backend.stage_upgrade(packages) callback(MessageType.FINISH, "Finished preparing", 100.0) post_event(AppEvents.OS_UPDATER_PREPARE, "success") except Exception as e: + logger.error(f"OSUpdater.stage_packages: {e}") post_event(AppEvents.OS_UPDATER_PREPARE, "failed") - callback(MessageType.ERROR, f"{e}", 0.0) + callback(message_type=MessageType.ERROR, percent=0.0, status_message=f"{e}") def stage_web_portal(self, ws=None): self.stage_packages(ws, packages=["pt-os-web-portal"]) @@ -77,39 +74,44 @@ def upgrade_size(self, ws=None): callback = self.message_handler.create_emit_os_size_message(ws) try: callback( - MessageType.STATUS, - { - "downloadSize": self.active_backend.download_size(), - "requiredSpace": self.active_backend.required_space(), + message_type=MessageType.STATUS, + size={ + "downloadSize": self.backend.download_size(), + "requiredSpace": self.backend.required_space(), }, ) except Exception as e: - logger.error(f"OSUpdater upgrade_size: {e}") - callback(MessageType.ERROR, {"downloadSize": 0, "requiredSpace": 0}) + logger.error(f"OSUpdater.upgrade_size: {e}") + callback( + message_type=MessageType.ERROR, + size={"downloadSize": 0, "requiredSpace": 0}, + ) def start_os_upgrade(self, ws=None): post_event(AppEvents.OS_UPDATER_UPGRADE, "started") - callback = self.message_handler.create_emit_os_upgrade_message(ws) + + def on_state_update(status_message): + return callback( + message_type=MessageType.STATUS, + percent=0.0, + status_message=status_message, + ) + try: - self.active_backend.upgrade(callback) + callback(MessageType.START, "Starting install & upgrade process", 0.0) + self.backend.upgrade(on_state_update) + callback(MessageType.FINISH, "Finished upgrade", 100.0) post_event(AppEvents.OS_UPDATER_UPGRADE, "success") except Exception as e: - callback(MessageType.ERROR, f"{e}", 0.0) + logger.error(f"OSUpdater.start_os_upgrade: {e}") + callback(message_type=MessageType.ERROR, percent=0.0, status_message=f"{e}") post_event(AppEvents.OS_UPDATER_UPGRADE, "failed") - def use_legacy_backend(self, ws=None): - logger.info("OSUpdater: Using legacy backend...") - self.active_backend = self.backends[UpdaterBackend.LEGACY] - - def use_default_backend(self, ws=None): - logger.info("OSUpdater: Using default backend...") - self.active_backend = self.backends[UpdaterBackend.PY_APT] - def state(self, ws=None): callback = self.message_handler.create_emit_state_message(ws) try: - callback(MessageType.STATUS, self.active_backend.lock) + callback(MessageType.STATUS, self.backend.lock) except Exception as e: - logger.error(f"OSUpdater state: {e}") + logger.error(f"OSUpdater.state: {e}") callback(MessageType.ERROR, False) diff --git a/tests/data/apt_stdout.py b/tests/data/apt_stdout.py new file mode 100644 index 00000000..1e1fa1d2 --- /dev/null +++ b/tests/data/apt_stdout.py @@ -0,0 +1,143 @@ +apt_update_output = """Reading package lists... Done +Building dependency tree... Done +Reading state information... Done +Calculating upgrade... Done +The following packages will be upgraded: + not-the-package-youre-looking-for +1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. +Need to get 2.155 GB of archives. +After this operation, 99,3 MB of additional disk space will be used. +Do you want to continue? [Y/n] N +Abort. +""" # noqa: E501 + +apt_upgrade_output = """ +Reading package lists... Done +Building dependency tree... Done +Reading state information... Done +Calculating upgrade... Done +The following NEW packages will be installed: + raspi-utils-core raspi-utils-dt raspi-utils-eeprom raspi-utils-otp raspinfo +The following packages will be upgraded: + bluez bubblewrap ffmpeg libavcodec59 libavdevice59 libavfilter8 libavformat59 libavutil57 libbluetooth3 libpostproc56 libswresample4 libswscale6 raspi-utils rpi-eeprom +14 upgraded, 5 newly installed, 0 to remove and 0 not upgraded. +Need to get 64.5 MB of archives. +After this operation, 6,995 kB of additional disk space will be used. +Do you want to continue? [Y/n] N +Abort. +pi@pi-top:~ $ sudo apt dist-upgrade --assume-no^C +pi@pi-top:~ $ sudo apt dist-upgrade --assume-no^C +pi@pi-top:~ $ sudo apt dist-upgrade -y +Reading package lists... Done +Building dependency tree... Done +Reading state information... Done +Calculating upgrade... Done +The following NEW packages will be installed: + raspi-utils-core raspi-utils-dt raspi-utils-eeprom raspi-utils-otp raspinfo +The following packages will be upgraded: + bluez bubblewrap ffmpeg libavcodec59 libavdevice59 libavfilter8 libavformat59 libavutil57 libbluetooth3 libpostproc56 libswresample4 libswscale6 raspi-utils rpi-eeprom +14 upgraded, 5 newly installed, 0 to remove and 0 not upgraded. +Need to get 64.5 MB of archives. +After this operation, 6,995 kB of additional disk space will be used. +Get:1 http://archive.raspberrypi.com/debian bookworm/main armhf bluez armhf 5.66-1+rpt1+deb12u2 [1,346 kB] +Get:2 http://raspbian.raspberrypi.com/raspbian bookworm/main armhf bubblewrap armhf 0.8.0-2+deb12u1 [42.6 kB] +Get:3 http://archive.raspberrypi.com/debian bookworm/main armhf libswscale6 armhf 8:5.1.6-0+deb12u1+rpt1 [386 kB] +Get:4 http://archive.raspberrypi.com/debian bookworm/main armhf libavfilter8 armhf 8:5.1.6-0+deb12u1+rpt1 [8,721 kB] +Get:5 http://archive.raspberrypi.com/debian bookworm/main armhf libavdevice59 armhf 8:5.1.6-0+deb12u1+rpt1 [214 kB] +Get:6 http://archive.raspberrypi.com/debian bookworm/main armhf libavformat59 armhf 8:5.1.6-0+deb12u1+rpt1 [2,627 kB] +Get:7 http://archive.raspberrypi.com/debian bookworm/main armhf libavcodec59 armhf 8:5.1.6-0+deb12u1+rpt1 [12.6 MB] +Get:8 http://archive.raspberrypi.com/debian bookworm/main armhf libavutil57 armhf 8:5.1.6-0+deb12u1+rpt1 [790 kB] +Get:9 http://archive.raspberrypi.com/debian bookworm/main armhf libpostproc56 armhf 8:5.1.6-0+deb12u1+rpt1 [102 kB] +Get:10 http://archive.raspberrypi.com/debian bookworm/main armhf libswresample4 armhf 8:5.1.6-0+deb12u1+rpt1 [151 kB] +Get:11 http://archive.raspberrypi.com/debian bookworm/main armhf ffmpeg armhf 8:5.1.6-0+deb12u1+rpt1 [1,982 kB] +Get:12 http://archive.raspberrypi.com/debian bookworm/main armhf libbluetooth3 armhf 5.66-1+rpt1+deb12u2 [120 kB] +Get:13 http://archive.raspberrypi.com/debian bookworm/main armhf raspi-utils all 20240402-4 [4,390 B] +Get:14 http://archive.raspberrypi.com/debian bookworm/main armhf raspi-utils-core armhf 20240402-4 [45.9 kB] +Get:15 http://archive.raspberrypi.com/debian bookworm/main armhf raspi-utils-otp all 20240402-4 [6,718 B] +Get:16 http://archive.raspberrypi.com/debian bookworm/main armhf raspi-utils-dt armhf 20240402-4 [67.7 kB] +Get:17 http://archive.raspberrypi.com/debian bookworm/main armhf raspi-utils-eeprom armhf 20240402-4 [17.6 kB] +Get:18 http://archive.raspberrypi.com/debian bookworm/main armhf rpi-eeprom all 24.0-1 [35.4 MB] +Get:19 http://archive.raspberrypi.com/debian bookworm/main armhf raspinfo all 20240402-4 [6,416 B] +Fetched 64.5 MB in 35s (1,860 kB/s) +apt-listchanges: Reading changelogs... +(Reading database ... 320693 files and directories currently installed.) +Preparing to unpack .../00-bluez_5.66-1+rpt1+deb12u2_armhf.deb ... +Unpacking bluez (5.66-1+rpt1+deb12u2) over (5.66-1+rpt1+deb12u1) ... +Preparing to unpack .../01-bubblewrap_0.8.0-2+deb12u1_armhf.deb ... +Unpacking bubblewrap (0.8.0-2+deb12u1) over (0.8.0-2) ... +Preparing to unpack .../02-libswscale6_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libswscale6:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../03-libavfilter8_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libavfilter8:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../04-libavdevice59_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libavdevice59:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../05-libavformat59_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libavformat59:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../06-libavcodec59_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libavcodec59:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../07-libavutil57_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libavutil57:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../08-libpostproc56_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libpostproc56:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../09-libswresample4_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking libswresample4:armhf (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../10-ffmpeg_8%3a5.1.6-0+deb12u1+rpt1_armhf.deb ... +Unpacking ffmpeg (8:5.1.6-0+deb12u1+rpt1) over (8:5.1.5-0+rpt1+deb12u1) ... +Preparing to unpack .../11-libbluetooth3_5.66-1+rpt1+deb12u2_armhf.deb ... +Unpacking libbluetooth3:armhf (5.66-1+rpt1+deb12u2) over (5.66-1+rpt1+deb12u1) ... +Preparing to unpack .../12-raspi-utils_20240402-4_all.deb ... +Unpacking raspi-utils (20240402-4) over (20240402-3) ... +Selecting previously unselected package raspi-utils-core. +Preparing to unpack .../13-raspi-utils-core_20240402-4_armhf.deb ... +Unpacking raspi-utils-core (20240402-4) ... +Selecting previously unselected package raspi-utils-otp. +Preparing to unpack .../14-raspi-utils-otp_20240402-4_all.deb ... +Unpacking raspi-utils-otp (20240402-4) ... +Selecting previously unselected package raspi-utils-dt. +Preparing to unpack .../15-raspi-utils-dt_20240402-4_armhf.deb ... +Unpacking raspi-utils-dt (20240402-4) ... +Selecting previously unselected package raspi-utils-eeprom. +Preparing to unpack .../16-raspi-utils-eeprom_20240402-4_armhf.deb ... +Unpacking raspi-utils-eeprom (20240402-4) ... +Preparing to unpack .../17-rpi-eeprom_24.0-1_all.deb ... +Unpacking rpi-eeprom (24.0-1) over (23.2-1) ... +Selecting previously unselected package raspinfo. +Preparing to unpack .../18-raspinfo_20240402-4_all.deb ... +Unpacking raspinfo (20240402-4) ... +Setting up bubblewrap (0.8.0-2+deb12u1) ... +Setting up raspi-utils-eeprom (20240402-4) ... +Setting up raspi-utils-otp (20240402-4) ... +Setting up raspi-utils-dt (20240402-4) ... +Setting up libavutil57:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up bluez (5.66-1+rpt1+deb12u2) ... +Setting up libswresample4:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up libbluetooth3:armhf (5.66-1+rpt1+deb12u2) ... +Setting up libpostproc56:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up libavcodec59:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up libswscale6:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up raspi-utils-core (20240402-4) ... +Setting up libavformat59:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up libavfilter8:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up libavdevice59:armhf (8:5.1.6-0+deb12u1+rpt1) ... +Setting up ffmpeg (8:5.1.6-0+deb12u1+rpt1) ... +Setting up raspinfo (20240402-4) ... +Setting up raspi-utils (20240402-4) ... +Setting up rpi-eeprom (24.0-1) ... +Processing triggers for libc-bin (2.36-9+rpt2+deb12u7) ... +Processing triggers for man-db (2.11.2-2) ... +Processing triggers for dbus (1.14.10-1~deb12u1) ... +""" # noqa: E501 + + +apt_upgrade_no_output = """ +Building dependency tree... Done +Reading state information... Done +Calculating upgrade... Done +The following NEW packages will be installed: + raspi-utils-core raspi-utils-dt raspi-utils-eeprom raspi-utils-otp raspinfo +The following packages will be upgraded: + bluez bubblewrap ffmpeg libavcodec59 libavdevice59 libavfilter8 libavformat59 libavutil57 libbluetooth3 libpostproc56 libswresample4 libswscale6 raspi-utils rpi-eeprom +14 upgraded, 5 newly installed, 0 to remove and 0 not upgraded. +Need to get 64.5 MB/723 GB of archives. +After this operation, 6,995 kB of additional disk space will be used. +Abort""" # noqa: E501 diff --git a/tests/test_os_updater_class.py b/tests/test_os_updater_class.py index e28b79e5..46ca1858 100644 --- a/tests/test_os_updater_class.py +++ b/tests/test_os_updater_class.py @@ -1,5 +1,23 @@ from json import loads as jloads from threading import Thread +from unittest.mock import MagicMock + +from .data.apt_stdout import apt_update_output, apt_upgrade_output + + +def mock_apt_output(mocker, stdout, returncode): + context_mock = MagicMock() + context_mock.returncode = returncode + context_mock.stdout = stdout.split("\n") + + mock_popen = MagicMock() + mock_popen.return_value.__enter__.return_value = context_mock + mock_popen.return_value.__exit__.return_value = None + + mocker.patch( + "pt_os_web_portal.os_updater.backend.Popen", + mock_popen, + ) class WsMock: @@ -15,7 +33,7 @@ def test_lock_default_value(patch_modules): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - assert os_updater.active_backend.lock is False + assert os_updater.backend.lock is False def test_send_error_message_when_locked(patch_modules, mocker): @@ -27,7 +45,6 @@ def test_send_error_message_when_locked(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0.1 ws_mock = WsMock() # Register WS client with app os_updater.state(ws_mock) @@ -50,7 +67,7 @@ def test_send_error_message_when_locked(patch_modules, mocker): assert error_message["payload"]["status"] == "ERROR" assert error_message["payload"]["percent"] == 0.0 - assert error_message["payload"]["message"] == "OsUpdateManager is locked" + assert error_message["payload"]["message"] == "OsUpdaterBackend is locked" # wait until unlocked os_updater.stop() @@ -85,7 +102,6 @@ def test_send_status_messages_on_update(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0 ws_mock = WsMock() os_updater.state(ws_mock) @@ -101,6 +117,7 @@ def test_send_status_messages_on_update(patch_modules, mocker): def test_send_start_finish_messages_on_update_sources(patch_modules, mocker): + mock_apt_output(mocker, stdout=apt_update_output, returncode=0) mocker.patch( "pt_os_web_portal.os_updater.updater.is_system_clock_synchronized", return_value=True, @@ -109,8 +126,6 @@ def test_send_start_finish_messages_on_update_sources(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0 - ws_mock = WsMock() os_updater.state(ws_mock) ws_mock.messages.clear() @@ -131,6 +146,7 @@ def test_send_start_finish_messages_on_update_sources(patch_modules, mocker): def test_send_start_finish_messages_on_upgrade(patch_modules, mocker): + mock_apt_output(mocker, stdout=apt_upgrade_output, returncode=0) mocker.patch( "pt_os_web_portal.os_updater.updater.is_system_clock_synchronized", return_value=True, @@ -139,7 +155,6 @@ def test_send_start_finish_messages_on_upgrade(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0 ws_mock = WsMock() os_updater.state(ws_mock) @@ -168,7 +183,6 @@ def test_send_start_finish_messages_on_stage_packages(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0 ws_mock = WsMock() os_updater.state(ws_mock) @@ -187,6 +201,8 @@ def test_send_start_finish_messages_on_stage_packages(patch_modules, mocker): def test_download_size_format(patch_modules, mocker): + mock_apt_output(mocker, stdout=apt_update_output, returncode=0) + mocker.patch( "pt_os_web_portal.os_updater.updater.is_system_clock_synchronized", return_value=True, @@ -194,22 +210,27 @@ def test_download_size_format(patch_modules, mocker): from pt_os_web_portal.os_updater import OSUpdater os_updater = OSUpdater() - os_updater.active_backend.cache.sleep_time = 0 ws_mock = WsMock() os_updater.state(ws_mock) - ws_mock.messages.clear() + # After instantiation, we don't know if there's an upgrade + os_updater.upgrade_size(ws_mock) + assert ws_mock.messages[-1].get("type") == "SIZE" + assert ws_mock.messages[-1].get("payload", {}).get("status") == "STATUS" + assert ws_mock.messages[-1].get("payload", {}).get("size").get("downloadSize") == 0 + assert ws_mock.messages[-1].get("payload", {}).get("size").get("requiredSpace") == 0 + + # After updating sources and staging packages, we know the update size + os_updater.update_sources(ws_mock) + os_updater.stage_packages(ws_mock) os_updater.upgrade_size(ws_mock) - assert len(ws_mock.messages) == 1 - assert ws_mock.messages[0].get("type") == "SIZE" - assert ws_mock.messages[0].get("payload", {}).get("status") == "STATUS" assert ( - ws_mock.messages[0].get("payload", {}).get("size").get("downloadSize") + ws_mock.messages[-1].get("payload", {}).get("size").get("downloadSize") == 2155000000 ) assert ( - ws_mock.messages[0].get("payload", {}).get("size").get("requiredSpace") + ws_mock.messages[-1].get("payload", {}).get("size").get("requiredSpace") == 99300000 )