diff options
-rw-r--r-- | changes/feature_2060_single-instance-app | 2 | ||||
-rw-r--r-- | pkg/requirements.pip | 2 | ||||
-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 |
5 files changed, 386 insertions, 18 deletions
diff --git a/changes/feature_2060_single-instance-app b/changes/feature_2060_single-instance-app new file mode 100644 index 00000000..eeab3f2c --- /dev/null +++ b/changes/feature_2060_single-instance-app @@ -0,0 +1,2 @@ + o Avoids multiple instances of leap-client. Each new one just raises + the existing instance and quits. diff --git a/pkg/requirements.pip b/pkg/requirements.pip index 0051380a..ad06fd56 100644 --- a/pkg/requirements.pip +++ b/pkg/requirements.pip @@ -13,4 +13,4 @@ keyring python-dateutil psutil -leap.common +leap.common>=0.2.1-dev 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 |