From 51948a6d9ee78929b72b0affdbfce36e65e073c2 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 19 Sep 2013 15:49:30 -0400 Subject: State Machine Builder and eip connection machine This implements an abstract definition of a LEAP state machine, and refactors eip connections to use it. --- src/leap/bitmask/gui/mainwindow.py | 216 +++++++++++++++------------ src/leap/bitmask/gui/statemachines.py | 223 ++++++++++++++++++++++++++++ src/leap/bitmask/gui/statuspanel.py | 162 ++++++++------------ src/leap/bitmask/services/connections.py | 125 ++++++++++++++++ src/leap/bitmask/services/eip/connection.py | 48 ++++++ src/leap/bitmask/services/eip/eipconfig.py | 1 - src/leap/bitmask/services/eip/vpnprocess.py | 1 + src/leap/bitmask/util/averages.py | 92 ++++++++++++ 8 files changed, 673 insertions(+), 195 deletions(-) create mode 100644 src/leap/bitmask/gui/statemachines.py create mode 100644 src/leap/bitmask/services/connections.py create mode 100644 src/leap/bitmask/services/eip/connection.py create mode 100644 src/leap/bitmask/util/averages.py (limited to 'src/leap/bitmask') 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 . +""" +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 . - """ 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 . +""" +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 . +""" +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 . - """ 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 . +""" +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 -- cgit v1.2.3