Esempio n. 1
0
class AppUpdater(PollWorker):
    """Class for updating a frozen application.

    Basically an Esky wrapper.
    """
    refreshStatus = QtCore.pyqtSignal()
    _doUpdate = QtCore.pyqtSignal(str)
    appUpdated = QtCore.pyqtSignal(str)
    updateAvailable = QtCore.pyqtSignal()

    def __init__(self,
                 manager,
                 version_finder=None,
                 check_interval=DEFAULT_UPDATE_CHECK_DELAY,
                 esky_app=None,
                 local_update_site=False):
        super(AppUpdater, self).__init__(check_interval)
        self.refreshStatus.connect(self._poll)
        self._doUpdate.connect(self._update)
        self._manager = manager
        self._enable = False
        if esky_app is not None:
            self.esky_app = esky_app
            self._enable = True
        elif not hasattr(sys, 'frozen'):
            log.debug("Application is not frozen, cannot build Esky"
                      " instance, as a consequence update features"
                      " won't be available")
        elif version_finder is None:
            log.debug("Cannot initialize Esky instance with no"
                      " version finder, as a consequence update"
                      " features won't be available")
        else:
            try:
                executable = sys.executable
                log.debug(
                    "Application is frozen, building Esky instance from"
                    " executable %s and version finder %s", executable,
                    version_finder)
                self.esky_app = Esky(executable, version_finder=version_finder)
                self._enable = True
            except EskyBrokenError as e:
                log.error(e, exc_info=True)
                log.debug("Error initializing Esky instance, as a"
                          " consequence update features won't be"
                          " available")
        self.local_update_site = local_update_site
        if self._enable:
            self.update_site = self.esky_app.version_finder.download_url
            if not self.local_update_site and not self.update_site.endswith(
                    '/'):
                self.update_site = self.update_site + '/'
        self.last_status = (UPDATE_STATUS_UP_TO_DATE, None)

    def get_status(self):
        return self.last_status

    def force_status(self, status, version):
        if status == 'updating':
            # Put a percentage
            self.last_status = (status, version, 40)
        else:
            self.last_status = (status, version)
        if status == UPDATE_STATUS_UPDATE_AVAILABLE:
            self.updateAvailable.emit()

    def refresh_status(self):
        if self._enable:
            self.refreshStatus.emit()

    @QtCore.pyqtSlot()
    def _poll(self):
        if self.last_status != UPDATE_STATUS_UPDATING:
            # Refresh update site URL
            self.set_version_finder(
                self._manager.get_version_finder(refresh_engines=True))
            log.debug(
                'Polling %s for application update, current version is %s',
                self.update_site, self._manager.get_version())
            status = self._get_update_status()
            if status != self.last_status:
                self.last_status = status
            self._handle_status()
            return status != UPDATE_STATUS_UNAVAILABLE_SITE
        else:
            return True

    def _handle_status(self):
        update_status = self.last_status[0]
        update_version = self.last_status[1]
        if update_status == UPDATE_STATUS_UNAVAILABLE_SITE:
            # Update site unavailable
            log.warning("Update site is unavailable, as a consequence"
                        " update features won't be available")
        elif update_status in [
                UPDATE_STATUS_MISSING_INFO, UPDATE_STATUS_MISSING_VERSION
        ]:
            # Information or version missing in update site
            log.warning("Some information or version file is missing in"
                        " the update site, as a consequence update"
                        " features won't be available")
        else:
            # Update information successfully fetched
            log.debug(
                "Fetched information from update site %s: update"
                " status = '%s', update version = '%s'", self.update_site,
                update_status, update_version)
            if update_status in [
                    UPDATE_STATUS_DOWNGRADE_NEEDED,
                    UPDATE_STATUS_UPGRADE_NEEDED
            ]:
                # Current client version not compatible with server
                # version, upgrade or downgrade needed.
                # Let's stop synchronization.
                log.info("As current client version is not compatible with"
                         " server version, an upgrade or downgrade is"
                         " needed. Synchronization won't start until then.")
                self._manager.stop()
            elif update_status == UPDATE_STATUS_UPDATE_AVAILABLE and self._manager.get_auto_update(
            ):
                # Update available and auto-update checked, let's process update
                log.info("An application update is available and"
                         " auto-update is checked")
                self.last_status = (UPDATE_STATUS_UPDATING, update_version, 0)
                try:
                    self._update(update_version)
                except UpdateError:
                    log.error(
                        "An error occurred while trying to automatically update Nuxeo Drive to version %s,"
                        " setting 'Auto update' to False",
                        update_version,
                        exc_info=True)
                    self._manager.set_auto_update(False)
            elif update_status == UPDATE_STATUS_UPDATE_AVAILABLE and not self._manager.get_auto_update(
            ):
                # Update available and auto-update not checked, let's just
                # update the systray notification and let the user explicitly choose to  update
                log.info("An update is available and auto-update is not"
                         " checked, let's just update the systray notification"
                         " and let the user explicitly choose to update")
                self.updateAvailable.emit()
            else:
                # Application is up-to-date
                log.debug("Application is up-to-date")

    def set_version_finder(self, version_finder):
        self.esky_app._set_version_finder(version_finder)
        self.update_site = self.esky_app.version_finder.download_url

    def get_active_version(self):
        return self.esky_app.active_version

    def get_current_latest_version(self):
        return self.esky_app.version

    def find_versions(self):
        try:
            return sorted(self.esky_app.version_finder.find_versions(
                self.esky_app),
                          cmp=version_compare)
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)

    def get_server_min_version(self, client_version):
        info_file = client_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " server minimum version for client version %s" %
            (info_file, self.update_site, client_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoPlatformMinVersion']
            log.debug(
                "Fetched server minimum version for client version %s"
                " from %s: %s", client_version, url, version)
            return version
        except HTTPError as e:
            version = DEFAULT_SERVER_MIN_VERSION
            log.debug(missing_msg + ", using default one: %s",
                      DEFAULT_SERVER_MIN_VERSION)
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def _get_client_min_version(self, server_version):
        info_file = server_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " client minimum version for server version %s" %
            (info_file, self.update_site, server_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoDriveMinVersion']
            log.debug(
                "Fetched client minimum version for server version %s"
                " from %s: %s", server_version, url, version)
            return version
        except HTTPError as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def compute_common_versions(self):
        # Get the max minimal client version
        # Get the min minimal server version
        self.min_client_version = None
        self.min_server_version = None
        for engine in self._manager.get_engines().values():
            server_version = engine.get_server_version()
            if server_version is None:
                continue
            if self.min_server_version is None:
                self.min_server_version = server_version
            if version_compare(self.min_server_version, server_version) > 0:
                self.min_server_version = server_version
            client_version = self._get_client_min_version(server_version)
            if self.min_client_version is None:
                self.min_client_version = client_version
                continue
            # Get the maximal "minimum"
            if version_compare(self.min_client_version, client_version) < 0:
                self.min_client_version = client_version

    def get_latest_compatible_version(self):
        self.compute_common_versions()
        latest_version = None
        client_versions = self.find_versions()
        client_versions.append(self.get_current_latest_version())
        client_versions = sorted(client_versions, cmp=version_compare)
        for client_version in client_versions:
            if self.min_client_version <= client_version:
                server_min_version = self.get_server_min_version(
                    client_version)
                if server_min_version <= self.min_server_version:
                    latest_version = client_version
        if latest_version is None:
            raise MissingCompatibleVersion(
                "No client version compatible with server version %s"
                " available in update site '%s'" %
                (self.min_server_version, self.update_site))
        return latest_version

    def _get_update_status(self):
        try:
            client_version = self._manager.get_version()
            latest_version = self.get_latest_compatible_version()
            # TO_REVIEW What the need for that
            self.get_server_min_version(client_version)
            server_version = self.min_server_version
            client_min_version = self.min_client_version
            server_min_version = self.min_server_version
            if (client_version == latest_version):
                log.debug(
                    "Client version %s is up-to-date regarding server"
                    " version %s.", client_version, self.min_server_version)
                return (UPDATE_STATUS_UP_TO_DATE, None)

            if version_compare(client_version, client_min_version) < 0:
                log.info(
                    "Client version %s is lighter than %s, the minimum"
                    " version compatible with the server version %s."
                    " An upgrade to version %s is needed.", client_version,
                    client_min_version, server_version, latest_version)
                return (UPDATE_STATUS_UPGRADE_NEEDED, latest_version)

            if (version_compare(server_version, server_min_version) < 0
                    or version_compare(latest_version, client_version) < 0):
                log.info(
                    "Server version %s is lighter than %s, the minimum"
                    " version compatible with the client version %s."
                    " A downgrade to version %s is needed.", server_version,
                    server_min_version, client_version, latest_version)
                return (UPDATE_STATUS_DOWNGRADE_NEEDED, latest_version)

            log.info(
                "Client version %s is compatible with server version %s,"
                " yet an update is available: version %s.", client_version,
                server_version, latest_version)
            return (UPDATE_STATUS_UPDATE_AVAILABLE, latest_version)
        except UnavailableUpdateSite as e:
            log.error(e)
            return (UPDATE_STATUS_UNAVAILABLE_SITE, None)
        except MissingUpdateSiteInfo as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_INFO, None)
        except MissingCompatibleVersion as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_VERSION, None)

    def update(self, version):
        self.last_status = (UPDATE_STATUS_UPDATING, str(version), 0)
        self._doUpdate.emit(version)

    @QtCore.pyqtSlot(str)
    def _update(self, version):
        version = str(version)
        if sys.platform == 'win32':
            # Try to update frozen application with the given version. If it
            # fails with a permission error, escalate to root and try again.
            try:
                self._do_update(version)
                self.appUpdated.emit(version)
                return
            except:
                log.exception("Updater issue, will try to get root")
            try:
                self.esky_app.get_root()
                self._do_update(version)
                self.esky_app.drop_root()
            except EnvironmentError as e:
                if e.errno == errno.EINVAL:
                    # Under Windows, this means that the sudo popup was
                    # rejected
                    self.esky_app.sudo_proxy = None
                    log.warn("RootPrivilegeRequired", exc_info=True)
                    return
                # Other EnvironmentError, probably not related to permissions
                log.warn("UpdateError", exc_info=True)
                return
            except:
                # Error during update process, not related to permissions
                log.warn("UpdateError", exc_info=True)
                return
            finally:
                self.last_status = self._get_update_status()
        else:
            try:
                self._do_update(version)
            except:
                log.warn("UpdateError", exc_info=True)
                return
            finally:
                self.last_status = self._get_update_status()
        self.appUpdated.emit(version)

    def _update_callback(self, status):
        if "received" in status and "size" in status:
            self.action.progress = (status["received"] * 100 / status["size"])
            self.last_status = (self.last_status[0], self.last_status[1],
                                self.action.progress)

    def _do_update(self, version):
        log.info("Starting application update process")

        log.info("Fetching version %s from update site %s", version,
                 self.update_site)
        self.action = Action("Downloading %s version" % version)
        self.action.progress = 0
        self._update_action(self.action)
        self.esky_app.fetch_version(version, self._update_callback)

        log.info("Installing version %s", version)
        self._update_action(Action("Installing %s version" % version))
        self.esky_app.install_version(version)

        log.debug("Reinitializing Esky internal state")
        self.action.type = "Reinitializing"
        self.esky_app.reinitialize()

        log.info("Ended application update process")
        self._end_action()

    def cleanup(self, version):
        log.info("Uninstalling version %s", version)
        self.esky_app.uninstall_version(version)
        log.info("Cleaning up Esky application", version)
        self.esky_app.cleanup()

    def get_update_site(self):
        return self.update_site

    def _handle_URL_error(self, e):
        log.error(e, exc_info=True)
        raise UnavailableUpdateSite("Cannot connect to update site '%s'" %
                                    self.update_site)

    def _handle_timeout_error(self, e):
        log.error(e, exc_info=True)
        raise UnavailableUpdateSite(
            "Connection to update site '%s' timed out" % self.update_site)
