32
32
import webbrowser
33
33
import urllib
34
34
import inspect
35
+ import pathlib
35
36
36
37
from base64 import encodebytes
37
38
try :
102
103
from jupyter_server ._sysinfo import get_sys_info
103
104
104
105
from ._tz import utcnow , utcfromtimestamp
105
- from .utils import url_path_join , check_pid , url_escape , urljoin , pathname2url
106
+ from .utils import (
107
+ url_path_join ,
108
+ check_pid ,
109
+ url_escape ,
110
+ urljoin ,
111
+ pathname2url
112
+ )
106
113
107
114
from jupyter_server .extension .serverextension import ServerExtensionApp
108
115
from jupyter_server .extension .manager import ExtensionManager
@@ -620,10 +627,15 @@ def _default_log_format(self):
620
627
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
621
628
622
629
# file to be opened in the Jupyter server
623
- file_to_run = Unicode ('' , config = True )
630
+ file_to_run = Unicode ('' ,
631
+ help = "Open the named file when the application is launched."
632
+ ).tag (config = True )
624
633
625
- # Network related information
634
+ file_url_prefix = Unicode ('notebooks' ,
635
+ help = "The URL prefix where files are opened directly."
636
+ ).tag (config = True )
626
637
638
+ # Network related information
627
639
allow_origin = Unicode ('' , config = True ,
628
640
help = """Set the Access-Control-Allow-Origin header
629
641
@@ -1195,6 +1207,13 @@ def _default_browser_open_file(self):
1195
1207
basename = "jpserver-%s-open.html" % os .getpid ()
1196
1208
return os .path .join (self .runtime_dir , basename )
1197
1209
1210
+ browser_open_file_to_run = Unicode ()
1211
+
1212
+ @default ('browser_open_file_to_run' )
1213
+ def _default_browser_open_file_to_run (self ):
1214
+ basename = "jpserver-file-to-run-%s-open.html" % os .getpid ()
1215
+ return os .path .join (self .runtime_dir , basename )
1216
+
1198
1217
pylab = Unicode ('disabled' , config = True ,
1199
1218
help = _ ("""
1200
1219
DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
@@ -1254,7 +1273,7 @@ def _root_dir_validate(self, proposal):
1254
1273
# If we receive a non-absolute path, make it absolute.
1255
1274
value = os .path .abspath (value )
1256
1275
if not os .path .isdir (value ):
1257
- raise TraitError (trans .gettext ("No such notebook dir : '%r'" ) % value )
1276
+ raise TraitError (trans .gettext ("No such directory : '%r'" ) % value )
1258
1277
return value
1259
1278
1260
1279
@observe ('root_dir' )
@@ -1874,10 +1893,71 @@ def remove_server_info_file(self):
1874
1893
os .unlink (self .info_file )
1875
1894
except OSError as e :
1876
1895
if e .errno != errno .ENOENT :
1877
- raise
1896
+ raise ;
1897
+
1898
+ def _resolve_file_to_run_and_root_dir (self ):
1899
+ """Returns a relative path from file_to_run
1900
+ to root_dir. If root_dir and file_to_run
1901
+ are incompatible, i.e. on different subtrees,
1902
+ crash the app and log a critical message. Note
1903
+ that if root_dir is not configured and file_to_run
1904
+ is configured, root_dir will be set to the parent
1905
+ directory of file_to_run.
1906
+ """
1907
+ rootdir_abspath = pathlib .Path (self .root_dir ).resolve ()
1908
+ file_rawpath = pathlib .Path (self .file_to_run )
1909
+ combined_path = (rootdir_abspath / file_rawpath ).resolve ()
1910
+ is_child = str (combined_path ).startswith (str (rootdir_abspath ))
1911
+
1912
+ if is_child :
1913
+ if combined_path .parent != rootdir_abspath :
1914
+ self .log .debug (
1915
+ "The `root_dir` trait is set to a directory that's not "
1916
+ "the immediate parent directory of `file_to_run`. Note that "
1917
+ "the server will start at `root_dir` and open the "
1918
+ "the file from the relative path to the `root_dir`."
1919
+ )
1920
+ return str (combined_path .relative_to (rootdir_abspath ))
1921
+
1922
+ self .log .critical (
1923
+ "`root_dir` and `file_to_run` are incompatible. They "
1924
+ "don't share the same subtrees. Make sure `file_to_run` "
1925
+ "is on the same path as `root_dir`."
1926
+ )
1927
+ self .exit (1 )
1928
+
1929
+ def _write_browser_open_file (self , url , fh ):
1930
+ if self .token :
1931
+ url = url_concat (url , {'token' : self .token })
1932
+ url = url_path_join (self .connection_url , url )
1933
+
1934
+ jinja2_env = self .web_app .settings ['jinja2_env' ]
1935
+ template = jinja2_env .get_template ('browser-open.html' )
1936
+ fh .write (template .render (open_url = url , base_url = self .base_url ))
1937
+
1938
+ def write_browser_open_files (self ):
1939
+ """Write an `browser_open_file` and `browser_open_file_to_run` files
1940
+
1941
+ This can be used to open a file directly in a browser.
1942
+ """
1943
+ # default_url contains base_url, but so does connection_url
1944
+ self .write_browser_open_file ()
1945
+
1946
+ # Create a second browser open file if
1947
+ # file_to_run is set.
1948
+ if self .file_to_run :
1949
+ # Make sure file_to_run and root_dir are compatible.
1950
+ file_to_run_relpath = self ._resolve_file_to_run_and_root_dir ()
1951
+
1952
+ file_open_url = url_escape (
1953
+ url_path_join (self .file_url_prefix , * file_to_run_relpath .split (os .sep ))
1954
+ )
1955
+
1956
+ with open (self .browser_open_file_to_run , 'w' , encoding = 'utf-8' ) as f :
1957
+ self ._write_browser_open_file (file_open_url , f )
1878
1958
1879
1959
def write_browser_open_file (self ):
1880
- """Write an nbserver -<pid>-open.html file
1960
+ """Write an jpserver -<pid>-open.html file
1881
1961
1882
1962
This can be used to open the notebook in a browser
1883
1963
"""
@@ -1887,17 +1967,21 @@ def write_browser_open_file(self):
1887
1967
with open (self .browser_open_file , 'w' , encoding = 'utf-8' ) as f :
1888
1968
self ._write_browser_open_file (open_url , f )
1889
1969
1890
- def _write_browser_open_file (self , url , fh ):
1891
- if self .token :
1892
- url = url_concat (url , {'token' : self .token })
1893
- url = url_path_join (self .connection_url , url )
1970
+ def remove_browser_open_files (self ):
1971
+ """Remove the `browser_open_file` and `browser_open_file_to_run` files
1972
+ created for this server.
1894
1973
1895
- jinja2_env = self .web_app .settings ['jinja2_env' ]
1896
- template = jinja2_env .get_template ('browser-open.html' )
1897
- fh .write (template .render (open_url = url , base_url = self .base_url ))
1974
+ Ignores the error raised when the file has already been removed.
1975
+ """
1976
+ self .remove_browser_open_file ()
1977
+ try :
1978
+ os .unlink (self .browser_open_file_to_run )
1979
+ except OSError as e :
1980
+ if e .errno != errno .ENOENT :
1981
+ raises
1898
1982
1899
1983
def remove_browser_open_file (self ):
1900
- """Remove the nbserver -<pid>-open.html file created for this server.
1984
+ """Remove the jpserver -<pid>-open.html file created for this server.
1901
1985
1902
1986
Ignores the error raised when the file has already been removed.
1903
1987
"""
@@ -1907,42 +1991,40 @@ def remove_browser_open_file(self):
1907
1991
if e .errno != errno .ENOENT :
1908
1992
raise
1909
1993
1910
- def launch_browser (self ):
1911
- try :
1912
- browser = webbrowser .get (self .browser or None )
1913
- except webbrowser .Error as e :
1914
- self .log .warning (_ ('No web browser found: %s.' ) % e )
1915
- browser = None
1916
-
1917
- if not browser :
1918
- return
1919
-
1994
+ def _prepare_browser_open (self ):
1920
1995
if not self .use_redirect_file :
1921
1996
uri = self .default_url [len (self .base_url ):]
1922
1997
1923
1998
if self .token :
1924
1999
uri = url_concat (uri , {'token' : self .token })
1925
2000
1926
2001
if self .file_to_run :
1927
- if not os .path .exists (self .file_to_run ):
1928
- self .log .critical (_ ("%s does not exist" ) % self .file_to_run )
1929
- self .exit (1 )
1930
-
1931
- relpath = os .path .relpath (self .file_to_run , self .root_dir )
1932
- uri = url_escape (url_path_join ('notebooks' , * relpath .split (os .sep )))
1933
-
1934
- # Write a temporary file to open in the browser
1935
- fd , open_file = tempfile .mkstemp (suffix = '.html' )
1936
- with open (fd , 'w' , encoding = 'utf-8' ) as fh :
1937
- self ._write_browser_open_file (uri , fh )
2002
+ # Create a separate, temporary open-browser-file
2003
+ # pointing at a specific file.
2004
+ open_file = self .browser_open_file_to_run
1938
2005
else :
2006
+ # otherwise, just return the usual open browser file.
1939
2007
open_file = self .browser_open_file
1940
2008
1941
2009
if self .use_redirect_file :
1942
2010
assembled_url = urljoin ('file:' , pathname2url (open_file ))
1943
2011
else :
1944
2012
assembled_url = url_path_join (self .connection_url , uri )
1945
2013
2014
+ return assembled_url , open_file
2015
+
2016
+ def launch_browser (self ):
2017
+ try :
2018
+ browser = webbrowser .get (self .browser or None )
2019
+ except webbrowser .Error as e :
2020
+ self .log .warning (_ ('No web browser found: %s.' ) % e )
2021
+ browser = None
2022
+
2023
+ if not browser :
2024
+ return
2025
+
2026
+ assembled_url , _ = self ._prepare_browser_open ()
2027
+
1946
2028
b = lambda : browser .open (assembled_url , new = self .webbrowser_open_new )
1947
2029
threading .Thread (target = b ).start ()
1948
2030
@@ -1970,7 +2052,7 @@ def start_app(self):
1970
2052
"resources section at https://jupyter.org/community.html." ))
1971
2053
1972
2054
self .write_server_info_file ()
1973
- self .write_browser_open_file ()
2055
+ self .write_browser_open_files ()
1974
2056
1975
2057
# Handle the browser opening.
1976
2058
if self .open_browser :
@@ -1987,6 +2069,14 @@ def start_app(self):
1987
2069
' %s' % self .display_url ,
1988
2070
]))
1989
2071
2072
+ def _cleanup (self ):
2073
+ """General cleanup of files and kernels created
2074
+ by this instance ServerApp.
2075
+ """
2076
+ self .remove_server_info_file ()
2077
+ self .remove_browser_open_files ()
2078
+ self .cleanup_kernels ()
2079
+
1990
2080
def start_ioloop (self ):
1991
2081
"""Start the IO Loop."""
1992
2082
self .io_loop = ioloop .IOLoop .current ()
@@ -2000,9 +2090,7 @@ def start_ioloop(self):
2000
2090
except KeyboardInterrupt :
2001
2091
self .log .info (_ ("Interrupted..." ))
2002
2092
finally :
2003
- self .remove_server_info_file ()
2004
- self .remove_browser_open_file ()
2005
- self .cleanup_kernels ()
2093
+ self ._cleanup ()
2006
2094
2007
2095
def start (self ):
2008
2096
""" Start the Jupyter server app, after initialization
0 commit comments