diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/app.py | 33 | ||||
| -rw-r--r-- | src/leap/gui/mainwindow.py | 55 | ||||
| -rw-r--r-- | src/leap/platform_init/locks.py | 312 | 
3 files changed, 383 insertions, 17 deletions
| diff --git a/src/leap/app.py b/src/leap/app.py index 4112b404..c4a3156e 100644 --- a/src/leap/app.py +++ b/src/leap/app.py @@ -17,16 +17,20 @@  import logging  import signal +import socket  import sys  from functools import partial +  from PySide import QtCore, QtGui -from leap.common.events import server +from leap.common.events import server as event_server  from leap.util import __version__ as VERSION  from leap.util import leap_argparse  from leap.gui import locale_rc  from leap.gui.mainwindow import MainWindow +from leap.platform_init import IS_MAC +from leap.platform_init.locks import we_are_the_one_and_only  import codecs  codecs.register(lambda name: codecs.lookup('utf-8') @@ -34,8 +38,12 @@ codecs.register(lambda name: codecs.lookup('utf-8')  def sigint_handler(*args, **kwargs): +    """ +    Signal handler for SIGINT +    """      logger = kwargs.get('logger', None) -    logger.debug('SIGINT catched. shutting down...') +    if logger: +        logger.debug("SIGINT catched. shutting down...")      mainwindow = args[0]      mainwindow.quit() @@ -44,8 +52,7 @@ def main():      """      Launches the main event loop      """ - -    server.ensure_server(port=8090) +    event_server.ensure_server(event_server.SERVER_PORT)      _, opts = leap_argparse.init_leapc_args()      debug = opts.debug @@ -67,6 +74,13 @@ def main():      console.setFormatter(formatter)      logger.addHandler(console) +    if not we_are_the_one_and_only(): +        # leap-client is already running +        logger.warning("Tried to launch more than one instance " +                       "of leap-client. Raising the existing " +                       "one instead.") +        sys.exit(1) +      logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')      logger.info('LEAP client version %s', VERSION)      logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') @@ -98,22 +112,19 @@ def main():      app.setApplicationName("leap")      app.setOrganizationDomain("leap.se") -    # TODO: check if the leap-client is already running and quit -    # gracefully in that case. - -    window = MainWindow(standalone) -    window.show() -      # This dummy timer ensures that control is given to the outside      # loop, so we can hook our sigint handler.      timer = QtCore.QTimer()      timer.start(500)      timer.timeout.connect(lambda: None) +    window = MainWindow(standalone) +    window.show() +      sigint_window = partial(sigint_handler, window, logger=logger)      signal.signal(signal.SIGINT, sigint_window) -    if sys.platform == "darwin": +    if IS_MAC:          window.raise_()      # Run main loop diff --git a/src/leap/gui/mainwindow.py b/src/leap/gui/mainwindow.py index f84cb00c..bf8491d0 100644 --- a/src/leap/gui/mainwindow.py +++ b/src/leap/gui/mainwindow.py @@ -26,7 +26,10 @@ from functools import partial  import keyring  from PySide import QtCore, QtGui +  from leap.common.check import leap_assert +from leap.common.events import register +from leap.common.events import events_pb2 as proto  from leap.config.leapsettings import LeapSettings  from leap.config.providerconfig import ProviderConfig  from leap.crypto.srpauth import SRPAuth @@ -34,7 +37,7 @@ from leap.gui.wizard import Wizard  from leap.services.eip.eipbootstrapper import EIPBootstrapper  from leap.services.eip.eipconfig import EIPConfig  from leap.services.eip.providerbootstrapper import ProviderBootstrapper -from leap.platform_init import IS_MAC +from leap.platform_init import IS_MAC, IS_WIN  from leap.platform_init.initializers import init_platform  from leap.services.eip.vpn import VPN  from leap.services.eip.vpnlaunchers import (VPNLauncherException, @@ -43,14 +46,9 @@ from leap.services.eip.vpnlaunchers import (VPNLauncherException,                                              EIPNoPolkitAuthAgentAvailable)  from leap.util import __version__ as VERSION  from leap.util.checkerthread import CheckerThread -from leap.common.events import ( -    register, -    events_pb2 as proto, -)  from ui_mainwindow import Ui_MainWindow -  logger = logging.getLogger(__name__) @@ -66,7 +64,9 @@ class MainWindow(QtGui.QMainWindow):      # Keyring      KEYRING_KEY = "leap_client" +    # Signals      new_updates = QtCore.Signal(object) +    raise_window = QtCore.Signal([])      def __init__(self, standalone=False):          """ @@ -78,8 +78,12 @@ class MainWindow(QtGui.QMainWindow):          """          QtGui.QMainWindow.__init__(self) +        # register leap events          register(signal=proto.UPDATER_NEW_UPDATES,                   callback=self._new_updates_available) +        register(signal=proto.RAISE_WINDOW, +                 callback=self._on_raise_window_event) +          self._updates_content = ""          if IS_MAC: @@ -122,6 +126,7 @@ class MainWindow(QtGui.QMainWindow):          self._standalone = standalone          self._provider_config = ProviderConfig()          self._eip_config = EIPConfig() +          # This is created once we have a valid provider config          self._srp_auth = None @@ -178,6 +183,10 @@ class MainWindow(QtGui.QMainWindow):              QtCore.QCoreApplication.instance(),              QtCore.SIGNAL("aboutToQuit()"),              self._checker_thread.wait) +        QtCore.QCoreApplication.instance().connect( +            QtCore.QCoreApplication.instance(), +            QtCore.SIGNAL("aboutToQuit()"), +            self._cleanup_pidfiles)          self.ui.chkRemember.stateChanged.connect(              self._remember_state_changed) @@ -188,6 +197,7 @@ class MainWindow(QtGui.QMainWindow):          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) +        self.raise_window.connect(self._do_raise_mainwindow)          # Used to differentiate between real quits and close to tray          self._really_quit = False @@ -932,6 +942,39 @@ class MainWindow(QtGui.QMainWindow):          logger.debug("Finished VPN with exitCode %s" % (exitCode,))          self._stop_eip() +    def _on_raise_window_event(self, req): +        """ +        Callback for the raise window event +        """ +        self.raise_window.emit() + +    def _do_raise_mainwindow(self): +        """ +        SLOT +        TRIGGERS: +            self._on_raise_window_event + +        Triggered when we receive a RAISE_WINDOW event. +        """ +        TOPFLAG = QtCore.Qt.WindowStaysOnTopHint +        self.setWindowFlags(self.windowFlags() | TOPFLAG) +        self.show() +        self.setWindowFlags(self.windowFlags() & ~TOPFLAG) +        self.show() + +    def _cleanup_pidfiles(self): +        """ +        SLOT +        TRIGGERS: +            self.aboutToQuit + +        Triggered on about to quit signal, removes lockfiles on a clean +        shutdown +        """ +        if IS_WIN: +            lockfile = WindowsLock() +            lockfile.release_lock() +  if __name__ == "__main__":      import signal diff --git a/src/leap/platform_init/locks.py b/src/leap/platform_init/locks.py new file mode 100644 index 00000000..2cdee3d9 --- /dev/null +++ b/src/leap/platform_init/locks.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +# locks.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/>. +""" +Utilities for handling multi-platform file locking mechanisms +""" +import commands +import logging +import os +import platform + +from leap.common.events import signal as signal_event +from leap.common.events import events_pb2 as proto +from leap import platform_init + +if platform_init.IS_UNIX: +    from fcntl import flock, LOCK_EX, LOCK_NB +else: +    import errno +    import glob +    import shutil +    import socket + +    from tempfile import gettempdir + +logger = logging.getLogger(__name__) + +if platform_init.IS_UNIX: + +    class UnixLock(object): +        """ +        Uses flock to get an exclusive lock over a file. +        See man 2 flock +        """ + +        def __init__(self, path): +            """ +            iniializes t he UnixLock with the path of the +            desired lockfile +            """ + +            self._fd = None +            self.path = path + +        def get_lock(self): +            """ +            Tries to get a lock, and writes the running pid there if successful +            """ +            gotit, pid = self._get_lock_and_pid() +            return gotit + +        def get_pid(self): +            """ +            Returns the pid of the locking process +            """ +            gotit, pid = self._get_lock_and_pid() +            return pid + +        def _get_lock(self): +            """ +            Tries to get a lock, returning True if successful + +            @rtype: bool +            """ +            self._fd = os.open(self.path, os.O_CREAT | os.O_RDWR) + +            try: +                flock(self._fd, LOCK_EX | LOCK_NB) +            except IOError as exc: +                # could not get the lock +                if exc.args[0] == 11: +                    # Resource temporarily unavailable +                    return False +                else: +                    raise +            return True + +        @property +        def locked_by_us(self): +            """ +            Returns True if the pid in the pidfile +            is ours. + +            @rtype: bool +            """ +            gotit, pid = self._get_lock_and_pid() +            return pid == os.getpid() + +        def _get_lock_and_pid(self): +            """ +            Tries to get a lock over the file. +            Returns (locked, pid) tuple. + +            @rtype: tuple +            """ + +            if self._get_lock(): +                self._write_to_pidfile() +                return True, None + +            return False, self._read_from_pidfile() + +        def _read_from_pidfile(self): +            """ +            Tries to read pid from the pidfile, +            returns False if no content found. +            """ + +            pidfile = os.read( +                self._fd, 16) +            if not pidfile: +                return False + +            try: +                return int(pidfile.strip()) +            except Exception as exc: +                exc.args += (pidfile, self.lock_file) +                raise + +        def _write_to_pidfile(self): +            """ +            Writes the pid of the running process +            to the pidfile +            """ +            fd = self._fd +            os.ftruncate(fd, 0) +            os.write(fd, '%d\n' % os.getpid()) +            os.fsync(fd) + + +if platform_init.IS_WIN: + +    class WindowsLock(object): +        """ +        Creates a lock based on the atomic nature of mkdir on Windows +        system calls. +        """ +        LOCKBASE = os.path.join(gettempdir(), "leap-client-lock") + +        def __init__(self): +            """ +            Initializes the lock. +            Sets the lock name to basename plus the process pid. +            """ +            self._fd = None +            pid = os.getpid() +            self.name = "%s-%s" % (self.LOCKBASE, pid) +            self.pid = pid + +        def get_lock(self): +            """ +            Tries to get a lock, and writes the running pid there if successful +            """ +            gotit = self._get_lock() +            return gotit + +        def _get_lock(self): +            """ +            Tries to write to a file with the current pid as part of the name +            """ +            try: +                self._fd = os.makedirs(self.name) +            except WindowsError as exc: +                # could not create the dir +                if exc.args[0] == 183: +                    logger.debug('cannot create dir') +                    # cannot create dir with existing name +                    return False +                else: +                    raise +            return self._is_one_pidfile()[0] + +        def _is_one_pidfile(self): +            """ +            Returns True, pid if there is only one pidfile with the expected +            base path + +            @rtype: tuple +            """ +            pidfiles = glob.glob(self.LOCKBASE + '-*') +            if len(pidfiles) == 1: +                pid = pidfiles[0].split('-')[-1] +                return True, int(pid) +            else: +                return False, None + +        def get_pid(self): +            """ +            Returns the pid of the locking process + +            @rtype: int +            """ +            # XXX assert there is only one? +            _, pid = self._is_one_pidfile() +            return pid + +        def release_lock(self): +            """ +            Releases the pidfile dir for this process, by removing it. +            """ +            try: +                shutil.rmtree(self.name) +                return True + +            except WindowsError as exc: +                if exc.errno in (errno.EPIPE, errno.ENOENT, +                                 errno.ESRCH, errno.EACCES): +                    logger.warning( +                        'exception while trying to remove the lockfile dir') +                    logger.warning('errno %s: %s' % (exc.errno, exc.args[1])) +                    # path does not exist +                    return False +                else: +                    logger.debug('errno = %s' % (exc.errno,)) +                    # we did not foresee this error, better add it explicitely +                    raise + +        @property +        def locked_by_us(self): +            """ +            Returns True if the pid in the pidfile +            is ours. + +            @rtype: bool +            """ +            _, pid = self._is_one_pidfile() +            return pid == self.pid + +        def write_port(self, port): +            """ +            Writes the port for windows control to the pidfile folder +            Returns True if successful. + +            @rtype: bool +            """ +            if not self.locked_by_us: +                logger.warning("Tried to write control port to a " +                               "non-unique pidfile folder") +                return False +            port_file = os.path.join(self.name, "port") +            with open(port_file, 'w') as f: +                f.write("%s" % port) +            return True + +        def get_control_port(self): +            """ +            Reads control port of the main instance from the port file +            in the pidfile dir + +            @rtype: int +            """ +            pid = self.get_pid() +            port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") +            port = None +            try: +                with open(port_file) as f: +                    port_str = f.read() +                    port = int(port_str.strip()) +            except IOError as exc: +                if exc.errno == errno.ENOENT: +                    logger.error("Tried to read port from non-existent file") +                else: +                    # we did not know explicitely about this error +                    raise +            return port + + +def we_are_the_one_and_only(): +    """ +    Returns True if we are the only instance running, False otherwise. +    If we came later, send a raise signal to the main instance of the +    application + +    @rtype: bool +    """ +    _sys = platform.system() + +    if _sys in ("Linux", "Darwin"): +        locker = UnixLock('/tmp/leap-client.lock') +        locker.get_lock() +        we_are_the_one = locker.locked_by_us +        if not we_are_the_one: +            signal_event(proto.RAISE_WINDOW) +        return we_are_the_one + +    elif _sys == "Windows": +        locker = WindowsLock() +        locker.get_lock() +        we_are_the_one = locker.locked_by_us +        if not we_are_the_one: +            locker.release_lock() +            signal_event(proto.RAISE_WINDOW) +        return we_are_the_one + +    else: +        logger.warning("Multi-instance checker " +                       "not implemented for %s" % (_sys)) +        # lies, lies, lies... +        return True | 
