6363from tornado import web
6464from tornado .httputil import url_concat
6565from tornado .log import LogFormatter , app_log , access_log , gen_log
66+ if not sys .platform .startswith ('win' ):
67+ from tornado .netutil import bind_unix_socket
6668
6769from notebook import (
70+ DEFAULT_NOTEBOOK_PORT ,
6871 DEFAULT_STATIC_FILES_PATH ,
6972 DEFAULT_TEMPLATE_PATH_LIST ,
7073 __version__ ,
109112from notebook ._sysinfo import get_sys_info
110113
111114from ._tz import utcnow , utcfromtimestamp
112- from .utils import url_path_join , check_pid , url_escape , urljoin , pathname2url
115+ from .utils import (
116+ check_pid ,
117+ pathname2url ,
118+ url_escape ,
119+ url_path_join ,
120+ urldecode_unix_socket_path ,
121+ urlencode_unix_socket ,
122+ urlencode_unix_socket_path ,
123+ urljoin ,
124+ )
113125
114126#-----------------------------------------------------------------------------
115127# Module globals
@@ -213,7 +225,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
213225 warnings .warn (_ ("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0" ), DeprecationWarning )
214226
215227 now = utcnow ()
216-
228+
217229 root_dir = contents_manager .root_dir
218230 home = py3compat .str_to_unicode (os .path .expanduser ('~' ), encoding = sys .getfilesystemencoding ())
219231 if root_dir .startswith (home + os .path .sep ):
@@ -398,6 +410,7 @@ def start(self):
398410 set_password (config_file = self .config_file )
399411 self .log .info ("Wrote hashed password to %s" % self .config_file )
400412
413+
401414def shutdown_server (server_info , timeout = 5 , log = None ):
402415 """Shutdown a notebook server in a separate process.
403416
@@ -410,14 +423,39 @@ def shutdown_server(server_info, timeout=5, log=None):
410423 Returns True if the server was stopped by any means, False if stopping it
411424 failed (on Windows).
412425 """
413- from tornado .httpclient import HTTPClient , HTTPRequest
426+ from tornado import gen
427+ from tornado .httpclient import AsyncHTTPClient , HTTPClient , HTTPRequest
428+ from tornado .netutil import bind_unix_socket , Resolver
414429 url = server_info ['url' ]
415430 pid = server_info ['pid' ]
431+ resolver = None
432+
433+ # UNIX Socket handling.
434+ if url .startswith ('http+unix://' ):
435+ # This library doesn't understand our URI form, but it's just HTTP.
436+ url = url .replace ('http+unix://' , 'http://' )
437+
438+ class UnixSocketResolver (Resolver ):
439+ def initialize (self , resolver ):
440+ self .resolver = resolver
441+
442+ def close (self ):
443+ self .resolver .close ()
444+
445+ @gen .coroutine
446+ def resolve (self , host , port , * args , ** kwargs ):
447+ raise gen .Return ([
448+ (socket .AF_UNIX , urldecode_unix_socket_path (host ))
449+ ])
450+
451+ resolver = UnixSocketResolver (resolver = Resolver ())
452+
416453 req = HTTPRequest (url + 'api/shutdown' , method = 'POST' , body = b'' , headers = {
417454 'Authorization' : 'token ' + server_info ['token' ]
418455 })
419456 if log : log .debug ("POST request to %sapi/shutdown" , url )
420- HTTPClient ().fetch (req )
457+ AsyncHTTPClient .configure (None , resolver = resolver )
458+ HTTPClient (AsyncHTTPClient ).fetch (req )
421459
422460 # Poll to see if it shut down.
423461 for _ in range (timeout * 10 ):
@@ -448,13 +486,20 @@ class NbserverStopApp(JupyterApp):
448486 version = __version__
449487 description = "Stop currently running notebook server for a given port"
450488
451- port = Integer (8888 , config = True ,
452- help = "Port of the server to be killed. Default 8888" )
489+ port = Integer (DEFAULT_NOTEBOOK_PORT , config = True ,
490+ help = "Port of the server to be killed. Default %s" % DEFAULT_NOTEBOOK_PORT )
491+
492+ sock = Unicode (u'' , config = True ,
493+ help = "UNIX socket of the server to be killed." )
453494
454495 def parse_command_line (self , argv = None ):
455496 super (NbserverStopApp , self ).parse_command_line (argv )
456497 if self .extra_args :
457- self .port = int (self .extra_args [0 ])
498+ try :
499+ self .port = int (self .extra_args [0 ])
500+ except ValueError :
501+ # self.extra_args[0] was not an int, so it must be a string (unix socket).
502+ self .sock = self .extra_args [0 ]
458503
459504 def shutdown_server (self , server ):
460505 return shutdown_server (server , log = self .log )
@@ -464,16 +509,16 @@ def start(self):
464509 if not servers :
465510 self .exit ("There are no running servers" )
466511 for server in servers :
467- if server ['port' ] == self .port :
468- print ("Shutting down server on port" , self .port , "..." )
512+ if server . get ( 'sock' ) == self . sock or server ['port' ] == self .port :
513+ print ("Shutting down server on %s..." % self .sock or self . port )
469514 if not self .shutdown_server (server ):
470515 sys .exit ("Could not stop server" )
471516 return
472517 else :
473518 print ("There is currently no server running on port {}" .format (self .port ), file = sys .stderr )
474- print ("Ports currently in use:" , file = sys .stderr )
519+ print ("Ports/sockets currently in use:" , file = sys .stderr )
475520 for server in servers :
476- print (" - {}" .format (server ['port' ]), file = sys .stderr )
521+ print (" - {}" .format (server . get ( 'sock' , server ['port' ]) ), file = sys .stderr )
477522 self .exit (1 )
478523
479524
@@ -553,6 +598,8 @@ def start(self):
553598 'ip' : 'NotebookApp.ip' ,
554599 'port' : 'NotebookApp.port' ,
555600 'port-retries' : 'NotebookApp.port_retries' ,
601+ 'sock' : 'NotebookApp.sock' ,
602+ 'sock-umask' : 'NotebookApp.sock_umask' ,
556603 'transport' : 'KernelManager.transport' ,
557604 'keyfile' : 'NotebookApp.keyfile' ,
558605 'certfile' : 'NotebookApp.certfile' ,
@@ -692,10 +739,18 @@ def _valdate_ip(self, proposal):
692739 or containerized setups for example).""" )
693740 )
694741
695- port = Integer (8888 , config = True ,
742+ port = Integer (DEFAULT_NOTEBOOK_PORT , config = True ,
696743 help = _ ("The port the notebook server will listen on." )
697744 )
698745
746+ sock = Unicode (u'' , config = True ,
747+ help = _ ("The UNIX socket the notebook server will listen on." )
748+ )
749+
750+ sock_umask = Unicode (u'0600' , config = True ,
751+ help = _ ("The UNIX socket umask to set on creation (default: 0600)." )
752+ )
753+
699754 port_retries = Integer (50 , config = True ,
700755 help = _ ("The number of additional ports to try if the specified port is not available." )
701756 )
@@ -1400,6 +1455,27 @@ def init_webapp(self):
14001455 self .log .critical (_ ("\t $ python -m notebook.auth password" ))
14011456 sys .exit (1 )
14021457
1458+ # Socket options validation.
1459+ if self .sock :
1460+ if self .port != DEFAULT_NOTEBOOK_PORT :
1461+ self .log .critical (
1462+ _ ('Options --port and --sock are mutually exclusive. Aborting.' ),
1463+ )
1464+ sys .exit (1 )
1465+
1466+ if self .open_browser :
1467+ # If we're bound to a UNIX socket, we can't reliably connect from a browser.
1468+ self .log .critical (
1469+ _ ('Options --open-browser and --sock are mutually exclusive. Aborting.' ),
1470+ )
1471+ sys .exit (1 )
1472+
1473+ if sys .platform .startswith ('win' ):
1474+ self .log .critical (
1475+ _ ('Option --sock is not supported on Windows, but got value of %s. Aborting.' % self .sock ),
1476+ )
1477+ sys .exit (1 )
1478+
14031479 self .web_app = NotebookWebApplication (
14041480 self , self .kernel_manager , self .contents_manager ,
14051481 self .session_manager , self .kernel_spec_manager ,
@@ -1436,6 +1512,32 @@ def init_webapp(self):
14361512 max_body_size = self .max_body_size ,
14371513 max_buffer_size = self .max_buffer_size )
14381514
1515+ success = self ._bind_http_server ()
1516+ if not success :
1517+ self .log .critical (_ ('ERROR: the notebook server could not be started because '
1518+ 'no available port could be found.' ))
1519+ self .exit (1 )
1520+
1521+ def _bind_http_server (self ):
1522+ return self ._bind_http_server_unix () if self .sock else self ._bind_http_server_tcp ()
1523+
1524+ def _bind_http_server_unix (self ):
1525+ try :
1526+ sock = bind_unix_socket (self .sock , mode = int (self .sock_umask .encode (), 8 ))
1527+ self .http_server .add_socket (sock )
1528+ except socket .error as e :
1529+ if e .errno == errno .EADDRINUSE :
1530+ self .log .info (_ ('The socket %s is already in use.' ) % self .sock )
1531+ return False
1532+ elif e .errno in (errno .EACCES , getattr (errno , 'WSAEACCES' , errno .EACCES )):
1533+ self .log .warning (_ ("Permission to listen on sock %s denied" ) % self .sock )
1534+ return False
1535+ else :
1536+ raise
1537+ else :
1538+ return True
1539+
1540+ def _bind_http_server_tcp (self ):
14391541 success = None
14401542 for port in random_ports (self .port , self .port_retries + 1 ):
14411543 try :
@@ -1453,39 +1555,45 @@ def init_webapp(self):
14531555 self .port = port
14541556 success = True
14551557 break
1456- if not success :
1457- self .log .critical (_ ('ERROR: the notebook server could not be started because '
1458- 'no available port could be found.' ))
1459- self .exit (1 )
1558+ return success
1559+
1560+ def _concat_token (self , url ):
1561+ token = self .token if self ._token_generated else '...'
1562+ return url_concat (url , {'token' : token })
14601563
14611564 @property
14621565 def display_url (self ):
14631566 if self .custom_display_url :
14641567 url = self .custom_display_url
14651568 if not url .endswith ('/' ):
14661569 url += '/'
1570+ elif self .sock :
1571+ url = self ._unix_sock_url ()
14671572 else :
14681573 if self .ip in ('' , '0.0.0.0' ):
14691574 ip = "%s" % socket .gethostname ()
14701575 else :
14711576 ip = self .ip
1472- url = self ._url (ip )
1473- if self .token :
1474- # Don't log full token if it came from config
1475- token = self .token if self ._token_generated else '...'
1476- url = (url_concat (url , {'token' : token })
1477- + '\n or '
1478- + url_concat (self ._url ('127.0.0.1' ), {'token' : token }))
1577+ url = self ._tcp_url (ip )
1578+ if self .token and not self .sock :
1579+ url = self ._concat_token (url )
1580+ url += '\n or %s' % self ._concat_token (self ._tcp_url ('127.0.0.1' ))
14791581 return url
14801582
14811583 @property
14821584 def connection_url (self ):
1483- ip = self .ip if self .ip else 'localhost'
1484- return self ._url (ip )
1585+ if self .sock :
1586+ return self ._unix_sock_url ()
1587+ else :
1588+ ip = self .ip if self .ip else 'localhost'
1589+ return self ._tcp_url (ip )
14851590
1486- def _url (self , ip ):
1591+ def _unix_sock_url (self , token = None ):
1592+ return '%s%s' % (urlencode_unix_socket (self .sock ), self .base_url )
1593+
1594+ def _tcp_url (self , ip , port = None ):
14871595 proto = 'https' if self .certfile else 'http'
1488- return "%s://%s:%i%s" % (proto , ip , self .port , self .base_url )
1596+ return "%s://%s:%i%s" % (proto , ip , port or self .port , self .base_url )
14891597
14901598 def init_terminals (self ):
14911599 if not self .terminals_enabled :
@@ -1713,6 +1821,7 @@ def server_info(self):
17131821 return {'url' : self .connection_url ,
17141822 'hostname' : self .ip if self .ip else 'localhost' ,
17151823 'port' : self .port ,
1824+ 'sock' : self .sock ,
17161825 'secure' : bool (self .certfile ),
17171826 'base_url' : self .base_url ,
17181827 'token' : self .token ,
@@ -1833,19 +1942,31 @@ def start(self):
18331942 self .write_server_info_file ()
18341943 self .write_browser_open_file ()
18351944
1836- if self .open_browser or self .file_to_run :
1945+ if ( self .open_browser or self .file_to_run ) and not self . sock :
18371946 self .launch_browser ()
18381947
18391948 if self .token and self ._token_generated :
18401949 # log full URL with generated token, so there's a copy/pasteable link
18411950 # with auth info.
1842- self .log .critical ('\n ' .join ([
1843- '\n ' ,
1844- 'To access the notebook, open this file in a browser:' ,
1845- ' %s' % urljoin ('file:' , pathname2url (self .browser_open_file )),
1846- 'Or copy and paste one of these URLs:' ,
1847- ' %s' % self .display_url ,
1848- ]))
1951+ if self .sock :
1952+ self .log .critical ('\n ' .join ([
1953+ '\n ' ,
1954+ 'Notebook is listening on %s' % self .display_url ,
1955+ '' ,
1956+ (
1957+ 'UNIX sockets are not browser-connectable, but you can tunnel to '
1958+ 'the instance via e.g.`ssh -L 8888:%s -N user@this_host` and then '
1959+ 'opening e.g. %s in a browser.'
1960+ ) % (self .sock , self ._concat_token (self ._tcp_url ('localhost' , 8888 )))
1961+ ]))
1962+ else :
1963+ self .log .critical ('\n ' .join ([
1964+ '\n ' ,
1965+ 'To access the notebook, open this file in a browser:' ,
1966+ ' %s' % urljoin ('file:' , pathname2url (self .browser_open_file )),
1967+ 'Or copy and paste one of these URLs:' ,
1968+ ' %s' % self .display_url ,
1969+ ]))
18491970
18501971 self .io_loop = ioloop .IOLoop .current ()
18511972 if sys .platform .startswith ('win' ):
0 commit comments