Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Batch 4] Porting Notebook PRs #99

Merged
merged 15 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ __pycache__
\#*#
.#*
.coverage
.pytest_cache
src

*.swp
Expand All @@ -38,4 +39,5 @@ config.rst
/.project
/.pydevproject

package-lock.json
package-lock.json
geckodriver.log
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ before_install:
- |
if [[ $GROUP == docs ]]; then
pip install -r docs/doc-requirements.txt
pip install --upgrade pytest
fi

install:
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/Notebook/Running Code.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefor runs Python code."
"First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefore runs Python code."
]
},
{
Expand Down
6 changes: 0 additions & 6 deletions jupyter_server/auth/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,11 @@ def get_user_token(cls, handler):
return
# check login token from URL argument or Authorization header
user_token = cls.get_token(handler)
one_time_token = handler.one_time_token
authenticated = False
if user_token == token:
# token-authenticated, set the login cookie
handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True
elif one_time_token and user_token == one_time_token:
# one-time-token-authenticated, only allow this token once
handler.settings.pop('one_time_token', None)
handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip)
authenticated = True

if authenticated:
return uuid.uuid4().hex
Expand Down
7 changes: 1 addition & 6 deletions jupyter_server/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,6 @@ def token(self):
"""Return the login token for this application, if any."""
return self.settings.get('token', None)

@property
def one_time_token(self):
"""Return the one-time-use token for this application, if any."""
return self.settings.get('one_time_token', None)

@property
def login_available(self):
"""May a user proceed to log in?
Expand Down Expand Up @@ -458,7 +453,7 @@ def template_namespace(self):
logged_in=self.logged_in,
allow_password_change=self.settings.get('allow_password_change'),
login_available=self.login_available,
token_available=bool(self.token or self.one_time_token),
token_available=bool(self.token),
static_url=self.static_url,
sys_info=json_sys_info(),
contents_js_source=self.contents_js_source,
Expand Down
111 changes: 77 additions & 34 deletions jupyter_server/serverapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import signal
import socket
import sys
import tempfile
import threading
import time
import warnings
Expand Down Expand Up @@ -97,7 +98,7 @@
from jupyter_server._sysinfo import get_sys_info

from ._tz import utcnow, utcfromtimestamp
from .utils import url_path_join, check_pid, url_escape
from .utils import url_path_join, check_pid, url_escape, urljoin, pathname2url

#-----------------------------------------------------------------------------
# Module globals
Expand Down Expand Up @@ -738,12 +739,6 @@ def _write_cookie_secret_file(self, secret):
""")
).tag(config=True)

one_time_token = Unicode(
help=_("""One-time token used for opening a browser.
Once used, this token cannot be used again.
""")
)

_token_generated = True

@default('token')
Expand Down Expand Up @@ -1088,6 +1083,13 @@ def _default_info_file(self):
info_file = "jpserver-%s.json" % os.getpid()
return os.path.join(self.runtime_dir, info_file)

browser_open_file = Unicode()

@default('browser_open_file')
def _default_browser_open_file(self):
basename = "jpserver-%s-open.html" % os.getpid()
return os.path.join(self.runtime_dir, basename)

pylab = Unicode('disabled', config=True,
help=_("""
DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
Expand Down Expand Up @@ -1261,9 +1263,6 @@ def init_webapp(self):
self.tornado_settings['cookie_options'] = self.cookie_options
self.tornado_settings['get_secure_cookie_kwargs'] = self.get_secure_cookie_kwargs
self.tornado_settings['token'] = self.token
if (self.open_browser or self.file_to_run) and not self.password:
self.one_time_token = binascii.hexlify(os.urandom(24)).decode('ascii')
self.tornado_settings['one_time_token'] = self.one_time_token

# ensure default_url starts with base_url
if not self.default_url.startswith(self.base_url):
Expand Down Expand Up @@ -1594,6 +1593,67 @@ def remove_server_info_file(self):
if e.errno != errno.ENOENT:
raise

def write_browser_open_file(self):
"""Write an nbserver-<pid>-open.html file

This can be used to open the notebook in a browser
"""
# default_url contains base_url, but so does connection_url
open_url = self.default_url[len(self.base_url):]

with open(self.browser_open_file, 'w', encoding='utf-8') as f:
self._write_browser_open_file(open_url, f)

def _write_browser_open_file(self, url, fh):
if self.token:
url = url_concat(url, {'token': self.token})
url = url_path_join(self.connection_url, url)

jinja2_env = self.web_app.settings['jinja2_env']
template = jinja2_env.get_template('browser-open.html')
fh.write(template.render(open_url=url))

def remove_browser_open_file(self):
"""Remove the nbserver-<pid>-open.html file created for this server.

Ignores the error raised when the file has already been removed.
"""
try:
os.unlink(self.browser_open_file)
except OSError as e:
if e.errno != errno.ENOENT:
raise

def launch_browser(self):
try:
browser = webbrowser.get(self.browser or None)
except webbrowser.Error as e:
self.log.warning(_('No web browser found: %s.') % e)
browser = None

if not browser:
return

if self.file_to_run:
if not os.path.exists(self.file_to_run):
self.log.critical(_("%s does not exist") % self.file_to_run)
self.exit(1)

relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))

# Write a temporary file to open in the browser
fd, open_file = tempfile.mkstemp(suffix='.html')
with open(fd, 'w', encoding='utf-8') as fh:
self._write_browser_open_file(uri, fh)
else:
open_file = self.browser_open_file

b = lambda: browser.open(
urljoin('file:', pathname2url(open_file)),
new=self.webbrowser_open_new)
threading.Thread(target=b).start()

def start(self):
""" Start the Jupyter server app, after initialization

Expand Down Expand Up @@ -1623,37 +1683,19 @@ def start(self):
"resources section at https://jupyter.org/community.html."))

