def setUp(self):
     appdir = 'nxdrive/tests/resources/esky_app'
     version_finder = 'nxdrive/tests/resources/esky_versions'
     self.esky_app = MockEsky(appdir, version_finder=version_finder)
     self.manager = MockManager()
     self.updater = AppUpdater(self.manager,
                               esky_app=self.esky_app,
                               local_update_site=True)
Beispiel #2
0
 def _create_updater(self, update_check_delay):
     # Enable the capacity to extend the AppUpdater
     self._app_updater = AppUpdater(
         self,
         version_finder=self.get_version_finder(),
         check_interval=update_check_delay)
     self.started.connect(self._app_updater._thread.start)
     return self._app_updater
Beispiel #3
0
 def setUp(self):
     location = dirname(__file__)
     appdir = location + '/resources/esky_app'
     version_finder = location + '/resources/esky_versions'
     self.esky_app = MockEsky(appdir, version_finder=version_finder)
     self.manager = MockManager()
     self.updater = AppUpdater(self.manager,
                               esky_app=self.esky_app,
                               local_update_site=True)
Beispiel #4
0
 def _create_updater(self, update_check_delay):
     if (update_check_delay == 0):
         log.info("Update check delay is 0, disabling autoupdate")
         self._app_updater = FakeUpdater()
         return self._app_updater
     # Enable the capacity to extend the AppUpdater
     self._app_updater = AppUpdater(self, version_finder=self.get_version_finder(),
                                    check_interval=update_check_delay)
     self.started.connect(self._app_updater._thread.start)
     return self._app_updater
