##// END OF EJS Templates
Remove obsolete Windows workarounds, per @minrk's testing
Thomas Kluyver -
Show More
@@ -1,428 +1,420 b''
1 1 """Base class to manage a running kernel"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 # Standard library imports
9 9 import os
10 10 import re
11 11 import signal
12 12 import sys
13 13 import time
14 14 import warnings
15 15
16 16 import zmq
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import LoggingConfigurable
20 20 from IPython.utils.importstring import import_item
21 21 from IPython.utils.localinterfaces import is_local_ip, local_ips
22 22 from IPython.utils.path import get_ipython_dir
23 23 from IPython.utils.traitlets import (
24 24 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
25 25 )
26 26 from IPython.kernel import (
27 27 make_ipkernel_cmd,
28 28 launch_kernel,
29 29 kernelspec,
30 30 )
31 31 from .connect import ConnectionFileMixin
32 32 from .zmq.session import Session
33 33 from .managerabc import (
34 34 KernelManagerABC
35 35 )
36 36
37 37
38 38 class KernelManager(LoggingConfigurable, ConnectionFileMixin):
39 39 """Manages a single kernel in a subprocess on this host.
40 40
41 41 This version starts kernels with Popen.
42 42 """
43 43
44 44 # The PyZMQ Context to use for communication with the kernel.
45 45 context = Instance(zmq.Context)
46 46 def _context_default(self):
47 47 return zmq.Context.instance()
48 48
49 49 # The Session to use for communication with the kernel.
50 50 session = Instance(Session)
51 51 def _session_default(self):
52 52 return Session(parent=self)
53 53
54 54 # the class to create with our `client` method
55 55 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
56 56 client_factory = Type()
57 57 def _client_class_changed(self, name, old, new):
58 58 self.client_factory = import_item(str(new))
59 59
60 60 # The kernel process with which the KernelManager is communicating.
61 61 # generally a Popen instance
62 62 kernel = Any()
63 63
64 64 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
65 65
66 66 def _kernel_spec_manager_default(self):
67 67 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
68 68
69 69 kernel_name = Unicode('python')
70 70
71 71 kernel_spec = Instance(kernelspec.KernelSpec)
72 72
73 73 def _kernel_spec_default(self):
74 74 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
75 75
76 76 def _kernel_name_changed(self, name, old, new):
77 77 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
78 78 self.ipython_kernel = new in {'python', 'python2', 'python3'}
79 79
80 80 kernel_cmd = List(Unicode, config=True,
81 81 help="""DEPRECATED: Use kernel_name instead.
82 82
83 83 The Popen Command to launch the kernel.
84 84 Override this if you have a custom kernel.
85 85 If kernel_cmd is specified in a configuration file,
86 86 IPython does not pass any arguments to the kernel,
87 87 because it cannot make any assumptions about the
88 88 arguments that the kernel understands. In particular,
89 89 this means that the kernel does not receive the
90 90 option --debug if it given on the IPython command line.
91 91 """
92 92 )
93 93
94 94 def _kernel_cmd_changed(self, name, old, new):
95 95 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
96 96 "start different kernels.")
97 97 self.ipython_kernel = False
98 98
99 99 ipython_kernel = Bool(True)
100 100
101 101 ipython_dir = Unicode()
102 102 def _ipython_dir_default(self):
103 103 return get_ipython_dir()
104 104
105 105 # Protected traits
106 106 _launch_args = Any()
107 107 _control_socket = Any()
108 108
109 109 _restarter = Any()
110 110
111 111 autorestart = Bool(False, config=True,
112 112 help="""Should we autorestart the kernel if it dies."""
113 113 )
114 114
115 115 def __del__(self):
116 116 self._close_control_socket()
117 117 self.cleanup_connection_file()
118 118
119 119 #--------------------------------------------------------------------------
120 120 # Kernel restarter
121 121 #--------------------------------------------------------------------------
122 122
123 123 def start_restarter(self):
124 124 pass
125 125
126 126 def stop_restarter(self):
127 127 pass
128 128
129 129 def add_restart_callback(self, callback, event='restart'):
130 130 """register a callback to be called when a kernel is restarted"""
131 131 if self._restarter is None:
132 132 return
133 133 self._restarter.add_callback(callback, event)
134 134
135 135 def remove_restart_callback(self, callback, event='restart'):
136 136 """unregister a callback to be called when a kernel is restarted"""
137 137 if self._restarter is None:
138 138 return
139 139 self._restarter.remove_callback(callback, event)
140 140
141 141 #--------------------------------------------------------------------------
142 142 # create a Client connected to our Kernel
143 143 #--------------------------------------------------------------------------
144 144
145 145 def client(self, **kwargs):
146 146 """Create a client configured to connect to our kernel"""
147 147 if self.client_factory is None:
148 148 self.client_factory = import_item(self.client_class)
149 149
150 150 kw = {}
151 151 kw.update(self.get_connection_info())
152 152 kw.update(dict(
153 153 connection_file=self.connection_file,
154 154 session=self.session,
155 155 parent=self,
156 156 ))
157 157
158 158 # add kwargs last, for manual overrides
159 159 kw.update(kwargs)
160 160 return self.client_factory(**kw)
161 161
162 162 #--------------------------------------------------------------------------
163 163 # Kernel management
164 164 #--------------------------------------------------------------------------
165 165
166 166 def format_kernel_cmd(self, **kw):
167 167 """replace templated args (e.g. {connection_file})"""
168 168 if self.kernel_cmd:
169 169 cmd = self.kernel_cmd
170 170 elif self.kernel_name == 'python':
171 171 # The native kernel gets special handling
172 172 cmd = make_ipkernel_cmd(
173 173 'from IPython.kernel.zmq.kernelapp import main; main()',
174 174 **kw
175 175 )
176 176 else:
177 177 cmd = self.kernel_spec.argv
178 178
179 179 ns = dict(connection_file=self.connection_file)
180 180 ns.update(self._launch_args)
181 181
182 182 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
183 183 def from_ns(match):
184 184 """Get the key out of ns if it's there, otherwise no change."""
185 185 return ns.get(match.group(1), match.group())
186 186
187 187 return [ pat.sub(from_ns, arg) for arg in cmd ]
188 188
189 189 def _launch_kernel(self, kernel_cmd, **kw):
190 190 """actually launch the kernel
191 191
192 192 override in a subclass to launch kernel subprocesses differently
193 193 """
194 194 return launch_kernel(kernel_cmd, **kw)
195 195
196 196 # Control socket used for polite kernel shutdown
197 197
198 198 def _connect_control_socket(self):
199 199 if self._control_socket is None:
200 200 self._control_socket = self.connect_control()
201 201 self._control_socket.linger = 100
202 202
203 203 def _close_control_socket(self):
204 204 if self._control_socket is None:
205 205 return
206 206 self._control_socket.close()
207 207 self._control_socket = None
208 208
209 209 def start_kernel(self, **kw):
210 210 """Starts a kernel on this host in a separate process.
211 211
212 212 If random ports (port=0) are being used, this method must be called
213 213 before the channels are created.
214 214
215 215 Parameters
216 216 ----------
217 217 **kw : optional
218 218 keyword arguments that are passed down to build the kernel_cmd
219 219 and launching the kernel (e.g. Popen kwargs).
220 220 """
221 221 if self.transport == 'tcp' and not is_local_ip(self.ip):
222 222 raise RuntimeError("Can only launch a kernel on a local interface. "
223 223 "Make sure that the '*_address' attributes are "
224 224 "configured properly. "
225 225 "Currently valid addresses are: %s" % local_ips()
226 226 )
227 227
228 228 # write connection file / get default ports
229 229 self.write_connection_file()
230 230
231 231 # save kwargs for use in restart
232 232 self._launch_args = kw.copy()
233 233 # build the Popen cmd
234 234 kernel_cmd = self.format_kernel_cmd(**kw)
235 235 if self.kernel_cmd:
236 236 # If kernel_cmd has been set manually, don't refer to a kernel spec
237 237 env = os.environ
238 238 else:
239 239 # Environment variables from kernel spec are added to os.environ
240 240 env = os.environ.copy()
241 241 env.update(self.kernel_spec.env or {})
242 242 # launch the kernel subprocess
243 243 self.kernel = self._launch_kernel(kernel_cmd, env=env,
244 244 ipython_kernel=self.ipython_kernel,
245 245 **kw)
246 246 self.start_restarter()
247 247 self._connect_control_socket()
248 248
249 249 def request_shutdown(self, restart=False):
250 250 """Send a shutdown request via control channel
251 251
252 252 On Windows, this just kills kernels instead, because the shutdown
253 253 messages don't work.
254 254 """
255 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
256 if sys.platform == 'win32' and self.has_kernel:
257 return self._kill_kernel()
258 255 content = dict(restart=restart)
259 256 msg = self.session.msg("shutdown_request", content=content)
260 257 self.session.send(self._control_socket, msg)
261 258
262 259 def finish_shutdown(self, waittime=1, pollinterval=0.1):
263 260 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
264 261
265 262 This does not send shutdown requests - use :meth:`request_shutdown`
266 263 first.
267 264 """
268 265 for i in range(int(waittime/pollinterval)):
269 266 if self.is_alive():
270 267 time.sleep(pollinterval)
271 268 else:
272 269 break
273 270 else:
274 271 # OK, we've waited long enough.
275 272 if self.has_kernel:
276 273 self._kill_kernel()
277 274
278 275 def cleanup(self, connection_file=True):
279 276 """Clean up resources when the kernel is shut down"""
280 277 if connection_file:
281 278 self.cleanup_connection_file()
282 279
283 280 self.cleanup_ipc_files()
284 281 self._close_control_socket()
285 282
286 283 def shutdown_kernel(self, now=False, restart=False):
287 284 """Attempts to the stop the kernel process cleanly.
288 285
289 286 This attempts to shutdown the kernels cleanly by:
290 287
291 288 1. Sending it a shutdown message over the shell channel.
292 289 2. If that fails, the kernel is shutdown forcibly by sending it
293 290 a signal.
294 291
295 292 Parameters
296 293 ----------
297 294 now : bool
298 295 Should the kernel be forcible killed *now*. This skips the
299 296 first, nice shutdown attempt.
300 297 restart: bool
301 298 Will this kernel be restarted after it is shutdown. When this
302 299 is True, connection files will not be cleaned up.
303 300 """
304 301 # Stop monitoring for restarting while we shutdown.
305 302 self.stop_restarter()
306 303
307 304 if now:
308 305 self._kill_kernel()
309 306 else:
310 307 self.request_shutdown(restart=restart)
311 308 # Don't send any additional kernel kill messages immediately, to give
312 309 # the kernel a chance to properly execute shutdown actions. Wait for at
313 310 # most 1s, checking every 0.1s.
314 311 self.finish_shutdown()
315 312
316 313 self.cleanup(connection_file=not restart)
317 314
318 315 def restart_kernel(self, now=False, **kw):
319 316 """Restarts a kernel with the arguments that were used to launch it.
320 317
321 318 If the old kernel was launched with random ports, the same ports will be
322 319 used for the new kernel. The same connection file is used again.
323 320
324 321 Parameters
325 322 ----------
326 323 now : bool, optional
327 324 If True, the kernel is forcefully restarted *immediately*, without
328 325 having a chance to do any cleanup action. Otherwise the kernel is
329 326 given 1s to clean up before a forceful restart is issued.
330 327
331 328 In all cases the kernel is restarted, the only difference is whether
332 329 it is given a chance to perform a clean shutdown or not.
333 330
334 331 **kw : optional
335 332 Any options specified here will overwrite those used to launch the
336 333 kernel.
337 334 """
338 335 if self._launch_args is None:
339 336 raise RuntimeError("Cannot restart the kernel. "
340 337 "No previous call to 'start_kernel'.")
341 338 else:
342 339 # Stop currently running kernel.
343 340 self.shutdown_kernel(now=now, restart=True)
344 341
345 342 # Start new kernel.
346 343 self._launch_args.update(kw)
347 344 self.start_kernel(**self._launch_args)
348 345
349 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
350 # unless there is some delay here.
351 if sys.platform == 'win32':
352 time.sleep(0.2)
353
354 346 @property
355 347 def has_kernel(self):
356 348 """Has a kernel been started that we are managing."""
357 349 return self.kernel is not None
358 350
359 351 def _kill_kernel(self):
360 352 """Kill the running kernel.
361 353
362 354 This is a private method, callers should use shutdown_kernel(now=True).
363 355 """
364 356 if self.has_kernel:
365 357
366 358 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
367 359 # TerminateProcess() on Win32).
368 360 try:
369 361 self.kernel.kill()
370 362 except OSError as e:
371 363 # In Windows, we will get an Access Denied error if the process
372 364 # has already terminated. Ignore it.
373 365 if sys.platform == 'win32':
374 366 if e.winerror != 5:
375 367 raise
376 368 # On Unix, we may get an ESRCH error if the process has already
377 369 # terminated. Ignore it.
378 370 else:
379 371 from errno import ESRCH
380 372 if e.errno != ESRCH:
381 373 raise
382 374
383 375 # Block until the kernel terminates.
384 376 self.kernel.wait()
385 377 self.kernel = None
386 378 else:
387 379 raise RuntimeError("Cannot kill kernel. No kernel is running!")
388 380
389 381 def interrupt_kernel(self):
390 382 """Interrupts the kernel by sending it a signal.
391 383
392 384 Unlike ``signal_kernel``, this operation is well supported on all
393 385 platforms.
394 386 """
395 387 if self.has_kernel:
396 388 if sys.platform == 'win32':
397 389 from .zmq.parentpoller import ParentPollerWindows as Poller
398 390 Poller.send_interrupt(self.kernel.win32_interrupt_event)
399 391 else:
400 392 self.kernel.send_signal(signal.SIGINT)
401 393 else:
402 394 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
403 395
404 396 def signal_kernel(self, signum):
405 397 """Sends a signal to the kernel.
406 398
407 399 Note that since only SIGTERM is supported on Windows, this function is
408 400 only useful on Unix systems.
409 401 """
410 402 if self.has_kernel:
411 403 self.kernel.send_signal(signum)
412 404 else:
413 405 raise RuntimeError("Cannot signal kernel. No kernel is running!")
414 406
415 407 def is_alive(self):
416 408 """Is the kernel process still running?"""
417 409 if self.has_kernel:
418 410 if self.kernel.poll() is None:
419 411 return True
420 412 else:
421 413 return False
422 414 else:
423 415 # we don't have a kernel
424 416 return False
425 417
426 418
427 419 KernelManagerABC.register(KernelManager)
428 420
General Comments 0
You need to be logged in to leave comments. Login now