self.write_server_info_file()
self.write_browser_open_file()

if self.open_browser or self.file_to_run:
try:
browser = webbrowser.get(self.browser or None)
except webbrowser.Error as e:
self.log.warning(_('No web browser found: %s.') % e)
browser = None

if self.file_to_run:
if not os.path.exists(self.file_to_run):
self.log.critical(_("%s does not exist") % self.file_to_run)
self.exit(1)

relpath = os.path.relpath(self.file_to_run, self.root_dir)
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
else:
uri = self.base_url
if self.one_time_token:
uri = url_concat(uri, {'token': self.one_time_token})
if browser:
b = lambda : browser.open(url_path_join(self.connection_url, uri),
new=self.webbrowser_open_new)
threading.Thread(target=b).start()
self.launch_browser()

if self.token and self._token_generated:
# log full URL with generated token, so there's a copy/pasteable link
# with auth info.
self.log.critical('\n'.join([
'\n',
'Copy/paste this URL into your browser when you connect for the first time,',
'to login with a token:',
'To access the notebook, open this file in a browser:',
' %s' % urljoin('file:', pathname2url(self.browser_open_file)),
'Or copy and paste one of these URLs:',
' %s' % self.display_url,
]))

Expand All @@ -1669,6 +1711,7 @@ def start(self):
info(_("Interrupted..."))
finally:
self.remove_server_info_file()
self.remove_browser_open_file()
self.cleanup_kernels()

def stop(self):
Expand All @@ -1693,7 +1736,7 @@ def list_running_servers(runtime_dir=None):
return

for file_name in os.listdir(runtime_dir):
if file_name.startswith('jpserver-'):
if re.match('jpserver-(.+).json', file_name):
with io.open(os.path.join(runtime_dir, file_name), encoding='utf-8') as f:
info = json.load(f)

Expand Down
10 changes: 5 additions & 5 deletions jupyter_server/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,11 @@ def _dir_model(self, path, content=True):
self.log.debug("%s not a regular file", os_path)
continue

if self.should_list(name) and not is_file_hidden(os_path, stat_res=st):
contents.append(self.get(
path='%s/%s' % (path, name),
content=False)
)
if self.should_list(name):
if self.allow_hidden or not is_file_hidden(os_path, stat_res=st):
contents.append(
self.get(path='%s/%s' % (path, name), content=False)
)

model['format'] = 'json'

Expand Down
1 change: 1 addition & 0 deletions jupyter_server/services/kernels/tests/test_kernels_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from jupyter_server.tests.launchserver import ServerTestBase, assert_http_error



class KernelAPI(object):
"""Wrapper for kernel REST API requests"""
def __init__(self, request, base_url, headers):
Expand Down
31 changes: 24 additions & 7 deletions jupyter_server/services/sessions/sessionmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,17 @@ def __del__(self):
def session_exists(self, path):
"""Check to see if the session of a given name exists"""
self.cursor.execute("SELECT * FROM session WHERE path=?", (path,))
reply = self.cursor.fetchone()
if reply is None:
row = self.cursor.fetchone()
if row is None:
return False
else:
# Note, although we found a row for the session, the associated kernel may have
# been culled or died unexpectedly. If that's the case, we should delete the
# row, thereby terminating the session. This can be done via a call to
# row_to_model that tolerates that condition. If row_to_model returns None,
# we'll return false, since, at that point, the session doesn't exist anyway.
if self.row_to_model(row, tolerate_culled=True) is None:
return False
return True

def new_session_id(self):
Expand Down Expand Up @@ -198,15 +205,25 @@ def update_session(self, session_id, **kwargs):
query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
self.cursor.execute(query, list(kwargs.values()) + [session_id])

def row_to_model(self, row):
def row_to_model(self, row, tolerate_culled=False):
"""Takes sqlite database session row and turns it into a dictionary"""
if row['kernel_id'] not in self.kernel_manager:
# The kernel was killed or died without deleting the session.
# The kernel was culled or died without deleting the session.
# We can't use delete_session here because that tries to find
# and shut down the kernel.
self.cursor.execute("DELETE FROM session WHERE session_id=?",
# and shut down the kernel - so we'll delete the row directly.
#
# If caller wishes to tolerate culled kernels, log a warning
# and return None. Otherwise, raise KeyError with a similar
# message.
self.cursor.execute("DELETE FROM session WHERE session_id=?",
(row['session_id'],))
raise KeyError
msg = "Kernel '{kernel_id}' appears to have been culled or died unexpectedly, " \
"invalidating session '{session_id}'. The session has been removed.".\
format(kernel_id=row['kernel_id'],session_id=row['session_id'])
if tolerate_culled:
self.log.warning(msg + " Continuing...")
return None
raise KeyError(msg)

model = {
'id': row['session_id'],
Expand Down
18 changes: 18 additions & 0 deletions jupyter_server/templates/browser-open.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{# This template is not served, but written as a file to open in the browser,
passing the token without putting it in a command-line argument. #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="1;url={{ open_url }}" />
<title>Opening Jupyter Notebook</title>
</head>
<body>

<p>
This page should redirect you to Jupyter Notebook. If it doesn't,
<a href="{{ open_url }}">click here to go to Jupyter</a>.
</p>

</body>
</html>
Loading