From d4398c52acc54fb27a4b8bba2735a41f55b8f402 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 17 Oct 2013 12:04:12 -0300 Subject: Mail State Machine refactor. Closes: #4059 --- src/leap/bitmask/gui/mail_status.py | 105 +++--- src/leap/bitmask/gui/mainwindow.py | 195 +++------- src/leap/bitmask/gui/statemachines.py | 414 ++++++++++++++++++++- src/leap/bitmask/services/connections.py | 10 +- src/leap/bitmask/services/eip/connection.py | 2 +- src/leap/bitmask/services/mail/conductor.py | 383 +++++++++++++++++++ src/leap/bitmask/services/mail/connection.py | 103 +++++ .../services/soledad/soledadbootstrapper.py | 4 +- 8 files changed, 1006 insertions(+), 210 deletions(-) create mode 100644 src/leap/bitmask/services/mail/conductor.py create mode 100644 src/leap/bitmask/services/mail/connection.py (limited to 'src/leap/bitmask') diff --git a/src/leap/bitmask/gui/mail_status.py b/src/leap/bitmask/gui/mail_status.py index 83533666..c2c06dbb 100644 --- a/src/leap/bitmask/gui/mail_status.py +++ b/src/leap/bitmask/gui/mail_status.py @@ -50,6 +50,8 @@ class MailStatusWidget(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) self._systray = None + self._disabled = True + self._started = False self.ui = Ui_MailStatusWidget() self.ui.setupUi(self) @@ -98,29 +100,16 @@ class MailStatusWidget(QtGui.QWidget): callback=self._mail_handle_soledad_events, reqcbk=lambda req, resp: None) - register(signal=proto.SMTP_SERVICE_STARTED, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.SMTP_SERVICE_FAILED_TO_START, - callback=self._mail_handle_smtp_events, - reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_STARTED, + register(signal=proto.IMAP_UNREAD_MAIL, callback=self._mail_handle_imap_events, reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_SERVICE_FAILED_TO_START, + register(signal=proto.IMAP_SERVICE_STARTED, callback=self._mail_handle_imap_events, reqcbk=lambda req, resp: None) - - register(signal=proto.IMAP_UNREAD_MAIL, + register(signal=proto.SMTP_SERVICE_STARTED, callback=self._mail_handle_imap_events, reqcbk=lambda req, resp: None) - self._smtp_started = False - self._imap_started = False - self._soledad_event.connect( self._mail_handle_soledad_events_slot) self._imap_event.connect( @@ -176,6 +165,9 @@ class MailStatusWidget(QtGui.QWidget): """ # TODO: Figure out how to handle this with the two status in different # classes + # XXX right now we could connect the state transition signals of the + # two connection machines (EIP/Mail) to a class that keeps track of the + # state -- kali # status = self.tr("Encrypted Internet: {0}").format(self._eip_status) # status += '\n' # status += self.tr("Mail is {0}").format(self._mx_status) @@ -292,11 +284,9 @@ class MailStatusWidget(QtGui.QWidget): """ # We want to ignore this kind of events once everything has # started - if self._smtp_started and self._imap_started: + if self._started: return - self._set_mail_status(self.tr("Starting..."), ready=1) - ext_status = "" if req.event == proto.KEYMANAGER_LOOKING_FOR_KEY: @@ -340,14 +330,9 @@ class MailStatusWidget(QtGui.QWidget): ext_status = "" if req.event == proto.SMTP_SERVICE_STARTED: - ext_status = self.tr("SMTP has started...") self._smtp_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=2) - ext_status = "" elif req.event == proto.SMTP_SERVICE_FAILED_TO_START: ext_status = self.tr("SMTP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) else: leap_assert(False, "Don't know how to handle this state: %s" @@ -355,6 +340,8 @@ class MailStatusWidget(QtGui.QWidget): self._set_mail_status(ext_status, ready=2) + # ----- XXX deprecate (move to mail conductor) + def _mail_handle_imap_events(self, req): """ Callback for the IMAP events @@ -376,27 +363,17 @@ class MailStatusWidget(QtGui.QWidget): """ ext_status = None - if req.event == proto.IMAP_SERVICE_STARTED: - ext_status = self.tr("IMAP has started...") - self._imap_started = True - if self._smtp_started and self._imap_started: - self._set_mail_status(self.tr("ON"), ready=2) - ext_status = "" - elif req.event == proto.IMAP_SERVICE_FAILED_TO_START: - ext_status = self.tr("IMAP failed to start, check the logs.") - self._set_mail_status(self.tr("Failed")) - elif req.event == proto.IMAP_UNREAD_MAIL: - if self._smtp_started and self._imap_started: + if req.event == proto.IMAP_UNREAD_MAIL: + + if self._started: + print "printing foo" if req.content != "0": self._set_mail_status(self.tr("%s Unread Emails") % (req.content,), ready=2) else: self._set_mail_status("", ready=2) - else: - leap_assert(False, # XXX ??? - "Don't know how to handle this state: %s" - % (req.event)) - + elif req.event == proto.IMAP_SERVICE_STARTED: + self._imap_started = True if ext_status is not None: self._set_mail_status(ext_status, ready=1) @@ -414,8 +391,50 @@ class MailStatusWidget(QtGui.QWidget): """ self._set_mail_status(self.tr("Disabled"), -1) - def stopped_mail(self): + # statuses + + # XXX make the signal emit the label and state. + + @QtCore.Slot() + def mail_state_disconnected(self): + """ + Displays the correct UI for the disconnected state. + """ + # XXX this should handle the disabled state better. + self._started = False + if self._disabled: + self.mail_state_disabled() + else: + self._set_mail_status(self.tr("OFF"), -1) + + @QtCore.Slot() + def mail_state_connecting(self): + """ + Displays the correct UI for the connecting state. + """ + self._disabled = False + self._started = True + self._set_mail_status(self.tr("Starting..."), 1) + + @QtCore.Slot() + def mail_state_disconnecting(self): + """ + Displays the correct UI for the connecting state. + """ + self._set_mail_status(self.tr("Disconnecting..."), 1) + + @QtCore.Slot() + def mail_state_connected(self): + """ + Displays the correct UI for the connected state. + """ + self._set_mail_status(self.tr("ON"), 2) + + @QtCore.Slot() + def mail_state_disabled(self): """ - Displayes the correct UI for the stopped state. + Displays the correct UI for the disabled state. """ - self._set_mail_status(self.tr("OFF")) + self._disabled = True + self._set_mail_status( + self.tr("You must be logged in to use encrypted email."), -1) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index f5631c69..dddd53da 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -24,6 +24,7 @@ import keyring from PySide import QtCore, QtGui from twisted.internet import threads +from zope.proxy import ProxyBase, setProxiedObject, sameProxiedObjects from leap.bitmask import __version__ as VERSION from leap.bitmask.config.leapsettings import LeapSettings @@ -39,18 +40,15 @@ from leap.bitmask.gui.mail_status import MailStatusWidget from leap.bitmask.gui.wizard import Wizard from leap.bitmask import provider -from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper -from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper -from leap.bitmask.services.eip import eipconfig -# XXX: Soledad might not work out of the box in Windows, issue #2932 -from leap.bitmask.services.soledad.soledadbootstrapper import \ - SoledadBootstrapper -from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper -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.provider.providerbootstrapper import ProviderBootstrapper +from leap.bitmask.services.mail import conductor as mail_conductor + +from leap.bitmask.services.eip import eipconfig from leap.bitmask.services.eip import get_openvpn_management +from leap.bitmask.services.eip.eipbootstrapper import EIPBootstrapper from leap.bitmask.services.eip.connection import EIPConnection from leap.bitmask.services.eip.vpnprocess import VPN from leap.bitmask.services.eip.vpnprocess import OpenVPNAlreadyRunning @@ -62,12 +60,12 @@ from leap.bitmask.services.eip.linuxvpnlauncher import EIPNoPkexecAvailable from leap.bitmask.services.eip.linuxvpnlauncher import \ EIPNoPolkitAuthAgentAvailable from leap.bitmask.services.eip.darwinvpnlauncher import EIPNoTunKextLoaded +from leap.bitmask.services.soledad.soledadbootstrapper import \ + SoledadBootstrapper from leap.bitmask.util.keyring_helpers import has_keyring from leap.bitmask.util.leap_log_handler import LeapLogHandler -from leap.bitmask.services.mail.smtpconfig import SMTPConfig - if IS_WIN: from leap.bitmask.platform_init.locks import WindowsLock from leap.bitmask.platform_init.locks import raise_window_ack @@ -93,10 +91,6 @@ class MainWindow(QtGui.QMainWindow): # Keyring KEYRING_KEY = "bitmask" - # SMTP - PORT_KEY = "port" - IP_KEY = "ip_address" - OPENVPN_SERVICE = "openvpn" MX_SERVICE = "mx" @@ -257,10 +251,6 @@ class MainWindow(QtGui.QMainWindow): self._soledad_bootstrapper.soledad_failed.connect( self._mail_status.set_soledad_failed) - self._smtp_bootstrapper = SMTPBootstrapper() - self._smtp_bootstrapper.download_config.connect( - self._smtp_bootstrapped_stage) - self.ui.action_about_leap.triggered.connect(self._about) self.ui.action_quit.triggered.connect(self.quit) self.ui.action_wizard.triggered.connect(self._launch_wizard) @@ -307,12 +297,13 @@ class MainWindow(QtGui.QMainWindow): # Services signals/slots connection self.new_updates.connect(self._react_to_new_updates) + + # XXX should connect to mail_conductor.start_mail_service instead + self.soledad_ready.connect(self._start_smtp_bootstrapping) self.soledad_ready.connect(self._start_imap_service) - self.soledad_ready.connect(self._set_soledad_ready) self.mail_client_logged_in.connect(self._fetch_incoming_mail) self.logout.connect(self._stop_imap_service) self.logout.connect(self._stop_smtp_service) - self.logout.connect(self._mail_status.stopped_mail) ################################# end Qt Signals connection ######## @@ -325,17 +316,18 @@ class MainWindow(QtGui.QMainWindow): self._bypass_checks = bypass_checks - self._soledad = None - self._soledad_ready = False - self._keymanager = None - self._smtp_service = None - self._smtp_port = None - self._imap_service = None + # We initialize Soledad and Keymanager instances as + # transparent proxies, so we can pass the reference freely + # around. + self._soledad = ProxyBase(None) + self._keymanager = ProxyBase(None) self._login_defer = None self._download_provider_defer = None - self._smtp_config = SMTPConfig() + self._mail_conductor = mail_conductor.MailConductor( + self._soledad, self._keymanager) + self._mail_conductor.connect_mail_signals(self._mail_status) # Eip machine is a public attribute where the state machine for # the eip connection will be available to the different components. @@ -347,6 +339,7 @@ class MainWindow(QtGui.QMainWindow): self.eip_machine = None # start event machines self.start_eip_machine() + self._mail_conductor.start_mail_machine(parent=self) if self._first_run(): self._wizard_firstrun = True @@ -460,7 +453,7 @@ class MainWindow(QtGui.QMainWindow): """ preferences_window = PreferencesWindow(self, self._srp_auth) - if self._soledad_ready: + if sameProxiedObjects(self._soledad, None): preferences_window.set_soledad_ready(self._soledad) else: self.soledad_ready.connect( @@ -478,16 +471,6 @@ class MainWindow(QtGui.QMainWindow): """ EIPPreferencesWindow(self).show() - def _set_soledad_ready(self): - """ - SLOT - TRIGGERS: - self.soledad_ready - - It sets the soledad object as ready to use. - """ - self._soledad_ready = True - # # updates # @@ -803,6 +786,7 @@ class MainWindow(QtGui.QMainWindow): provider configuration if it's not present, otherwise will emit the corresponding signals inmediately """ + # XXX should rename this provider, name clash. provider = self._login_widget.get_selected_provider() pb = self._provider_bootstrapper @@ -823,6 +807,7 @@ class MainWindow(QtGui.QMainWindow): :type data: dict """ if data[self._provider_bootstrapper.PASSED_KEY]: + # XXX should rename this provider, name clash. provider = self._login_widget.get_selected_provider() # If there's no loaded provider or @@ -1023,114 +1008,58 @@ class MainWindow(QtGui.QMainWindow): logger.debug("ERROR on soledad bootstrapping:") logger.error("%r" % data[self._soledad_bootstrapper.ERROR_KEY]) return - else: - logger.debug("Done bootstrapping Soledad") - self._soledad = self._soledad_bootstrapper.soledad - self._keymanager = self._soledad_bootstrapper.keymanager + logger.debug("Done bootstrapping Soledad") + + # Update the proxy objects to point to + # the initialized instances. + setProxiedObject(self._soledad, + self._soledad_bootstrapper.soledad) + setProxiedObject(self._keymanager, + self._soledad_bootstrapper.keymanager) # Ok, now soledad is ready, so we can allow other things that # depend on soledad to start. # this will trigger start_imap_service + # and start_smtp_boostrapping self.soledad_ready.emit() - # TODO connect all these activations to the soledad_ready - # signal so the logic is clearer to follow. - - if self._provider_config.provides_mx() and \ - self._enabled_services.count(self.MX_SERVICE) > 0: - self._smtp_bootstrapper.run_smtp_setup_checks( - self._provider_config, - self._smtp_config, - True) - ################################################################### # Service control methods: smtp - def _smtp_bootstrapped_stage(self, data): + @QtCore.Slot() + def _start_smtp_bootstrapping(self): """ SLOT TRIGGERS: - self._smtp_bootstrapper.download_config - - If there was a problem, displays it, otherwise it does nothing. - This is used for intermediate bootstrapping stages, in case - they fail. - - :param data: result from the bootstrapping stage for Soledad - :type data: dict - """ - passed = data[self._smtp_bootstrapper.PASSED_KEY] - if not passed: - logger.error(data[self._smtp_bootstrapper.ERROR_KEY]) - return - logger.debug("Done bootstrapping SMTP") - self._check_smtp_config() - - def _check_smtp_config(self): - """ - Checks smtp config and tries to download smtp client cert if needed. + self.soledad_ready """ - hosts = self._smtp_config.get_hosts() - # TODO handle more than one host and define how to choose - if len(hosts) > 0: - hostname = hosts.keys()[0] - logger.debug("Using hostname %s for SMTP" % (hostname,)) - host = hosts[hostname][self.IP_KEY].encode("utf-8") - port = hosts[hostname][self.PORT_KEY] - - client_cert = self._smtp_config.get_client_cert_path( + # TODO for simmetry, this should be called start_smtp_service + # (and delegate all the checks to the conductor) + if self._provider_config.provides_mx() and \ + self._enabled_services.count(self.MX_SERVICE) > 0: + self._mail_conductor.smtp_bootstrapper.run_smtp_setup_checks( self._provider_config, - about_to_download=True) - - if not os.path.isfile(client_cert): - self._smtp_bootstrapper._download_client_certificates() - if os.path.isfile(client_cert): - self._start_smtp_service(host, port, client_cert) - else: - logger.warning("Tried to download email client " - "certificate, but could not find any") - - else: - logger.warning("No smtp hosts configured") - - def _start_smtp_service(self, host, port, cert): - """ - Starts the smtp service. - """ - # TODO Make the encrypted_only configurable - # TODO pick local smtp port in a better way - # TODO remove hard-coded port and let leap.mail set - # the specific default. - - from leap.mail.smtp import setup_smtp_relay - self._smtp_service, self._smtp_port = setup_smtp_relay( - port=2013, - keymanager=self._keymanager, - smtp_host=host, - smtp_port=port, - smtp_cert=cert, - smtp_key=cert, - encrypted_only=False) + self._mail_conductor.smtp_config, + download_if_needed=True) + # XXX --- should remove from here, and connecte directly to the state + # machine. + @QtCore.Slot() def _stop_smtp_service(self): """ SLOT TRIGGERS: self.logout """ - # There is a subtle difference here: - # we are stopping the factory for the smtp service here, - # but in the imap case we are just stopping the fetcher. - if self._smtp_service is not None: - logger.debug('Stopping smtp service.') - self._smtp_port.stopListening() - self._smtp_service.doStop() + # TODO call stop_mail_service + self._mail_conductor.stop_smtp_service() ################################################################### # Service control methods: imap + @QtCore.Slot() def _start_imap_service(self): """ SLOT @@ -1139,11 +1068,7 @@ class MainWindow(QtGui.QMainWindow): """ if self._provider_config.provides_mx() and \ self._enabled_services.count(self.MX_SERVICE) > 0: - logger.debug('Starting imap service') - - self._imap_service = imap.start_imap_service( - self._soledad, - self._keymanager) + self._mail_conductor.start_imap_service() def _on_mail_client_logged_in(self, req): """ @@ -1151,30 +1076,25 @@ class MainWindow(QtGui.QMainWindow): """ self.mail_client_logged_in.emit() + @QtCore.Slot() def _fetch_incoming_mail(self): """ SLOT TRIGGERS: self.mail_client_logged_in """ - # TODO have a mutex over fetch operation. - if self._imap_service: - logger.debug('Client connected, fetching mail...') - self._imap_service.fetch() + # TODO connect signal directly!!! + self._mail_conductor.fetch_incoming_mail() + @QtCore.Slot() def _stop_imap_service(self): """ SLOT TRIGGERS: self.logout """ - # There is a subtle difference here: - # we are just stopping the fetcher here, - # but in the smtp case we are stopping the factory. - # We should homogenize both services. - if self._imap_service is not None: - logger.debug('Stopping imap service.') - self._imap_service.stop() + # TODO call stop_mail_service + self._mail_conductor.stop_imap_service() # end service control methods (imap) @@ -1623,8 +1543,8 @@ class MainWindow(QtGui.QMainWindow): if ok: self._logged_user = None - self._login_widget.logged_out() + self._mail_status.mail_state_disabled() else: self._login_widget.set_login_status( @@ -1700,8 +1620,7 @@ class MainWindow(QtGui.QMainWindow): """ logger.debug('About to quit, doing cleanup...') - if self._imap_service is not None: - self._imap_service.stop() + self._mail_conductor.stop_imap_service() if self._srp_auth is not None: if self._srp_auth.get_session_id() is not None or \ diff --git a/src/leap/bitmask/gui/statemachines.py b/src/leap/bitmask/gui/statemachines.py index 94726720..ee16a4c6 100644 --- a/src/leap/bitmask/gui/statemachines.py +++ b/src/leap/bitmask/gui/statemachines.py @@ -19,7 +19,8 @@ State machines for the Bitmask app. """ import logging -from PySide.QtCore import QStateMachine, QState +from PySide import QtCore +from PySide.QtCore import QStateMachine, QState, Signal from PySide.QtCore import QObject from leap.bitmask.services import connections @@ -36,28 +37,255 @@ _CON = "connecting" _DIS = "disconnecting" -class IntermediateState(QState): +class SignallingState(QState): """ - Intermediate state that emits a custom signal on entry + A state that emits a custom signal on entry. """ - def __init__(self, signal): + def __init__(self, signal, parent=None, name=None): """ Initializer. :param signal: the signal to be emitted on entry on this state. :type signal: QtCore.QSignal """ - super(IntermediateState, self).__init__() + super(SignallingState, self).__init__(parent) self._signal = signal + self._name = name def onEntry(self, *args): """ Emits the signal on entry. """ - logger.debug('IntermediateState entered. Emitting signal ...') + logger.debug('State %s entered. Emitting signal ...' + % (self._name + self.__class__.__name__)) if self._signal is not None: self._signal.emit() +class States(object): + """ + States for composite objects + """ + + class Off(SignallingState): + pass + + class Connecting(SignallingState): + pass + + class On(SignallingState): + pass + + class Disconnecting(SignallingState): + pass + + class StepsTrack(QObject): + state_change = Signal() + + def __init__(self, target): + super(States.StepsTrack, self).__init__() + self.received = set([]) + self.target = set(target) + + def is_all_done(self): + return all([ev in self.target for ev in self.received]) + + def is_any_done(self): + return any([ev in self.target for ev in self.received]) + + def seen(self, _type): + if _type in self.target: + self.received.add(_type) + + def reset_seen(self): + self.received = set([]) + + class TransitionOR(QtCore.QSignalTransition): + + def __init__(self, state): + super(States.TransitionOR, self).__init__( + state, QtCore.SIGNAL('state_change()')) + self.state = state + + def eventTest(self, e): + self.state.seen(e.type()) + done = self.state.is_any_done() + if done: + self.state.reset_seen() + return done + + def onTransition(self, e): + pass + + class TransitionAND(QtCore.QSignalTransition): + + def __init__(self, state): + super(States.TransitionAND, self).__init__( + state, QtCore.SIGNAL('state_change()')) + self.state = state + + def eventTest(self, e): + self.state.seen(e.type()) + done = self.state.is_all_done() + if done: + self.state.reset_seen() + return done + + def onTransition(self, e): + pass + + +class CompositeEvent(QtCore.QEvent): + def __init__(self): + super(CompositeEvent, self).__init__( + QtCore.QEvent.Type(self.ID)) + + +class Composite(object): + # TODO we should generate the connectingEvents dinamycally, + # depending on how much composite states do we get. + # This only supports up to 2 composite states. + + class ConnectingEvent1(CompositeEvent): + ID = QtCore.QEvent.User + 1 + + class ConnectingEvent2(CompositeEvent): + ID = QtCore.QEvent.User + 2 + + class ConnectedEvent1(CompositeEvent): + ID = QtCore.QEvent.User + 3 + + class ConnectedEvent2(CompositeEvent): + ID = QtCore.QEvent.User + 4 + + class DisconnectingEvent1(CompositeEvent): + ID = QtCore.QEvent.User + 5 + + class DisconnectingEvent2(CompositeEvent): + ID = QtCore.QEvent.User + 6 + + class DisconnectedEvent1(CompositeEvent): + ID = QtCore.QEvent.User + 7 + + class DisconnectedEvent2(CompositeEvent): + ID = QtCore.QEvent.User + 8 + + +class Events(QtCore.QObject): + """ + A Wrapper object for containing the events that will be + posted to a composite state machine. + """ + def __init__(self, parent=None): + """ + Initializes the QObject with the given parent. + """ + QtCore.QObject.__init__(self, parent) + + +class CompositeMachine(QStateMachine): + + def __init__(self, parent=None): + QStateMachine.__init__(self, parent) + + # events + self.events = Events(parent) + self.create_events() + + def create_events(self): + """ + Creates a bunch of events to be posted to the state machine when + the transitions say so. + """ + # XXX refactor into a dictionary? + self.events.con_ev1 = Composite.ConnectingEvent1() + self.events.con_ev2 = Composite.ConnectingEvent2() + self.events.on_ev1 = Composite.ConnectedEvent1() + self.events.on_ev2 = Composite.ConnectedEvent2() + self.events.dis_ev1 = Composite.DisconnectingEvent1() + self.events.dis_ev2 = Composite.DisconnectingEvent2() + self.events.off_ev1 = Composite.DisconnectedEvent1() + self.events.off_ev2 = Composite.DisconnectedEvent2() + + def beginSelectTransitions(self, e): + """ + Weird. Having this method makes underlying backtraces + to appear magically on the transitions. + :param e: the received event + :type e: QEvent + """ + pass + + def _connect_children(self, child1, child2): + """ + Connects the state transition signals for children machines. + + :param child1: the first child machine + :type child1: QStateMachine + :param child2: the second child machine + :type child2: QStateMachine + """ + # TODO refactor and generalize for composites + # of more than 2 connections. + + c1 = child1.conn + c1.qtsigs.connecting_signal.connect(self.con_ev1_slot) + c1.qtsigs.connected_signal.connect(self.on_ev1_slot) + c1.qtsigs.disconnecting_signal.connect(self.dis_ev1_slot) + c1.qtsigs.disconnected_signal.connect(self.off_ev1_slot) + + c2 = child2.conn + c2.qtsigs.connecting_signal.connect(self.con_ev2_slot) + c2.qtsigs.connected_signal.connect(self.on_ev2_slot) + c2.qtsigs.disconnecting_signal.connect(self.dis_ev2_slot) + c2.qtsigs.disconnected_signal.connect(self.off_ev2_slot) + + # XXX why is this getting deletec in c++? + #Traceback (most recent call last): + #self.postEvent(self.events.on_ev2) + #RuntimeError: Internal C++ object (ConnectedEvent2) already deleted. + # XXX trying the following workaround, since + # I cannot find why in the world this is getting deleted :( + # XXX refactor! + + # slots connection1 + + def con_ev1_slot(self): + # XXX if we just postEvent, we get the Internal C++ object deleted... + # so the workaround is to re-create it each time. + self.events.con_ev1 = Composite.ConnectingEvent1() + self.postEvent(self.events.con_ev1) + + def on_ev1_slot(self): + self.events.on_ev1 = Composite.ConnectedEvent1() + self.postEvent(self.events.on_ev1) + + def dis_ev1_slot(self): + self.events.dis_ev1 = Composite.DisconnectingEvent1() + self.postEvent(self.events.dis_ev1) + + def off_ev1_slot(self): + self.events.off_ev1 = Composite.DisconnectedEvent1() + self.postEvent(self.events.off_ev1) + + # slots connection2 + + def con_ev2_slot(self): + self.events.con_ev2 = Composite.ConnectingEvent2() + self.postEvent(self.events.con_ev2) + + def on_ev2_slot(self): + self.events.on_ev2 = Composite.ConnectedEvent2() + self.postEvent(self.events.on_ev2) + + def dis_ev2_slot(self): + self.events.dis_ev2 = Composite.DisconnectingEvent2() + self.postEvent(self.events.dis_ev2) + + def off_ev2_slot(self): + self.events.off_ev2 = Composite.DisconnectedEvent2() + self.postEvent(self.events.off_ev2) + + class ConnectionMachineBuilder(object): """ Builder class for state machines made from LEAPConnections. @@ -65,16 +293,161 @@ class ConnectionMachineBuilder(object): def __init__(self, connection): """ :param connection: an instance of a concrete LEAPConnection - we will be building a state machine for. + 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): + def make_machine(self, **kwargs): """ Creates a statemachine associated with the passed controls. + It returns the state machine if the connection used for initializing + the ConnectionMachineBuilder inherits exactly from + LEAPAbstractConnection, and a tuple with the Composite Machine and its + individual parts in case that it is a composite machine which + connection definition inherits from more than one class that, on their + time, inherit from LEAPAbstractConnection. + + :params: see parameters for ``_make_simple_machine`` + :returns: a QStateMachine, or a tuple with the form: + (CompositeStateMachine, (StateMachine1, StateMachine2)) + :rtype: QStateMachine or tuple + """ + components = self._conn.components + + if components is None: + # simple case: connection definition inherits directly from + # the abstract connection. + + leap_assert_type(self._conn, connections.AbstractLEAPConnection) + return self._make_simple_machine(self._conn, **kwargs) + + if components: + # composite case: connection definition inherits from several + # classes, each one of which inherit from the abstract connection. + child_machines = tuple( + [ConnectionMachineBuilder(connection()).make_machine() + for connection in components]) + composite_machine = self._make_composite_machine( + self._conn, child_machines, **kwargs) + + composite_machine._connect_children( + *child_machines) + + # XXX should also connect its own states with the signals + # for the composite machine itself + + return (composite_machine, child_machines) + + def _make_composite_machine(self, conn, children, + **kwargs): + """ + Creates a composite machine. + + :param conn: an instance of a connection definition. + :type conn: LEAPAbstractConnection + :param children: children machines + :type children: tuple of state machines + :returns: A composite state machine + :rtype: QStateMachine + """ + # TODO split this method in smaller utility functions. + parent = kwargs.get('parent', None) + + # 1. create machine + machine = CompositeMachine(parent=parent) + + # 2. create states + off = States.Off(conn.qtsigs.disconnected_signal, + parent=machine, + name=conn.name) + off.setObjectName("off") + + on = States.On(conn.qtsigs.connected_signal, + parent=machine, + name=conn.name) + on.setObjectName("on") + + connecting_state = States.Connecting( + conn.qtsigs.connecting_signal, + parent=machine, + name=conn.name) + connecting_state.setObjectName("connecting") + + disconnecting_state = States.Disconnecting( + conn.qtsigs.disconnecting_signal, + parent=machine, + name=conn.name) + disconnecting_state.setObjectName("disconnecting") + + # 3. TODO create as many connectingEvents as needed (dynamically create + # classses for that) + # (we have manually created classes for events under CompositeEvent for + # now, to begin with the simple 2 states case for mail. + + # 4. state tracking objects for each transition stage + + connecting_track0 = States.StepsTrack( + (Composite.ConnectingEvent1.ID, + Composite.ConnectingEvent2.ID)) + connecting_track0.setObjectName("connecting_step_0") + + connecting_track1 = States.StepsTrack( + (Composite.ConnectedEvent1.ID, + Composite.ConnectedEvent2.ID)) + connecting_track1.setObjectName("connecting_step_1") + + disconnecting_track0 = States.StepsTrack( + (Composite.DisconnectingEvent1.ID, + Composite.DisconnectingEvent2.ID)) + disconnecting_track0.setObjectName("disconnecting_step_0") + + disconnecting_track1 = States.StepsTrack( + (Composite.DisconnectedEvent1.ID, + Composite.DisconnectedEvent2.ID)) + disconnecting_track1.setObjectName("disconnecting_step_1") + + # 5. definte the transitions with the matching state-tracking + # objects. + + # off -> connecting + connecting_transition = States.TransitionOR( + connecting_track0) + connecting_transition.setTargetState(connecting_state) + off.addTransition(connecting_transition) + + # connecting -> on + connected_transition = States.TransitionAND( + connecting_track1) + connected_transition.setTargetState(on) + connecting_state.addTransition(connected_transition) + + # on -> disconnecting + disconnecting_transition = States.TransitionOR( + disconnecting_track0) + disconnecting_transition.setTargetState(disconnecting_state) + on.addTransition(disconnecting_transition) + + # disconnecting -> off + disconnected_transition = States.TransitionAND( + disconnecting_track1) + disconnected_transition.setTargetState(off) + disconnecting_state.addTransition(disconnected_transition) + + machine.setInitialState(off) + machine.conn = conn + return machine + + def _make_simple_machine(self, conn, + button=None, action=None, label=None): + """ + Creates a statemachine associated with the passed controls. + + :param conn: the connection instance that defines this machine. + :type conn: AbstractLEAPConnection + :param button: the switch button. :type button: QPushButton @@ -88,9 +461,7 @@ class ConnectionMachineBuilder(object): :rtype: QStateMachine """ machine = QStateMachine() - conn = self._conn - - states = self._make_states(button, action, label) + states = self._make_states(conn, button, action, label) # transitions: @@ -151,11 +522,17 @@ class ConnectionMachineBuilder(object): for state in states.itervalues(): machine.addState(state) machine.setInitialState(states[_OFF]) + + machine.conn = conn return machine - def _make_states(self, button, action, label): + def _make_states(self, conn, button, action, label): """ - Creates the four states for the state machine + Creates the four states for the simple state machine. + Adds the needed properties for the passed controls. + + :param conn: the connection instance that defines this machine. + :type conn: AbstractLEAPConnection :param button: the switch button. :type button: QPushButton @@ -169,7 +546,6 @@ class ConnectionMachineBuilder(object): :returns: a dict of states :rtype: dict """ - conn = self._conn states = {} # TODO add tooltip @@ -190,8 +566,9 @@ class ConnectionMachineBuilder(object): states[_OFF] = off # CONNECTING State ---------------- - connecting = IntermediateState( - conn.qtsigs.connecting_signal) + connecting = SignallingState( + conn.qtsigs.connecting_signal, + name=conn.name) on_label = _tr("Turn {0}").format( conn.Disconnected.short_label) if button: @@ -224,8 +601,9 @@ class ConnectionMachineBuilder(object): states[_ON] = on # DISCONNECTING State ------------- - disconnecting = IntermediateState( - conn.qtsigs.disconnecting_signal) + disconnecting = SignallingState( + conn.qtsigs.disconnecting_signal, + name=conn.name) if button: disconnecting.assignProperty( button, 'enabled', False) diff --git a/src/leap/bitmask/services/connections.py b/src/leap/bitmask/services/connections.py index 8aeb4e0c..ecfd35ff 100644 --- a/src/leap/bitmask/services/connections.py +++ b/src/leap/bitmask/services/connections.py @@ -41,9 +41,6 @@ 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. """ @@ -79,12 +76,7 @@ class AbstractLEAPConnection(object): """ 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 = () + components = None # Signals that derived classes # have to implement. diff --git a/src/leap/bitmask/services/eip/connection.py b/src/leap/bitmask/services/eip/connection.py index 962d9cf2..8a35d550 100644 --- a/src/leap/bitmask/services/eip/connection.py +++ b/src/leap/bitmask/services/eip/connection.py @@ -46,5 +46,5 @@ class EIPConnectionSignals(QtCore.QObject): class EIPConnection(AbstractLEAPConnection): def __init__(self): - # XXX this should be public instead self._qtsigs = EIPConnectionSignals() + self._connection_name = "Encrypted Internet" diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py new file mode 100644 index 00000000..3a3e16a3 --- /dev/null +++ b/src/leap/bitmask/services/mail/conductor.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +# conductor.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 . +""" +Mail Services Conductor +""" +import logging +import os + +from PySide import QtCore +from zope.proxy import sameProxiedObjects + +from leap.bitmask.gui import statemachines +from leap.bitmask.services.mail import imap +from leap.bitmask.services.mail import connection as mail_connection +from leap.bitmask.services.mail.smtpbootstrapper import SMTPBootstrapper +from leap.bitmask.services.mail.smtpconfig import SMTPConfig +from leap.bitmask.util import is_file + +from leap.common.check import leap_assert + +from leap.common.events import register as leap_register +from leap.common.events import events_pb2 as leap_events + +logger = logging.getLogger(__name__) + + +class IMAPControl(object): + """ + Methods related to IMAP control. + """ + def __init__(self): + """ + Initializes smtp variables. + """ + self.imap_machine = None + self.imap_service = None + self.imap_connection = None + + leap_register(signal=leap_events.IMAP_SERVICE_STARTED, + callback=self._handle_imap_events, + reqcbk=lambda req, resp: None) + leap_register(signal=leap_events.IMAP_SERVICE_FAILED_TO_START, + callback=self._handle_imap_events, + reqcbk=lambda req, resp: None) + + def set_imap_connection(self, imap_connection): + """ + Sets the imap connection to an initialized connection. + + :param imap_connection: an initialized imap connection + :type imap_connection: IMAPConnection instance. + """ + self.imap_connection = imap_connection + + def start_imap_service(self): + """ + Starts imap service. + """ + logger.debug('Starting imap service') + leap_assert(sameProxiedObjects(self._soledad, None) + is not True, + "We need a non-null soledad for initializing imap service") + leap_assert(sameProxiedObjects(self._keymanager, None) + is not True, + "We need a non-null keymanager for initializing imap " + "service") + + if self.imap_service is None: + # first time. + self.imap_service = imap.start_imap_service( + self._soledad, + self._keymanager) + else: + # we have the fetcher. just start it. + self.imap_service.start_loop() + + def stop_imap_service(self): + """ + Stops imap service. + + There is a subtle difference here: + we are just stopping the fetcher here, + but in the smtp case we are stopping the factory. + """ + self.imap_connection.qtsigs.disconnecting_signal.emit() + # TODO We should homogenize both services. + if self.imap_service is not None: + logger.debug('Stopping imap service.') + self.imap_service.stop() + + def fetch_incoming_mail(self): + """ + Fetches incoming mail. + """ + # TODO have a mutex over fetch operation. + if self.imap_service: + logger.debug('Client connected, fetching mail...') + self.imap_service.fetch() + + # handle events + + def _handle_imap_events(self, req): + """ + Callback handler for the IMAP events + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + if req.event == leap_events.IMAP_SERVICE_STARTED: + self.on_imap_connected() + elif req.event == leap_events.IMAP_SERVICE_FAILED_TO_START: + self.on_imap_failed() + + # emit connection signals + + def on_imap_connecting(self): + """ + Callback for IMAP connecting state. + """ + self.imap_connection.qtsigs.connecting_signal.emit() + + def on_imap_connected(self): + """ + Callback for IMAP connected state. + """ + self.imap_connection.qtsigs.connected_signal.emit() + + def on_imap_failed(self): + """ + Callback for IMAP failed state. + """ + self.imap_connection.qtsigs.connetion_aborted_signal.emit() + + +class SMTPControl(object): + + PORT_KEY = "port" + IP_KEY = "ip_address" + + def __init__(self): + """ + Initializes smtp variables. + """ + self.smtp_config = SMTPConfig() + self.smtp_connection = None + self.smtp_machine = None + self._smtp_service = None + self._smtp_port = None + + self.smtp_bootstrapper = SMTPBootstrapper() + self.smtp_bootstrapper.download_config.connect( + self.smtp_bootstrapped_stage) + + leap_register(signal=leap_events.SMTP_SERVICE_STARTED, + callback=self._handle_smtp_events, + reqcbk=lambda req, resp: None) + leap_register(signal=leap_events.SMTP_SERVICE_FAILED_TO_START, + callback=self._handle_smtp_events, + reqcbk=lambda req, resp: None) + + def set_smtp_connection(self, smtp_connection): + """ + Sets the smtp connection to an initialized connection. + :param smtp_connection: an initialized smtp connection + :type smtp_connection: SMTPConnection instance. + """ + self.smtp_connection = smtp_connection + + def start_smtp_service(self, host, port, cert): + """ + Starts the smtp service. + + :param host: the hostname of the remove SMTP server. + :type host: str + :param port: the port of the remote SMTP server + :type port: str + :param cert: the client certificate for authentication + :type cert: str + """ + # TODO Make the encrypted_only configurable + # TODO pick local smtp port in a better way + # TODO remove hard-coded port and let leap.mail set + # the specific default. + self.smtp_connection.qtsigs.connecting_signal.emit() + from leap.mail.smtp import setup_smtp_relay + self._smtp_service, self._smtp_port = setup_smtp_relay( + port=2013, + keymanager=self._keymanager, + smtp_host=host, + smtp_port=port, + smtp_cert=cert, + smtp_key=cert, + encrypted_only=False) + + def stop_smtp_service(self): + """ + Stops the smtp service. + + There is a subtle difference here: + we are stopping the factory for the smtp service here, + but in the imap case we are just stopping the fetcher. + """ + self.smtp_connection.qtsigs.disconnecting_signal.emit() + # TODO We should homogenize both services. + if self._smtp_service is not None: + logger.debug('Stopping smtp service.') + self._smtp_port.stopListening() + self._smtp_service.doStop() + + @QtCore.Slot() + def smtp_bootstrapped_stage(self, data): + """ + SLOT + TRIGGERS: + self.smtp_bootstrapper.download_config + + If there was a problem, displays it, otherwise it does nothing. + This is used for intermediate bootstrapping stages, in case + they fail. + + :param data: result from the bootstrapping stage for Soledad + :type data: dict + """ + passed = data[self.smtp_bootstrapper.PASSED_KEY] + if not passed: + logger.error(data[self.smtp_bootstrapper.ERROR_KEY]) + return + logger.debug("Done bootstrapping SMTP") + self.check_smtp_config() + + def check_smtp_config(self): + """ + Checks smtp config and tries to download smtp client cert if needed. + Currently called when smtp_bootstrapped_stage has successfuly finished. + """ + logger.debug("Checking SMTP config...") + leap_assert(self.smtp_bootstrapper._provider_config, + "smtp bootstrapper does not have a provider_config") + + provider_config = self.smtp_bootstrapper._provider_config + smtp_config = self.smtp_config + hosts = smtp_config.get_hosts() + # TODO handle more than one host and define how to choose + if len(hosts) > 0: + hostname = hosts.keys()[0] + logger.debug("Using hostname %s for SMTP" % (hostname,)) + host = hosts[hostname][self.IP_KEY].encode("utf-8") + port = hosts[hostname][self.PORT_KEY] + + client_cert = smtp_config.get_client_cert_path( + provider_config, + about_to_download=True) + + # XXX change this logic! + # check_config should be called from within start_service, + # and not the other way around. + if not is_file(client_cert): + self.smtp_bootstrapper._download_client_certificates() + if os.path.isfile(client_cert): + self.start_smtp_service(host, port, client_cert) + else: + logger.warning("Tried to download email client " + "certificate, but could not find any") + + else: + logger.warning("No smtp hosts configured") + + # handle smtp events + + def _handle_smtp_events(self, req): + """ + Callback handler for the SMTP events. + + :param req: Request type + :type req: leap.common.events.events_pb2.SignalRequest + """ + if req.event == leap_events.SMTP_SERVICE_STARTED: + self.on_smtp_connected() + elif req.event == leap_events.SMTP_SERVICE_FAILED_TO_START: + self.on_smtp_failed() + + # emit connection signals + + def on_smtp_connecting(self): + """ + Callback for SMTP connecting state. + """ + self.smtp_connection.qtsigs.connecting_signal.emit() + + def on_smtp_connected(self): + """ + Callback for SMTP connected state. + """ + self.smtp_connection.qtsigs.connected_signal.emit() + + def on_smtp_failed(self): + """ + Callback for SMTP failed state. + """ + self.smtp_connection.qtsigs.connection_aborted_signal.emit() + + +class MailConductor(IMAPControl, SMTPControl): + """ + This class encapsulates everything related to the initialization and + process control for the mail services. + Currently, it initializes IMAPConnection and SMPTConnection. + """ + # XXX We could consider to use composition instead of inheritance here. + + def __init__(self, soledad, keymanager): + """ + Initializes the mail conductor. + + :param soledad: a transparent proxy that eventually will point to a + Soledad Instance. + :type soledad: zope.proxy.ProxyBase + + :param keymanager: a transparent proxy that eventually will point to a + Keymanager Instance. + :type soledad: zope.proxy.ProxyBase + """ + IMAPControl.__init__(self) + SMTPControl.__init__(self) + self._soledad = soledad + self._keymanager = keymanager + + self._mail_machine = None + + self._mail_connection = mail_connection.MailConnection() + + def start_mail_machine(self, **kwargs): + """ + Starts mail machine. + """ + logger.debug("Starting mail state machine...") + builder = statemachines.ConnectionMachineBuilder(self._mail_connection) + (mail, (imap, smtp)) = builder.make_machine(**kwargs) + + # we have instantiated the connections while building the composite + # machines, and we have to use the qtsigs instantiated there. + # XXX we could probably use a proxy here too to make the thing + # transparent. + self.set_imap_connection(imap.conn) + self.set_smtp_connection(smtp.conn) + + self._mail_machine = mail + # XXX ------------------- + # need to keep a reference? + #self._mail_events = mail.events + self._mail_machine.start() + + self._imap_machine = imap + self._imap_machine.start() + self._smtp_machine = smtp + self._smtp_machine.start() + + def connect_mail_signals(self, widget): + """ + Connects the mail signals to the mail_status widget slots. + + :param widget: the widget containing the slots. + :type widget: QtCore.QWidget + """ + qtsigs = self._mail_connection.qtsigs + qtsigs.connected_signal.connect(widget.mail_state_connected) + qtsigs.connecting_signal.connect(widget.mail_state_connecting) + qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) + qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py new file mode 100644 index 00000000..29378f62 --- /dev/null +++ b/src/leap/bitmask/services/mail/connection.py @@ -0,0 +1,103 @@ +# -*- 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 . +""" +Email Connections +""" +from PySide import QtCore + +from leap.bitmask.services.connections import AbstractLEAPConnection + + +class IMAPConnectionSignals(QtCore.QObject): + """ + Qt Signals used by IMAPConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + # intermediate stages + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class IMAPConnection(AbstractLEAPConnection): + + _connection_name = "IMAP" + + def __init__(self): + self._qtsigs = IMAPConnectionSignals() + + +class SMTPConnectionSignals(QtCore.QObject): + """ + Qt Signals used by SMTPConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + # intermediate stages + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class SMTPConnection(AbstractLEAPConnection): + + _connection_name = "IMAP" + + def __init__(self): + self._qtsigs = SMTPConnectionSignals() + + +class MailConnectionSignals(QtCore.QObject): + """ + Qt Signals used by MailConnection + """ + # commands + do_connect_signal = QtCore.Signal() + do_disconnect_signal = QtCore.Signal() + + connecting_signal = QtCore.Signal() + disconnecting_signal = QtCore.Signal() + + connected_signal = QtCore.Signal() + disconnected_signal = QtCore.Signal() + + connection_died_signal = QtCore.Signal() + connection_aborted_signal = QtCore.Signal() + + +class MailConnection(AbstractLEAPConnection): + + components = IMAPConnection, SMTPConnection + _connection_name = "Mail" + + def __init__(self): + self._qtsigs = MailConnectionSignals() diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index 4619ba80..1940fc68 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -25,6 +25,7 @@ from ssl import SSLError from PySide import QtCore from u1db import errors as u1db_errors +from zope.proxy import sameProxiedObjects from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig @@ -190,7 +191,8 @@ class SoledadBootstrapper(AbstractBootstrapper): # soledad-launcher in the gui. raise - leap_check(self._soledad is not None, + leap_assert(sameProxiedObjects(self._soledad, None) + is not True, "Null soledad, error while initializing") # and now, let's sync -- cgit v1.2.3