summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/feature_3900-eip-state-machine1
-rw-r--r--src/leap/bitmask/gui/mainwindow.py216
-rw-r--r--src/leap/bitmask/gui/statemachines.py223
-rw-r--r--src/leap/bitmask/gui/statuspanel.py162
-rw-r--r--src/leap/bitmask/services/connections.py125
-rw-r--r--src/leap/bitmask/services/eip/connection.py48
-rw-r--r--src/leap/bitmask/services/eip/eipconfig.py1
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py1
-rw-r--r--src/leap/bitmask/util/averages.py92
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