Skip to content

Commit e0f3c08

Browse files
Merge pull request #22 from SylvainCorlay/filter-message
WIP - Enable kernel message filtering
2 parents bdf256c + 8898a19 commit e0f3c08

File tree

7 files changed

+61
-28
lines changed

7 files changed

+61
-28
lines changed

jupyter_server/services/api/tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ def test_get_status(self):
2929
assert data['kernels'] == 0
3030
assert data['last_activity'].endswith('Z')
3131
assert data['started'].endswith('Z')
32-
assert data['started'] == isoformat(self.notebook.web_app.settings['started'])
32+
assert data['started'] == isoformat(self.server.web_app.settings['started'])

jupyter_server/services/contents/tests/test_contents_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def patch_cp_root(self, dirname):
681681
"""
682682
Temporarily patch the root dir of our checkpoint manager.
683683
"""
684-
cpm = self.notebook.contents_manager.checkpoints
684+
cpm = self.server.contents_manager.checkpoints
685685
old_dirname = cpm.root_dir
686686
cpm.root_dir = dirname
687687
try:
@@ -717,7 +717,7 @@ class GenericFileCheckpointsAPITest(APITest):
717717
def test_config_did_something(self):
718718

719719
self.assertIsInstance(
720-
self.notebook.contents_manager.checkpoints,
720+
self.server.contents_manager.checkpoints,
721721
GenericFileCheckpoints,
722722
)
723723

jupyter_server/services/kernels/handlers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def pre_get(self):
224224
kernel = self.kernel_manager.get_kernel(self.kernel_id)
225225
self.session.key = kernel.session.key
226226
future = self.request_kernel_info()
227-
227+
228228
def give_up():
229229
"""Don't wait forever for the kernel to reply"""
230230
if future.done():
@@ -307,8 +307,13 @@ def on_message(self, msg):
307307
if channel not in self.channels:
308308
self.log.warning("No such channel: %r", channel)
309309
return
310-
stream = self.channels[channel]
311-
self.session.send(stream, msg)
310+
am = self.kernel_manager.allowed_message_types
311+
mt = msg['header']['msg_type']
312+
if am and mt not in am:
313+
self.log.warning('Received message of type "%s", which is not allowed. Ignoring.' % mt)
314+
else:
315+
stream = self.channels[channel]
316+
self.session.send(stream, msg)
312317

313318
def _on_zmq_reply(self, stream, msg_list):
314319
idents, fed_msg_list = self.session.feed_identities(msg_list)

jupyter_server/services/kernels/kernelmanager.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828

2929

3030
class MappingKernelManager(MultiKernelManager):
31-
"""A KernelManager that handles file mapping and HTTP error handling"""
31+
"""A KernelManager that handles
32+
- File mapping
33+
- HTTP error handling
34+
- Kernel message filtering
35+
"""
3236

