summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/feature_2060_single-instance-app2
-rw-r--r--pkg/requirements.pip2
-rw-r--r--src/leap/app.py33
-rw-r--r--src/leap/gui/mainwindow.py55
-rw-r--r--src/leap/platform_init/locks.py312
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