63
63
from tornado import web
64
64
from tornado .httputil import url_concat
65
65
from 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
66
68
67
69
from notebook import (
70
+ DEFAULT_NOTEBOOK_PORT ,
68
71
DEFAULT_STATIC_FILES_PATH ,
69
72
DEFAULT_TEMPLATE_PATH_LIST ,
70
73
__version__ ,
109
112
from notebook ._sysinfo import get_sys_info
110
113
111
114
from ._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
+ )
113
125
114
126
#-----------------------------------------------------------------------------
115
127
# Module globals
@@ -213,7 +225,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
213
225
warnings .warn (_ ("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0" ), DeprecationWarning )
214
226
215
227
now = utcnow ()
216
-
228
+
217
229
root_dir = contents_manager .root_dir
218
230
home = py3compat .str_to_unicode (os .path .expanduser ('~' ), encoding = sys .getfilesystemencoding ())
219
231
if root_dir .startswith (home + os .path .sep ):
@@ -398,6 +410,7 @@ def start(self):
398
410
set_password (config_file = self .config_file )
399
411
self .log .info ("Wrote hashed password to %s" % self .config_file )
400
412
413
+
401
414
def shutdown_server (server_info , timeout = 5 , log = None ):
402
415
"""Shutdown a notebook server in a separate process.
403
416
@@ -410,14 +423,39 @@ def shutdown_server(server_info, timeout=5, log=None):
410
423
Returns True if the server was stopped by any means, False if stopping it
411
424
failed (on Windows).
412
425
"""
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
414
429
url = server_info ['url' ]
415
430
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
+
416
453
req = HTTPRequest (url + 'api/shutdown' , method = 'POST' , body = b'' , headers = {
417
454
'Authorization' : 'token ' + server_info ['token' ]
418
455
})
419
456
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 )
421
459
422
460
# Poll to see if it shut down.
423
461
for _ in range (timeout * 10 ):
@@ -448,13 +486,20 @@ class NbserverStopApp(JupyterApp):
448
486
version = __version__
449
487
description = "Stop currently running notebook server for a given port"
450
488
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." )
453
494
454
495
def parse_command_line (self , argv = None ):
455
496
super (NbserverStopApp , self ).parse_command_line (argv )
456
497
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 ]
458
503
459
504
def shutdown_server (self , server ):
460
505
return shutdown_server (server , log = self .log )
@@ -464,16 +509,16 @@ def start(self):
464
509
if not servers :
465
510
self .exit ("There are no running servers" )
466
511
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 )
469
514
if not self .shutdown_server (server ):
470
515
sys .exit ("Could not stop server" )
471
516
return
472
517
else :
473
518
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 )
475
520
for server in servers :
476
- print (" - {}" .format (server ['port' ]), file = sys .stderr )
521
+ print (" - {}" .format (server . get ( 'sock' , server ['port' ]) ), file = sys .stderr )
477
522
self .exit (1 )
478
523
479
524
@@ -553,6 +598,8 @@ def start(self):
553
598
'ip' : 'NotebookApp.ip' ,
554
599
'port' : 'NotebookApp.port' ,
555
600
'port-retries' : 'NotebookApp.port_retries' ,
601
+ 'sock' : 'NotebookApp.sock' ,
602
+ 'sock-umask' : 'NotebookApp.sock_umask' ,
556
603
'transport' : 'KernelManager.transport' ,
557
604
'keyfile' : 'NotebookApp.keyfile' ,
558
605
'certfile' : 'NotebookApp.certfile' ,
@@ -692,10 +739,18 @@ def _valdate_ip(self, proposal):
692
739
or containerized setups for example).""" )
693
740
)
694
741
695
- port = Integer (8888 , config = True ,
742
+ port = Integer (DEFAULT_NOTEBOOK_PORT , config = True ,
696
743
help = _ ("The port the notebook server will listen on." )
697
744
)
698
745
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
+
699
754
port_retries = Integer (50 , config = True ,
700
755
help = _ ("The number of additional ports to try if the specified port is not available." )
701
756
)
@@ -1400,6 +1455,27 @@ def init_webapp(self):
1400
1455
self .log .critical (_ ("\t $ python -m notebook.auth password" ))
1401
1456
sys .exit (1 )
1402
1457
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
+
1403
1479
self .web_app = NotebookWebApplication (
1404
1480
self , self .kernel_manager , self .contents_manager ,
1405
1481
self .session_manager , self .kernel_spec_manager ,
@@ -1436,6 +1512,32 @@ def init_webapp(self):
1436
1512
max_body_size = self .max_body_size ,
1437
1513
max_buffer_size = self .max_buffer_size )
1438
1514
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 ):
1439
1541
success = None
1440
1542
for port in random_ports (self .port , self .port_retries + 1 ):
1441
1543
try :
@@ -1453,39 +1555,45 @@ def init_webapp(self):
1453
1555
self .port = port
1454
1556
success = True
1455
1557
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 })
1460
1563
1461
1564
@property
1462
1565
def display_url (self ):
1463
1566
if self .custom_display_url :
1464
1567
url = self .custom_display_url
1465
1568
if not url .endswith ('/' ):
1466
1569
url += '/'
1570
+ elif self .sock :
1571
+ url = self ._unix_sock_url ()
1467
1572
else :
1468
1573
if self .ip in ('' , '0.0.0.0' ):
1469
1574
ip = "%s" % socket .gethostname ()
1470
1575
else :
1471
1576
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' ))
1479
1581
return url
1480
1582
1481
1583
@property
1482
1584
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 )
1485
1590
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 ):
1487
1595
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 )
1489
1597
1490
1598
def init_terminals (self ):
1491
1599
if not self .terminals_enabled :
@@ -1713,6 +1821,7 @@ def server_info(self):
1713
1821
return {'url' : self .connection_url ,
1714
1822
'hostname' : self .ip if self .ip else 'localhost' ,
1715
1823
'port' : self .port ,
1824
+ 'sock' : self .sock ,
1716
1825
'secure' : bool (self .certfile ),
1717
1826
'base_url' : self .base_url ,
1718
1827
'token' : self .token ,
@@ -1833,19 +1942,31 @@ def start(self):
1833
1942
self .write_server_info_file ()
1834
1943
self .write_browser_open_file ()
1835
1944
1836
- if self .open_browser or self .file_to_run :
1945
+ if ( self .open_browser or self .file_to_run ) and not self . sock :
1837
1946
self .launch_browser ()
1838
1947
1839
1948
if self .token and self ._token_generated :
1840
1949
# log full URL with generated token, so there's a copy/pasteable link
1841
1950
# 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
+ ]))
1849
1970
1850
1971
self .io_loop = ioloop .IOLoop .current ()
1851
1972
if sys .platform .startswith ('win' ):
0 commit comments