Skip to content

Commit 99bea54

Browse files
committed
Backport PR #294: Additional to the actual signal, send a message on the control port
In Erlang I have only on POSIX and only in the newest version some control over the normal runtime signals, in particular SIGINT. However, I can easily pick up and process the respective message on the control socket to interrupt execution. Can this functionality be added? If yes, I'd continue and document the functionality and add tests. Maybe it would also be good to have the option of not sending an actual signal (via `os.kill`) at all. Signed-off-by: Thomas Kluyver <[email protected]>
1 parent f6c6198 commit 99bea54

File tree

5 files changed

+55
-14
lines changed

5 files changed

+55
-14
lines changed

docs/kernels.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ JSON serialised dictionary containing the following keys and values:
132132
is found, a kernel with a matching `language` will be used.
133133
This allows a notebook written on any Python or Julia kernel to be properly associated
134134
with the user's Python or Julia kernel, even if they aren't listed under the same name as the author's.
135+
- **interrupt_mode** (optional): May be either ``signal`` or ``message`` and
136+
specifies how a client is supposed to interrupt cell execution on this kernel,
137+
either by sending an interrupt ``signal`` via the operating system's
138+
signalling facilities (e.g. `SIGINT` on POSIX systems), or by sending an
139+
``interrupt_request`` message on the control channel (see
140+
:ref:`msging_interrupt`). If this is not specified
141+
the client will default to ``signal`` mode.
135142
- **env** (optional): A dictionary of environment variables to set for the kernel.
136143
These will be added to the current environment variables before the kernel is
137144
started.

docs/messaging.rst

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Versioning
2121

2222
The Jupyter message specification is versioned independently of the packages
2323
that use it.
24-
The current version of the specification is 5.2.
24+
The current version of the specification is 5.3.
2525

2626
.. note::
2727
*New in* and *Changed in* messages in this document refer to versions of the
@@ -959,6 +959,27 @@ Message type: ``shutdown_reply``::
959959
socket, they simply send a forceful process termination signal, since a dead
960960
process is unlikely to respond in any useful way to messages.
961961

962+
.. _msging_interrupt:
963+
964+
Kernel interrupt
965+
----------------
966+
967+
In case a kernel can not catch operating system interrupt signals (e.g. the used
968+
runtime handles signals and does not allow a user program to define a callback),
969+
a kernel can choose to be notified using a message instead. For this to work,
970+
the kernels kernelspec must set `interrupt_mode` to ``message``. An interruption
971+
will then result in the following message on the `control` channel:
972+
973+
Message type: ``interrupt_request``::
974+
975+
content = {}
976+
977+
Message type: ``interrupt_reply``::
978+
979+
content = {}
980+
981+
.. versionadded:: 5.3
982+
962983

963984
Messages on the IOPub (PUB/SUB) channel
964985
=======================================

jupyter_client/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
version_info = (5, 1, 0)
22
__version__ = '.'.join(map(str, version_info))
33

4-
protocol_version_info = (5, 2)
4+
protocol_version_info = (5, 3)
55
protocol_version = "%i.%i" % protocol_version_info

jupyter_client/kernelspec.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
pjoin = os.path.join
1414

1515
from ipython_genutils.py3compat import PY3
16-
from traitlets import HasTraits, List, Unicode, Dict, Set, Bool, Type
16+
from traitlets import (
17+
HasTraits, List, Unicode, Dict, Set, Bool, Type, CaselessStrEnum
18+
)
1719
from traitlets.config import LoggingConfigurable
1820

1921
from jupyter_core.paths import jupyter_data_dir, jupyter_path, SYSTEM_JUPYTER_PATH
@@ -28,6 +30,9 @@ class KernelSpec(HasTraits):
2830
language = Unicode()
2931
env = Dict()
3032
resource_dir = Unicode()
33+
interrupt_mode = CaselessStrEnum(
34+
['message', 'signal'], default_value='signal'
35+
)
3136
metadata = Dict()
3237

3338
@classmethod
@@ -46,6 +51,7 @@ def to_dict(self):
4651
env=self.env,
4752
display_name=self.display_name,
4853
language=self.language,
54+
interrupt_mode=self.interrupt_mode,
4955
metadata=self.metadata,
5056
)
5157

@@ -227,7 +233,7 @@ def get_all_specs(self):
227233

228234
def remove_kernel_spec(self, name):
229235
"""Remove a kernel spec directory by name.
230-
236+
231237
Returns the path that was deleted.
232238
"""
233239
save_native = self.ensure_native_kernel
@@ -263,7 +269,7 @@ def install_kernel_spec(self, source_dir, kernel_name=None, user=False,
263269
If ``user`` is False, it will attempt to install into the systemwide
264270
kernel registry. If the process does not have appropriate permissions,
265271
an :exc:`OSError` will be raised.
266-
272+
267273
If ``prefix`` is given, the kernelspec will be installed to
268274
PREFIX/share/jupyter/kernels/KERNEL_NAME. This can be sys.prefix
269275
for installation inside virtual or conda envs.
@@ -284,16 +290,16 @@ def install_kernel_spec(self, source_dir, kernel_name=None, user=False,
284290
DeprecationWarning,
285291
stacklevel=2,
286292
)
287-
293+
288294
destination = self._get_destination_dir(kernel_name, user=user, prefix=prefix)
289295
self.log.debug('Installing kernelspec in %s', destination)
290-
296+
291297
kernel_dir = os.path.dirname(destination)
292298
if kernel_dir not in self.kernel_dirs:
293299
self.log.warning("Installing to %s, which is not in %s. The kernelspec may not be found.",
294300
kernel_dir, self.kernel_dirs,
295301
)
296-
302+
297303
if os.path.isdir(destination):
298304
self.log.info('Removing existing kernelspec in %s', destination)
299305
shutil.rmtree(destination)

jupyter_client/manager.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def start_kernel(self, **kw):
254254
# If kernel_cmd has been set manually, don't refer to a kernel spec
255255
# Environment variables from kernel spec are added to os.environ
256256
env.update(self.kernel_spec.env or {})
257-
257+
258258
# launch the kernel subprocess
259259
self.log.debug("Starting kernel: %s", kernel_cmd)
260260
self.kernel = self._launch_kernel(kernel_cmd, env=env,
@@ -411,11 +411,18 @@ def interrupt_kernel(self):
411411
platforms.
412412
"""
413413
if self.has_kernel:
414-
if sys.platform == 'win32':
415-
from .win_interrupt import send_interrupt
416-
send_interrupt(self.kernel.win32_interrupt_event)
417-
else:
418-
self.signal_kernel(signal.SIGINT)
414+
interrupt_mode = self.kernel_spec.interrupt_mode
415+
if interrupt_mode == 'signal':
416+
if sys.platform == 'win32':
417+
from .win_interrupt import send_interrupt
418+
send_interrupt(self.kernel.win32_interrupt_event)
419+
else:
420+
self.signal_kernel(signal.SIGINT)
421+
422+
elif interrupt_mode == 'message':
423+
msg = self.session.msg("interrupt_request", content={})
424+
self._connect_control_socket()
425+
self.session.send(self._control_socket, msg)
419426
else:
420427
raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
421428

0 commit comments

Comments
 (0)