Esempio n. 2
0
class AppUpdater:
    """Class for updating a frozen application.

    Basically an Esky wrapper.
    """

    def __init__(self, version_finder=None, esky_app=None,
                 local_update_site=False):

        if esky_app is not None:
            self.esky_app = esky_app
        elif not hasattr(sys, 'frozen'):
            raise AppNotFrozen("Application is not frozen, cannot build Esky"
                               " instance, as a consequence update features"
                               " won't be available")
        elif version_finder is None:
            raise UpdaterInitError("Cannot initialize Esky instance with no"
                                   " version finder, as a consequence update"
                                   " features won't be available")
        else:
            try:
                executable = sys.executable
                log.debug("Application is frozen, building Esky instance from"
                          " executable %s and version finder %s",
                          executable, version_finder)
                self.esky_app = Esky(executable, version_finder=version_finder)
            except EskyBrokenError as e:
                log.error(e, exc_info=True)
                raise UpdaterInitError("Error initializing Esky instance, as a"
                                       " consequence update features won't be"
                                       " available")
        self.local_update_site = local_update_site
        self.update_site = self.esky_app.version_finder.download_url
        if not self.local_update_site and not self.update_site.endswith('/'):
            self.update_site = self.update_site + '/'

    def set_version_finder(self, version_finder):
        self.esky_app._set_version_finder(version_finder)

    def get_active_version(self):
        return self.esky_app.active_version

    def get_current_latest_version(self):
        return self.esky_app.version

    def find_versions(self):
        return sorted(self.esky_app.version_finder.find_versions(
                                        self.esky_app), cmp=version_compare)

    def get_server_min_version(self, client_version):
        info_file = client_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " server minimum version for client version %s" % (
                                info_file, self.update_site, client_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoPlatformMinVersion']
            log.debug("Fetched server minimum version for client version %s"
                      " from %s: %s", client_version, url, version)
            return version
        except HTTPError as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            log.error(e, exc_info=True)
            raise UnavailableUpdateSite("Cannot connect to update site '%s'"
                                        % self.update_site)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def get_client_min_version(self, server_version):
        info_file = server_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " client minimum version for server version %s" % (
                                info_file, self.update_site, server_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoDriveMinVersion']
            log.debug("Fetched client minimum version for server version %s"
                      " from %s: %s", server_version, url, version)
            return version
        except HTTPError as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            log.error(e, exc_info=True)
            raise UnavailableUpdateSite("Cannot connect to update site '%s'"
                                        % self.update_site)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def get_latest_compatible_version(self, server_version):
        client_min_version = self.get_client_min_version(server_version)
        latest_version = None
        client_versions = self.find_versions()
        client_versions.append(self.get_current_latest_version())
        client_versions = sorted(client_versions, cmp=version_compare)
        for client_version in client_versions:
            if client_min_version <= client_version:
                server_min_version = self.get_server_min_version(
                                                    client_version)
                if server_min_version <= server_version:
                    latest_version = client_version
        if latest_version is None:
            raise MissingCompatibleVersion(
                    "No client version compatible with server version %s"
                    " available in update site '%s'" % (
                                server_version, self.update_site))
        return latest_version

    def get_update_status(self, client_version, server_version):
        try:
            latest_version = self.get_latest_compatible_version(server_version)
            if (client_version == latest_version):
                log.info("Client version %s is up-to-date regarding server"
                         " version %s.", client_version, server_version)
                return (UPDATE_STATUS_UP_TO_DATE, None)

            client_min_version = self.get_client_min_version(server_version)
            server_min_version = self.get_server_min_version(client_version)

            if version_compare(client_version, client_min_version) < 0:
                log.info("Client version %s is lighter than %s, the minimum"
                         " version compatible with the server version %s."
                         " An upgrade to version %s is needed.",
                         client_version, client_min_version, server_version,
                         latest_version)
                return (UPDATE_STATUS_UPGRADE_NEEDED, latest_version)

            if version_compare(server_version, server_min_version) < 0:
                log.info("Server version %s is lighter than %s, the minimum"
                         " version compatible with the client version %s."
                         " A downgrade to version %s is needed.",
                         server_version, server_min_version, client_version,
                         latest_version)
                return (UPDATE_STATUS_DOWNGRADE_NEEDED, latest_version)

            log.info("Client version %s is compatible with server version %s,"
                     " yet an update is available: version %s.",
                     client_version, server_version, latest_version)
            return (UPDATE_STATUS_UPDATE_AVAILABLE, latest_version)
        except UnavailableUpdateSite as e:
            log.warning(e)
            return (UPDATE_STATUS_UNAVAILABLE_SITE, None)
        except MissingUpdateSiteInfo as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_INFO, None)
        except MissingCompatibleVersion as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_VERSION, None)

    def update(self, version):
        if sys.platform == 'win32':
            # Try to update frozen application with the given version. If it
            # fails with a permission error, escalate to root and try again.
            try:
                self.esky_app.get_root()
                self._do_update(version)
                self.esky_app.drop_root()
                return True
            except EnvironmentError as e:
                if e.errno == errno.EINVAL:
                    # Under Windows, this means that the sudo popup was
                    # rejected
                    self.esky_app.sudo_proxy = None
                    raise RootPrivilegeRequired(e)
                # Other EnvironmentError, probably not related to permissions
                raise UpdateError(e)
            except Exception as e:
                # Error during update process, not related to permissions
                raise UpdateError(e)
        else:
            try:
                self._do_update(version)
                return True
            except Exception as e:
                raise UpdateError(e)

    def _do_update(self, version):
        log.info("Starting application update process")
        log.info("Fetching version %s from update site %s", version,
                      self.update_site)
        self.esky_app.fetch_version(version)
        log.info("Installing version %s", version)
        self.esky_app.install_version(version)
        log.debug("Reinitializing Esky internal state")
        self.esky_app.reinitialize()
        log.info("Ended application update process")

    def cleanup(self, version):
        log.info("Uninstalling version %s", version)
        self.esky_app.uninstall_version(version)
        log.info("Cleaning up Esky application", version)
        self.esky_app.cleanup()

    def get_update_label(self, status):
        return UPDATE_STATUS_LABEL[status]

    def get_update_site(self):
        return self.update_site