Beispiel #5
0
class Application(QApplication):
    """Main Nuxeo drive application controlled by a system tray icon + menu"""

    sync_thread = None

    def __init__(self, controller, options, argv=()):
        super(Application, self).__init__(list(argv))
        self.controller = controller
        self.options = options
        self.binding_info = {}

        # Put communication channel in place for intra and inter-thread
        # communication for UI change notifications
        self.communicator = Communicator()
        self.communicator.icon.connect(self.set_icon_state)
        self.communicator.stop.connect(self.handle_stop)
        self.communicator.change.connect(self.handle_change)
        self.communicator.invalid_credentials.connect(
            self.handle_invalid_credentials)
        self.communicator.update_check.connect(
            self.refresh_update_status)

        # Timer to spin the transferring icon
        self.icon_spin_timer = QtCore.QTimer()
        self.icon_spin_timer.timeout.connect(self.spin_transferring_icon)
        self.icon_spin_count = 0

        # Application update
        self.updater = None
        self.update_status = None
        self.update_version = None
        self.restart_updated_app = False

        # This is a windowless application mostly using the system tray
        self.setQuitOnLastWindowClosed(False)
        # Current state
        self.state = 'disabled'
        # Last state before suspend
        self.last_state = 'enabled'

        self.setup_systray()

        # Application update notification
        if self.controller.is_updated():
            notify_updated(self.controller.get_version())

        # Check if actions is required, separate method so it can be override
        self.init_checks()

    def init_checks(self):
        if self.controller.is_credentials_update_required():
            # Prompt for settings if needed (performs a check for application
            # update)
            self.settings()
        else:
            # Initial check for application update (then periodic checks will
            # be done by the synchronizer thread)
            self.refresh_update_status()
            # Start long running synchronization thread
            self.start_synchronization_thread()

    def get_systray_menu(self):
        return SystrayMenu(self, self.controller.list_server_bindings())

    def refresh_update_status(self):
        # TODO: first read update site URL from local configuration
        # See https://jira.nuxeo.com/browse/NXP-14403
        server_bindings = self.controller.list_server_bindings()
        if not server_bindings:
            log.warning("Found no server binding, thus no update site URL,"
                        " can't check for application update")
        elif self.state != 'paused':
            # Let's refresh_update_info of the first server binding
            sb = server_bindings[0]
            self.controller.refresh_update_info(sb.local_folder)
            # Use server binding's update site URL as a version finder to
            # build / update the application updater.
            update_url = sb.update_url
            server_version = sb.server_version
            if update_url is None or server_version is None:
                log.warning("Update site URL or server version unavailable,"
                            " as a consequence update features won't be"
                            " available")
                return
            if self.updater is None:
                # Build application updater if it doesn't exist
                try:
                    self.updater = AppUpdater(version_finder=update_url)
                except Exception as e:
                    log.warning(e)
                    return
            else:
                # If application updater exists, simply update its version
                # finder
                self.updater.set_version_finder(update_url)
            # Set update status and update version
            self.update_status, self.update_version = (
                        self.updater.get_update_status(
                            self.controller.get_version(), server_version))
            if self.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 self.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.info("Fetched information from update site %s: update"
                         " status = '%s', update version = '%s'",
                         self.updater.get_update_site(), self.update_status,
                         self.update_version)
                if self._is_update_required():
                    # Current client version not compatible with server
                    # version, upgrade or downgrade needed.
                    # Let's stop synchronization thread.
                    log.info("As current client version is not compatible with"
                             " server version, an upgrade or downgrade is"
                             " needed. Synchronization thread won't start"
                             " until then.")
                    self.stop_sync_thread()
                elif (self._is_update_available()
                      and self.controller.is_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.action_update(auto_update=True)
                    return
                elif (self._is_update_available()
                      and not self.controller.is_auto_update()):
                    # Update available and auto-update not checked, let's just
                    # update the systray icon and menu 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 icon and"
                             " menu and let the user explicitly choose to"
                             " update")
                else:
                    # Application is up-to-date
                    log.info("Application is up-to-date")
            self.state = self._get_current_active_state()
            self.update_running_icon()
            self.communicator.menu.emit()

    def _is_update_required(self):
        return self.update_status in [UPDATE_STATUS_UPGRADE_NEEDED,
                                      UPDATE_STATUS_DOWNGRADE_NEEDED]

    def _is_update_available(self):
        return self.update_status == UPDATE_STATUS_UPDATE_AVAILABLE

    def set_icon_state(self, state):
        """Execute systray icon change operations triggered by state change

        The synchronization thread can update the state info but cannot
        directly call QtGui widget methods. This should be executed by the main
        thread event loop, hence the delegation to this method that is
        triggered by a signal to allow for message passing between the 2
        threads.

        Return True of the icon has changed state.

        """
        if self.get_icon_state() == state:
            # Nothing to update
            return False
        # Handle animated transferring icon
        if state == 'transferring':
            self.icon_spin_timer.start(150)
        else:
            self.icon_spin_timer.stop()
            icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state)
            if icon is not None:
                self._tray_icon.setIcon(QtGui.QIcon(icon))
            else:
                log.warning('Icon not found: %s', icon)
        self._icon_state = state
        log.debug('Updated icon state to: %s', state)
        return True

    def get_icon_state(self):
        return getattr(self, '_icon_state', None)

    def spin_transferring_icon(self):
        icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png'
                         % (self.icon_spin_count + 1))
        self._tray_icon.setIcon(QtGui.QIcon(icon))
        self.icon_spin_count = (self.icon_spin_count + 1) % 10

    def suspend_resume(self):
        if self.state != 'paused':
            # Suspend sync
            if self._is_sync_thread_started():
                # A sync thread is active, first update last state, current
                # state, icon and menu.
                self.last_state = self.state
                # If sync thread is asleep (waiting for next sync batch) set
                # current state to 'paused' directly, else set current state
                # to 'suspending' waiting for feedback from sync thread.
                if self.state == 'asleep':
                    self.state = 'paused'
                else:
                    self.state = 'suspending'
                self.update_running_icon()
                self.communicator.menu.emit()
                # Suspend the synchronizer thread: it will call
                # notify_sync_suspended() then wait until it gets notified by
                # a call to resume().
                self.sync_thread.suspend()
            else:
                log.debug('No active synchronization thread, suspending sync'
                          ' has no effect, keeping current state: %s',
                          self.state)
        else:
            # Update state, icon and menu
            self.state = self.last_state
            self.update_running_icon()
            self.communicator.menu.emit()
            # Resume sync
            self.sync_thread.resume()

    def action_quit(self):
        self.quit_app_after_sync_stopped = True
        self.restart_updated_app = False
        self._stop()

    def action_update(self, auto_update=False):
        updated = False
        if auto_update:
            try:
                updated = self.updater.update(self.update_version)
            except Exception as e:
                log.error(e, exc_info=True)
                log.warning("An error occurred while trying to automatically"
                            " update Nuxeo Drive to version %s, setting"
                            " 'Auto update' to False", self.update_version)
                self.controller.set_auto_update(False)
        else:
            updated = prompt_update(self.controller,
                                    self._is_update_required(),
                                    self.controller.get_version(),
                                    self.update_version, self.updater)
        if updated:
            log.info("Will quit Nuxeo Drive and restart updated version %s",
                     self.update_version)
            self.quit_app_after_sync_stopped = True
            self.restart_updated_app = True
            self._stop()

    def stop_sync_thread(self):
        self.quit_app_after_sync_stopped = False
        self._stop()

    def _stop(self):
        if self._is_sync_thread_started():
            # A sync thread is active, first update state, icon and menu
            if self.quit_app_after_sync_stopped:
                self.state = 'stopping'
                self.update_running_icon()
                self.communicator.menu.emit()
            # Ask the controller to stop: the synchronization loop will break
            # and call notify_sync_stopped() which will finally emit a signal
            # to handle_stop() to quit the application.
            self.controller.stop()
            # Notify synchronization thread in case it was suspended
            self.sync_thread.resume()
        else:
            # Quit directly
            self.handle_stop()

    @QtCore.pyqtSlot()
    def handle_stop(self):
        if self.quit_app_after_sync_stopped:
            log.info('Quitting Nuxeo Drive')
            # Close thread-local Session
            log.debug("Calling Controller.dispose() from Qt Application to"
                      " close thread-local Session")
            self.controller.dispose()
            if self.restart_updated_app:
                # Restart application by loading updated executable into
                # current process
                log.debug("Exiting Qt application")
                self.quit()

                current_version = self.updater.get_active_version()
                updated_version = self.update_version
                log.info("Current application version: %s", current_version)
                log.info("Updated application version: %s", updated_version)

                executable = sys.executable
                # TODO NXP-13818: better handle this!
                if sys.platform == 'darwin':
                    executable = executable.replace('python',
                                                    self.get_mac_app())
                log.info("Current executable is: %s", executable)
                updated_executable = executable.replace(current_version,
                                                        updated_version)
                log.info("Updated executable is: %s", updated_executable)

                args = [updated_executable]
                args.extend(sys.argv[1:])
                log.info("Opening subprocess with args: %r", args)
                subprocess.Popen(args)
            else:
                self.quit()

    def get_mac_app(self):
        return 'ndrive'

    def update_running_icon(self):
        if self.state not in ['enabled', 'update_available', 'transferring']:
            self.communicator.icon.emit(self.state)
            return
        infos = self.binding_info.values()
        if len(infos) > 0 and any(i.online for i in infos):
            self.communicator.icon.emit(self.state)
        else:
            self.communicator.icon.emit('disabled')

    def notify_change(self, doc_pair, old_state):
        self.communicator.change.emit(doc_pair, old_state)

    def handle_change(self, doc_pair, old_state):
        pass

    def notify_local_folders(self, server_bindings):
        """Cleanup unbound server bindings if any"""
        local_folders = [sb.local_folder for sb in server_bindings]
        refresh = False
        for registered_folder in self.binding_info.keys():
            if registered_folder not in local_folders:
                del self.binding_info[registered_folder]
                refresh = True
        for sb in server_bindings:
            if sb.local_folder not in self.binding_info:
                self.binding_info[sb.local_folder] = BindingInfo(sb)
                refresh = True
        if refresh:
            log.debug(u'Detected changes in the list of local folders: %s',
                      u", ".join(local_folders))
            self.update_running_icon()
            self.communicator.menu.emit()

    def get_binding_info(self, server_binding):
        local_folder = server_binding.local_folder
        if local_folder not in self.binding_info:
            self.binding_info[local_folder] = BindingInfo(server_binding)
        return self.binding_info[local_folder]

    def notify_sync_started(self):
        log.debug('Synchronization started')
        # Update state, icon and menu
        self.state = self._get_current_active_state()
        self.update_running_icon()
        self.communicator.menu.emit()

    def notify_sync_stopped(self):
        log.debug('Synchronization stopped')
        self.sync_thread = None
        # Send stop signal
        self.communicator.stop.emit()

    def notify_sync_asleep(self):
        # Update state to 'asleep' when sync thread is going to sleep
        # (waiting for next sync batch)
        self.state = 'asleep'

    def notify_sync_woken_up(self):
        # Update state to current active state when sync thread is woken up and
        # was not suspended
        if self.state != 'paused':
            self.state = self._get_current_active_state()
        else:
            self.last_state = self._get_current_active_state()

    def notify_sync_suspended(self):
        log.debug('Synchronization suspended')
        # Update state, icon and menu
        self.state = 'paused'
        self.update_running_icon()
        self.communicator.menu.emit()

    def notify_online(self, server_binding):
        info = self.get_binding_info(server_binding)
        if not info.online:
            # Mark binding as offline and update UI
            log.debug('Switching to online mode for: %s',
                      server_binding.local_folder)
            info.online = True
            self.update_running_icon()
            self.communicator.menu.emit()

    def notify_offline(self, server_binding, exception):
        info = self.get_binding_info(server_binding)
        code = getattr(exception, 'code', None)
        if code is not None:
            reason = "Server returned HTTP code %r" % code
        else:
            reason = str(exception)
        local_folder = server_binding.local_folder
        if info.online:
            # Mark binding as offline and update UI
            log.debug('Switching to offline mode (reason: %s) for: %s',
                      reason, local_folder)
            info.online = False
            self.state = 'disabled'
            self.update_running_icon()
            self.communicator.menu.emit()

        if code == 401:
            log.debug('Detected invalid credentials for: %s', local_folder)
            self.communicator.invalid_credentials.emit(local_folder)

    def notify_pending(self, server_binding, n_pending, or_more=False):
        # Update icon
        if n_pending > 0:
            self.state = 'transferring'
        else:
            self.state = self._get_current_active_state()
        self.update_running_icon()

        if server_binding is not None:
            local_folder = server_binding.local_folder
            info = self.get_binding_info(server_binding)
            if n_pending != info.n_pending:
                log.debug("%d pending operations for: %s", n_pending,
                          local_folder)
                if n_pending == 0 and info.n_pending > 0:
                    current_time = time.time()
                    log.debug("Updating last ended synchronization date"
                              " to %s for: %s",
                              time.strftime(TIME_FORMAT_PATTERN,
                                            time.localtime(current_time)),
                              local_folder)
                    server_binding.last_ended_sync_date = current_time
                    self.controller.get_session().commit()
                self.communicator.menu.emit()
            # Update pending stats
            info.n_pending = n_pending
            info.has_more_pending = or_more

            if not info.online:
                log.debug("Switching to online mode for: %s", local_folder)
                # Mark binding as online and update UI
                info.online = True
                self.update_running_icon()
                self.communicator.menu.emit()

    def notify_check_update(self):
        log.debug('Checking for application update')
        self.communicator.update_check.emit()

    def _get_current_active_state(self):
        if self._is_update_available():
            return 'update_available'
        elif self._is_update_required():
            return 'disabled'
        elif self.state == 'paused':
            return 'paused'
        else:
            return 'enabled'

    def setup_systray(self):
        self._tray_icon = QtGui.QSystemTrayIcon()
        self._tray_icon.setToolTip('Nuxeo Drive')
        self.update_running_icon()
        self._tray_icon.show()
        self.tray_icon_menu = self.get_systray_menu()
        self._tray_icon.setContextMenu(self.tray_icon_menu)
        self.communicator.menu.connect(self.update_menu)

    def update_menu(self):
        self.tray_icon_menu.update_menu(self.controller.list_server_bindings())

    @QtCore.pyqtSlot(str)
    def handle_invalid_credentials(self, local_folder):
        sb = self.controller.get_server_binding(unicode(local_folder))
        sb.invalidate_credentials()
        self.controller.get_session().commit()
        self.communicator.menu.emit()

    def settings(self):
        sb_settings = self.controller.get_server_binding_settings()
        proxy_settings = self.controller.get_proxy_settings()
        general_settings = self.controller.get_general_settings()
        version = self.controller.get_version()
        settings_accepted = prompt_settings(self.controller, sb_settings,
                                            proxy_settings, general_settings,
                                            version)
        if settings_accepted:
            # Check for application udpate
            self.refresh_update_status()
            # Start synchronization thread if needed
            self.start_synchronization_thread()
        return settings_accepted

    def start_synchronization_thread(self):
        # Make sure an application update is not required and synchronization
        # thread is not already started before actually starting it
        if (not self._is_update_required()
            and not self._is_sync_thread_started()):
            delay = getattr(self.options, 'delay', 5.0)
            max_sync_step = getattr(self.options, 'max_sync_step', 10)
            update_check_delay = getattr(self.options, 'update_check_delay',
                                         3600)
            # Controller and its database session pool are thread safe,
            # hence reuse it directly
            self.controller.synchronizer.register_frontend(self)
            self.controller.synchronizer.delay = delay
            self.controller.synchronizer.max_sync_step = max_sync_step
            self.controller.synchronizer.update_check_delay = (
                                                update_check_delay)

            self.sync_thread = SynchronizerThread(self.controller)
            log.info("Starting new synchronization thread %r",
                     self.sync_thread)
            self.sync_thread.start()
            log.info("Synchronization thread %r started", self.sync_thread)

    def _is_sync_thread_started(self):
        return self.sync_thread is not None and self.sync_thread.isAlive()

    def event(self, event):
        """Handle URL scheme events under OSX"""
        log.trace("Received Qt application event")
        if hasattr(event, 'url'):
            url = str(event.url().toString())
            log.debug("Event URL: %s", url)
            try:
                info = parse_protocol_url(url)
                log.debug("URL info: %r", info)
                if info is not None:
                    log.debug("Received nxdrive URL scheme event: %s", url)
                    if info.get('command') == 'edit':
                        # This is a quick operation, no need to fork a QThread
                        self.controller.launch_file_editor(
                            info['server_url'], info['item_id'])
            except:
                log.error("Error handling URL event: %s", url, exc_info=True)
        return super(Application, self).event(event)
