diff options
| -rw-r--r-- | changes/feature_3900-eip-state-machine | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 216 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/statemachines.py | 223 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/statuspanel.py | 162 | ||||
| -rw-r--r-- | src/leap/bitmask/services/connections.py | 125 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/connection.py | 48 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/eipconfig.py | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/util/averages.py | 92 | 
9 files changed, 674 insertions, 195 deletions
| diff --git a/changes/feature_3900-eip-state-machine b/changes/feature_3900-eip-state-machine new file mode 100644 index 00000000..63aad3d3 --- /dev/null +++ b/changes/feature_3900-eip-state-machine @@ -0,0 +1 @@ +  o Refactors EIPConnection to use LEAPConnection state machine. Closes: #3900 diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index a818a5f8..200d68aa 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,24 +19,23 @@ Main window for Bitmask.  """  import logging  import os -import platform -import tempfile -from functools import partial  import keyring  from PySide import QtCore, QtGui  from twisted.internet import threads -from leap.bitmask.config import flags +from leap.bitmask import __version__ as VERSION  from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.gui.loggerwindow import LoggerWindow -from leap.bitmask.gui.preferenceswindow import PreferencesWindow -from leap.bitmask.gui.wizard import Wizard  from leap.bitmask.gui.login import LoginWidget +from leap.bitmask.gui.preferenceswindow import PreferencesWindow +from leap.bitmask.gui import statemachines  from leap.bitmask.gui.statuspanel import StatusPanelWidget +from leap.bitmask.gui.wizard import Wizard +  from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper  from leap.bitmask.services.eip.eipconfig import EIPConfig  from leap.bitmask.services.eip.providerbootstrapper import ProviderBootstrapper @@ -48,6 +47,8 @@ from leap.bitmask.services.mail import imap  from leap.bitmask.platform_init import IS_WIN, IS_MAC  from leap.bitmask.platform_init.initializers import init_platform +from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.connection import EIPConnection  from leap.bitmask.services.eip.vpnprocess import VPN  from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning  from leap.bitmask.services.eip.vpnprocess import AlienOpenVPNAlreadyRunning @@ -59,7 +60,6 @@ from leap.bitmask.services.eip.vpnlaunchers import \      EIPNoPolkitAuthAgentAvailable  from leap.bitmask.services.eip.vpnlaunchers import EIPNoTunKextLoaded -from leap.bitmask import __version__ as VERSION  from leap.bitmask.util.keyring_helpers import has_keyring  from leap.bitmask.util.leap_log_handler import LeapLogHandler @@ -167,8 +167,14 @@ class MainWindow(QtGui.QMainWindow):          self.ui.stackedWidget.setCurrentIndex(self.LOGIN_INDEX) -        self._status_panel.start_eip.connect(self._start_eip) -        self._status_panel.stop_eip.connect(self._stop_eip) +        self._eip_connection = EIPConnection() + +        self._eip_connection.qtsigs.connecting_signal.connect( +            self._start_eip) +        self._eip_connection.qtsigs.disconnecting_signal.connect( +            self._stop_eip) +        self._status_panel.eip_connection_connected.connect( +            self._on_eip_connected)          # This is loaded only once, there's a bug when doing that more          # than once @@ -206,12 +212,20 @@ class MainWindow(QtGui.QMainWindow):          # This thread is similar to the provider bootstrapper          self._eip_bootstrapper = EIPBootstrapper() +        # EIP signals ---- move to eip conductor.          # TODO change the name of "download_config" signal to          # something less confusing (config_ready maybe)          self._eip_bootstrapper.download_config.connect(              self._eip_intermediate_stage)          self._eip_bootstrapper.download_client_certificate.connect(              self._finish_eip_bootstrap) +        self._vpn = VPN(openvpn_verb=openvpn_verb) +        self._vpn.qtsigs.state_changed.connect( +            self._status_panel.update_vpn_state) +        self._vpn.qtsigs.status_changed.connect( +            self._status_panel.update_vpn_status) +        self._vpn.qtsigs.process_finished.connect( +            self._eip_finished)          self._soledad_bootstrapper = SoledadBootstrapper()          self._soledad_bootstrapper.download_config.connect( @@ -225,14 +239,6 @@ class MainWindow(QtGui.QMainWindow):          self._smtp_bootstrapper.download_config.connect(              self._smtp_bootstrapped_stage) -        self._vpn = VPN(openvpn_verb=openvpn_verb) -        self._vpn.qtsigs.state_changed.connect( -            self._status_panel.update_vpn_state) -        self._vpn.qtsigs.status_changed.connect( -            self._status_panel.update_vpn_status) -        self._vpn.qtsigs.process_finished.connect( -            self._eip_finished) -          self.ui.action_log_out.setEnabled(False)          self.ui.action_log_out.triggered.connect(self._logout)          self.ui.action_about_leap.triggered.connect(self._about) @@ -250,9 +256,7 @@ class MainWindow(QtGui.QMainWindow):          self._action_mail_status.setEnabled(False)          self._status_panel.set_action_mail_status(self._action_mail_status) -        self._action_eip_startstop = QtGui.QAction(self.tr("Turn ON"), self) -        self._action_eip_startstop.triggered.connect(self._stop_eip) -        self._action_eip_startstop.setEnabled(False) +        self._action_eip_startstop = QtGui.QAction("", self)          self._status_panel.set_action_eip_startstop(self._action_eip_startstop)          self._action_preferences = QtGui.QAction(self.tr("Preferences"), self) @@ -309,6 +313,17 @@ class MainWindow(QtGui.QMainWindow):          else:              self._finish_init() +        # Eip machine is a public attribute where the state machine for +        # the eip connection will be available to the different components. +        # Remember that this will not live in the  +1600LOC mainwindow for +        # all the eternity, so at some point we will be moving this to +        # the EIPConductor or some other clever component that we will +        # instantiate from here. +        self.eip_machine = None + +        # start event machines +        self.start_eip_machine() +      def _rejected_wizard(self):          """          SLOT @@ -579,6 +594,10 @@ class MainWindow(QtGui.QMainWindow):                           "providers",                           default_provider,                           "provider.json")): +            # XXX I think we should not try to re-download config every time, +            # it adds some delay. +            # Maybe if it's the first run in a session, +            # or we can try only if it fails.              self._download_eip_config()          else:              # XXX: Display a proper message to the user @@ -943,6 +962,7 @@ class MainWindow(QtGui.QMainWindow):              self._login_widget.set_enabled(True)      def _switch_to_status(self): +        # TODO this method name is confusing as hell.          """          Changes the stackedWidget index to the EIP status one and          triggers the eip bootstrapping @@ -954,6 +974,8 @@ class MainWindow(QtGui.QMainWindow):          self.ui.stackedWidget.setCurrentIndex(self.EIP_STATUS_INDEX) +        # TODO separate UI from logic. +        # TODO soledad should check if we want to run only over EIP.          self._soledad_bootstrapper.run_soledad_setup_checks(              self._provider_config,              self._login_widget.get_user(), @@ -1169,26 +1191,36 @@ class MainWindow(QtGui.QMainWindow):      ###################################################################      # Service control methods: eip -    def _get_socket_host(self): +    def start_eip_machine(self):          """ -        Returns the socket and port to be used for VPN - -        :rtype: tuple (str, str) (host, port) +        Initializes and starts the EIP state machine          """ -        # TODO make this properly multiplatform -        # TODO get this out of gui/ +        button = self._status_panel.eip_button +        action = self._action_eip_startstop +        label = self._status_panel.eip_label +        builder = statemachines.ConnectionMachineBuilder(self._eip_connection) +        eip_machine = builder.make_machine(button=button, +                                           action=action, +                                           label=label) +        self.eip_machine = eip_machine +        self.eip_machine.start() -        if platform.system() == "Windows": -            host = "localhost" -            port = "9876" -        else: -            # XXX cleanup this on exit too -            host = os.path.join(tempfile.mkdtemp(prefix="leap-tmp"), -                                'openvpn.socket') -            port = "unix" +    @QtCore.Slot() +    def _on_eip_connected(self): +        """ +        SLOT +        TRIGGERS: +            self._status_panel.eip_connection_connected +        Emits the EIPConnection.qtsigs.connected_signal -        return host, port +        This is a little workaround for connecting the vpn-connected +        signal that currently is beeing processed under status_panel. +        After the refactor to EIPConductor this should not be necessary. +        """ +        logger.debug('EIP connected signal received ...') +        self._eip_connection.qtsigs.connected_signal.emit() +    @QtCore.Slot()      def _start_eip(self):          """          SLOT @@ -1199,9 +1231,10 @@ class MainWindow(QtGui.QMainWindow):          Starts EIP          """ +        provider_config = self._get_best_provider_config() +        provider = provider_config.get_domain()          self._status_panel.eip_pre_up()          self.user_stopped_eip = False -        provider_config = self._get_best_provider_config()          try:              # XXX move this to EIPConductor @@ -1210,22 +1243,15 @@ class MainWindow(QtGui.QMainWindow):                              providerconfig=provider_config,                              socket_host=host,                              socket_port=port) - -            self._settings.set_defaultprovider( -                provider_config.get_domain()) - -            provider = provider_config.get_domain() +            self._settings.set_defaultprovider(provider)              if self._logged_user is not None:                  provider = "%s@%s" % (self._logged_user, provider) +            # XXX move to the state machine too              self._status_panel.set_provider(provider) -            self._status_panel.eip_started() -            # XXX refactor into status_panel method? -            self._action_eip_startstop.setText(self.tr("Turn OFF")) -            self._action_eip_startstop.disconnect(self) -            self._action_eip_startstop.triggered.connect( -                self._stop_eip) +        # TODO refactor exceptions so they provide translatable +        # usef-facing messages.          except EIPNoPolkitAuthAgentAvailable:              self._status_panel.set_global_status(                  # XXX this should change to polkit-kde where @@ -1277,26 +1303,7 @@ class MainWindow(QtGui.QMainWindow):          else:              self._already_started_eip = True -    def _set_eipstatus_off(self): -        """ -        Sets eip status to off -        """ -        self._status_panel.set_eip_status(self.tr("OFF"), error=True) -        self._status_panel.set_eip_status_icon("error") -        self._status_panel.set_startstop_enabled(True) -        self._status_panel.eip_stopped() - -        self._set_action_eipstart_off() - -    def _set_action_eipstart_off(self): -        """ -        Sets eip startstop action to OFF status. -        """ -        self._action_eip_startstop.setText(self.tr("Turn ON")) -        self._action_eip_startstop.disconnect(self) -        self._action_eip_startstop.triggered.connect( -            self._start_eip) - +    @QtCore.Slot()      def _stop_eip(self, abnormal=False):          """          SLOT @@ -1320,34 +1327,20 @@ class MainWindow(QtGui.QMainWindow):          self._set_eipstatus_off()          self._already_started_eip = False + +        # XXX do via signal          self._settings.set_defaultprovider(None)          if self._logged_user:              self._status_panel.set_provider(                  "%s@%s" % (self._logged_user,                             self._get_best_provider_config().get_domain())) -    def _get_best_provider_config(self): +    def _set_eipstatus_off(self):          """ -        Returns the best ProviderConfig to use at a moment. We may -        have to use self._provider_config or -        self._provisional_provider_config depending on the start -        status. - -        :rtype: ProviderConfig +        Sets eip status to off          """ -        leap_assert(self._provider_config is not None or -                    self._provisional_provider_config is not None, -                    "We need a provider config") - -        provider_config = None -        if self._provider_config.loaded(): -            provider_config = self._provider_config -        elif self._provisional_provider_config.loaded(): -            provider_config = self._provisional_provider_config -        else: -            leap_assert(False, "We could not find any usable ProviderConfig.") - -        return provider_config +        self._status_panel.set_eip_status(self.tr("OFF"), error=True) +        self._status_panel.set_eip_status_icon("error")      def _download_eip_config(self):          """ @@ -1361,6 +1354,7 @@ class MainWindow(QtGui.QMainWindow):                  self._enabled_services.count(self.OPENVPN_SERVICE) > 0 and \                  not self._already_started_eip: +            # XXX this should be handled by the state machine.              self._status_panel.set_eip_status(                  self.tr("Starting..."))              self._eip_bootstrapper.run_eip_setup_checks( @@ -1374,7 +1368,6 @@ class MainWindow(QtGui.QMainWindow):                      error=True)              else:                  self._status_panel.set_eip_status(self.tr("Disabled")) -            self._status_panel.set_startstop_enabled(False)      def _finish_eip_bootstrap(self, data):          """ @@ -1395,7 +1388,6 @@ class MainWindow(QtGui.QMainWindow):              return          provider_config = self._get_best_provider_config() -          domain = provider_config.get_domain()          loaded = self._eip_config.loaded() @@ -1407,13 +1399,41 @@ class MainWindow(QtGui.QMainWindow):              loaded = self._eip_config.load(eip_config_path)          if loaded: -            self._start_eip() +            # DO START EIP Connection! +            self._eip_connection.qtsigs.do_connect_signal.emit()          else:              self._status_panel.set_eip_status(                  self.tr("Could not load Encrypted Internet "                          "Configuration."),                  error=True) +    # end eip methods ------------------------------------------- + +    def _get_best_provider_config(self): +        """ +        Returns the best ProviderConfig to use at a moment. We may +        have to use self._provider_config or +        self._provisional_provider_config depending on the start +        status. + +        :rtype: ProviderConfig +        """ +        # TODO move this out of gui. +        leap_assert(self._provider_config is not None or +                    self._provisional_provider_config is not None, +                    "We need a provider config") + +        provider_config = None +        if self._provider_config.loaded(): +            provider_config = self._provider_config +        elif self._provisional_provider_config.loaded(): +            provider_config = self._provisional_provider_config +        else: +            leap_assert(False, "We could not find any usable ProviderConfig.") + +        return provider_config + +    @QtCore.Slot()      def _logout(self):          """          SLOT @@ -1494,6 +1514,7 @@ class MainWindow(QtGui.QMainWindow):          Triggered when the EIP/VPN process finishes to set the UI          accordingly.          """ +        # TODO move to EIPConductor.          logger.info("VPN process finished with exitCode %s..."                      % (exitCode,)) @@ -1528,7 +1549,20 @@ class MainWindow(QtGui.QMainWindow):          if exitCode == 0 and IS_MAC:              # XXX remove this warning after I fix cocoasudo.              logger.warning("The above exit code MIGHT BE WRONG.") -        self._stop_eip(abnormal) + +        # We emit signals to trigger transitions in the state machine: +        qtsigs = self._eip_connection.qtsigs +        if abnormal: +            signal = qtsigs.connection_died_signal +        else: +            signal = qtsigs.disconnected_signal + +        # XXX verify that the logic kees the same w/o the abnormal flag +        # after the refactor to EIPConnection has been completed +        # (eipconductor taking the most of the logic under transitions +        # that right now are handled under status_panel) +        #self._stop_eip(abnormal) +        signal.emit()      def _on_raise_window_event(self, req):          """ diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py new file mode 100644 index 00000000..c3dd5ed3 --- /dev/null +++ b/src/leap/bitmask/gui/statemachines.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# statemachines.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +State machines for the Bitmask app. +""" +import logging + +from PySide.QtCore import QStateMachine, QState +from PySide.QtCore import QObject + +from leap.bitmask.services import connections +from leap.common.check import leap_assert_type + +logger = logging.getLogger(__name__) + +_tr = QObject().tr + +# Indexes for the state dict +_ON = "on" +_OFF = "off" +_CON = "connecting" +_DIS = "disconnecting" + + +class IntermediateState(QState): +    """ +    Intermediate state that emits a custom signal on entry +    """ +    def __init__(self, signal): +        """ +        Initializer. +        :param signal: the signal to be emitted on entry on this state. +        :type signal: QtCore.QSignal +        """ +        super(IntermediateState, self).__init__() +        self._signal = signal + +    def onEntry(self, *args): +        """ +        Emits the signal on entry. +        """ +        logger.debug('IntermediateState entered. Emitting signal ...') +        if self._signal is not None: +            self._signal.emit() + + +class ConnectionMachineBuilder(object): +    """ +    Builder class for state machines made from LEAPConnections. +    """ +    def __init__(self, connection): +        """ +        :param connection: an instance of a concrete LEAPConnection +                      we will be building a state machine for. +        :type connection: AbstractLEAPConnection +        """ +        self._conn = connection +        leap_assert_type(self._conn, connections.AbstractLEAPConnection) + +    def make_machine(self, button=None, action=None, label=None): +        """ +        Creates a statemachine associated with the passed controls. + +        :param button: the switch button. +        :type button: QPushButton + +        :param action: the actionh that controls connection switch in a menu. +        :type action: QAction + +        :param label: the label that displays the connection state +        :type label: QLabel + +        :returns: a state machine +        :rtype: QStateMachine +        """ +        machine = QStateMachine() +        conn = self._conn + +        states = self._make_states(button, action, label) + +        # transitions: + +        states[_OFF].addTransition( +            conn.qtsigs.do_connect_signal, +            states[_CON]) + +        # * Clicking the buttons or actions transitions to the +        #   intermediate stage. +        if button: +            states[_OFF].addTransition( +                button.clicked, +                states[_CON]) +            states[_ON].addTransition( +                button.clicked, +                states[_DIS]) + +        if action: +            states[_OFF].addTransition( +                action.triggered, +                states[_CON]) +            states[_ON].addTransition( +                action.triggered, +                states[_DIS]) + +        # * We transition to the completed stages when +        #   we receive the matching signal from the underlying +        #   conductor. + +        states[_CON].addTransition( +            conn.qtsigs.connected_signal, +            states[_ON]) +        states[_DIS].addTransition( +            conn.qtsigs.disconnected_signal, +            states[_OFF]) + +        # * If we receive the connection_died, we transition +        #   to the off state +        states[_ON].addTransition( +            conn.qtsigs.connection_died_signal, +            states[_OFF]) + +        # adding states to the machine +        for state in states.itervalues(): +            machine.addState(state) +        machine.setInitialState(states[_OFF]) +        return machine + +    def _make_states(self, button, action, label): +        """ +        Creates the four states for the state machine + +        :param button: the switch button. +        :type button: QPushButton + +        :param action: the actionh that controls connection switch in a menu. +        :type action: QAction + +        :param label: the label that displays the connection state +        :type label: QLabel + +        :returns: a dict of states +        :rtype: dict +        """ +        conn = self._conn +        states = {} + +        # TODO add tooltip + +        # OFF State ---------------------- +        off = QState() +        off_label = _tr("Turn {0}").format( +            conn.Connected.short_label) +        if button: +            off.assignProperty( +                button, 'text', off_label) +            off.assignProperty( +                button, 'enabled', True) +        if action: +            off.assignProperty( +                action, 'text', off_label) +        off.setObjectName(_OFF) +        states[_OFF] = off + +        # CONNECTING State ---------------- +        connecting = IntermediateState( +            conn.qtsigs.connecting_signal) +        on_label = _tr("Turn {0}").format( +            conn.Disconnected.short_label) +        if button: +            connecting.assignProperty( +                button, 'text', on_label) +            connecting.assignProperty( +                button, 'enabled', False) +        if action: +            connecting.assignProperty( +                action, 'text', on_label) +            connecting.assignProperty( +                action, 'enabled', False) +        connecting.setObjectName(_CON) +        states[_CON] = connecting + +        # ON State ------------------------ +        on = QState() +        if button: +            on.assignProperty( +                button, 'text', on_label) +            on.assignProperty( +                button, 'enabled', True) +        if action: +            on.assignProperty( +                action, 'text', on_label) +            on.assignProperty( +                action, 'enabled', True) +        # TODO set label for ON state +        on.setObjectName(_ON) +        states[_ON] = on + +        # DISCONNECTING State ------------- +        disconnecting = IntermediateState( +            conn.qtsigs.disconnecting_signal) +        if button: +            disconnecting.assignProperty( +                button, 'enabled', False) +        # XXX complete disconnecting +        # TODO disable button +        disconnecting.setObjectName(_DIS) +        states[_DIS] = disconnecting + +        return states diff --git a/src/leap/bitmask/gui/statuspanel.py b/src/leap/bitmask/gui/statuspanel.py index 39a8079f..679f00b1 100644 --- a/src/leap/bitmask/gui/statuspanel.py +++ b/src/leap/bitmask/gui/statuspanel.py @@ -14,7 +14,6 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """  Status Panel widget implementation  """ @@ -25,9 +24,10 @@ from functools import partial  from PySide import QtCore, QtGui +from leap.bitmask.services.eip.connection import EIPConnection  from leap.bitmask.services.eip.vpnprocess import VPNManager  from leap.bitmask.platform_init import IS_WIN, IS_LINUX -from leap.bitmask.util import first +from leap.bitmask.util.averages import RateMovingAverage  from leap.common.check import leap_assert, leap_assert_type  from leap.common.events import register  from leap.common.events import events_pb2 as proto @@ -37,83 +37,10 @@ from ui_statuspanel import Ui_StatusPanel  logger = logging.getLogger(__name__) -class RateMovingAverage(object): -    """ -    Moving window average for calculating -    upload and download rates. -    """ -    SAMPLE_SIZE = 5 - -    def __init__(self): -        """ -        Initializes an empty array of fixed size -        """ -        self.reset() - -    def reset(self): -        self._data = [None for i in xrange(self.SAMPLE_SIZE)] - -    def append(self, x): -        """ -        Appends a new data point to the collection. - -        :param x: A tuple containing timestamp and traffic points -                  in the form (timestamp, traffic) -        :type x: tuple -        """ -        self._data.pop(0) -        self._data.append(x) - -    def get(self): -        """ -        Gets the collection. -        """ -        return self._data - -    def get_average(self): -        """ -        Gets the moving average. -        """ -        data = filter(None, self.get()) -        traff = [traffic for (ts, traffic) in data] -        times = [ts for (ts, traffic) in data] - -        try: -            deltatraffic = traff[-1] - first(traff) -            deltat = (times[-1] - first(times)).seconds -        except IndexError: -            deltatraffic = 0 -            deltat = 0 - -        try: -            rate = float(deltatraffic) / float(deltat) / 1024 -        except ZeroDivisionError: -            rate = 0 - -        # In some cases we get negative rates -        if rate < 0: -            rate = 0 - -        return rate - -    def get_total(self): -        """ -        Gets the total accumulated throughput. -        """ -        try: -            return self._data[-1][1] / 1024 -        except TypeError: -            return 0 - -  class StatusPanelWidget(QtGui.QWidget):      """      Status widget that displays the current state of the LEAP services      """ - -    start_eip = QtCore.Signal() -    stop_eip = QtCore.Signal() -      DISPLAY_TRAFFIC_RATES = True      RATE_STR = "%14.2f KB/s"      TOTAL_STR = "%14.2f Kb" @@ -121,6 +48,7 @@ class StatusPanelWidget(QtGui.QWidget):      MAIL_OFF_ICON = ":/images/mail-unlocked.png"      MAIL_ON_ICON = ":/images/mail-locked.png" +    eip_connection_connected = QtCore.Signal()      _soledad_event = QtCore.Signal(object)      _smtp_event = QtCore.Signal(object)      _imap_event = QtCore.Signal(object) @@ -135,9 +63,7 @@ class StatusPanelWidget(QtGui.QWidget):          self.ui = Ui_StatusPanel()          self.ui.setupUi(self) -        self.ui.btnEipStartStop.setEnabled(False) -        self.ui.btnEipStartStop.clicked.connect( -            self.start_eip) +        self.eipconnection = EIPConnection()          self.hide_status_box() @@ -330,6 +256,8 @@ class StatusPanelWidget(QtGui.QWidget):          self.CONNECTED_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[1])          self.ERROR_ICON_TRAY = QtGui.QPixmap(EIP_ICONS_TRAY[2]) +    # Systray and actions +      def set_systray(self, systray):          """          Sets the systray object to use. @@ -401,6 +329,25 @@ class StatusPanelWidget(QtGui.QWidget):          """          self.ui.globalStatusBox.hide() +    # EIP status --- + +    @property +    def eip_button(self): +        return self.ui.btnEipStartStop + +    @property +    def eip_label(self): +        return self.ui.lblEIPStatus + +    def eip_pre_up(self): +        """ +        Triggered when the app activates eip. +        Hides the status box and disables the start/stop button. +        """ +        self.hide_status_box() +        self.set_startstop_enabled(False) + +    # XXX disable (later) --------------------------      def set_eip_status(self, status, error=False):          """          Sets the status label at the VPN stage to status @@ -420,6 +367,7 @@ class StatusPanelWidget(QtGui.QWidget):          self.ui.lblEIPStatus.setText(status)          self._update_systray_tooltip() +    # XXX disable ---------------------------------      def set_startstop_enabled(self, value):          """          Enable or disable btnEipStartStop and _action_eip_startstop @@ -432,14 +380,7 @@ class StatusPanelWidget(QtGui.QWidget):          self.ui.btnEipStartStop.setEnabled(value)          self._action_eip_startstop.setEnabled(value) -    def eip_pre_up(self): -        """ -        Triggered when the app activates eip. -        Hides the status box and disables the start/stop button. -        """ -        self.hide_status_box() -        self.set_startstop_enabled(False) - +    # XXX disable -----------------------------      def eip_started(self):          """          Sets the state of the widget to how it should look after EIP @@ -448,27 +389,21 @@ class StatusPanelWidget(QtGui.QWidget):          self.ui.btnEipStartStop.setText(self.tr("Turn OFF"))          self.ui.btnEipStartStop.disconnect(self)          self.ui.btnEipStartStop.clicked.connect( -            self.stop_eip) +            self.eipconnection.qtsigs.do_connect_signal) +    # XXX disable -----------------------------      def eip_stopped(self):          """          Sets the state of the widget to how it should look after EIP          has stopped          """ +        # XXX should connect this to EIPConnection.disconnected_signal          self._reset_traffic_rates() +        # XXX disable -----------------------------          self.ui.btnEipStartStop.setText(self.tr("Turn ON"))          self.ui.btnEipStartStop.disconnect(self)          self.ui.btnEipStartStop.clicked.connect( -            self.start_eip) - -    def set_icon(self, icon): -        """ -        Sets the icon to display for EIP - -        :param icon: icon to display -        :type icon: QPixmap -        """ -        self.ui.lblVPNStatusIcon.setPixmap(icon) +            self.eipconnection.qtsigs.do_disconnect_signal)      def update_vpn_status(self, data):          """ @@ -507,14 +442,21 @@ class StatusPanelWidget(QtGui.QWidget):          TRIGGER: VPN.state_changed          Updates the displayed VPN state based on the data provided by -        the VPN thread +        the VPN thread. + +        Emits: +            If the status is connected, we emit EIPConnection.qtsigs. +            connected_signal          """          status = data[VPNManager.STATUS_STEP_KEY]          self.set_eip_status_icon(status)          if status == "CONNECTED": +            # XXX should be handled by the state machine too.              self.set_eip_status(self.tr("ON")) -            # Only now we can properly enable the button. -            self.set_startstop_enabled(True) +            logger.debug("STATUS IS CONNECTED --- emitting signal") +            self.eip_connection_connected.emit() + +        # XXX should lookup status map in EIPConnection          elif status == "AUTH":              self.set_eip_status(self.tr("Authenticating..."))          elif status == "GET_CONFIG": @@ -528,7 +470,8 @@ class StatusPanelWidget(QtGui.QWidget):          elif status == "ALREADYRUNNING":              # Put the following calls in Qt's event queue, otherwise              # the UI won't update properly -            QtCore.QTimer.singleShot(0, self.stop_eip) +            QtCore.QTimer.singleShot( +                0, self.eipconnection.qtsigs.do_disconnect_signal)              QtCore.QTimer.singleShot(0, partial(self.set_global_status,                                                  self.tr("Unable to start VPN, "                                                          "it's already " @@ -536,6 +479,15 @@ class StatusPanelWidget(QtGui.QWidget):          else:              self.set_eip_status(status) +    def set_eip_icon(self, icon): +        """ +        Sets the icon to display for EIP + +        :param icon: icon to display +        :type icon: QPixmap +        """ +        self.ui.lblVPNStatusIcon.setPixmap(icon) +      def set_eip_status_icon(self, status):          """          Given a status step from the VPN thread, set the icon properly @@ -556,13 +508,17 @@ class StatusPanelWidget(QtGui.QWidget):              selected_pixmap = self.CONNECTED_ICON              selected_pixmap_tray = self.CONNECTED_ICON_TRAY -        self.set_icon(selected_pixmap) +        self.set_eip_icon(selected_pixmap)          self._systray.setIcon(QtGui.QIcon(selected_pixmap_tray))          self._eip_status_menu.setTitle(tray_message)      def set_provider(self, provider):          self.ui.lblProvider.setText(provider) +    # +    # mail methods +    # +      def _set_mail_status(self, status, ready=False):          """          Sets the Mail status in the label and in the tray icon. @@ -742,7 +698,7 @@ class StatusPanelWidget(QtGui.QWidget):                  self.ui.lblUnread.setVisible(req.content != "0")                  self._set_mail_status(self.tr("ON"), ready=True)          else: -            leap_assert(False, +            leap_assert(False,  # XXX ???                          "Don't know how to handle this state: %s"                          % (req.event)) diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py new file mode 100644 index 00000000..f3ab9e8e --- /dev/null +++ b/src/leap/bitmask/services/connections.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# connections.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +Abstract LEAP connections. +""" +# TODO use zope.interface instead +from abc import ABCMeta + +from PySide import QtCore + +from leap.common.check import leap_assert + +_tr = QtCore.QObject().tr + + +class State(object): +    """ +    Abstract state class +    """ +    __metaclass__ = ABCMeta + +    label = None +    short_label = None + +""" +The different services should declare a ServiceConnection class that +inherits from AbstractLEAPConnection, so an instance of such class +can be used to inform the StateMachineBuilder of the particularities +of the state transitions for each particular connection. + +In the future, we will extend this class to allow composites in connections, +so we can apply conditional logic to the transitions. +""" + + +class AbstractLEAPConnection(object): +    """ +    Abstract LEAP Connection class. + +    This class is likely to undergo heavy transformations +    in the coming releases, to better accomodate the use cases +    of the different connections that we use in the Bitmask +    client. +    """ +    __metaclass__ = ABCMeta + +    _connection_name = None + +    @property +    def name(self): +        """ +        Name of the connection +        """ +        con_name = self._connection_name +        leap_assert(con_name is not None) +        return con_name + +    _qtsigs = None + +    @property +    def qtsigs(self): +        """ +        Object that encapsulates the Qt Signals emitted +        by this connection. +        """ +        return self._qtsigs + +    # XXX for conditional transitions with composites, +    #     we might want to add +    #     a field with dependencies: what this connection +    #     needs for (ON) state. +    # XXX Look also at child states in the state machine. +    #depends = () + +    # Signals that derived classes +    # have to implement. + +    # Commands +    do_connect_signal = None +    do_disconnect_signal = None + +    # Intermediate stages +    connecting_signal = None +    disconnecting_signal = None + +    # Complete stages +    connected_signal = None +    disconnected_signal = None + +    # Bypass stages +    connection_died_signal = None + +    class Disconnected(State): +        """Disconnected state""" +        label = _tr("Disconnected") +        short_label = _tr("OFF") + +    class Connected(State): +        """Connected state""" +        label = _tr("Connected") +        short_label = _tr("ON") + +    class Connecting(State): +        """Connecting state""" +        label = _tr("Connecting") +        short_label = _tr("...") + +    class Disconnecting(State): +        """Disconnecting state""" +        label = _tr("Disconnecting") +        short_label = _tr("...") diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py new file mode 100644 index 00000000..5f05ba07 --- /dev/null +++ b/src/leap/bitmask/services/eip/connection.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# connection.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +EIP Connection +""" +from PySide import QtCore + +from leap.bitmask.services.connections import AbstractLEAPConnection + + +class EIPConnectionSignals(QtCore.QObject): +    """ +    Qt Signals used by EIPConnection +    """ +    # commands +    do_connect_signal = QtCore.Signal() +    do_disconnect_signal = QtCore.Signal() + +    # intermediate stages +    # this is currently binded to mainwindow._start_eip +    connecting_signal = QtCore.Signal() +    # this is currently binded to mainwindow._stop_eip +    disconnecting_signal = QtCore.Signal() + +    connected_signal = QtCore.Signal() +    disconnected_signal = QtCore.Signal() + +    connection_died_signal = QtCore.Signal() + + +class EIPConnection(AbstractLEAPConnection): + +    def __init__(self): +        self._qtsigs = EIPConnectionSignals() diff --git a/src/leap/bitmask/services/eip/eipconfig.py b/src/leap/bitmask/services/eip/eipconfig.py index 466a644c..7d8995b4 100644 --- a/src/leap/bitmask/services/eip/eipconfig.py +++ b/src/leap/bitmask/services/eip/eipconfig.py @@ -14,7 +14,6 @@  #  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -  """  Provider configuration  """ diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index c01da372..15ac812b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -95,6 +95,7 @@ class VPN(object):          self._reactor = reactor          self._qtsigs = VPNSignals() +        # XXX should get it from config.flags          self._openvpn_verb = kwargs.get(self.OPENVPN_VERB, None)      @property diff --git a/src/leap/bitmask/util/averages.py b/src/leap/bitmask/util/averages.py new file mode 100644 index 00000000..65953f8f --- /dev/null +++ b/src/leap/bitmask/util/averages.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# averages.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" +Utility class for moving averages. + +It is used in the status panel widget for displaying up and down +download rates. +""" +from leap.bitmask.util import first + + +class RateMovingAverage(object): +    """ +    Moving window average for calculating +    upload and download rates. +    """ +    SAMPLE_SIZE = 5 + +    def __init__(self): +        """ +        Initializes an empty array of fixed size +        """ +        self.reset() + +    def reset(self): +        self._data = [None for i in xrange(self.SAMPLE_SIZE)] + +    def append(self, x): +        """ +        Appends a new data point to the collection. + +        :param x: A tuple containing timestamp and traffic points +                  in the form (timestamp, traffic) +        :type x: tuple +        """ +        self._data.pop(0) +        self._data.append(x) + +    def get(self): +        """ +        Gets the collection. +        """ +        return self._data + +    def get_average(self): +        """ +        Gets the moving average. +        """ +        data = filter(None, self.get()) +        traff = [traffic for (ts, traffic) in data] +        times = [ts for (ts, traffic) in data] + +        try: +            deltatraffic = traff[-1] - first(traff) +            deltat = (times[-1] - first(times)).seconds +        except IndexError: +            deltatraffic = 0 +            deltat = 0 + +        try: +            rate = float(deltatraffic) / float(deltat) / 1024 +        except ZeroDivisionError: +            rate = 0 + +        # In some cases we get negative rates +        if rate < 0: +            rate = 0 + +        return rate + +    def get_total(self): +        """ +        Gets the total accumulated throughput. +        """ +        try: +            return self._data[-1][1] / 1024 +        except TypeError: +            return 0 | 
