diff --git a/.travis.yml b/.travis.yml index cd7304aa0..df263d4e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,6 @@ cache: python: - 3.6 -sudo: required - env: global: diff --git a/bower.json b/bower.json index 0136a82da..f2e439e90 100644 --- a/bower.json +++ b/bower.json @@ -16,9 +16,9 @@ "marked": "~0.5", "MathJax": "^2.7.4", "moment": "~2.19.3", - "preact": "https://unpkg.com/preact@^7.2.0/dist/preact.min.js", - "preact-compat": "https://unpkg.com/preact-compat@^3.14.3/dist/preact-compat.min.js", - "proptypes": "https://unpkg.com/proptypes@^0.14.4/index.js", + "preact": "https://unpkg.com/preact@~7.2.0/dist/preact.min.js", + "preact-compat": "https://unpkg.com/preact-compat@~3.14.3/dist/preact-compat.min.js", + "proptypes": "https://unpkg.com/proptypes@~0.14.4/index.js", "requirejs": "~2.2", "requirejs-text": "~2.0.15", "requirejs-plugins": "~1.0.3", diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 51931f20c..daa7a858d 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -21,6 +21,16 @@ We strongly recommend that you upgrade pip to version 9+ of pip before upgrading Use ``pip install pip --upgrade`` to upgrade pip. Check pip version with ``pip --version``. +.. _release-6.0.0: + +6.0.0 +----- + +- add ``?no_track_activity=1`` argument to allow API requests + to not be registered as activity (e.g. API calls by external activity monitors). +- Kernels shutting down due to an idle timeout is no longer considered + an activity-updating event. + .. _release-5.7.8: 5.7.8 diff --git a/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb index aee390a3c..3b284ac53 100644 --- a/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb +++ b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb @@ -226,7 +226,7 @@ }, "source": [ "### Defining the server extension and nbextension\n", - "This example again shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file. This time, there is also a function `_jupyter_nbextension_path` for the nbextension.\n", + "This example again shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file. This time, there is also a function `_jupyter_nbextension_paths` for the nbextension.\n", "\n", "#### `my_fancy_module/__init__.py`\n", "\n", diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index cd801c9a1..31805ebde 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -650,7 +650,11 @@ class APIHandler(IPythonHandler): def update_api_activity(self): """Update last_activity of API requests""" # record activity of authenticated requests - if self._track_activity and getattr(self, '_user_cache', None): + if ( + self._track_activity + and getattr(self, '_user_cache', None) + and self.get_argument('no_track_activity', None) is None + ): self.settings['api_last_activity'] = utcnow() def finish(self, *args, **kwargs): diff --git a/notebook/gateway/handlers.py b/notebook/gateway/handlers.py index 0fbbfb18b..cdfbb64e5 100644 --- a/notebook/gateway/handlers.py +++ b/notebook/gateway/handlers.py @@ -10,7 +10,7 @@ from ..utils import url_path_join from tornado import gen, web from tornado.concurrent import Future -from tornado.ioloop import IOLoop +from tornado.ioloop import IOLoop, PeriodicCallback from tornado.websocket import WebSocketHandler, websocket_connect from tornado.httpclient import HTTPRequest from tornado.escape import url_escape, json_decode, utf8 @@ -21,12 +21,16 @@ from traitlets.config.configurable import LoggingConfigurable from .managers import GatewayClient +# Keepalive ping interval (default: 30 seconds) +GATEWAY_WS_PING_INTERVAL_SECS = int(os.getenv('GATEWAY_WS_PING_INTERVAL_SECS', 30)) + class WebSocketChannelsHandler(WebSocketHandler, IPythonHandler): session = None gateway = None kernel_id = None + ping_callback = None def set_default_headers(self): """Undo the set_default_headers in IPythonHandler which doesn't make sense for websockets""" @@ -63,8 +67,18 @@ class WebSocketChannelsHandler(WebSocketHandler, IPythonHandler): self.kernel_id = cast_unicode(kernel_id, 'ascii') super(WebSocketChannelsHandler, self).get(kernel_id=kernel_id, *args, **kwargs) + def send_ping(self): + if self.ws_connection is None and self.ping_callback is not None: + self.ping_callback.stop() + return + + self.ping(b'') + def open(self, kernel_id, *args, **kwargs): """Handle web socket connection open to notebook server and delegate to gateway web socket handler """ + self.ping_callback = PeriodicCallback(self.send_ping, GATEWAY_WS_PING_INTERVAL_SECS * 1000) + self.ping_callback.start() + self.gateway.on_open( kernel_id=kernel_id, message_callback=self.write_message, @@ -80,6 +94,8 @@ class WebSocketChannelsHandler(WebSocketHandler, IPythonHandler): """Send message back to notebook client. This is called via callback from self.gateway._read_messages.""" self.log.debug("Receiving message from gateway: {}".format(message)) if self.ws_connection: # prevent WebSocketClosedError + if isinstance(message, bytes): + binary = True super(WebSocketChannelsHandler, self).write_message(message, binary=binary) elif self.log.isEnabledFor(logging.DEBUG): msg_summary = WebSocketChannelsHandler._get_message_summary(json_decode(utf8(message))) diff --git a/notebook/gateway/managers.py b/notebook/gateway/managers.py index 6c97e57a9..7f2ff8695 100644 --- a/notebook/gateway/managers.py +++ b/notebook/gateway/managers.py @@ -35,6 +35,7 @@ class GatewayClient(SingletonConfigurable): ) url_env = 'JUPYTER_GATEWAY_URL' + @default('url') def _url_default(self): return os.environ.get(self.url_env) @@ -55,6 +56,7 @@ class GatewayClient(SingletonConfigurable): ) ws_url_env = 'JUPYTER_GATEWAY_WS_URL' + @default('ws_url') def _ws_url_default(self): default_value = os.environ.get(self.ws_url_env) @@ -100,7 +102,7 @@ class GatewayClient(SingletonConfigurable): def _kernelspecs_resource_endpoint_default(self): return os.environ.get(self.kernelspecs_resource_endpoint_env, self.kernelspecs_resource_endpoint_default_value) - connect_timeout_default_value = 20.0 + connect_timeout_default_value = 60.0 connect_timeout_env = 'JUPYTER_GATEWAY_CONNECT_TIMEOUT' connect_timeout = Float(default_value=connect_timeout_default_value, config=True, help="""The time allowed for HTTP connection establishment with the Gateway server. @@ -110,7 +112,7 @@ class GatewayClient(SingletonConfigurable): def connect_timeout_default(self): return float(os.environ.get('JUPYTER_GATEWAY_CONNECT_TIMEOUT', self.connect_timeout_default_value)) - request_timeout_default_value = 20.0 + request_timeout_default_value = 60.0 request_timeout_env = 'JUPYTER_GATEWAY_REQUEST_TIMEOUT' request_timeout = Float(default_value=request_timeout_default_value, config=True, help="""The time allowed for HTTP request completion. (JUPYTER_GATEWAY_REQUEST_TIMEOUT env var)""") @@ -171,7 +173,7 @@ class GatewayClient(SingletonConfigurable): headers_default_value = '{}' headers_env = 'JUPYTER_GATEWAY_HEADERS' - headers = Unicode(default_value=headers_default_value, allow_none=True,config=True, + headers = Unicode(default_value=headers_default_value, allow_none=True, config=True, help="""Additional HTTP headers to pass on the request. This value will be converted to a dict. (JUPYTER_GATEWAY_HEADERS env var) """ @@ -189,7 +191,7 @@ class GatewayClient(SingletonConfigurable): @default('auth_token') def _auth_token_default(self): - return os.environ.get(self.auth_token_env) + return os.environ.get(self.auth_token_env, '') validate_cert_default_value = True validate_cert_env = 'JUPYTER_GATEWAY_VALIDATE_CERT' @@ -222,13 +224,26 @@ class GatewayClient(SingletonConfigurable): def gateway_enabled(self): return bool(self.url is not None and len(self.url) > 0) + # Ensure KERNEL_LAUNCH_TIMEOUT has a default value. + KERNEL_LAUNCH_TIMEOUT = int(os.environ.get('KERNEL_LAUNCH_TIMEOUT', 40)) + os.environ['KERNEL_LAUNCH_TIMEOUT'] = str(KERNEL_LAUNCH_TIMEOUT) + + LAUNCH_TIMEOUT_PAD = int(os.environ.get('LAUNCH_TIMEOUT_PAD', 2)) + def init_static_args(self): """Initialize arguments used on every request. Since these are static values, we'll perform this operation once. """ + # Ensure that request timeout is at least "pad" greater than launch timeout. + if self.request_timeout < float(GatewayClient.KERNEL_LAUNCH_TIMEOUT + GatewayClient.LAUNCH_TIMEOUT_PAD): + self.request_timeout = float(GatewayClient.KERNEL_LAUNCH_TIMEOUT + GatewayClient.LAUNCH_TIMEOUT_PAD) + self._static_args['headers'] = json.loads(self.headers) - self._static_args['headers'].update({'Authorization': 'token {}'.format(self.auth_token)}) + if 'Authorization' not in self._static_args['headers'].keys(): + self._static_args['headers'].update({ + 'Authorization': 'token {}'.format(self.auth_token) + }) self._static_args['connect_timeout'] = self.connect_timeout self._static_args['request_timeout'] = self.request_timeout self._static_args['validate_cert'] = self.validate_cert @@ -270,13 +285,13 @@ def gateway_request(endpoint, **kwargs): "Check to be sure the Gateway instance is running.".format(GatewayClient.instance().url)) except HTTPError: # This can occur if the host is valid (e.g., foo.com) but there's nothing there. - raise web.HTTPError(504, "Error attempting to connect to Gateway server url '{}'. " \ - "Ensure gateway url is valid and the Gateway instance is running.".format( - GatewayClient.instance().url)) + raise web.HTTPError(504, "Error attempting to connect to Gateway server url '{}'. " + "Ensure gateway url is valid and the Gateway instance is running.". + format(GatewayClient.instance().url)) except gaierror as e: raise web.HTTPError(404, "The Gateway server specified in the gateway_url '{}' doesn't appear to be valid. " - "Ensure gateway url is valid and the Gateway instance is running.".format( - GatewayClient.instance().url)) + "Ensure gateway url is valid and the Gateway instance is running.". + format(GatewayClient.instance().url)) raise gen.Return(response) @@ -409,7 +424,7 @@ class GatewayKernelManager(MappingKernelManager): self.log.debug("Request list kernels: %s", kernel_url) response = yield gateway_request(kernel_url, method='GET') kernels = json_decode(response.body) - self._kernels = {x['id']:x for x in kernels} + self._kernels = {x['id']: x for x in kernels} raise gen.Return(kernels) @gen.coroutine @@ -420,6 +435,10 @@ class GatewayKernelManager(MappingKernelManager): ========== kernel_id : uuid The id of the kernel to shutdown. + now : bool + Shutdown the kernel immediately (True) or gracefully (False) + restart : bool + The purpose of this shutdown is to restart the kernel (True) """ kernel_url = self._get_kernel_endpoint_url(kernel_id) self.log.debug("Request shutdown kernel at: %s", kernel_url) diff --git a/notebook/i18n/fr_FR/LC_MESSAGES/nbjs.po b/notebook/i18n/fr_FR/LC_MESSAGES/nbjs.po index f01c7e97d..a8b01bf13 100644 --- a/notebook/i18n/fr_FR/LC_MESSAGES/nbjs.po +++ b/notebook/i18n/fr_FR/LC_MESSAGES/nbjs.po @@ -1187,7 +1187,7 @@ msgstr "" #: notebook/static/notebook/js/notificationarea.js:166 msgid "Connection failed" -msgstr "Échec de la connection" +msgstr "Échec de la connexion" #: notebook/static/notebook/js/notificationarea.js:179 msgid "No kernel" diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index a1a210826..ac97cf847 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -421,7 +421,7 @@ def shutdown_server(server_info, timeout=5, log=None): # Poll to see if it shut down. for _ in range(timeout*10): - if check_pid(pid): + if not check_pid(pid): if log: log.debug("Server PID %s is gone", pid) return True time.sleep(0.1) @@ -434,7 +434,7 @@ def shutdown_server(server_info, timeout=5, log=None): # Poll to see if it shut down. for _ in range(timeout * 10): - if check_pid(pid): + if not check_pid(pid): if log: log.debug("Server PID %s is gone", pid) return True time.sleep(0.1) diff --git a/notebook/services/api/tests/test_api.py b/notebook/services/api/tests/test_api.py index 0a48b793e..d09a06032 100644 --- a/notebook/services/api/tests/test_api.py +++ b/notebook/services/api/tests/test_api.py @@ -1,27 +1,27 @@ """Test the basic /api endpoints""" -import requests +from datetime import timedelta -from notebook._tz import isoformat +from notebook._tz import isoformat, utcnow from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase -class KernelAPITest(NotebookTestBase): +class APITest(NotebookTestBase): """Test the kernels web service API""" - + def _req(self, verb, path, **kwargs): r = self.request(verb, url_path_join('api', path)) r.raise_for_status() return r - + def get(self, path, **kwargs): return self._req('GET', path) - + def test_get_spec(self): r = self.get('spec.yaml') assert r.text - + def test_get_status(self): r = self.get('status') data = r.json() @@ -30,3 +30,18 @@ class KernelAPITest(NotebookTestBase): assert data['last_activity'].endswith('Z') assert data['started'].endswith('Z') assert data['started'] == isoformat(self.notebook.web_app.settings['started']) + + def test_no_track_activity(self): + # initialize with old last api activity + old = utcnow() - timedelta(days=1) + settings = self.notebook.web_app.settings + settings['api_last_activity'] = old + # accessing status doesn't update activity + self.get('status') + assert settings['api_last_activity'] == old + # accessing with ?no_track_activity doesn't update activity + self.get('contents?no_track_activity=1') + assert settings['api_last_activity'] == old + # accessing without ?no_track_activity does update activity + self.get('contents') + assert settings['api_last_activity'] > old diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index b072e014b..53ae5d937 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -292,7 +292,6 @@ class MappingKernelManager(MultiKernelManager): kernel._activity_stream = None self.stop_buffering(kernel_id) self._kernel_connections.pop(kernel_id, None) - self.last_kernel_activity = utcnow() # Decrease the metric of number of kernels # running for the relevant kernel type by 1 diff --git a/notebook/static/base/js/dialog.js b/notebook/static/base/js/dialog.js index b59d9908b..f97523cae 100644 --- a/notebook/static/base/js/dialog.js +++ b/notebook/static/base/js/dialog.js @@ -60,6 +60,7 @@ define(['jquery', }) .append($(" - -
- - {% trans %}File size{% endtrans %} - - -
-
- - {% trans %}Last Modified{% endtrans %} - - -
-
- - {% trans %}Name{% endtrans %} - - +
+
+ +
+
+ +
+
+ +