From 42278abc7a181f2ee22c9351cb88812248e5fd4a Mon Sep 17 00:00:00 2001 From: monsieur_h Date: Tue, 26 Dec 2017 12:52:13 +0100 Subject: [PATCH 1/5] fixes a bug where importing ZeroApp could lead to considering the abstract class ZeroApp as the main class for the current module --- apps/app_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/app_manager.py b/apps/app_manager.py index 66eb5573..baeff2ff 100755 --- a/apps/app_manager.py +++ b/apps/app_manager.py @@ -3,9 +3,9 @@ import traceback from apps import zero_app +from helpers import setup_logger from ui import Printer, Menu -from helpers import setup_logger logger = setup_logger(__name__, "info") @@ -200,10 +200,10 @@ def get_zeroapp_class_in_module(module_): for item in module_content: class_ = getattr(module_, item) try: - if issubclass(class_, zero_app.ZeroApp): + if issubclass(class_, zero_app.ZeroApp) and item != 'ZeroApp': return class_ except Exception as e: - pass # todo : check why isinstance(class_, ClassType)==False in python2 + pass # not a class : ignore return None From 976163d60a571d6bd51de3ae44ebfee9d3b1e917 Mon Sep 17 00:00:00 2001 From: monsieur_h Date: Tue, 26 Dec 2017 15:16:34 +0100 Subject: [PATCH 2/5] moves ZPUI_HOME to helpers adds ZPUI_INSTALL_DIR to helpers --- apps/personal/contacts/address_book.py | 3 ++- helpers/general.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/personal/contacts/address_book.py b/apps/personal/contacts/address_book.py index d2ff146c..fac04dff 100644 --- a/apps/personal/contacts/address_book.py +++ b/apps/personal/contacts/address_book.py @@ -3,6 +3,8 @@ from helpers import Singleton, flatten from helpers import setup_logger +from helpers.general import ZPUI_HOME + logger = setup_logger(__name__, "warning") class AddressBook(Singleton): @@ -252,4 +254,3 @@ def _is_contained_in_other_element_of_the_list(p_element, the_list): SAVE_FILENAME = "contacts.pickle" -ZPUI_HOME = "~/.phone/" diff --git a/helpers/general.py b/helpers/general.py index 665d50cf..968f5e85 100644 --- a/helpers/general.py +++ b/helpers/general.py @@ -1,6 +1,8 @@ import os import sys +ZPUI_INSTALL_DIR = "/opt/zpui/" +ZPUI_HOME = "~/.phone/" def local_path_gen(_name_): """This function generates a ``local_path`` function you can use From bd77eb583a46969bf2e14f4f1dedb090f7347d32 Mon Sep 17 00:00:00 2001 From: monsieur_h Date: Tue, 26 Dec 2017 15:17:14 +0100 Subject: [PATCH 3/5] fixes import error --- output/drivers/pygame_emulator_factory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/output/drivers/pygame_emulator_factory.py b/output/drivers/pygame_emulator_factory.py index 79da99da..94ce0912 100644 --- a/output/drivers/pygame_emulator_factory.py +++ b/output/drivers/pygame_emulator_factory.py @@ -6,12 +6,11 @@ """ import luma.emulator.device + from helpers import setup_logger # ignore PIL debug messages -logging.getLogger("PIL").setLevel(logging.ERROR) - logger = setup_logger(__name__) def get_pygame_emulator_device(width=128, height=64): From 14247a3d3fa712bed51a7f93d1691bd65e2acda0 Mon Sep 17 00:00:00 2001 From: monsieur_h Date: Tue, 26 Dec 2017 15:17:53 +0100 Subject: [PATCH 4/5] adds optional message to `GraphicalProgressBar` --- ui/loading_indicators.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/loading_indicators.py b/ui/loading_indicators.py index 30fecc24..ba0c78af 100644 --- a/ui/loading_indicators.py +++ b/ui/loading_indicators.py @@ -250,6 +250,7 @@ class GraphicalProgressBar(ProgressIndicator, CenteredTextRenderer): Allows to adjust padding and margin for a little bit of customization, as well as to show or hide the progress percentage.""" def __init__(self, i, o, *args, **kwargs): + self.message = kwargs.pop("message") if "message" in kwargs else None self.show_percentage = kwargs.pop("show_percentage") if "show_percentage" in kwargs else True self.margin = kwargs.pop("margin") if "margin" in kwargs else 15 self.padding = kwargs.pop("padding") if "padding" in kwargs else 2 @@ -269,8 +270,17 @@ def refresh(self): self.draw_bar(draw, bar_top) + if self.message: + self.draw_message(draw) + self.o.display_image(c.image) + def draw_message(self, draw): + # type: (ImageDraw) -> None + bounds = self.get_centered_text_bounds(draw, self.message, self.o.device.size) + + draw.text((bounds.left, 0), self.message, fill=True) # Drawn top-centered (with margin) + def draw_bar(self, draw, top_y): # type: (ImageDraw, float) -> None width, height = self.o.device.size @@ -306,4 +316,4 @@ def ProgressBar(i, o, *args, **kwargs): elif "char" in o.type: return TextProgressBar(i, o, *args, **kwargs) else: - raise ValueError("Unsupported display type: {}".format(repr(self.o.type))) + raise ValueError("Unsupported display type: {}".format(repr(o.type))) From b498be87ae715338828fc6137711d0b89b13e8d8 Mon Sep 17 00:00:00 2001 From: monsieur_h Date: Tue, 26 Dec 2017 15:41:35 +0100 Subject: [PATCH 5/5] (#34) adds a python-rollbackable installation process --- apps/settings/main.py | 364 +++++++++++++++++------------------------- 1 file changed, 143 insertions(+), 221 deletions(-) diff --git a/apps/settings/main.py b/apps/settings/main.py index fa53fb3f..5ecb3e6b 100755 --- a/apps/settings/main.py +++ b/apps/settings/main.py @@ -1,236 +1,158 @@ +from __future__ import division import os -import signal -from subprocess import check_output +import shlex +import shutil +import subprocess +import tempfile from time import sleep -try: - import httplib -except: - import http.client as httplib - -# Using a TextProgressBar because only it shows a message on the screen for now -from ui import Menu, PrettyPrinter, DialogBox, TextProgressBar, Listbox +from apps import ZeroApp +from helpers.general import ZPUI_INSTALL_DIR from helpers.logger import setup_logger +from ui import Menu, DialogBox, Printer, ProgressBar -menu_name = "Settings" logger = setup_logger(__name__, "info") -class GitInterface(): - - @classmethod - def git_available(cls): - try: - cls.command("--help") - except OSError: - return False - return True - - @staticmethod - def command(command): - commandline = "git {}".format(command) - logger.debug("Executing: {}".format(commandline)) - return check_output(commandline, shell=True) - - @classmethod - def get_head_for_branch(cls, branch): - output = cls.command("rev-parse {}".format(branch)).strip() - return output - - @classmethod - def get_current_branch(cls): - return cls.get_head_for_branch("--abbrev-ref HEAD").strip() - - @classmethod - def checkout(cls, reference): - return cls.command("checkout {}".format(reference)) - - @classmethod - def pull(cls, source = "origin", branch = "master", opts="--no-edit"): - return cls.command("pull {2} {0} {1}".format(source, branch, opts)) - - -class UpdateUnnecessary(Exception): - pass - - -class GenericUpdater(object): - steps = [] - progressbar_messages = {} - failed_messages = {} - - def run_step(self, step_name): - logger.info("Running update step: '{}'".format(step_name)) - getattr(self, "do_" + step_name)() - logger.debug("Update step '{}' completed!".format(step_name)) - - def revert_step(self, step_name): - if hasattr(self, "revert_" + step_name): - logger.info("Reverting update step: '{}'".format(step_name)) - getattr(self, "revert_" + step_name)() - logger.debug("Update step '{}' reverted!".format(step_name)) +class ZpuiUpdaterApp(ZeroApp): + def __init__(self, i, o): + super(ZpuiUpdaterApp, self).__init__(i, o) + self.menu_name = "Update ZPUI" + self.menu = Menu( + [ + ["Update ZPUI", self.update_zpui], + ["Update ZPUI(devel)", self.update_zpui_devel] + ], + i, + o, + "ZPUI settings menu" + ) + self.steps = [] + + def on_start(self): + super(ZpuiUpdaterApp, self).on_start() + self.menu.activate() + + def update_zpui(self): + self.start_update(branch="master") + + def update_zpui_devel(self): + if DialogBox("yn", self.i, self.o, message="You sure ?").activate(): + self.start_update(branch="devel") else: - logger.debug("Can't revert step {} - no reverter available.".format(step_name)) - - def update(self): - logger.info("Starting update process") - pb = TextProgressBar(i, o, message="Updating ZPUI") - pb.run_in_background() - progress_per_step = 1.0 / len(self.steps) + Printer("Update canceled", self.i, self.o) - completed_steps = [] - try: + def start_update(self, branch): + self.create_steps(branch) + finished_steps = [] + with ProgressBar(self.i, self.o) as bar: for step in self.steps: - pb.set_message(self.progressbar_messages.get(step, "Loading...")) - sleep(0.5) # The user needs some time to read the message - self.run_step(step) - completed_steps.append(step) - pb.progress += progress_per_step - except UpdateUnnecessary: - logger.info("Update is unnecessary!") - pb.stop() - PrettyPrinter("ZPUI already up-to-date!", i, o, 2) - except: - # Name of the failed step is contained in `step` variable - failed_step = step - logger.exception("Failed on step {}".format(failed_step)) - failed_message = self.failed_messages.get(failed_step, "Failed on step '{}'".format(failed_step)) - pb.stop() - PrettyPrinter(failed_message, i, o, 2) - pb.set_message("Reverting update") - pb.run_in_background() - try: - logger.info("Reverting the failed step: {}".format(failed_step)) - self.revert_step(failed_step) - except: - logger.exception("Can't revert failed step {}".format(failed_step)) - pb.stop() - PrettyPrinter("Can't revert failed step '{}'".format(step), i, o, 2) - pb.run_in_background() - logger.info("Reverting the previous steps") - for step in completed_steps: try: - self.revert_step(step) - except: - logger.exception("Failed to revert step {}".format(failed_step)) - pb.stop() - PrettyPrinter("Failed to revert step '{}'".format(step), i, o, 2) - pb.run_in_background() - pb.progress -= progress_per_step - sleep(1) # Needed here so that 1) the progressbar goes to 0 2) run_in_background launches the thread before the final stop() call - #TODO: add a way to pause the Refresher - pb.stop() - logger.info("Update failed") - PrettyPrinter("Update failed, try again later?", i, o, 3) + bar.message = step.name + step.do() + finished_steps.append(step) + bar.progress = len(finished_steps) / len(self.steps) + bar.refresh() + sleep(0.5) # so the user has time to read what happens + except Exception as e: + if DialogBox("yn", self.i, self.o, + message="'{}'failed\ncancel update?".format(step.name)).activate(): + self.rollback_update(bar, e, finished_steps, step) + return -1 + else: + pass + + def rollback_update(self, bar, e, finished_steps, step): + logger.error("Error updating step '{}'".format(step.name)) + logger.exception(e) + Printer([step.name, 'update failed'], self.i, self.o) + bar.message = "reverting" + for i, passed in enumerate(finished_steps): + passed.undo() + bar.progress = (len(finished_steps) - i) / len(self.steps) + bar.refresh() + sleep(0.5) + + def create_steps(self, branch="master"): + tmp_dir = tempfile.mkdtemp(prefix='zpui') + os.chmod(tmp_dir, 0o777) + old_cwd = os.getcwd() + change_cwd = UpdateStep("opening tmp dir", lambda: os.chdir(tmp_dir), lambda: os.chdir(old_cwd)) + self.steps.append(change_cwd) + + git_copy = UpdateStep( + "copying repo", + "git clone {src_dir} {dir}/".format(src_dir=ZPUI_INSTALL_DIR, dir=tmp_dir, branch=branch) + ) + self.steps.append(git_copy) + + git_pull = UpdateStep( + "pulling git", + "git pull origin {branch} --ff-only".format(dir=tmp_dir, branch=branch) + ) + self.steps.append(git_pull) + + pip_install = UpdateStep("installing deps", + "pip2 install -r requirements.txt", + "pip2 install -r {src_dir}requirements.txt".format(src_dir=ZPUI_INSTALL_DIR) + ) + self.steps.append(pip_install) + + run_tests = UpdateStep( + "running tests", + "python2 -B -m pytest --doctest-modules -v --doctest-ignore-import-errors --ignore=output/drivers " + "--ignore=input/drivers --ignore=apps/hardware_apps/status/ --ignore=apps/example_apps/fire_detector " + "--ignore=apps/test_hardware", + accepted_return_code=0 + ) + self.steps.append(run_tests) + + change_cwd_back = UpdateStep("change cwd back", lambda: os.chdir(old_cwd)) + self.steps.append(change_cwd_back) + + copy_update_files = UpdateStep( + "patching ZPUI", + "rsync -av --delete {tmp_dir} --exclude='*.pyc' {dst_dir}".format(tmp_dir=tmp_dir, dst_dir=ZPUI_INSTALL_DIR) + ) + self.steps.append(copy_update_files) + + restart_service = UpdateStep( + "restarting zpui", + "systemctl restart zpui.service" + ) + self.steps.append(restart_service) + + clean_tmp_dir = UpdateStep("cleaning tmp dir", lambda: shutil.rmtree(tmp_dir)) + self.steps.append(clean_tmp_dir) + + +class UpdateStep(object): + def __init__(self, name, do=None, undo=None, accepted_return_code=None): + self.accepted_return_code = accepted_return_code + self.name = name + self._do = do + self._undo = undo + self.return_value = None + + def do(self): + if not self._do: + return + logger.debug("running '{}'".format(self.name)) + if isinstance(self._do, basestring): + print(shlex.split(self._do)) + self.return_value = subprocess.call(shlex.split(self._do)) else: - logger.info("Update successful!") - sleep(0.5) # showing the completed progressbar - pb.stop() - PrettyPrinter("Update successful!", i, o, 3) - self.suggest_restart() - - def suggest_restart(self): - needs_restart = DialogBox('yn', i, o, message="Restart ZPUI?").activate() - if needs_restart: - os.kill(os.getpid(), signal.SIGTERM) - - -class GitUpdater(GenericUpdater): - branch = "master" - - steps = ["check_connection", "check_git", "check_revisions", "pull", "install_requirements", "tests"] - progressbar_messages = { - "check_connection": "Connection check", - "check_git": "Running git", - "check_revisions": "Comparing code", - "pull": "Fetching code", - "install_requirements": "Installing packages", - "tests": "Running tests", - } - failed_messages = { - "check_connection": "No Internet connection!", - "check_git": "Git binary not found!", - "check_revisions": "Exception while comparing revisions!", - "pull": "Couldn't get new code!", - "install_requirements": "Failed to install new packages!", - "tests": "Tests failed!" - } - - def do_check_git(self): - if not GitInterface.git_available(): - logger.exception("Couldn't execute git - not found?") - raise OSError() - - def do_check_revisions(self): - GitInterface.command("fetch") - current_branch_name = GitInterface.get_current_branch() - current_revision = GitInterface.get_head_for_branch(current_branch_name) - remote_revision = GitInterface.get_head_for_branch("origin/"+current_branch_name) - if current_revision == remote_revision: - raise UpdateUnnecessary + self.return_value = self._do() + logger.debug("ran '{}' with return value '{}'".format(self.name, self.return_value)) + if self.accepted_return_code is not None: + if not self.return_value == self.accepted_return_code: + raise Exception("'{}' failed".format(self.name)) + return self.return_value + + def undo(self): + if not self._undo: + logger.warning("no undo method for '{}'".format(self.name)) else: - self.previous_revision = current_revision - - def do_check_connection(self): - conn = httplib.HTTPConnection("github.com", timeout=10) - try: - conn.request("HEAD", "/") - except: - raise - finally: - conn.close() - - def do_install_requirements(self): - output = check_output(["pip", "install", "-r", "requirements.txt"]) - logger.debug("pip output:") - logger.debug(output) - - def do_pull(self): - current_branch_name = GitInterface.get_current_branch() - GitInterface.pull(branch = current_branch_name) - - def do_tests(self): - commandline = "python -B -m pytest --doctest-modules -v --doctest-ignore-import-errors --ignore=output/drivers --ignore=input/drivers --ignore=apps/hardware_apps/status/ --ignore=apps/test_hardware" - output = check_output(commandline.split(" ")) - logger.debug("pytest output:") - logger.debug(output) - - def revert_pull(self): - # do_check_revisions already ran, we now have the previous revision's - # commit hash in self.previous_revision - GitInterface.command("reset --mixed {}".format(self.previous_revision)) - # requirements.txt now contains old requirements, let's install them back - self.do_install_requirements() - - def pick_branch(self): - #TODO: add branches dynamically instead of having a whitelist - available_branches = [["master"], ["devel"]] - branch = Listbox(available_branches, i, o, name="Git updater listbox").activate() - if branch: - try: - GitInterface.checkout(branch) - except: - PrettyPrinter("Couldn't check out the {} branch! Try resolving the conflict through the command-line.".format(branch), i, o, 3) - else: - PrettyPrinter("Now on {} branch!".format(branch), i, o, 2) - self.suggest_restart() - #TODO: run tests? - - -def settings(): - git_updater = GitUpdater() - c = [["Update ZPUI", git_updater.update], - ["Select branch", git_updater.pick_branch]] - Menu(c, i, o, "ZPUI settings menu").activate() - - -callback = settings -i = None # Input device -o = None # Output device - - -def init_app(input, output): - global i, o - i = input - o = output + logger.debug("Undoing '{}'".format(self.name)) + self._undo()