Skip to content

Commit d6e6bc9

Browse files
authored
Merge pull request #279 from rolweber/port-already-in-use
Port already in use
2 parents 4b5586f + ed7f9d0 commit d6e6bc9

File tree

4 files changed

+87
-7
lines changed

4 files changed

+87
-7
lines changed

jupyter_client/connect.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ def _ip_changed(self, name, old, new):
333333
control_port = Integer(0, config=True,
334334
help="set the control (ROUTER) port [default: random]")
335335

336+
# names of the ports with random assignment
337+
_random_port_names = None
338+
336339
@property
337340
def ports(self):
338341
return [ getattr(self, name) for name in port_names ]
@@ -417,6 +420,37 @@ def cleanup_ipc_files(self):
417420
except (IOError, OSError):
418421
pass
419422

423+
def _record_random_port_names(self):
424+
"""Records which of the ports are randomly assigned.
425+
426+
Records on first invocation, if the transport is tcp.
427+
Does nothing on later invocations."""
428+
429+
if self.transport != 'tcp':
430+
return
431+
if self._random_port_names is not None:
432+
return
433+
434+
self._random_port_names = []
435+
for name in port_names:
436+
if getattr(self, name) <= 0:
437+
self._random_port_names.append(name)
438+
439+
def cleanup_random_ports(self):
440+
"""Forgets randomly assigned port numbers and cleans up the connection file.
441+
442+
Does nothing if no port numbers have been randomly assigned.
443+
In particular, does nothing unless the transport is tcp.
444+
"""
445+
446+
if not self._random_port_names:
447+
return
448+
449+
for name in self._random_port_names:
450+
setattr(self, name, 0)
451+
452+
self.cleanup_connection_file()
453+
420454
def write_connection_file(self):
421455
"""Write connection info to JSON dict in self.connection_file."""
422456
if self._connection_file_written and os.path.exists(self.connection_file):
@@ -431,6 +465,7 @@ def write_connection_file(self):
431465
kernel_name=self.kernel_name
432466
)
433467
# write_connection_file also sets default ports:
468+
self._record_random_port_names()
434469
for name in port_names:
435470
setattr(self, name, cfg[name])
436471

@@ -467,6 +502,7 @@ def load_connection_info(self, info):
467502
self.transport = info.get('transport', self.transport)
468503
self.ip = info.get('ip', self._ip_default())
469504

505+
self._record_random_port_names()
470506
for name in port_names:
471507
if getattr(self, name) == 0 and name in info:
472508
# not overridden by config or cl_args

jupyter_client/manager.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,9 @@ def shutdown_kernel(self, now=False, restart=False):
326326

327327
self.cleanup(connection_file=not restart)
328328

329-
def restart_kernel(self, now=False, **kw):
329+
def restart_kernel(self, now=False, newports=False, **kw):
330330
"""Restarts a kernel with the arguments that were used to launch it.
331331
332-
If the old kernel was launched with random ports, the same ports will be
333-
used for the new kernel. The same connection file is used again.
334-
335332
Parameters
336333
----------
337334
now : bool, optional
@@ -342,6 +339,14 @@ def restart_kernel(self, now=False, **kw):
342339
In all cases the kernel is restarted, the only difference is whether
343340
it is given a chance to perform a clean shutdown or not.
344341
342+
newports : bool, optional
343+
If the old kernel was launched with random ports, this flag decides
344+
whether the same ports and connection file will be used again.
345+
If False, the same ports and connection file are used. This is
346+
the default. If True, new random port numbers are chosen and a
347+
new connection file is written. It is still possible that the newly
348+
chosen random port numbers happen to be the same as the old ones.
349+
345350
`**kw` : optional
346351
Any options specified here will overwrite those used to launch the
347352
kernel.
@@ -353,6 +358,9 @@ def restart_kernel(self, now=False, **kw):
353358
# Stop currently running kernel.
354359
self.shutdown_kernel(now=now, restart=True)
355360

361+
if newports:
362+
self.cleanup_random_ports()
363+
356364
# Start new kernel.
357365
self._launch_args.update(kw)
358366
self.start_kernel(**self._launch_args)

jupyter_client/restarter.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ class KernelRestarter(LoggingConfigurable):
3434
restart_limit = Integer(5, config=True,
3535
help="""The number of consecutive autorestarts before the kernel is presumed dead."""
3636
)
37+
38+
random_ports_until_alive = Bool(True, config=True,
39+
help="""Whether to choose new random ports when restarting before the kernel is alive."""
40+
)
3741
_restarting = Bool(False)
3842
_restart_count = Integer(0)
43+
_initial_startup = Bool(True)
3944

4045
callbacks = Dict()
4146
def _callbacks_default(self):
@@ -98,14 +103,18 @@ def poll(self):
98103
self._restart_count = 0
99104
self.stop()
100105
else:
101-
self.log.info('KernelRestarter: restarting kernel (%i/%i)',
106+
newports = self.random_ports_until_alive and self._initial_startup
107+
self.log.info('KernelRestarter: restarting kernel (%i/%i), %s random ports',
102108
self._restart_count,
103-
self.restart_limit
109+
self.restart_limit,
110+
'new' if newports else 'keep'
104111
)
105112
self._fire_callbacks('restart')
106-
self.kernel_manager.restart_kernel(now=True)
113+
self.kernel_manager.restart_kernel(now=True, newports=newports)
107114
self._restarting = True
108115
else:
116+
if self._initial_startup:
117+
self._initial_startup = False
109118
if self._restarting:
110119
self.log.debug("KernelRestarter: restart apparently succeeded")
111120
self._restarting = False

jupyter_client/tests/test_connect.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def initialize(self, argv=[]):
2121
JupyterApp.initialize(self, argv=argv)
2222
self.init_connection_file()
2323

24+
class DummyConfigurable(connect.ConnectionFileMixin):
25+
def initialize(self):
26+
pass
27+
2428
sample_info = dict(ip='1.2.3.4', transport='ipc',
2529
shell_port=1, hb_port=2, iopub_port=3, stdin_port=4, control_port=5,
2630
key=b'abc123', signature_scheme='hmac-md5', kernel_name='python'
@@ -31,6 +35,7 @@ def initialize(self, argv=[]):
3135
key=b'abc123', signature_scheme='hmac-md5', kernel_name='test'
3236
)
3337

38+
3439
def test_write_connection_file():
3540
with TemporaryDirectory() as d:
3641
cf = os.path.join(d, 'kernel.json')
@@ -171,4 +176,26 @@ def test_find_connection_file_abspath():
171176
with open(cf, 'w') as f:
172177
f.write('{}')
173178
assert connect.find_connection_file(abs_cf, path=jupyter_runtime_dir()) == abs_cf
179+
os.remove(abs_cf)
180+
181+
182+
def test_mixin_record_random_ports():
183+
with TemporaryDirectory() as d:
184+
dc = DummyConfigurable(data_dir=d, kernel_name='via-tcp', transport='tcp')
185+
dc.write_connection_file()
186+
187+
assert dc._connection_file_written
188+
assert os.path.exists(dc.connection_file)
189+
assert dc._random_port_names == connect.port_names
174190

191+
192+
def test_mixin_cleanup_random_ports():
193+
with TemporaryDirectory() as d:
194+
dc = DummyConfigurable(data_dir=d, kernel_name='via-tcp', transport='tcp')
195+
dc.write_connection_file()
196+
filename = dc.connection_file
197+
dc.cleanup_random_ports()
198+
199+
assert not os.path.exists(filename)
200+
for name in dc._random_port_names:
201+
assert getattr(dc, name) == 0

0 commit comments

Comments
 (0)