Esempio n. 3
0
def autoupdate(update_page, ui, name=""):
    from esky import Esky
    assert isinstance(ui, UI)

    if hasattr(sys, "frozen"):
        esky = Esky(sys.executable, update_page)
    else:
        #return
        esky = Esky(os.path.dirname(__file__), update_page)
        esky.name = name

    got_root = False
    cleaned = False
    ui.start_blocking_task("Searching for updates")
    try:
        version = esky.find_update()
    except Exception as e:
        sys.stderr.write("Could not find updates: \n" + str(e))
        version = None
    finally:
        ui.end_blocking_task()
    if version is not None:
        msg = "Current version: %s\nAvailable version: %s\n\nDo you want to download and install the update?" % (esky.version, version)
        if ui.get_confirmation("New version available", msg):
            def callback(kwargs):
                if kwargs['status'] == "downloading":
                    current = kwargs['received']
                    maximum = kwargs['size']
                    ui._callback(current, maximum)
                else:
                    title = kwargs['status']
                    ui.start_blocking_task(title[0].upper() + title[1:])

            try:
                loc = ui.exec_long_task_with_callback("Downloading new version", True, esky.fetch_version, version, callback)
            except Exception as e:

                e.args = ("Downloading new version failed\n",)
                ui.show_error(e)
                return
            finally:
                ui.end_blocking_task()

            if os.path.isdir(loc):
                ui.start_blocking_task("Installing")
                esky.install_version(version)
                ui.end_blocking_task()

                try:
                    ui.start_blocking_task("Uninstalling previous version")
                    esky.uninstall_version(esky.version)
                except Exception:
                    pass
                finally:
                    ui.end_blocking_task()
                try:
                    esky.get_root()
                except Exception as e:
                    pass
                else:
                    got_root = True
                    callback({"status":"cleaning up"})

                    cleaned = esky.cleanup()
                #  Drop root privileges as soon as possible.
                if not cleaned and esky.needs_cleanup():
                    try:
                        esky.cleanup_at_exit()
                    except:
                        pass

                if got_root:
                    esky.drop_root()
                ui.end_blocking_task()
                ui.show_message("Update complete\n\nPlease restart %s" % esky.name, "Update complete")
