diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index 41ffbcc39..667df15d9 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -460,6 +460,10 @@ def close(self): self.log.debug("Closing iopub channel") self.iopub_thread.stop() self.iopub_thread.close() + elif self.iopub_socket is not None and not self.iopub_socket.closed: + # The iopub port could not be bound, so no IOPub thread owns the socket. + # Left open, it would block context.term() below. + self.iopub_socket.close() if self.control_thread and self.control_thread.is_alive(): self.log.debug("Closing control thread") self.control_thread.stop() diff --git a/tests/test_kernelapp.py b/tests/test_kernelapp.py index 44a06886a..21fbc3ad9 100644 --- a/tests/test_kernelapp.py +++ b/tests/test_kernelapp.py @@ -1,10 +1,13 @@ +import atexit import json import os +import socket import threading import time from unittest.mock import patch import pytest +import zmq from jupyter_client import KernelManager from jupyter_client.kernelspec import KernelSpecManager from jupyter_core.paths import secure_write @@ -30,6 +33,21 @@ def test_init_ipc_socket(): app.close() +def test_close_after_iopub_port_in_use(): + app = IPKernelApp(transport="tcp") + with socket.socket() as taken: + taken.bind((app.ip, 0)) + taken.listen(1) + app.iopub_port = taken.getsockname()[1] + with pytest.raises(zmq.ZMQError): + app.init_sockets() + atexit.unregister(app.close) + closing = threading.Thread(target=app.close, daemon=True) + closing.start() + closing.join(timeout=10) + assert not closing.is_alive(), "close() is still waiting for the zmq context to terminate" + + def test_blackhole(): app = IPKernelApp() app.no_stderr = True