Beispiel #6
0
 def refresh_update_status(self):
     # TODO: first read update site URL from local configuration
     # See https://jira.nuxeo.com/browse/NXP-14403
     server_bindings = self.controller.list_server_bindings()
     if not server_bindings:
         log.warning("Found no server binding, thus no update site URL,"
                     " can't check for application update")
     elif self.state != 'paused':
         # Let's refresh_update_info of the first server binding
         sb = server_bindings[0]
         self.controller.refresh_update_info(sb.local_folder)
         # Use server binding's update site URL as a version finder to
         # build / update the application updater.
         update_url = sb.update_url
         server_version = sb.server_version
         if update_url is None or server_version is None:
             log.warning("Update site URL or server version unavailable,"
                         " as a consequence update features won't be"
                         " available")
             return
         if self.updater is None:
             # Build application updater if it doesn't exist
             try:
                 self.updater = AppUpdater(version_finder=update_url)
             except Exception as e:
                 log.warning(e)
                 return
         else:
             # If application updater exists, simply update its version
             # finder
             self.updater.set_version_finder(update_url)
         # Set update status and update version
         self.update_status, self.update_version = (
                     self.updater.get_update_status(
                         self.controller.get_version(), server_version))
         if self.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 self.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.info("Fetched information from update site %s: update"
                      " status = '%s', update version = '%s'",
                      self.updater.get_update_site(), self.update_status,
                      self.update_version)
             if self._is_update_required():
                 # Current client version not compatible with server
                 # version, upgrade or downgrade needed.
                 # Let's stop synchronization thread.
                 log.info("As current client version is not compatible with"
                          " server version, an upgrade or downgrade is"
                          " needed. Synchronization thread won't start"
                          " until then.")
                 self.stop_sync_thread()
             elif (self._is_update_available()
                   and self.controller.is_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.action_update(auto_update=True)
                 return
             elif (self._is_update_available()
                   and not self.controller.is_auto_update()):
                 # Update available and auto-update not checked, let's just
                 # update the systray icon and menu 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 icon and"
                          " menu and let the user explicitly choose to"
                          " update")
             else:
                 # Application is up-to-date
                 log.info("Application is up-to-date")
         self.state = self._get_current_active_state()
         self.update_running_icon()
         self.communicator.menu.emit()