Esempio n. 4
0
class AppUpdater(PollWorker):
    """Class for updating a frozen application.

    Basically an Esky wrapper.
    """
    refreshStatus = QtCore.pyqtSignal()
    _doUpdate = QtCore.pyqtSignal(str)
    appUpdated = QtCore.pyqtSignal(str)
    updateAvailable = QtCore.pyqtSignal()

    def __init__(self, manager, version_finder=None,
                 check_interval=Options.update_check_delay,
                 esky_app=None, local_update_site=False):
        super(AppUpdater, self).__init__(check_interval)
        self.refreshStatus.connect(self._poll)
        self._doUpdate.connect(self._update)
        self._manager = manager
        self._enable = False
        if esky_app is not None:
            self.esky_app = esky_app
            self._enable = True
        elif not hasattr(sys, 'frozen'):
            log.debug("Application is not frozen, cannot build Esky"
                      " instance, as a consequence update features"
                      " won't be available")
        elif version_finder is None:
            log.debug("Cannot initialize Esky instance with no"
                      " version finder, as a consequence update"
                      " features won't be available")
        else:
            try:
                executable = sys.executable
                log.debug("Application is frozen, building Esky instance from"
                          " executable %s and version finder %s",
                          executable.decode('utf-8'), version_finder)
                self.esky_app = Esky(executable, version_finder=version_finder)
                self._enable = True
            except UpdateError:
                log.exception('Error initializing Esky instance, as a'
                              ' consequence update features will not'
                              ' be available')
        self.local_update_site = local_update_site
        if self._enable:
            self.update_site = self.esky_app.version_finder.download_url
            if not self.local_update_site and not self.update_site.endswith('/'):
                self.update_site += '/'
        self.last_status = (UPDATE_STATUS_UP_TO_DATE, None)

    def get_status(self):
        return self.last_status

    def force_status(self, status, version):
        if status == 'updating':
            # Put a percentage
            self.last_status = (status, version, 40)
        else:
            self.last_status = (status, version)
        if status == UPDATE_STATUS_UPDATE_AVAILABLE:
            self.updateAvailable.emit()

    def refresh_status(self):
        if self._enable:
            self.refreshStatus.emit()

    @QtCore.pyqtSlot()
    def _poll(self):
        if self.last_status != UPDATE_STATUS_UPDATING:
            # Refresh update site URL
            self.set_version_finder(
                self._manager.get_version_finder(refresh_engines=True))
            log.debug(
                'Polling %s for application update, current version is %s',
                self.update_site, self._manager.get_version())
            status = self._get_update_status()
            if status != self.last_status:
                self.last_status = status
            self._handle_status()
            return status != UPDATE_STATUS_UNAVAILABLE_SITE
        else:
            return True

    def _handle_status(self):
        update_status = self.last_status[0]
        update_version = self.last_status[1]
        if update_status == UPDATE_STATUS_UNAVAILABLE_SITE:
            # Update site unavailable
            log.warning("Update site is unavailable, as a consequence"
                        " update features won't be available")
        elif update_status in [UPDATE_STATUS_MISSING_INFO,
                                  UPDATE_STATUS_MISSING_VERSION]:
            # Information or version missing in update site
            log.warning("Some information or version file is missing in"
                        " the update site, as a consequence update"
                        " features won't be available")
        else:
            # Update information successfully fetched
            log.debug("Fetched information from update site %s: update"
                     " status = '%s', update version = '%s'",
                     self.update_site, update_status, update_version)
            if update_status in (UPDATE_STATUS_DOWNGRADE_NEEDED,
                                 UPDATE_STATUS_UPGRADE_NEEDED):
                # Current client version not compatible with server
                # version, upgrade or downgrade needed.
                # Let's stop synchronization.
                log.info("As current client version is not compatible with"
                         " server version, an upgrade or downgrade is"
                         " needed. Synchronization won't start until then.")
                self._manager.stop()
            elif update_status == UPDATE_STATUS_UPDATE_AVAILABLE and \
                    self._manager.get_auto_update():
                # Update available and auto-update checked, let's process update
                log.info("An application update is available and"
                         " auto-update is checked")
                self.last_status = (UPDATE_STATUS_UPDATING, update_version, 0)
                try:
                    self._update(update_version)
                except UpdateError:
                    log.exception('An error occurred while trying to '
                                  'automatically update Nuxeo Drive to '
                                  'version %s, disaling auto-update.',
                                  update_version)
                    self._manager.set_auto_update(False)
            elif update_status == UPDATE_STATUS_UPDATE_AVAILABLE and \
                    not self._manager.get_auto_update():
                # Update available and auto-update not checked, let's just
                # update the systray notification and let the user explicitly
                # choose to  update
                log.info("An update is available and auto-update is not"
                         " checked, let's just update the systray notification"
                         " and let the user explicitly choose to update")
                self.updateAvailable.emit()
            else:
                # Application is up-to-date
                log.debug("Application is up-to-date")

    def set_version_finder(self, version_finder):
        self.esky_app._set_version_finder(version_finder)
        self.update_site = self.esky_app.version_finder.download_url

    def get_active_version(self):
        return self.esky_app.active_version

    def get_current_latest_version(self):
        return self.esky_app.version

    def find_versions(self):
        versions = [self.get_current_latest_version()]

        try:
            versions.extend(
                self.esky_app.version_finder.find_versions(self.esky_app))
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)
        except:
            log.exception('Impossible to find versions')

        return sorted(versions, cmp=version_compare_client)

    def get_server_min_version(self, client_version):
        info_file = client_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " server minimum version for client version %s" % (
                                info_file, self.update_site, client_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoPlatformMinVersion']
            log.debug("Fetched server minimum version for client version %s"
                      " from %s: %s", client_version, url, version)
            return version
        except HTTPError:
            version = DEFAULT_SERVER_MIN_VERSION
            log.debug(missing_msg + ', using default one: %s', version)
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)
        except:
            log.exception(missing_msg)
            raise MissingUpdateSiteInfo(missing_msg)

    def _get_client_min_version(self, server_version):
        info_file = server_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " client minimum version for server version %s" % (
                                info_file, self.update_site, server_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoDriveMinVersion']
            log.debug("Fetched client minimum version for server version %s"
                      " from %s: %s", server_version, url, version)
            return version
        except HTTPError:
            log.exception('Network error')
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            self._handle_URL_error(e)
        except socket.timeout as e:
            self._handle_timeout_error(e)
        except:
            log.exception(missing_msg)
            raise MissingUpdateSiteInfo(missing_msg)

    def compute_common_versions(self):
        # Get the max minimal client version
        # Get the min minimal server version
        self.min_client_version = None
        self.min_server_version = None
        for engine in self._manager.get_engines().values():
            server_version = engine.get_server_version()
            if server_version is None:
                continue
            if self.min_server_version is None:
                self.min_server_version = server_version
            if version_compare(self.min_server_version, server_version) > 0:
                self.min_server_version = server_version
            client_version = self._get_client_min_version(server_version)
            if self.min_client_version is None:
                self.min_client_version = client_version
                continue
            # Get the maximal "minimum"
            if version_compare_client(self.min_client_version, client_version) < 0:
                self.min_client_version = client_version

    def get_latest_compatible_version(self):
        self.compute_common_versions()
        latest_version = None
        for client_version in self.find_versions():
            if version_compare_client(self.min_client_version, client_version) < 1:
                server_min_version = self.get_server_min_version(
                                                    client_version)
                if version_compare(server_min_version, self.min_server_version) < 1:
                    latest_version = client_version
        if latest_version is None:
            raise MissingCompatibleVersion(
                    "No client version compatible with server version %s"
                    " available in update site '%s'" % (
                                self.min_server_version, self.update_site))
        return latest_version

    def _get_update_status(self):
        try:
            client_version = self._manager.get_version()
            latest_version = self.get_latest_compatible_version()
            # TO_REVIEW What the need for that
            self.get_server_min_version(client_version)
            server_version = self.min_server_version
            client_min_version = self.min_client_version
            server_min_version = self.min_server_version
            if client_version == latest_version:
                log.debug("Client version %s is up-to-date regarding server"
                         " version %s.", client_version, self.min_server_version)
                return UPDATE_STATUS_UP_TO_DATE, None

            if version_compare_client(client_version, client_min_version) < 0:
                log.info("Client version %s is lighter than %s, the minimum"
                         " version compatible with the server version %s."
                         " An upgrade to version %s is needed.",
                         client_version, client_min_version, server_version,
                         latest_version)
                return UPDATE_STATUS_UPGRADE_NEEDED, latest_version

            if (version_compare(server_version, server_min_version) < 0
                    or version_compare_client(latest_version, client_version) < 0):
                log.info("Server version %s is lighter than %s, the minimum"
                         " version compatible with the client version %s."
                         " A downgrade to version %s is needed.",
                         server_version, server_min_version, client_version,
                         latest_version)
                return UPDATE_STATUS_DOWNGRADE_NEEDED, latest_version

            log.info("Client version %s is compatible with server version %s,"
                     " yet an update is available: version %s.",
                     client_version, server_version, latest_version)
            return UPDATE_STATUS_UPDATE_AVAILABLE, latest_version
        except UnavailableUpdateSite as e:
            log.error(e)
            return UPDATE_STATUS_UNAVAILABLE_SITE, None
        except MissingUpdateSiteInfo as e:
            log.warning(e)
            return UPDATE_STATUS_MISSING_INFO, None
        except MissingCompatibleVersion as e:
            log.warning(e)
            return UPDATE_STATUS_MISSING_VERSION, None

    def update(self, version):
        self.last_status = (UPDATE_STATUS_UPDATING, str(version), 0)
        self._doUpdate.emit(version)

    @QtCore.pyqtSlot(str)
    def _update(self, version):
        version = str(version)
        if sys.platform == 'win32':
            # Try to update frozen application with the given version. If it
            # fails with a permission error, escalate to root and try again.
            try:
                self._do_update(version)
                self.appUpdated.emit(version)
                return
            except:
                log.exception('Updater issue, will try to get root')
            try:
                self.esky_app.get_root()
                self._do_update(version)
                self.esky_app.drop_root()
            except EnvironmentError as e:
                if e.errno == errno.EINVAL:
                    # Under Windows, this means that the sudo popup was
                    # rejected
                    self.esky_app.sudo_proxy = None
                    log.exception('RootPrivilegeRequired')
                    return
                # Other EnvironmentError, probably not related to permissions
                log.exception('UpdateError')
                return
            except UpdateError:
                # Error during update process, not related to permissions
                log.exception('UpdateError')
                return
            finally:
                self.last_status = self._get_update_status()
        else:
            try:
                self._do_update(version)
            except UpdateError:
                log.exception('UpdateError')
                return
            finally:
                self.last_status = self._get_update_status()
        self.appUpdated.emit(version)

    def _update_callback(self, status):
        if "received" in status and "size" in status:
            self.action.progress = status["received"] * 100 / status["size"]
            self.last_status = (self.last_status[0],
                                self.last_status[1],
                                self.action.progress)

    def _do_update(self, version):
        log.info("Starting application update process")

        log.info("Fetching version %s from update site %s",
                 version, self.update_site)
        self.action = Action("Downloading %s version" % version)
        self.action.progress = 0
        self._update_action(self.action)
        self.esky_app.fetch_version(version, self._update_callback)

        log.info("Installing version %s", version)
        self._update_action(Action("Installing %s version" % version))
        self.esky_app.install_version(version)

        log.debug("Reinitializing Esky internal state")
        self.action.type = "Reinitializing"
        self.esky_app.reinitialize()

        log.info("Ended application update process")
        self._end_action()

    def cleanup(self, version):
        log.info("Uninstalling version %s", version)
        self.esky_app.uninstall_version(version)
        log.info("Cleaning up Esky application")
        self.esky_app.cleanup()

    def get_update_site(self):
        return self.update_site

    def _handle_URL_error(self, e):
        log.exception(repr(e))
        raise UnavailableUpdateSite(
            "Cannot connect to update site '%s'" % self.update_site)

    def _handle_timeout_error(self, e):
        log.exception(repr(e))
        raise UnavailableUpdateSite(
            "Connection to update site '%s' timed out" % self.update_site)
Esempio n. 5
0
class AppUpdater:
    """Class for updating a frozen application.

    Basically an Esky wrapper.
    """
    def __init__(self,
                 version_finder=None,
                 esky_app=None,
                 local_update_site=False):

        if esky_app is not None:
            self.esky_app = esky_app
        elif not hasattr(sys, 'frozen'):
            raise AppNotFrozen("Application is not frozen, cannot build Esky"
                               " instance, as a consequence update features"
                               " won't be available")
        elif version_finder is None:
            raise UpdaterInitError("Cannot initialize Esky instance with no"
                                   " version finder, as a consequence update"
                                   " features won't be available")
        else:
            try:
                executable = sys.executable
                log.debug(
                    "Application is frozen, building Esky instance from"
                    " executable %s and version finder %s", executable,
                    version_finder)
                self.esky_app = Esky(executable, version_finder=version_finder)
            except EskyBrokenError as e:
                log.error(e, exc_info=True)
                raise UpdaterInitError("Error initializing Esky instance, as a"
                                       " consequence update features won't be"
                                       " available")
        self.local_update_site = local_update_site
        self.update_site = self.esky_app.version_finder.download_url
        if not self.local_update_site and not self.update_site.endswith('/'):
            self.update_site = self.update_site + '/'

    def set_version_finder(self, version_finder):
        self.esky_app._set_version_finder(version_finder)

    def get_active_version(self):
        return self.esky_app.active_version

    def get_current_latest_version(self):
        return self.esky_app.version

    def find_versions(self):
        return sorted(self.esky_app.version_finder.find_versions(
            self.esky_app),
                      cmp=version_compare)

    def get_server_min_version(self, client_version):
        info_file = client_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " server minimum version for client version %s" %
            (info_file, self.update_site, client_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoPlatformMinVersion']
            log.debug(
                "Fetched server minimum version for client version %s"
                " from %s: %s", client_version, url, version)
            return version
        except HTTPError as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            log.error(e, exc_info=True)
            raise UnavailableUpdateSite("Cannot connect to update site '%s'" %
                                        self.update_site)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def get_client_min_version(self, server_version):
        info_file = server_version + '.json'
        missing_msg = (
            "Missing or invalid file '%s' in update site '%s', can't get"
            " client minimum version for server version %s" %
            (info_file, self.update_site, server_version))
        try:
            if not self.local_update_site:
                url = urljoin(self.update_site, info_file)
            else:
                url = info_file
            info = self.esky_app.version_finder.open_url(url)
            version = json.loads(info.read())['nuxeoDriveMinVersion']
            log.debug(
                "Fetched client minimum version for server version %s"
                " from %s: %s", server_version, url, version)
            return version
        except HTTPError as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)
        except URLError as e:
            log.error(e, exc_info=True)
            raise UnavailableUpdateSite("Cannot connect to update site '%s'" %
                                        self.update_site)
        except Exception as e:
            log.error(e, exc_info=True)
            raise MissingUpdateSiteInfo(missing_msg)

    def get_latest_compatible_version(self, server_version):
        client_min_version = self.get_client_min_version(server_version)
        latest_version = None
        client_versions = self.find_versions()
        client_versions.append(self.get_current_latest_version())
        client_versions = sorted(client_versions, cmp=version_compare)
        for client_version in client_versions:
            if client_min_version <= client_version:
                server_min_version = self.get_server_min_version(
                    client_version)
                if server_min_version <= server_version:
                    latest_version = client_version
        if latest_version is None:
            raise MissingCompatibleVersion(
                "No client version compatible with server version %s"
                " available in update site '%s'" %
                (server_version, self.update_site))
        return latest_version

    def get_update_status(self, client_version, server_version):
        try:
            latest_version = self.get_latest_compatible_version(server_version)
            if (client_version == latest_version):
                log.info(
                    "Client version %s is up-to-date regarding server"
                    " version %s.", client_version, server_version)
                return (UPDATE_STATUS_UP_TO_DATE, None)

            client_min_version = self.get_client_min_version(server_version)
            server_min_version = self.get_server_min_version(client_version)

            if version_compare(client_version, client_min_version) < 0:
                log.info(
                    "Client version %s is lighter than %s, the minimum"
                    " version compatible with the server version %s."
                    " An upgrade to version %s is needed.", client_version,
                    client_min_version, server_version, latest_version)
                return (UPDATE_STATUS_UPGRADE_NEEDED, latest_version)

            if version_compare(server_version, server_min_version) < 0:
                log.info(
                    "Server version %s is lighter than %s, the minimum"
                    " version compatible with the client version %s."
                    " A downgrade to version %s is needed.", server_version,
                    server_min_version, client_version, latest_version)
                return (UPDATE_STATUS_DOWNGRADE_NEEDED, latest_version)

            log.info(
                "Client version %s is compatible with server version %s,"
                " yet an update is available: version %s.", client_version,
                server_version, latest_version)
            return (UPDATE_STATUS_UPDATE_AVAILABLE, latest_version)
        except UnavailableUpdateSite as e:
            log.warning(e)
            return (UPDATE_STATUS_UNAVAILABLE_SITE, None)
        except MissingUpdateSiteInfo as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_INFO, None)
        except MissingCompatibleVersion as e:
            log.warning(e)
            return (UPDATE_STATUS_MISSING_VERSION, None)

    def update(self, version):
        if sys.platform == 'win32':
            # Try to update frozen application with the given version. If it
            # fails with a permission error, escalate to root and try again.
            try:
                self.esky_app.get_root()
                self._do_update(version)
                self.esky_app.drop_root()
                return True
            except EnvironmentError as e:
                if e.errno == errno.EINVAL:
                    # Under Windows, this means that the sudo popup was
                    # rejected
                    self.esky_app.sudo_proxy = None
                    raise RootPrivilegeRequired(e)
                # Other EnvironmentError, probably not related to permissions
                raise UpdateError(e)
            except Exception as e:
                # Error during update process, not related to permissions
                raise UpdateError(e)
        else:
            try:
                self._do_update(version)
                return True
            except Exception as e:
                raise UpdateError(e)

    def _do_update(self, version):
        log.info("Starting application update process")
        log.info("Fetching version %s from update site %s", version,
                 self.update_site)
        self.esky_app.fetch_version(version)
        log.info("Installing version %s", version)
        self.esky_app.install_version(version)
        log.debug("Reinitializing Esky internal state")
        self.esky_app.reinitialize()
        log.info("Ended application update process")

    def cleanup(self, version):
        log.info("Uninstalling version %s", version)
        self.esky_app.uninstall_version(version)
        log.info("Cleaning up Esky application", version)
        self.esky_app.cleanup()

    def get_update_label(self, status):
        return UPDATE_STATUS_LABEL[status]

    def get_update_site(self):
        return self.update_site