6666from tornado import web
6767from tornado .httputil import url_concat
6868from tornado .log import LogFormatter , app_log , access_log , gen_log
69+ if not sys .platform .startswith ('win' ):
70+ from tornado .netutil import bind_unix_socket
6971
7072from notebook import (
73+ DEFAULT_NOTEBOOK_PORT ,
7174 DEFAULT_STATIC_FILES_PATH ,
7275 DEFAULT_TEMPLATE_PATH_LIST ,
7376 __version__ ,
107110from notebook ._sysinfo import get_sys_info
108111
109112from ._tz import utcnow , utcfromtimestamp
110- from .utils import url_path_join , check_pid , url_escape , urljoin , pathname2url , run_sync
113+ from .utils import (
114+ check_pid ,
115+ pathname2url ,
116+ run_sync ,
117+ url_escape ,
118+ url_path_join ,
119+ urldecode_unix_socket_path ,
120+ urlencode_unix_socket ,
121+ urlencode_unix_socket_path ,
122+ urljoin ,
123+ )
111124
112125# Check if we can use async kernel management
113126try :
@@ -218,7 +231,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
218231 warnings .warn (_ ("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0" ), DeprecationWarning )
219232
220233 now = utcnow ()
221-
234+
222235 root_dir = contents_manager .root_dir
223236 home = py3compat .str_to_unicode (os .path .expanduser ('~' ), encoding = sys .getfilesystemencoding ())
224237 if root_dir .startswith (home + os .path .sep ):
@@ -403,6 +416,7 @@ def start(self):
403416 set_password (config_file = self .config_file )
404417 self .log .info ("Wrote hashed password to %s" % self .config_file )
405418
419+
406420def shutdown_server (server_info , timeout = 5 , log = None ):
407421 """Shutdown a notebook server in a separate process.
408422
@@ -415,14 +429,39 @@ def shutdown_server(server_info, timeout=5, log=None):
415429 Returns True if the server was stopped by any means, False if stopping it
416430 failed (on Windows).
417431 """
418- from tornado .httpclient import HTTPClient , HTTPRequest
432+ from tornado import gen
433+ from tornado .httpclient import AsyncHTTPClient , HTTPClient , HTTPRequest
434+ from tornado .netutil import bind_unix_socket , Resolver
419435 url = server_info ['url' ]
420436 pid = server_info ['pid' ]
437+ resolver = None
438+
439+ # UNIX Socket handling.
440+ if url .startswith ('http+unix://' ):
441+ # This library doesn't understand our URI form, but it's just HTTP.
442+ url = url .replace ('http+unix://' , 'http://' )
443+
444+ class UnixSocketResolver (Resolver ):
445+ def initialize (self , resolver ):
446+ self .resolver = resolver
447+
448+ def close (self ):
449+ self .resolver .close ()
450+
451+ @gen .coroutine
452+ def resolve (self , host , port , * args , ** kwargs ):
453+ raise gen .Return ([
454+ (socket .AF_UNIX , urldecode_unix_socket_path (host ))
455+ ])
456+
457+ resolver = UnixSocketResolver (resolver = Resolver ())
458+
421459 req = HTTPRequest (url + 'api/shutdown' , method = 'POST' , body = b'' , headers = {
422460 'Authorization' : 'token ' + server_info ['token' ]
423461 })
424462 if log : log .debug ("POST request to %sapi/shutdown" , url )
425- HTTPClient ().fetch (req )
463+ AsyncHTTPClient .configure (None , resolver = resolver )
464+ HTTPClient (AsyncHTTPClient ).fetch (req )
426465
427466 # Poll to see if it shut down.
428467 for _ in range (timeout * 10 ):
@@ -453,13 +492,20 @@ class NbserverStopApp(JupyterApp):
453492 version = __version__
454493 description = "Stop currently running notebook server for a given port"
455494
456- port = Integer (8888 , config = True ,
457- help = "Port of the server to be killed. Default 8888" )
495+ port = Integer (DEFAULT_NOTEBOOK_PORT , config = True ,
496+ help = "Port of the server to be killed. Default %s" % DEFAULT_NOTEBOOK_PORT )
497+
498+ sock = Unicode (u'' , config = True ,
499+ help = "UNIX socket of the server to be killed." )
458500
459501 def parse_command_line (self , argv = None ):
460502 super (NbserverStopApp , self ).parse_command_line (argv )
461503 if self .extra_args :
462- self .port = int (self .extra_args [0 ])
504+ try :
505+ self .port = int (self .extra_args [0 ])
506+ except ValueError :
507+ # self.extra_args[0] was not an int, so it must be a string (unix socket).
508+ self .sock = self .extra_args [0 ]
463509
464510 def shutdown_server (self , server ):
465511 return shutdown_server (server , log = self .log )
@@ -469,16 +515,16 @@ def start(self):
469515 if not servers :
470516 self .exit ("There are no running servers" )
471517 for server in servers :
472- if server ['port' ] == self .port :
473- print ("Shutting down server on port" , self .port , "..." )
518+ if server . get ( 'sock' ) == self . sock or server ['port' ] == self .port :
519+ print ("Shutting down server on %s..." % self .sock or self . port )
474520 if not self .shutdown_server (server ):
475521 sys .exit ("Could not stop server" )
476522 return
477523 else :
478524 print ("There is currently no server running on port {}" .format (self .port ), file = sys .stderr )
479- print ("Ports currently in use:" , file = sys .stderr )
525+ print ("Ports/sockets currently in use:" , file = sys .stderr )
480526 for server in servers :
481- print (" - {}" .format (server ['port' ]), file = sys .stderr )
527+ print (" - {}" .format (server . get ( 'sock' , server ['port' ]) ), file = sys .stderr )
482528 self .exit (1 )
483529
484530
@@ -558,6 +604,8 @@ def start(self):
558604 'ip' : 'NotebookApp.ip' ,
559605 'port' : 'NotebookApp.port' ,
560606 'port-retries' : 'NotebookApp.port_retries' ,
607+ 'sock' : 'NotebookApp.sock' ,
608+ 'sock-umask' : 'NotebookApp.sock_umask' ,
561609 'transport' : 'KernelManager.transport' ,
562610 'keyfile' : 'NotebookApp.keyfile' ,
563611 'certfile' : 'NotebookApp.certfile' ,
@@ -715,10 +763,18 @@ def _valdate_ip(self, proposal):
715763 or containerized setups for example).""" )
716764 )
717765
718- port = Integer (8888 , config = True ,
766+ port = Integer (DEFAULT_NOTEBOOK_PORT , config = True ,
719767 help = _ ("The port the notebook server will listen on." )
720768 )
721769
770+ sock = Unicode (u'' , config = True ,
771+ help = _ ("The UNIX socket the notebook server will listen on." )
772+ )
773+
774+ sock_umask = Unicode (u'0600' , config = True ,
775+ help = _ ("The UNIX socket umask to set on creation (default: 0600)." )
776+ )
777+
722778 port_retries = Integer (50 , config = True ,
723779 help = _ ("The number of additional ports to try if the specified port is not available." )
724780 )
@@ -1469,6 +1525,27 @@ def init_webapp(self):
14691525 self .log .critical (_ ("\t $ python -m notebook.auth password" ))
14701526 sys .exit (1 )
14711527
1528+ # Socket options validation.
1529+ if self .sock :
1530+ if self .port != DEFAULT_NOTEBOOK_PORT :
1531+ self .log .critical (
1532+ _ ('Options --port and --sock are mutually exclusive. Aborting.' ),
1533+ )
1534+ sys .exit (1 )
1535+
1536+ if self .open_browser :
1537+ # If we're bound to a UNIX socket, we can't reliably connect from a browser.
1538+ self .log .critical (
1539+ _ ('Options --open-browser and --sock are mutually exclusive. Aborting.' ),
1540+ )
1541+ sys .exit (1 )
1542+
1543+ if sys .platform .startswith ('win' ):
1544+ self .log .critical (
1545+ _ ('Option --sock is not supported on Windows, but got value of %s. Aborting.' % self .sock ),
1546+ )
1547+ sys .exit (1 )
1548+
14721549 self .web_app = NotebookWebApplication (
14731550 self , self .kernel_manager , self .contents_manager ,
14741551 self .session_manager , self .kernel_spec_manager ,
@@ -1505,6 +1582,32 @@ def init_webapp(self):
15051582 max_body_size = self .max_body_size ,
15061583 max_buffer_size = self .max_buffer_size )
15071584
1585+ success = self ._bind_http_server ()
1586+ if not success :
1587+ self .log .critical (_ ('ERROR: the notebook server could not be started because '
1588+ 'no available port could be found.' ))
1589+ self .exit (1 )
1590+
1591+ def _bind_http_server (self ):
1592+ return self ._bind_http_server_unix () if self .sock else self ._bind_http_server_tcp ()
1593+
1594+ def _bind_http_server_unix (self ):
1595+ try :
1596+ sock = bind_unix_socket (self .sock , mode = int (self .sock_umask .encode (), 8 ))
1597+ self .http_server .add_socket (sock )
1598+ except socket .error as e :
1599+ if e .errno == errno .EADDRINUSE :
1600+ self .log .info (_ ('The socket %s is already in use.' ) % self .sock )
1601+ return False
1602+ elif e .errno in (errno .EACCES , getattr (errno , 'WSAEACCES' , errno .EACCES )):
1603+ self .log .warning (_ ("Permission to listen on sock %s denied" ) % self .sock )
1604+ return False
1605+ else :
1606+ raise
1607+ else :
1608+ return True
1609+
1610+ def _bind_http_server_tcp (self ):
15081611 success = None
15091612 for port in random_ports (self .port , self .port_retries + 1 ):
15101613 try :
@@ -1533,35 +1636,45 @@ def init_webapp(self):
15331636 self .log .critical (_ ('ERROR: the notebook server could not be started because '
15341637 'port %i is not available.' ) % port )
15351638 self .exit (1 )
1536-
1639+ return success
1640+
1641+ def _concat_token (self , url ):
1642+ token = self .token if self ._token_generated else '...'
1643+ return url_concat (url , {'token' : token })
1644+
15371645 @property
15381646 def display_url (self ):
15391647 if self .custom_display_url :
15401648 url = self .custom_display_url
15411649 if not url .endswith ('/' ):
15421650 url += '/'
1651+ elif self .sock :
1652+ url = self ._unix_sock_url ()
15431653 else :
15441654 if self .ip in ('' , '0.0.0.0' ):
15451655 ip = "%s" % socket .gethostname ()
15461656 else :
15471657 ip = self .ip
1548- url = self ._url (ip )
1549- if self .token :
1550- # Don't log full token if it came from config
1551- token = self .token if self ._token_generated else '...'
1552- url = (url_concat (url , {'token' : token })
1553- + '\n or '
1554- + url_concat (self ._url ('127.0.0.1' ), {'token' : token }))
1658+ url = self ._tcp_url (ip )
1659+ if self .token and not self .sock :
1660+ url = self ._concat_token (url )
1661+ url += '\n or %s' % self ._concat_token (self ._tcp_url ('127.0.0.1' ))
15551662 return url
15561663
15571664 @property
15581665 def connection_url (self ):
1559- ip = self .ip if self .ip else 'localhost'
1560- return self ._url (ip )
1666+ if self .sock :
1667+ return self ._unix_sock_url ()
1668+ else :
1669+ ip = self .ip if self .ip else 'localhost'
1670+ return self ._tcp_url (ip )
15611671
1562- def _url (self , ip ):
1672+ def _unix_sock_url (self , token = None ):
1673+ return '%s%s' % (urlencode_unix_socket (self .sock ), self .base_url )
1674+
1675+ def _tcp_url (self , ip , port = None ):
15631676 proto = 'https' if self .certfile else 'http'
1564- return "%s://%s:%i%s" % (proto , ip , self .port , self .base_url )
1677+ return "%s://%s:%i%s" % (proto , ip , port or self .port , self .base_url )
15651678
15661679 def init_terminals (self ):
15671680 if not self .terminals_enabled :
@@ -1825,6 +1938,7 @@ def server_info(self):
18251938 return {'url' : self .connection_url ,
18261939 'hostname' : self .ip if self .ip else 'localhost' ,
18271940 'port' : self .port ,
1941+ 'sock' : self .sock ,
18281942 'secure' : bool (self .certfile ),
18291943 'base_url' : self .base_url ,
18301944 'token' : self .token ,
@@ -1954,19 +2068,31 @@ def start(self):
19542068 self .write_server_info_file ()
19552069 self .write_browser_open_file ()
19562070
1957- if self .open_browser or self .file_to_run :
2071+ if ( self .open_browser or self .file_to_run ) and not self . sock :
19582072 self .launch_browser ()
19592073
19602074 if self .token and self ._token_generated :
19612075 # log full URL with generated token, so there's a copy/pasteable link
19622076 # with auth info.
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- ]))
2077+ if self .sock :
2078+ self .log .critical ('\n ' .join ([
2079+ '\n ' ,
2080+ 'Notebook is listening on %s' % self .display_url ,
2081+ '' ,
2082+ (
2083+ 'UNIX sockets are not browser-connectable, but you can tunnel to '
2084+ 'the instance via e.g.`ssh -L 8888:%s -N user@this_host` and then '
2085+ 'opening e.g. %s in a browser.'
2086+ ) % (self .sock , self ._concat_token (self ._tcp_url ('localhost' , 8888 )))
2087+ ]))
2088+ else :
2089+ self .log .critical ('\n ' .join ([
2090+ '\n ' ,
2091+ 'To access the notebook, open this file in a browser:' ,
2092+ ' %s' % urljoin ('file:' , pathname2url (self .browser_open_file )),
2093+ 'Or copy and paste one of these URLs:' ,
2094+ ' %s' % self .display_url ,
2095+ ]))
19702096
19712097 self .io_loop = ioloop .IOLoop .current ()
19722098 if sys .platform .startswith ('win' ):
0 commit comments