3337
@default('kernel_manager_class')
3438
def _default_kernel_manager_class(self):
@@ -93,15 +97,15 @@ def _update_root_dir(self, proposal):
9397
no frontends are connected.
9498
"""
9599
)
96-
100+
97101
kernel_info_timeout = Float(60, config=True,
98102
help="""Timeout for giving up on a kernel (in seconds).
99103
100104
On starting and restarting kernels, we check whether the
101105
kernel is running and responsive by sending kernel_info_requests.
102106
This sets the timeout in seconds for how long the kernel can take
103-
before being presumed dead.
104-
This affects the MappingKernelManager (which handles kernel restarts)
107+
before being presumed dead.
108+
This affects the MappingKernelManager (which handles kernel restarts)
105109
and the ZMQChannelsHandler (which handles the startup).
106110
"""
107111
)
@@ -118,6 +122,12 @@ def __init__(self, **kwargs):
118122
super(MappingKernelManager, self).__init__(**kwargs)
119123
self.last_kernel_activity = utcnow()
120124

125+
allowed_message_types = List(trait=Unicode(), config=True,
126+
help="""White list of allowed kernel message types.
127+
When the list is empty, all message types are allowed.
128+
"""
129+
)
130+
121131
#-------------------------------------------------------------------------
122132
# Methods for managing kernels and sessions
123133
#-------------------------------------------------------------------------
@@ -287,32 +297,32 @@ def restart_kernel(self, kernel_id):
287297
# return a Future that will resolve when the kernel has successfully restarted
288298
channel = kernel.connect_shell()
289299
future = Future()
290-
300+
291301
def finish():
292302
"""Common cleanup when restart finishes/fails for any reason."""
293303
if not channel.closed():
294304
channel.close()
295305
loop.remove_timeout(timeout)
296306
kernel.remove_restart_callback(on_restart_failed, 'dead')
297-
307+
298308
def on_reply(msg):
299309
self.log.debug("Kernel info reply received: %s", kernel_id)
300310
finish()
301311
if not future.done():
302312
future.set_result(msg)
303-
313+
304314
def on_timeout():
305315
self.log.warning("Timeout waiting for kernel_info_reply: %s", kernel_id)
306316
finish()
307317
if not future.done():
308318
future.set_exception(gen.TimeoutError("Timeout waiting for restart"))
309-
319+
310320
def on_restart_failed():
311321
self.log.warning("Restarting kernel failed: %s", kernel_id)
312322
finish()
313323
if not future.done():
314324
future.set_exception(RuntimeError("Restart failed"))
315-
325+
316326
kernel.add_restart_callback(on_restart_failed, 'dead')
317327
kernel.session.send(channel, "kernel_info_request")
318328
channel.on_recv(on_reply)
@@ -366,7 +376,7 @@ def _check_kernel_id(self, kernel_id):
366376

367377
def start_watching_activity(self, kernel_id):
368378
"""Start watching IOPub messages on a kernel for activity.
369-
379+
370380
- update last_activity on every message
371381
- record execution_state from status messages
372382
"""

jupyter_server/services/kernels/tests/test_kernels_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import json
44
import time
55

6+
from traitlets.config import Config
7+
68
from tornado.httpclient import HTTPRequest
79
from tornado.ioloop import IOLoop
810
from tornado.websocket import websocket_connect
@@ -184,3 +186,20 @@ def test_connections(self):
184186
break
185187
model = self.kern_api.get(kid).json()
186188
self.assertEqual(model['connections'], 0)
189+
190+
191+
class KernelFilterTest(ServerTestBase):
192+
# A special install of ServerTestBase where only `kernel_info_request`
193+
# messages are allowed.
194+
195+
config = Config({
196+
'ServerApp': {
197+
'MappingKernelManager': {
198+
'allowed_message_types': ['kernel_info_request']
199+
}
200+
}
201+
})
202+
203+
# Sanity check verifying that the configurable was properly set.
204+
def test_config(self):
205+
self.assertEqual(self.server.kernel_manager.allowed_message_types, ['kernel_info_request'])

jupyter_server/tests/launchserver.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def wait_until_alive(cls):
5959
try:
6060
requests.get(url)
6161
except Exception as e:
62-
if not cls.notebook_thread.is_alive():
62+
if not cls.server_thread.is_alive():
6363
raise RuntimeError("The Jupyter server failed to start")
6464
time.sleep(POLL_INTERVAL)
6565
else:
@@ -70,8 +70,8 @@ def wait_until_alive(cls):
7070
@classmethod
7171
def wait_until_dead(cls):
7272
"""Wait for the server process to terminate after shutdown"""
73-
cls.notebook_thread.join(timeout=MAX_WAITTIME)
74-
if cls.notebook_thread.is_alive():
73+
cls.server_thread.join(timeout=MAX_WAITTIME)
74+
if cls.server_thread.is_alive():
7575
raise TimeoutError("Undead Jupyter server")
7676

7777
@classmethod
@@ -110,11 +110,10 @@ def tmp(*parts):
110110
data_dir = cls.data_dir = tmp('data')
111111
config_dir = cls.config_dir = tmp('config')
112112
runtime_dir = cls.runtime_dir = tmp('runtime')
113-
cls.root_dir = tmp('notebooks')
113+
cls.root_dir = tmp('root_dir')
114114
cls.env_patch = patch.dict('os.environ', {
115115
'HOME': cls.home_dir,
116116
'PYTHONPATH': os.pathsep.join(sys.path),
117-
'IPYTHONDIR': pjoin(cls.home_dir, '.ipython'),
118117
'JUPYTER_NO_CONFIG': '1', # needed in the future
119118
'JUPYTER_CONFIG_DIR' : config_dir,
120119
'JUPYTER_DATA_DIR' : data_dir,
@@ -140,7 +139,7 @@ def start_thread():
140139
if 'asyncio' in sys.modules:
141140
import asyncio
142141
asyncio.set_event_loop(asyncio.new_event_loop())
143-
app = cls.notebook = ServerApp(
142+
app = cls.server = ServerApp(
144143
port=cls.port,
145144
port_retries=0,
146145
open_browser=False,
@@ -170,15 +169,15 @@ def start_thread():
170169
# set the event, so failure to start doesn't cause a hang
171170
started.set()
172171
app.session_manager.close()
173-
cls.notebook_thread = Thread(target=start_thread)
174-
cls.notebook_thread.daemon = True
175-
cls.notebook_thread.start()
172+
cls.server_thread = Thread(target=start_thread)
173+
cls.server_thread.daemon = True
174+
cls.server_thread.start()
176175
started.wait()
177176
cls.wait_until_alive()
178177

179178
@classmethod
180179
def teardown_class(cls):
181-
cls.notebook.stop()
180+
cls.server.stop()
182181
cls.wait_until_dead()
183182
cls.env_patch.stop()
184183
cls.path_patch.stop()

jupyter_server/tests/test_files.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_hidden_files(self):
5757
r = self.request('GET', url_path_join('files', d, foo))
5858
self.assertEqual(r.status_code, 404)
5959

60-
self.notebook.contents_manager.allow_hidden = True
60+
self.server.contents_manager.allow_hidden = True
6161
try:
6262
for d in not_hidden:
6363
path = pjoin(rootdir, d.replace('/', os.sep))
@@ -75,7 +75,7 @@ def test_hidden_files(self):
7575
r.raise_for_status()
7676
self.assertEqual(r.text, foo)
7777
finally:
78-
self.notebook.contents_manager.allow_hidden = False
78+
self.server.contents_manager.allow_hidden = False
7979

8080
def test_contents_manager(self):
8181
"make sure ContentsManager returns right files (ipynb, bin, txt)."

0 commit comments

Comments
 (0)