class TestUpdater(unittest.TestCase):
    def setUp(self):
        appdir = 'nxdrive/tests/resources/esky_app'
        version_finder = 'nxdrive/tests/resources/esky_versions'
        self.esky_app = MockEsky(appdir, version_finder=version_finder)
        self.manager = MockManager()
        self.updater = AppUpdater(self.manager,
                                  esky_app=self.esky_app,
                                  local_update_site=True)

    def test_version_compare(self):
        # Compare server versions
        # Releases
        self.assertEquals(version_compare('5.9.3', '5.9.3'), 0)
        self.assertEquals(version_compare('5.9.3', '5.9.2'), 1)
        self.assertEquals(version_compare('5.9.2', '5.9.3'), -1)
        self.assertEquals(version_compare('5.9.3', '5.8'), 1)
        self.assertEquals(version_compare('5.8', '5.6.0'), 1)
        self.assertEquals(version_compare('5.9.1', '5.9.0.1'), 1)
        self.assertEquals(version_compare('6.0', '5.9.3'), 1)
        self.assertEquals(version_compare('5.10', '5.1.2'), 1)

        # Date-based
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.4-I20140415_0120'), 0)
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.4-I20140410_0120'), 1)
        self.assertEquals(
            version_compare('5.9.4-I20140515_0120', '5.9.4-I20140415_0120'), 1)
        self.assertEquals(
            version_compare('5.9.4-I20150102_0120', '5.9.4-I20143112_0120'), 1)
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.3-I20140415_0120'), 1)

        # Releases and date-based
        self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.3'), 1)
        self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.4'), -1)
        self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.5'), -1)

        self.assertEquals(version_compare('5.9.3', '5.9.4-I20140415_0120'), -1)
        self.assertEquals(version_compare('5.9.4', '5.9.4-I20140415_0120'), 1)
        self.assertEquals(version_compare('5.9.5', '5.9.4-I20140415_0120'), 1)

        # Snapshots
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.4-SNAPSHOT'),
                          0)
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.3-SNAPSHOT'),
                          1)
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.8-SNAPSHOT'), 1)
        self.assertEquals(version_compare('5.9.3-SNAPSHOT', '5.9.4-SNAPSHOT'),
                          -1)
        self.assertEquals(version_compare('5.8-SNAPSHOT', '5.9.4-SNAPSHOT'),
                          -1)

        # Releases and snapshots
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.3'), 1)
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.4'), -1)
        self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.5'), -1)

        self.assertEquals(version_compare('5.9.3', '5.9.4-SNAPSHOT'), -1)
        self.assertEquals(version_compare('5.9.4', '5.9.4-SNAPSHOT'), 1)
        self.assertEquals(version_compare('5.9.5', '5.9.4-SNAPSHOT'), 1)

        # Date-based and snapshots
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.3-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.5-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.9.3-SNAPSHOT', '5.9.4-I20140415_0120'), -1)
        self.assertEquals(
            version_compare('5.9.5-SNAPSHOT', '5.9.4-I20140415_0120'), 1)
        # Can't decide, consider as equal
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.9.4-SNAPSHOT'), 0)
        self.assertEquals(
            version_compare('5.9.4-SNAPSHOT', '5.9.4-I20140415_0120'), 0)

        # Hotfixes
        self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF14'), 0)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF13'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF15'), -1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.6.0-HF35'), 1)
        self.assertEquals(version_compare('5.6.0-H35', '5.8.0-HF14'), -1)

        # Releases and hotfixes
        self.assertEquals(version_compare('5.8.0-HF14', '5.6'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.9.1'), -1)

        self.assertEquals(version_compare('5.6', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.8', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.9.1', '5.8.0-HF14'), 1)

        # Date-based and hotfixes
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.8.0-HF14'), 1)
        self.assertEquals(
            version_compare('5.8.1-I20140415_0120', '5.8.0-HF14'), 1)
        self.assertEquals(
            version_compare('5.8.0-I20140415_0120', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.8-I20140415_0120', '5.8.0-HF14'),
                          -1)
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.10.0-HF01'), -1)

        self.assertEquals(
            version_compare('5.8.0-HF14', '5.9.4-I20140415_0120'), -1)
        self.assertEquals(
            version_compare('5.8.0-HF14', '5.8.1-I20140415_0120'), -1)
        self.assertEquals(
            version_compare('5.8.0-HF14', '5.8.0-I20140415_0120'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8-I20140415_0120'),
                          1)
        self.assertEquals(
            version_compare('5.10.0-HF01', '5.9.4-I20140415_0120'), 1)

        # Snaphsots and hotfixes
        self.assertEquals(version_compare('5.8.0-HF14', '5.7.1-SNAPSHOT'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-SNAPSHOT'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.8-SNAPSHOT'), 1)
        self.assertEquals(version_compare('5.8.0-HF14', '5.9.1-SNAPSHOT'), -1)

        self.assertEquals(version_compare('5.7.1-SNAPSHOT', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.8.0-SNAPSHOT', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.8-SNAPSHOT', '5.8.0-HF14'), -1)
        self.assertEquals(version_compare('5.9.1-SNAPSHOT', '5.8.0-HF14'), 1)

        # Snapshot hotfixes
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), 0)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF13-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF15-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.6.0-HF35-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.6.0-H35-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1)

        # Releases and snapshot hotfixes
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.6'), 1)
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8'), 1)
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.9.1'), -1)

        self.assertEquals(version_compare('5.6', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(version_compare('5.8', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(version_compare('5.9.1', '5.8.0-HF14-SNAPSHOT'), 1)

        # Date-based and snapshot hotfixes
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.8.0-HF14-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.8.0-I20140415_0120', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.9.4-I20140415_0120', '5.10.0-HF01-SNAPSHOT'),
            -1)

        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.9.4-I20140415_0120'), -1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-I20140415_0120'), 1)
        self.assertEquals(
            version_compare('5.10.0-HF01-SNAPSHOT', '5.9.4-I20140415_0120'), 1)

        # Snaphsots and snapshot hotfixes
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.7.1-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-SNAPSHOT'), 1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.9.1-SNAPSHOT'), -1)

        self.assertEquals(
            version_compare('5.7.1-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.8-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.8.0-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1)
        self.assertEquals(
            version_compare('5.9.1-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), 1)

        # Hotfixes and snapshot hotfixes
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.6.0-HF35'),
                          1)
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF13'),
                          1)
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF14'),
                          -1)
        self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF15'),
                          -1)
        self.assertEquals(
            version_compare('5.8.0-HF14-SNAPSHOT', '5.10.0-HF01'), -1)

        # Compare client versions
        self.assertEquals(version_compare('0.1', '1.0'), -1)
        self.assertEquals(version_compare('1.0', '1.0'), 0)
        self.assertEquals(version_compare('1.3.0424', '1.3.0424'), 0)
        self.assertEquals(version_compare('1.3.0524', '1.3.0424'), 1)
        self.assertEquals(version_compare('1.4', '1.3.0524'), 1)
        self.assertEquals(version_compare('1.4.0622', '1.3.0524'), 1)
        self.assertEquals(version_compare('1.10', '1.1.2'), 1)
        self.assertEquals(version_compare('2.1.0528', '1.10'), 1)
        self.assertEquals(version_compare('2.0.0626', '2.0.806'), -1)
        self.assertEquals(version_compare('2.0.0805', '2.0.806'), -1)
        self.assertEquals(version_compare('2.0.0905', '2.0.806'), 1)
        self.assertEquals(version_compare('2.0.805', '2.0.1206'), -1)

    def test_get_active_version(self):
        # Active version is None because Esky instance is built from a
        # directory, see Esky._init_from_appdir
        self.assertIsNone(self.updater.get_active_version())

    def test_get_current_latest_version(self):
        self.assertEquals(self.updater.get_current_latest_version(),
                          '1.3.0424')

    def test_find_versions(self):
        versions = self.updater.find_versions()
        self.assertEquals(versions, ['1.3.0524', '1.4.0622'])

    def test_get_server_min_version(self):
        # Unexisting version
        self.assertRaises(MissingUpdateSiteInfo,
                          self.updater.get_server_min_version, '4.6.2012')
        self.assertEquals(self.updater.get_server_min_version('1.3.0424'),
                          '5.8')
        self.assertEquals(self.updater.get_server_min_version('1.3.0524'),
                          '5.9.1')
        self.assertEquals(self.updater.get_server_min_version('1.4.0622'),
                          '5.9.2')

    def test_get_client_min_version(self):
        # Unexisting version
        self.assertRaises(MissingUpdateSiteInfo,
                          self.updater._get_client_min_version, '5.6')
        self.assertEquals(self.updater._get_client_min_version('5.8'),
                          '1.2.0110')
        self.assertEquals(self.updater._get_client_min_version('5.9.1'),
                          '1.3.0424')
        self.assertEquals(self.updater._get_client_min_version('5.9.2'),
                          '1.3.0424')
        self.assertEquals(self.updater._get_client_min_version('5.9.3'),
                          '1.4.0622')
        self.assertEquals(self.updater._get_client_min_version('5.9.4'),
                          '1.5.0715')

    def _get_latest_compatible_version(self, version):
        self.manager.clean_engines()
        self.manager.add_engine(version)
        return self.updater.get_latest_compatible_version()

    def test_get_latest_compatible_version(self):
        # No update info available for server version
        self.assertRaises(MissingUpdateSiteInfo,
                          self._get_latest_compatible_version, '5.6')
        # No compatible client version with server version
        self.assertRaises(MissingCompatibleVersion,
                          self._get_latest_compatible_version, '5.9.4')
        # Compatible versions
        self.assertEqual(self._get_latest_compatible_version('5.9.3'),
                         '1.4.0622')
        self.assertEqual(self._get_latest_compatible_version('5.9.2'),
                         '1.4.0622')
        self.assertEqual(self._get_latest_compatible_version('5.9.1'),
                         '1.3.0524')
        self.assertEqual(self._get_latest_compatible_version('5.8'),
                         '1.3.0424')

    def _get_update_status(self,
                           client_version,
                           server_version,
                           add_version=None):
        self.manager.set_version(client_version)
        self.manager.clean_engines()
        self.manager.add_engine(server_version)
        if add_version is not None:
            self.manager.add_engine(add_version)
        return self.updater._get_update_status()

    def test_get_update_status(self):
        # No update info available (missing client version info)
        status = self._get_update_status('1.2.0207', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_MISSING_INFO, None))

        # No update info available (missing server version info)
        status = self._get_update_status('1.3.0424', '5.6')
        self.assertEquals(status, (UPDATE_STATUS_MISSING_INFO, None))

        # No compatible client version with server version
        status = self._get_update_status('1.4.0622', '5.9.4')
        self.assertEquals(status, (UPDATE_STATUS_MISSING_VERSION, None))

        # Upgraded needed
        status = self._get_update_status('1.3.0424', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622'))
        status = self._get_update_status('1.3.0524', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622'))

        # Downgrade needed
        status = self._get_update_status('1.3.0524', '5.8')
        self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424'))
        status = self._get_update_status('1.4.0622', '5.8')
        self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424'))
        status = self._get_update_status('1.4.0622', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0524'))

        # Upgrade available
        status = self._get_update_status('1.3.0424', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.3.0524'))
        status = self._get_update_status('1.3.0424', '5.9.2')
        self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.4.0622'))
        status = self._get_update_status('1.3.0524', '5.9.2')
        self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.4.0622'))
        # Up-to-date
        status = self._get_update_status('1.3.0424', '5.8')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))
        status = self._get_update_status('1.3.0524', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))
        status = self._get_update_status('1.4.0622', '5.9.2')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))
        status = self._get_update_status('1.4.0622', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))

        # Test multi server
        status = self._get_update_status('1.3.0524', '5.9.2', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))
        # Force upgrade for the 5.9.3 server
        status = self._get_update_status('1.3.0524', '5.9.2', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622'))
        # No compatible version with 5.9.1 and 5.9.3
        status = self._get_update_status('1.3.0524', '5.9.1', '5.9.3')
        self.assertEquals(status, (UPDATE_STATUS_MISSING_VERSION, None))
        # Need to downgrade for 5.8 server
        status = self._get_update_status('1.3.0524', '5.8', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424'))
        # Up to date once downgrade
        status = self._get_update_status('1.3.0424', '5.8', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None))
        # Limit the range of upgrade because of 5.9.1 server
        status = self._get_update_status('1.3.0424', '5.9.2', '5.9.1')
        self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.3.0524'))
Beispiel #8
0
class Manager(QtCore.QObject):
    '''
    classdocs
    '''
    proxyUpdated = QtCore.pyqtSignal(object)
    clientUpdated = QtCore.pyqtSignal(object, object)
    engineNotFound = QtCore.pyqtSignal(object)
    newEngine = QtCore.pyqtSignal(object)
    dropEngine = QtCore.pyqtSignal(object)
    initEngine = QtCore.pyqtSignal(object)
    aboutToStart = QtCore.pyqtSignal(object)
    started = QtCore.pyqtSignal()
    stopped = QtCore.pyqtSignal()
    suspended = QtCore.pyqtSignal()
    resumed = QtCore.pyqtSignal()
    _singleton = None

    @staticmethod
    def get():
        return Manager._singleton

    def __init__(self, options):
        '''
        Constructor
        '''
        if Manager._singleton is not None:
            raise Exception("Only one instance of Manager can be create")
        Manager._singleton = self
        super(Manager, self).__init__()

        # Let's bypass HTTPS verification unless --consider-ssl-errors is passed
        # since many servers unfortunately have invalid certificates.
        # See https://www.python.org/dev/peps/pep-0476/
        # and https://jira.nuxeo.com/browse/NXDRIVE-506
        if not options.consider_ssl_errors:
            log.warn("--consider-ssl-errors option is False, won't verify HTTPS certificates")
            import ssl
            try:
                _create_unverified_https_context = ssl._create_unverified_context
            except AttributeError:
                log.info("Legacy Python that doesn't verify HTTPS certificates by default")
            else:
                log.info("Handle target environment that doesn't support HTTPS verification:"
                         " globally disable verification by monkeypatching the ssl module though highly discouraged")
                ssl._create_default_https_context = _create_unverified_https_context
        else:
            log.info("--consider-ssl-errors option is True, will verify HTTPS certificates")
        self._autolock_service = None
        self.nxdrive_home = os.path.expanduser(options.nxdrive_home)
        self.nxdrive_home = os.path.realpath(self.nxdrive_home)
        if not os.path.exists(self.nxdrive_home):
            os.mkdir(self.nxdrive_home)
        self.remote_watcher_delay = options.delay
        self._nofscheck = options.nofscheck
        self._debug = options.debug
        self._engine_definitions = None
        self._engine_types = dict()
        from nxdrive.engine.next.engine_next import EngineNext
        from nxdrive.engine.engine import Engine
        self._engine_types["NXDRIVE"] = Engine
        self._engine_types["NXDRIVENEXT"] = EngineNext
        self._engines = None
        self.proxies = None
        self.proxy_exceptions = None
        self._app_updater = None
        self._dao = None
        self._create_dao()
        if options.proxy_server is not None:
            proxy = ProxySettings()
            proxy.from_url(options.proxy_server)
            proxy.save(self._dao)
        # Now we can update the logger if needed
        if options.log_level_file is not None:
            # Set the log_level_file option
            handler = self._get_file_log_handler()
            if handler is not None:
                handler.setLevel(options.log_level_file)
                # Store it in the database
                self._dao.update_config("log_level_file", str(handler.level))
        else:
            # No log_level provide, use the one from db default is INFO
            self._update_logger(int(self._dao.get_config("log_level_file", "20")))
        # Add auto lock on edit
        res = self._dao.get_config("direct_edit_auto_lock")
        if res is None:
            self._dao.update_config("direct_edit_auto_lock", "1")
        # Persist update URL infos
        self._dao.update_config("update_url", options.update_site_url)
        self._dao.update_config("beta_update_url", options.beta_update_site_url)
        self.refresh_proxies()
        self._os = AbstractOSIntegration.get(self)
        # Create DirectEdit
        self._create_autolock_service()
        self._create_direct_edit(options.protocol_url)
        # Create notification service
        self._script_engine = None
        self._script_object = None
        self._create_notification_service()
        self._started = False
        # Pause if in debug
        self._pause = self.is_debug()
        self.device_id = self._dao.get_config("device_id")
        self.updated = False  # self.update_version()
        if self.device_id is None:
            self.generate_device_id()

        self.load()

        # Create the application update verification thread
        self._create_updater(options.update_check_delay)

        # Force language
        if options.force_locale is not None:
            self.set_config("locale", options.force_locale)
        # Setup analytics tracker
        self._tracker = None
        if self.get_tracking():
            self._create_tracker()

    def _get_file_log_handler(self):
        # Might store it in global static
        return FILE_HANDLER

    def get_metrics(self):
        result = dict()
        result["version"] = self.get_version()
        result["auto_start"] = self.get_auto_start()
        result["auto_update"] = self.get_auto_update()
        result["beta_channel"] = self.get_beta_channel()
        result["device_id"] = self.get_device_id()
        result["tracker_id"] = self.get_tracker_id()
        result["tracking"] = self.get_tracking()
        result["qt_version"] = QtCore.QT_VERSION_STR
        result["pyqt_version"] = QtCore.PYQT_VERSION_STR
        result["python_version"] = platform.python_version()
        result["platform"] = platform.system()
        result["appname"] = self.get_appname()
        return result

    def open_help(self):
        self.open_local_file("http://doc.nuxeo.com/display/USERDOC/Nuxeo+Drive")

    def get_log_level(self):
        handler = self._get_file_log_handler()
        if handler:
            return handler.level
        return logging.getLogger().getEffectiveLevel()

    def set_log_level(self, log_level):
        self._dao.update_config("log_level_file", str(log_level))
        self._update_logger(log_level)

    def _update_logger(self, log_level):
        logging.getLogger().setLevel(
                        min(log_level, logging.getLogger().getEffectiveLevel()))
        handler = self._get_file_log_handler()
        if handler:
            handler.setLevel(log_level)

    def get_osi(self):
        return self._os

    def _handle_os(self):
        # Be sure to register os
        self._os.register_contextual_menu()
        self._os.register_protocol_handlers()
        if self.get_auto_start():
            self._os.register_startup()

    def get_appname(self):
        return "Nuxeo Drive"

    def is_debug(self):
        return self._debug

    def is_checkfs(self):
        return not self._nofscheck

    def get_device_id(self):
        return self.device_id

    def get_notification_service(self):
        return self._notification_service

    def _create_notification_service(self):
        # Dont use it for now
        from nxdrive.notification import DefaultNotificationService
        self._notification_service = DefaultNotificationService(self)
        return self._notification_service

    def get_autolock_service(self):
        return self._autolock_service

    def _create_autolock_service(self):
        from nxdrive.autolocker import ProcessAutoLockerWorker
        self._autolock_service = ProcessAutoLockerWorker(30, self)
        self.started.connect(self._autolock_service._thread.start)
        return self._autolock_service

    def _create_tracker(self):
        from nxdrive.engine.tracker import Tracker
        self._tracker = Tracker(self)
        # Start the tracker when we launch
        self.started.connect(self._tracker._thread.start)
        return self._tracker

    def get_tracker_id(self):
        if self.get_tracking() and self._tracker is not None:
            return self._tracker.uid
        return ""

    def get_tracker(self):
        return self._tracker

    def _get_db(self):
        return os.path.join(normalized_path(self.nxdrive_home), "manager.db")

    def get_dao(self):
        return self._dao

    def _migrate(self):
        from nxdrive.engine.dao.sqlite import ManagerDAO
        self._dao = ManagerDAO(self._get_db())
        old_db = os.path.join(normalized_path(self.nxdrive_home), "nxdrive.db")
        if os.path.exists(old_db):
            import sqlite3
            from nxdrive.engine.dao.sqlite import CustomRow
            conn = sqlite3.connect(old_db)
            conn.row_factory = CustomRow
            c = conn.cursor()
            cfg = c.execute("SELECT * FROM device_config LIMIT 1").fetchone()
            if cfg is not None:
                self.device_id = cfg.device_id
                self._dao.update_config("device_id", cfg.device_id)
                self._dao.update_config("proxy_config", cfg.proxy_config)
                self._dao.update_config("proxy_type", cfg.proxy_type)
                self._dao.update_config("proxy_server", cfg.proxy_server)
                self._dao.update_config("proxy_port", cfg.proxy_port)
                self._dao.update_config("proxy_authenticated", cfg.proxy_authenticated)
                self._dao.update_config("proxy_username", cfg.proxy_username)
                self._dao.update_config("auto_update", cfg.auto_update)
            # Copy first server binding
            rows = c.execute("SELECT * FROM server_bindings").fetchall()
            if not rows:
                return
            first_row = True
            for row in rows:
                row.url = row.server_url
                log.debug("Binding server from Nuxeo Drive V1: [%s, %s]", row.url, row.remote_user)
                row.username = row.remote_user
                row.password = None
                row.token = row.remote_token
                row.no_fscheck = True
                engine = self.bind_engine(self._get_default_server_type(), row["local_folder"],
                                          self._get_engine_name(row.url), row, starts=False)
                log.trace("Resulting server binding remote_token %r", row.remote_token)
                if first_row:
                    first_engine_def = row
                    first_engine = engine
                    first_row = False
                else:
                    engine.dispose_db()
            # Copy filters for first engine as V1 only supports filtering for the first server binding
            filters = c.execute("SELECT * FROM filters")
            for filter_obj in filters:
                if first_engine_def.local_folder != filter_obj.local_folder:
                    continue
                log.trace("Filter Row from DS1 %r", filter_obj)
                first_engine.add_filter(filter_obj["path"])
            first_engine.dispose_db()

    def _create_dao(self):
        from nxdrive.engine.dao.sqlite import ManagerDAO
        if not os.path.exists(self._get_db()):
            try:
                self._migrate()
                return
            except Exception as e:
                log.error(e, exc_info=True)
        self._dao = ManagerDAO(self._get_db())

    def _create_updater(self, update_check_delay):
        if (update_check_delay == 0):
            log.info("Update check delay is 0, disabling autoupdate")
            self._app_updater = FakeUpdater()
            return self._app_updater
        # Enable the capacity to extend the AppUpdater
        self._app_updater = AppUpdater(self, version_finder=self.get_version_finder(),
                                       check_interval=update_check_delay)
        self.started.connect(self._app_updater._thread.start)
        return self._app_updater

    def get_version_finder(self, refresh_engines=False):
        # Used by extended application to inject version finder
        if self.get_beta_channel():
            log.debug('Update beta channel activated')
            update_site_url = self._get_beta_update_url(refresh_engines)
        else:
            update_site_url = self._get_update_url(refresh_engines)
        if update_site_url is None:
            update_site_url = DEFAULT_UPDATE_SITE_URL
        if not update_site_url.endswith('/'):
            update_site_url += '/'
        return update_site_url

    def _get_update_url(self, refresh_engines):
        update_url = self._dao.get_config("update_url", DEFAULT_UPDATE_SITE_URL)
        # If update site URL is not overridden in config.ini nor through the command line, refresh engine update infos
        # and use first engine configuration
        if update_url == DEFAULT_UPDATE_SITE_URL:
            try:
                if refresh_engines:
                    self._refresh_engine_update_infos()
                engines = self.get_engines()
                if engines:
                    first_engine = engines.itervalues().next()
                    update_url = first_engine.get_update_url()
                    log.debug('Update site URL has not been overridden in config.ini nor through the command line,'
                              ' using configuration from first engine [%s]: %s', first_engine._name, update_url)
            except URLError as e:
                log.error('Cannot refresh engine update infos, using default update site URL', exc_info=True)
        return update_url

    def _get_beta_update_url(self, refresh_engines):
        beta_update_url = self._dao.get_config("beta_update_url")
        if beta_update_url is None:
            try:
                if refresh_engines:
                    self._refresh_engine_update_infos()
                engines = self.get_engines()
                if engines:
                    for engine in engines.itervalues():
                        beta_update_url = engine.get_beta_update_url()
                        if beta_update_url is not None:
                            log.debug('Beta update site URL has not been defined in config.ini nor through the command'
                                      ' line, using configuration from engine [%s]: %s', engine._name, beta_update_url)
                            return beta_update_url
            except URLError:
                log.exception('Cannot refresh engine update infos, not using beta update site URL')
        return beta_update_url

    def is_beta_channel_available(self):
        return self._get_beta_update_url(False) is not None

    def get_updater(self):
        return self._app_updater

    def refresh_update_status(self):
        if self.get_updater() is not None:
            self.get_updater().refresh_status()

    def _refresh_engine_update_infos(self):
        log.debug('Refreshing engine infos')
        engines = self.get_engines()
        if engines:
            for engine in engines.itervalues():
                engine.get_update_infos()

    def _create_direct_edit(self, url):
        from nxdrive.direct_edit import DirectEdit
        self._direct_edit = DirectEdit(self, os.path.join(normalized_path(self.nxdrive_home), "edit"), url)
        self.started.connect(self._direct_edit._thread.start)
        return self._direct_edit

    def get_direct_edit(self):
        return self._direct_edit

    def is_paused(self):
        return self._pause

    def resume(self, euid=None):
        if not self._pause:
            return
        self._pause = False
        for uid, engine in self._engines.items():
            if euid is not None and euid != uid:
                continue
            log.debug("Resume engine %s", uid)
            engine.resume()
        self.resumed.emit()

    def suspend(self, euid=None):
        if self._pause:
            return
        self._pause = True
        for uid, engine in self._engines.items():
            if euid is not None and euid != uid:
                continue
            log.debug("Suspend engine %s", uid)
            engine.suspend()
        self.suspended.emit()

    def stop(self, euid=None):
        for uid, engine in self._engines.items():
            if euid is not None and euid != uid:
                continue
            if engine.is_started():
                log.debug("Stop engine %s", uid)
                engine.stop()
        self.stopped.emit()

    def start(self, euid=None):
        self._started = True
        for uid, engine in self._engines.items():
            if euid is not None and euid != uid:
                continue
            if not self._pause:
                self.aboutToStart.emit(engine)
                log.debug("Launch engine %s", uid)
                try:
                    engine.start()
                except Exception as e:
                    log.debug("Could not start the engine: %s [%r]", uid, e)
        log.debug("Emitting started")
        # Check only if manager is started
        self._handle_os()
        self.started.emit()

    def load(self):
        if self._engine_definitions is None:
            self._engine_definitions = self._dao.get_engines()
        in_error = dict()
        self._engines = dict()
        for engine in self._engine_definitions:
            if not engine.engine in self._engine_types:
                log.warn("Can't find engine %s anymore", engine.engine)
                if not engine.engine in in_error:
                    in_error[engine.engine] = True
                    self.engineNotFound.emit(engine)
            self._engines[engine.uid] = self._engine_types[engine.engine](self, engine,
                                                                        remote_watcher_delay=self.remote_watcher_delay)
            self._engines[engine.uid].online.connect(self._force_autoupdate)
            self.initEngine.emit(self._engines[engine.uid])

    def _get_default_nuxeo_drive_name(self):
        return 'Nuxeo Drive'

    def _force_autoupdate(self):
        if (self._app_updater.get_next_poll() > 60 and self._app_updater.get_last_poll() > 1800):
            self._app_updater.force_poll()

    def get_default_nuxeo_drive_folder(self):
        # TODO: Factorize with utils.default_nuxeo_drive_folder
        """Find a reasonable location for the root Nuxeo Drive folder

        This folder is user specific, typically under the home folder.

        Under Windows, try to locate My Documents as a home folder, using the
        win32com shell API if allowed, else falling back on a manual detection.

        Note that we need to decode the path returned by os.path.expanduser with
        the local encoding because the value of the HOME environment variable is
        read as a byte string. Using os.path.expanduser(u'~') fails if the home
        path contains non ASCII characters since Unicode coercion attempts to
        decode the byte string as an ASCII string.
        """
        if sys.platform == "win32":
            from win32com.shell import shell, shellcon
            try:
                my_documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL,
                                                     None, 0)
            except:
                # In some cases (not really sure how this happens) the current user
                # is not allowed to access its 'My Documents' folder path through
                # the win32com shell API, which raises the following error:
                # com_error: (-2147024891, 'Access is denied.', None, None)
                # We noticed that in this case the 'Location' tab is missing in the
                # Properties window of 'My Documents' accessed through the
                # Explorer.
                # So let's fall back on a manual (and poor) detection.
                # WARNING: it's important to check 'Documents' first as under
                # Windows 7 there also exists a 'My Documents' folder invisible in
                # the Explorer and cmd / powershell but visible from Python.
                # First try regular location for documents under Windows 7 and up
                log.debug("Access denied to win32com shell API: SHGetFolderPath,"
                          " falling back on manual detection of My Documents")
                my_documents = os.path.expanduser(r'~\Documents')
                my_documents = unicode(my_documents.decode(ENCODING))
                if not os.path.exists(my_documents):
                    # Compatibility for Windows XP
                    my_documents = os.path.expanduser(r'~\My Documents')
                    my_documents = unicode(my_documents.decode(ENCODING))

            if os.path.exists(my_documents):
                nuxeo_drive_folder = self._increment_local_folder(my_documents, self._get_default_nuxeo_drive_name())
                log.debug("Will use '%s' as default Nuxeo Drive folder location under Windows", nuxeo_drive_folder)
                return nuxeo_drive_folder

        # Fall back on home folder otherwise
        user_home = os.path.expanduser('~')
        user_home = unicode(user_home.decode(ENCODING))
        nuxeo_drive_folder = self._increment_local_folder(user_home, self._get_default_nuxeo_drive_name())
        log.debug("Will use '%s' as default Nuxeo Drive folder location", nuxeo_drive_folder)
        return nuxeo_drive_folder

    def _increment_local_folder(self, basefolder, name):
        nuxeo_drive_folder = os.path.join(basefolder, name)
        num = 2
        while (not self.check_local_folder_available(nuxeo_drive_folder)):
            nuxeo_drive_folder = os.path.join(basefolder, name + " " + str(num))
            num = num + 1
            if num > 10:
                return ""
        return nuxeo_drive_folder

    def get_configuration_folder(self):
        return self.nxdrive_home

    def open_local_file(self, file_path):
        """Launch the local OS program on the given file / folder."""
        log.debug('Launching editor on %s', file_path)
        if sys.platform == 'win32':
            os.startfile(file_path)
        elif sys.platform == 'darwin':
            subprocess.Popen(['open', file_path])
        else:
            try:
                subprocess.Popen(['xdg-open', file_path])
            except OSError:
                # xdg-open should be supported by recent Gnome, KDE, Xfce
                log.error("Failed to find and editor for: '%s'", file_path)

    def check_version_updated(self):
        last_version = self._dao.get_config("client_version")
        if last_version != self.get_version():
            self.clientUpdated.emit(last_version, self.get_version())

    def generate_device_id(self):
        self.device_id = uuid.uuid1().hex
        self._dao.update_config("device_id", self.device_id)

    def get_proxy_settings(self, device_config=None):
        """Fetch proxy settings from database"""
        return ProxySettings(dao=self._dao)

    def list_server_bindings(self):
        if self._engines is None:
            self.load()
        result = []
        for definition in self._engine_definitions:
            row = definition
            row.server_version = None
            row.update_url = ""
            self._engines[row.uid].complete_binder(row)
            result.append(row)
        return result

    def get_config(self, value, default=None):
        return self._dao.get_config(value, default)

    def set_config(self, key, value):
        return self._dao.update_config(key, value)

    def get_direct_edit_auto_lock(self):
        return self._dao.get_config("direct_edit_auto_lock", "1") == "1"

    def set_direct_edit_auto_lock(self, value):
        self._dao.update_config("direct_edit_auto_lock", value)

    def get_auto_update(self):
        # By default auto update
        return self._dao.get_config("auto_update", "1") == "1"

    def set_auto_update(self, value):
        self._dao.update_config("auto_update", value)

    def get_auto_start(self):
        return self._dao.get_config("auto_start", "1") == "1"

    def _get_binary_name(self):
        return 'ndrive'

    def generate_report(self, path=None):
        from nxdrive.report import Report
        report = Report(self, path)
        report.generate()
        return report.get_path()

    def find_exe_path(self):
        """Introspect the Python runtime to find the frozen Windows exe"""
        import nxdrive
        nxdrive_path = os.path.realpath(os.path.dirname(nxdrive.__file__))
        log.trace("nxdrive_path: %s", nxdrive_path)

        # Detect frozen win32 executable under Windows
        executable = sys.executable
        if "appdata" in executable:
            executable = os.path.join(os.path.dirname(executable),
                                      "..","..",os.path.basename(
                                      sys.executable))
            exe_path = os.path.abspath(executable)
            if os.path.exists(exe_path):
                log.trace("Returning exe path: %s", exe_path)
                return exe_path

        # Detect OSX frozen app
        if nxdrive_path.endswith(OSX_SUFFIX):
            log.trace("Detected OS X frozen app")
            exe_path = nxdrive_path.replace(OSX_SUFFIX, "Contents/MacOS/"
                                                + self._get_binary_name())
            if os.path.exists(exe_path):
                log.trace("Returning exe path: %s", exe_path)
                return exe_path

        # Fall-back to the regular method that should work both the ndrive script
        exe_path = sys.argv[0]
        log.trace("Returning default exe path: %s", exe_path)
        return exe_path

    def set_auto_start(self, value):
        self._dao.update_config("auto_start", value)
        if value:
            self._os.register_startup()
        else:
            self._os.unregister_startup()

    def get_beta_channel(self):
        return self._dao.get_config("beta_channel", "0") == "1"

    def set_beta_channel(self, value):
        self._dao.update_config("beta_channel", value)
        # Trigger update status refresh
        self.refresh_update_status()

    def get_tracking(self):
        return self._dao.get_config("tracking", "1") == "1" and not self.get_version().endswith("-dev")

    def set_tracking(self, value):
        self._dao.update_config("tracking", value)
        if value:
            self._create_tracker()
        elif self._tracker is not None:
            self._tracker._thread.quit()
            self._tracker = None

    def validate_proxy_settings(self, proxy_settings):
        try:
            import urllib2
            proxies, _ = get_proxies_for_handler(proxy_settings)
            # Try google website
            url = "http://www.google.com"
            opener = urllib2.build_opener(urllib2.ProxyHandler(proxies),
                                      urllib2.HTTPBasicAuthHandler(),
                                      urllib2.HTTPHandler)
            urllib2.install_opener(opener)
            conn = urllib2.urlopen(url)
            conn.read()
        except Exception as e:
            log.error("Exception setting proxy : %s", e)
            return False
        return True

    def set_proxy_settings(self, proxy_settings, force=False):
        if force or self.validate_proxy_settings(proxy_settings):
            proxy_settings.save(self._dao)
            self.refresh_proxies(proxy_settings)
            log.info("Proxy settings successfully updated: %r", proxy_settings)
            return ""
        else:
            return "PROXY_INVALID"

    def refresh_proxies(self, proxy_settings=None, device_config=None):
        """Refresh current proxies with the given settings"""
        # If no proxy settings passed fetch them from database
        proxy_settings = (proxy_settings if proxy_settings is not None
                          else self.get_proxy_settings())
        self.proxies, self.proxy_exceptions = get_proxies_for_handler(
                                                            proxy_settings)
        self.proxyUpdated.emit(proxy_settings)

    def get_proxies(self):
        return self.proxies

    def get_engine(self, local_folder):
        if self._engines is None:
            self.load()
        for engine_def in self._engine_definitions:
            if local_folder.startswith(engine_def.local_folder):
                return self._engines[engine_def.uid]
        return None

    def edit(self, engine, remote_ref):
        """Find the local file if any and start OS editor on it."""

        doc_pair = engine.get_dao().get_normal_state_from_remote(remote_ref)
        if doc_pair is None:
            log.warning('Could not find local file for engine %s and remote_ref %s', engine.get_uid(), remote_ref)
            return

        # TODO: check synchronization of this state first

        # Find the best editor for the file according to the OS configuration
        local_client = engine.get_local_client()
        self.open_local_file(local_client._abspath(doc_pair.local_path))

    def _get_default_server_type(self):
        return "NXDRIVE"

    def bind_server(self, local_folder, url, username, password, token=None, name=None, start_engine=True, check_credentials=True):
        from collections import namedtuple
        if name is None:
            name = self._get_engine_name(url)
        binder = namedtuple('binder', ['username', 'password', 'token', 'url', 'no_check', 'no_fscheck'])
        binder.username = username
        binder.password = password
        binder.token = token
        binder.no_check = not check_credentials
        binder.no_fscheck = False
        binder.url = url
        return self.bind_engine(self._get_default_server_type(), local_folder, name, binder, starts=start_engine)

    def _get_engine_name(self, server_url):
        import urlparse
        urlp = urlparse.urlparse(server_url)
        return urlp.hostname

    def check_local_folder_available(self, local_folder):
        if self._engine_definitions is None:
            return True
        if not local_folder.endswith('/'):
            local_folder = local_folder + '/'
        for engine in self._engine_definitions:
            other = engine.local_folder
            if not other.endswith('/'):
                other = other + '/'
            if (other.startswith(local_folder) or local_folder.startswith(other)):
                return False
        return True

    def update_engine_path(self, uid, local_folder):
        # Dont update the engine by itself, should be only used by engine.update_engine_path
        if uid in self._engine_definitions:
            self._engine_definitions[uid].local_folder = local_folder
        self._dao.update_engine_path(uid, local_folder)

    def bind_engine(self, engine_type, local_folder, name, binder, starts=True):
        """Bind a local folder to a remote nuxeo server"""
        if name is None and hasattr(binder, 'url'):
            name = self._get_engine_name(binder.url)
        if hasattr(binder, 'url'):
            url = binder.url
            if '#' in url:
                # Last part of the url is the engine type
                engine_type = url.split('#')[1]
                binder.url = url.split('#')[0]
                log.debug("Engine type has been specified in the url: %s will be used", engine_type)
        if not self.check_local_folder_available(local_folder):
            raise FolderAlreadyUsed()
        if not engine_type in self._engine_types:
            raise EngineTypeMissing()
        if self._engines is None:
            self.load()
        local_folder = normalized_path(local_folder)
        if local_folder == self.get_configuration_folder():
            # Prevent from binding in the configuration folder
            raise FolderAlreadyUsed()
        uid = uuid.uuid1().hex
        # TODO Check that engine is not inside another or same position
        engine_def = self._dao.add_engine(engine_type, local_folder, uid, name)
        try:
            self._engines[uid] = self._engine_types[engine_type](self, engine_def, binder=binder,
                                                                 remote_watcher_delay=self.remote_watcher_delay)
            self._engine_definitions.append(engine_def)
        except Exception as e:
            log.exception(e)
            if uid in self._engines:
                del self._engines[uid]
            self._dao.delete_engine(uid)
            # TODO Remove the db ?
            raise e
        # As new engine was just bound, refresh application update status
        self.refresh_update_status()
        if starts:
            self._engines[uid].start()
        self.newEngine.emit(self._engines[uid])
        return self._engines[uid]
        #server_url, username, password
        # check the connection to the server by issuing an authentication
        # request

    def unbind_engine(self, uid):
        if self._engines is None:
            self.load()
        self._engines[uid].suspend()
        self._engines[uid].unbind()
        self._dao.delete_engine(uid)
        # Refresh the engines definition
        del self._engines[uid]
        self.dropEngine.emit(uid)
        self._engine_definitions = self._dao.get_engines()

    def unbind_all(self):
        if self._engines is None:
            self.load()
        for engine in self._engine_definitions:
            self.unbind_engine(engine.uid)

    def dispose_db(self):
        if self._dao is not None:
            self._dao.dispose()

    def dispose_all(self):
        for engine in self.get_engines().values():
            engine.dispose_db()
        self.dispose_db()

    def get_engines(self):
        return self._engines

    def get_engines_type(self):
        return self._engine_types

    def get_version(self):
        return __version__

    def update_version(self, device_config):
        if self.version != device_config.client_version:
            log.info("Detected version upgrade: current version = %s,"
                      " new version = %s => upgrading current version,"
                      " yet DB upgrade might be needed.",
                      device_config.client_version,
                      self.version)
            device_config.client_version = self.version
            self.get_session().commit()
            return True
        return False

    def is_started(self):
        return self._started

    def is_updated(self):
        return self.updated

    def is_syncing(self):
        syncing_engines = []
        for uid, engine in self._engines.items():
            if engine.is_syncing():
                syncing_engines.append(uid)
        if syncing_engines:
            log.debug("Some engines are currently synchronizing: %s", syncing_engines)
            return True
        else:
            log.debug("No engine currently synchronizing")
            return False

    def get_root_id(self, file_path):
        from nxdrive.client import LocalClient
        ref = LocalClient.get_path_remote_id(file_path, 'ndriveroot')
        if ref is None:
            parent = os.path.dirname(file_path)
            # We can't find in any parent
            if parent == file_path or parent is None:
                return None
            return self.get_root_id(parent)
        return ref

    def get_cf_bundle_identifier(self):
        return "org.nuxeo.drive"

    def get_metadata_infos(self, file_path):
        from nxdrive.client import LocalClient
        remote_ref = LocalClient.get_path_remote_id(file_path)
        if remote_ref is None:
            raise ValueError('Could not find file %s as Nuxeo Drive managed' % file_path)
        root_id = self.get_root_id(file_path)
        # TODO Add a class to handle root info
        root_values = root_id.split("|")
        try:
            engine = self.get_engines()[root_values[3]]
        except:
            raise ValueError('Unknown engine %s for %s' %
                             (root_values[3], file_path))
        metadata_url = engine.get_metadata_url(remote_ref)
        return (metadata_url, engine.get_remote_token(), engine, remote_ref)

    def set_script_object(self, obj):
        # Used to enhance scripting with UI
        self._script_object = obj

    def _create_script_engine(self):
        from nxdrive.scripting import DriveScript
        self._script_engine = QScriptEngine()
        if self._script_object is None:
            self._script_object = DriveScript(self)
        self._script_engine.globalObject().setProperty("drive", self._script_engine.newQObject(self._script_object))

    def execute_script(self, script, engine_uid=None):
        if self._script_engine is None:
            self._create_script_engine()
            if self._script_engine is None:
                return
        self._script_object.set_engine_uid(engine_uid)
        log.debug("Will execute '%s'", script)
        result = self._script_engine.evaluate(script)
        if self._script_engine.hasUncaughtException():
            log.debug("Execution exception: %r", result.toString())