diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 62f0e7658..5ba9605ec 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -115,6 +115,7 @@ from .utils import ( check_pid, pathname2url, run_sync, + unix_socket_in_use, url_escape, url_path_join, urldecode_unix_socket_path, @@ -1638,12 +1639,16 @@ class NotebookApp(JupyterApp): return self._bind_http_server_unix() if self.sock else self._bind_http_server_tcp() def _bind_http_server_unix(self): + if unix_socket_in_use(self.sock): + self.log.warning(_('The socket %s is already in use.') % self.sock) + return False + try: sock = bind_unix_socket(self.sock, mode=int(self.sock_mode.encode(), 8)) self.http_server.add_socket(sock) except socket.error as e: if e.errno == errno.EADDRINUSE: - self.log.info(_('The socket %s is already in use.') % self.sock) + self.log.warning(_('The socket %s is already in use.') % self.sock) return False elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)): self.log.warning(_("Permission to listen on sock %s denied") % self.sock) diff --git a/notebook/tests/test_notebookapp_integration.py b/notebook/tests/test_notebookapp_integration.py index 1ae7e3536..9af505342 100644 --- a/notebook/tests/test_notebookapp_integration.py +++ b/notebook/tests/test_notebookapp_integration.py @@ -135,3 +135,32 @@ def test_stop_multi_integration(): p1.wait() p2.wait() p3.wait() + + +@skip_win32 +def test_launch_socket_collision(): + """Tests UNIX socket in-use detection for lifecycle correctness.""" + sock = UNIXSocketNotebookTestBase.sock + check_msg = 'socket %s is already in use' % sock + + _ensure_stopped() + + # Start a server. + cmd = ['jupyter-notebook', '--sock=%s' % sock] + p1 = subprocess.Popen(cmd) + time.sleep(3) + + # Try to start a server bound to the same UNIX socket. + try: + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + assert check_msg in e.output.decode() + else: + raise AssertionError('expected error, instead got %s' % e.output.decode()) + + # Stop the background server, ensure it's stopped and wait on the process to exit. + subprocess.check_call(['jupyter-notebook', 'stop', sock]) + + _ensure_stopped() + + p1.wait() diff --git a/notebook/utils.py b/notebook/utils.py index 47d7a26fa..f0ad0e26c 100644 --- a/notebook/utils.py +++ b/notebook/utils.py @@ -11,6 +11,7 @@ import ctypes import errno import inspect import os +import socket import stat import sys from distutils.version import LooseVersion @@ -22,6 +23,7 @@ from urllib.request import pathname2url # in tornado >=5 with Python 3 from tornado.concurrent import Future as TornadoFuture from tornado import gen +import requests_unixsocket from ipython_genutils import py3compat # UF_HIDDEN is a stat flag not defined in the stat module. @@ -382,3 +384,19 @@ def urldecode_unix_socket_path(socket_path): def urlencode_unix_socket(socket_path): """Encodes a UNIX socket URL from a socket path for the `http+unix` URI form.""" return 'http+unix://%s' % urlencode_unix_socket_path(socket_path) + + +def unix_socket_in_use(socket_path): + """Checks whether a UNIX socket path on disk is in use by attempting to connect to it.""" + if not os.path.exists(socket_path): + return False + + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + except socket.error: + return False + else: + return True + finally: + sock.close()