##// END OF EJS Templates
Merge pull request #7099 from minrk/parent-env...
Thomas Kluyver -
r19268:9c06d407 merge
parent child Browse files
Show More
@@ -1,258 +1,226 b''
1 1 """Utilities for launching kernels
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import sys
9 9 from subprocess import Popen, PIPE
10 10
11 11 from IPython.utils.encoding import getdefaultencoding
12 12 from IPython.utils.py3compat import cast_bytes_py2
13 13
14 14
15 15 def swallow_argv(argv, aliases=None, flags=None):
16 16 """strip frontend-specific aliases and flags from an argument list
17 17
18 18 For use primarily in frontend apps that want to pass a subset of command-line
19 19 arguments through to a subprocess, where frontend-specific flags and aliases
20 20 should be removed from the list.
21 21
22 22 Parameters
23 23 ----------
24 24
25 25 argv : list(str)
26 26 The starting argv, to be filtered
27 27 aliases : container of aliases (dict, list, set, etc.)
28 28 The frontend-specific aliases to be removed
29 29 flags : container of flags (dict, list, set, etc.)
30 30 The frontend-specific flags to be removed
31 31
32 32 Returns
33 33 -------
34 34
35 35 argv : list(str)
36 36 The argv list, excluding flags and aliases that have been stripped
37 37 """
38 38
39 39 if aliases is None:
40 40 aliases = set()
41 41 if flags is None:
42 42 flags = set()
43 43
44 44 stripped = list(argv) # copy
45 45
46 46 swallow_next = False
47 47 was_flag = False
48 48 for a in argv:
49 49 if a == '--':
50 50 break
51 51 if swallow_next:
52 52 swallow_next = False
53 53 # last arg was an alias, remove the next one
54 54 # *unless* the last alias has a no-arg flag version, in which
55 55 # case, don't swallow the next arg if it's also a flag:
56 56 if not (was_flag and a.startswith('-')):
57 57 stripped.remove(a)
58 58 continue
59 59 if a.startswith('-'):
60 60 split = a.lstrip('-').split('=')
61 61 name = split[0]
62 62 # we use startswith because argparse accepts any arg to be specified
63 63 # by any leading section, as long as it is unique,
64 64 # so `--no-br` means `--no-browser` in the notebook, etc.
65 65 if any(alias.startswith(name) for alias in aliases):
66 66 stripped.remove(a)
67 67 if len(split) == 1:
68 68 # alias passed with arg via space
69 69 swallow_next = True
70 70 # could have been a flag that matches an alias, e.g. `existing`
71 71 # in which case, we might not swallow the next arg
72 72 was_flag = name in flags
73 73 elif len(split) == 1 and any(flag.startswith(name) for flag in flags):
74 74 # strip flag, but don't swallow next, as flags don't take args
75 75 stripped.remove(a)
76 76
77 77 # return shortened list
78 78 return stripped
79 79
80 80
81 81 def make_ipkernel_cmd(mod='IPython.kernel', executable=None, extra_arguments=[], **kw):
82 82 """Build Popen command list for launching an IPython kernel.
83 83
84 84 Parameters
85 85 ----------
86 86 mod : str, optional (default 'IPython.kernel')
87 87 A string of an IPython module whose __main__ starts an IPython kernel
88 88
89 89 executable : str, optional (default sys.executable)
90 90 The Python executable to use for the kernel process.
91 91
92 92 extra_arguments : list, optional
93 93 A list of extra arguments to pass when executing the launch code.
94 94
95 95 Returns
96 96 -------
97 97
98 98 A Popen command list
99 99 """
100 100 if executable is None:
101 101 executable = sys.executable
102 102 arguments = [ executable, '-m', mod, '-f', '{connection_file}' ]
103 103 arguments.extend(extra_arguments)
104
105 if sys.platform == 'win32':
106
107 # If the kernel is running on pythonw and stdout/stderr are not been
108 # re-directed, it will crash when more than 4KB of data is written to
109 # stdout or stderr. This is a bug that has been with Python for a very
110 # long time; see http://bugs.python.org/issue706263.
111 # A cleaner solution to this problem would be to pass os.devnull to
112 # Popen directly. Unfortunately, that does not work.
113 if executable.endswith('pythonw.exe'):
114 arguments.append('--no-stdout')
115 arguments.append('--no-stderr')
116
104
117 105 return arguments
118 106
119 107
120 108 def launch_kernel(cmd, stdin=None, stdout=None, stderr=None, env=None,
121 109 independent=False,
122 cwd=None, ipython_kernel=True,
110 cwd=None,
123 111 **kw
124 112 ):
125 113 """ Launches a localhost kernel, binding to the specified ports.
126 114
127 115 Parameters
128 116 ----------
129 117 cmd : Popen list,
130 118 A string of Python code that imports and executes a kernel entry point.
131 119
132 120 stdin, stdout, stderr : optional (default None)
133 121 Standards streams, as defined in subprocess.Popen.
134 122
135 123 independent : bool, optional (default False)
136 124 If set, the kernel process is guaranteed to survive if this process
137 125 dies. If not set, an effort is made to ensure that the kernel is killed
138 126 when this process dies. Note that in this case it is still good practice
139 127 to kill kernels manually before exiting.
140 128
141 129 cwd : path, optional
142 130 The working dir of the kernel process (default: cwd of this process).
143 131
144 ipython_kernel : bool, optional
145 Whether the kernel is an official IPython one,
146 and should get a bit of special treatment.
147
148 132 Returns
149 133 -------
150 134
151 135 Popen instance for the kernel subprocess
152 136 """
153 137
154 138 # Popen will fail (sometimes with a deadlock) if stdin, stdout, and stderr
155 139 # are invalid. Unfortunately, there is in general no way to detect whether
156 140 # they are valid. The following two blocks redirect them to (temporary)
157 141 # pipes in certain important cases.
158 142
159 143 # If this process has been backgrounded, our stdin is invalid. Since there
160 144 # is no compelling reason for the kernel to inherit our stdin anyway, we'll
161 145 # place this one safe and always redirect.
162 146 redirect_in = True
163 147 _stdin = PIPE if stdin is None else stdin
164 148
165 149 # If this process in running on pythonw, we know that stdin, stdout, and
166 150 # stderr are all invalid.
167 151 redirect_out = sys.executable.endswith('pythonw.exe')
168 152 if redirect_out:
169 _stdout = PIPE if stdout is None else stdout
170 _stderr = PIPE if stderr is None else stderr
153 blackhole = open(os.devnull, 'w')
154 _stdout = blackhole if stdout is None else stdout
155 _stderr = blackhole if stderr is None else stderr
171 156 else:
172 157 _stdout, _stderr = stdout, stderr
173 158
174 159 env = env if (env is not None) else os.environ.copy()
175 160
176 161 encoding = getdefaultencoding(prefer_stream=False)
162 kwargs = dict(
163 stdin=_stdin,
164 stdout=_stdout,
165 stderr=_stderr,
166 cwd=cwd,
167 env=env,
168 )
177 169
178 170 # Spawn a kernel.
179 171 if sys.platform == 'win32':
180 172 # Popen on Python 2 on Windows cannot handle unicode args or cwd
181 173 cmd = [ cast_bytes_py2(c, encoding) for c in cmd ]
182 174 if cwd:
183 175 cwd = cast_bytes_py2(cwd, sys.getfilesystemencoding() or 'ascii')
176 kwargs['cwd'] = cwd
184 177
185 178 from IPython.kernel.zmq.parentpoller import ParentPollerWindows
186 # Create a Win32 event for interrupting the kernel.
179 # Create a Win32 event for interrupting the kernel
180 # and store it in an environment variable.
187 181 interrupt_event = ParentPollerWindows.create_interrupt_event()
188 # Store this in an environment variable for third party kernels, but at
189 # present, our own kernel expects this as a command line argument.
190 env["IPY_INTERRUPT_EVENT"] = str(interrupt_event)
191 if ipython_kernel:
192 cmd += [ '--interrupt=%i' % interrupt_event ]
193
194 # If the kernel is running on pythonw and stdout/stderr are not been
195 # re-directed, it will crash when more than 4KB of data is written to
196 # stdout or stderr. This is a bug that has been with Python for a very
197 # long time; see http://bugs.python.org/issue706263.
198 # A cleaner solution to this problem would be to pass os.devnull to
199 # Popen directly. Unfortunately, that does not work.
200 if cmd[0].endswith('pythonw.exe'):
201 if stdout is None:
202 cmd.append('--no-stdout')
203 if stderr is None:
204 cmd.append('--no-stderr')
205
206 # Launch the kernel process.
182 env["JPY_INTERRUPT_EVENT"] = str(interrupt_event)
183 # deprecated old env name:
184 env["IPY_INTERRUPT_EVENT"] = env["JPY_INTERRUPT_EVENT"]
185
186 try:
187 from _winapi import DuplicateHandle, GetCurrentProcess, \
188 DUPLICATE_SAME_ACCESS, CREATE_NEW_PROCESS_GROUP
189 except:
190 from _subprocess import DuplicateHandle, GetCurrentProcess, \
191 DUPLICATE_SAME_ACCESS, CREATE_NEW_PROCESS_GROUP
192 # Launch the kernel process
207 193 if independent:
208 proc = Popen(cmd,
209 creationflags=512, # CREATE_NEW_PROCESS_GROUP
210 stdin=_stdin, stdout=_stdout, stderr=_stderr, env=env)
194 kwargs['creationflags'] = CREATE_NEW_PROCESS_GROUP
211 195 else:
212 if ipython_kernel:
213 try:
214 from _winapi import DuplicateHandle, GetCurrentProcess, \
215 DUPLICATE_SAME_ACCESS
216 except:
217 from _subprocess import DuplicateHandle, GetCurrentProcess, \
218 DUPLICATE_SAME_ACCESS
219 pid = GetCurrentProcess()
220 handle = DuplicateHandle(pid, pid, pid, 0,
221 True, # Inheritable by new processes.
222 DUPLICATE_SAME_ACCESS)
223 cmd +=[ '--parent=%i' % handle ]
224
225
226 proc = Popen(cmd,
227 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
196 pid = GetCurrentProcess()
197 handle = DuplicateHandle(pid, pid, pid, 0,
198 True, # Inheritable by new processes.
199 DUPLICATE_SAME_ACCESS)
200 env['JPY_PARENT_PID'] = str(handle)
201
202 proc = Popen(cmd, **kwargs)
228 203
229 204 # Attach the interrupt event to the Popen objet so it can be used later.
230 205 proc.win32_interrupt_event = interrupt_event
231 206
232 207 else:
233 208 if independent:
234 proc = Popen(cmd, preexec_fn=lambda: os.setsid(),
235 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
209 kwargs['preexec_fn'] = lambda: os.setsid()
236 210 else:
237 if ipython_kernel:
238 cmd += ['--parent=1']
239 proc = Popen(cmd,
240 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
211 env['JPY_PARENT_PID'] = str(os.getpid())
212
213 proc = Popen(cmd, **kwargs)
241 214
242 215 # Clean up pipes created to work around Popen bug.
243 216 if redirect_in:
244 217 if stdin is None:
245 218 proc.stdin.close()
246 if redirect_out:
247 if stdout is None:
248 proc.stdout.close()
249 if stderr is None:
250 proc.stderr.close()
251 219
252 220 return proc
253 221
254 222 __all__ = [
255 223 'swallow_argv',
256 224 'make_ipkernel_cmd',
257 225 'launch_kernel',
258 226 ]
@@ -1,452 +1,451 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 from contextlib import contextmanager
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 try:
16 16 from queue import Empty # Py 3
17 17 except ImportError:
18 18 from Queue import Empty # Py 2
19 19
20 20 import zmq
21 21
22 22 from IPython.utils.importstring import import_item
23 23 from IPython.utils.localinterfaces import is_local_ip, local_ips
24 24 from IPython.utils.path import get_ipython_dir
25 25 from IPython.utils.traitlets import (
26 26 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
27 27 )
28 28 from IPython.kernel import (
29 29 launch_kernel,
30 30 kernelspec,
31 31 )
32 32 from .connect import ConnectionFileMixin
33 33 from .zmq.session import Session
34 34 from .managerabc import (
35 35 KernelManagerABC
36 36 )
37 37
38 38
39 39 class KernelManager(ConnectionFileMixin):
40 40 """Manages a single kernel in a subprocess on this host.
41 41
42 42 This version starts kernels with Popen.
43 43 """
44 44
45 45 # The PyZMQ Context to use for communication with the kernel.
46 46 context = Instance(zmq.Context)
47 47 def _context_default(self):
48 48 return zmq.Context.instance()
49 49
50 50 # the class to create with our `client` method
51 51 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
52 52 client_factory = Type()
53 53 def _client_class_changed(self, name, old, new):
54 54 self.client_factory = import_item(str(new))
55 55
56 56 # The kernel process with which the KernelManager is communicating.
57 57 # generally a Popen instance
58 58 kernel = Any()
59 59
60 60 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
61 61
62 62 def _kernel_spec_manager_default(self):
63 63 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
64 64
65 65 kernel_name = Unicode(kernelspec.NATIVE_KERNEL_NAME)
66 66
67 67 kernel_spec = Instance(kernelspec.KernelSpec)
68 68
69 69 def _kernel_spec_default(self):
70 70 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
71 71
72 72 def _kernel_name_changed(self, name, old, new):
73 73 if new == 'python':
74 74 self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
75 75 # This triggered another run of this function, so we can exit now
76 76 return
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, extra_arguments=None):
167 167 """replace templated args (e.g. {connection_file})"""
168 168 extra_arguments = extra_arguments or []
169 169 if self.kernel_cmd:
170 170 cmd = self.kernel_cmd + extra_arguments
171 171 else:
172 172 cmd = self.kernel_spec.argv + extra_arguments
173 173
174 174 ns = dict(connection_file=self.connection_file)
175 175 ns.update(self._launch_args)
176 176
177 177 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
178 178 def from_ns(match):
179 179 """Get the key out of ns if it's there, otherwise no change."""
180 180 return ns.get(match.group(1), match.group())
181 181
182 182 return [ pat.sub(from_ns, arg) for arg in cmd ]
183 183
184 184 def _launch_kernel(self, kernel_cmd, **kw):
185 185 """actually launch the kernel
186 186
187 187 override in a subclass to launch kernel subprocesses differently
188 188 """
189 189 return launch_kernel(kernel_cmd, **kw)
190 190
191 191 # Control socket used for polite kernel shutdown
192 192
193 193 def _connect_control_socket(self):
194 194 if self._control_socket is None:
195 195 self._control_socket = self.connect_control()
196 196 self._control_socket.linger = 100
197 197
198 198 def _close_control_socket(self):
199 199 if self._control_socket is None:
200 200 return
201 201 self._control_socket.close()
202 202 self._control_socket = None
203 203
204 204 def start_kernel(self, **kw):
205 205 """Starts a kernel on this host in a separate process.
206 206
207 207 If random ports (port=0) are being used, this method must be called
208 208 before the channels are created.
209 209
210 210 Parameters
211 211 ----------
212 212 **kw : optional
213 213 keyword arguments that are passed down to build the kernel_cmd
214 214 and launching the kernel (e.g. Popen kwargs).
215 215 """
216 216 if self.transport == 'tcp' and not is_local_ip(self.ip):
217 217 raise RuntimeError("Can only launch a kernel on a local interface. "
218 218 "Make sure that the '*_address' attributes are "
219 219 "configured properly. "
220 220 "Currently valid addresses are: %s" % local_ips()
221 221 )
222 222
223 223 # write connection file / get default ports
224 224 self.write_connection_file()
225 225
226 226 # save kwargs for use in restart
227 227 self._launch_args = kw.copy()
228 228 # build the Popen cmd
229 229 extra_arguments = kw.pop('extra_arguments', [])
230 230 kernel_cmd = self.format_kernel_cmd(extra_arguments=extra_arguments)
231 231 if self.kernel_cmd:
232 232 # If kernel_cmd has been set manually, don't refer to a kernel spec
233 233 env = os.environ
234 234 else:
235 235 # Environment variables from kernel spec are added to os.environ
236 236 env = os.environ.copy()
237 237 env.update(self.kernel_spec.env or {})
238 238 # launch the kernel subprocess
239 239 self.kernel = self._launch_kernel(kernel_cmd, env=env,
240 ipython_kernel=self.ipython_kernel,
241 240 **kw)
242 241 self.start_restarter()
243 242 self._connect_control_socket()
244 243
245 244 def request_shutdown(self, restart=False):
246 245 """Send a shutdown request via control channel
247 246
248 247 On Windows, this just kills kernels instead, because the shutdown
249 248 messages don't work.
250 249 """
251 250 content = dict(restart=restart)
252 251 msg = self.session.msg("shutdown_request", content=content)
253 252 self.session.send(self._control_socket, msg)
254 253
255 254 def finish_shutdown(self, waittime=1, pollinterval=0.1):
256 255 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
257 256
258 257 This does not send shutdown requests - use :meth:`request_shutdown`
259 258 first.
260 259 """
261 260 for i in range(int(waittime/pollinterval)):
262 261 if self.is_alive():
263 262 time.sleep(pollinterval)
264 263 else:
265 264 break
266 265 else:
267 266 # OK, we've waited long enough.
268 267 if self.has_kernel:
269 268 self._kill_kernel()
270 269
271 270 def cleanup(self, connection_file=True):
272 271 """Clean up resources when the kernel is shut down"""
273 272 if connection_file:
274 273 self.cleanup_connection_file()
275 274
276 275 self.cleanup_ipc_files()
277 276 self._close_control_socket()
278 277
279 278 def shutdown_kernel(self, now=False, restart=False):
280 279 """Attempts to the stop the kernel process cleanly.
281 280
282 281 This attempts to shutdown the kernels cleanly by:
283 282
284 283 1. Sending it a shutdown message over the shell channel.
285 284 2. If that fails, the kernel is shutdown forcibly by sending it
286 285 a signal.
287 286
288 287 Parameters
289 288 ----------
290 289 now : bool
291 290 Should the kernel be forcible killed *now*. This skips the
292 291 first, nice shutdown attempt.
293 292 restart: bool
294 293 Will this kernel be restarted after it is shutdown. When this
295 294 is True, connection files will not be cleaned up.
296 295 """
297 296 # Stop monitoring for restarting while we shutdown.
298 297 self.stop_restarter()
299 298
300 299 if now:
301 300 self._kill_kernel()
302 301 else:
303 302 self.request_shutdown(restart=restart)
304 303 # Don't send any additional kernel kill messages immediately, to give
305 304 # the kernel a chance to properly execute shutdown actions. Wait for at
306 305 # most 1s, checking every 0.1s.
307 306 self.finish_shutdown()
308 307
309 308 self.cleanup(connection_file=not restart)
310 309
311 310 def restart_kernel(self, now=False, **kw):
312 311 """Restarts a kernel with the arguments that were used to launch it.
313 312
314 313 If the old kernel was launched with random ports, the same ports will be
315 314 used for the new kernel. The same connection file is used again.
316 315
317 316 Parameters
318 317 ----------
319 318 now : bool, optional
320 319 If True, the kernel is forcefully restarted *immediately*, without
321 320 having a chance to do any cleanup action. Otherwise the kernel is
322 321 given 1s to clean up before a forceful restart is issued.
323 322
324 323 In all cases the kernel is restarted, the only difference is whether
325 324 it is given a chance to perform a clean shutdown or not.
326 325
327 326 **kw : optional
328 327 Any options specified here will overwrite those used to launch the
329 328 kernel.
330 329 """
331 330 if self._launch_args is None:
332 331 raise RuntimeError("Cannot restart the kernel. "
333 332 "No previous call to 'start_kernel'.")
334 333 else:
335 334 # Stop currently running kernel.
336 335 self.shutdown_kernel(now=now, restart=True)
337 336
338 337 # Start new kernel.
339 338 self._launch_args.update(kw)
340 339 self.start_kernel(**self._launch_args)
341 340
342 341 @property
343 342 def has_kernel(self):
344 343 """Has a kernel been started that we are managing."""
345 344 return self.kernel is not None
346 345
347 346 def _kill_kernel(self):
348 347 """Kill the running kernel.
349 348
350 349 This is a private method, callers should use shutdown_kernel(now=True).
351 350 """
352 351 if self.has_kernel:
353 352
354 353 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
355 354 # TerminateProcess() on Win32).
356 355 try:
357 356 self.kernel.kill()
358 357 except OSError as e:
359 358 # In Windows, we will get an Access Denied error if the process
360 359 # has already terminated. Ignore it.
361 360 if sys.platform == 'win32':
362 361 if e.winerror != 5:
363 362 raise
364 363 # On Unix, we may get an ESRCH error if the process has already
365 364 # terminated. Ignore it.
366 365 else:
367 366 from errno import ESRCH
368 367 if e.errno != ESRCH:
369 368 raise
370 369
371 370 # Block until the kernel terminates.
372 371 self.kernel.wait()
373 372 self.kernel = None
374 373 else:
375 374 raise RuntimeError("Cannot kill kernel. No kernel is running!")
376 375
377 376 def interrupt_kernel(self):
378 377 """Interrupts the kernel by sending it a signal.
379 378
380 379 Unlike ``signal_kernel``, this operation is well supported on all
381 380 platforms.
382 381 """
383 382 if self.has_kernel:
384 383 if sys.platform == 'win32':
385 384 from .zmq.parentpoller import ParentPollerWindows as Poller
386 385 Poller.send_interrupt(self.kernel.win32_interrupt_event)
387 386 else:
388 387 self.kernel.send_signal(signal.SIGINT)
389 388 else:
390 389 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
391 390
392 391 def signal_kernel(self, signum):
393 392 """Sends a signal to the kernel.
394 393
395 394 Note that since only SIGTERM is supported on Windows, this function is
396 395 only useful on Unix systems.
397 396 """
398 397 if self.has_kernel:
399 398 self.kernel.send_signal(signum)
400 399 else:
401 400 raise RuntimeError("Cannot signal kernel. No kernel is running!")
402 401
403 402 def is_alive(self):
404 403 """Is the kernel process still running?"""
405 404 if self.has_kernel:
406 405 if self.kernel.poll() is None:
407 406 return True
408 407 else:
409 408 return False
410 409 else:
411 410 # we don't have a kernel
412 411 return False
413 412
414 413
415 414 KernelManagerABC.register(KernelManager)
416 415
417 416
418 417 def start_new_kernel(startup_timeout=60, kernel_name='python', **kwargs):
419 418 """Start a new kernel, and return its Manager and Client"""
420 419 km = KernelManager(kernel_name=kernel_name)
421 420 km.start_kernel(**kwargs)
422 421 kc = km.client()
423 422 kc.start_channels()
424 423
425 424 kc.kernel_info()
426 425 kc.get_shell_msg(block=True, timeout=startup_timeout)
427 426
428 427 # Flush channels
429 428 for channel in (kc.shell_channel, kc.iopub_channel):
430 429 while True:
431 430 try:
432 431 channel.get_msg(block=True, timeout=0.1)
433 432 except Empty:
434 433 break
435 434 return km, kc
436 435
437 436 @contextmanager
438 437 def run_kernel(**kwargs):
439 438 """Context manager to create a kernel in a subprocess.
440 439
441 440 The kernel is shut down when the context exits.
442 441
443 442 Returns
444 443 -------
445 444 kernel_client: connected KernelClient instance
446 445 """
447 446 km, kc = start_new_kernel(**kwargs)
448 447 try:
449 448 yield kc
450 449 finally:
451 450 kc.stop_channels()
452 451 km.shutdown_kernel(now=True)
@@ -1,391 +1,388 b''
1 1 """An Application for launching a 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 print_function
7 7
8 8 import atexit
9 9 import os
10 10 import sys
11 11 import signal
12 12
13 13 import zmq
14 14 from zmq.eventloop import ioloop
15 15 from zmq.eventloop.zmqstream import ZMQStream
16 16
17 17 from IPython.core.ultratb import FormattedTB
18 18 from IPython.core.application import (
19 19 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
20 20 )
21 21 from IPython.core.profiledir import ProfileDir
22 22 from IPython.core.shellapp import (
23 23 InteractiveShellApp, shell_flags, shell_aliases
24 24 )
25 25 from IPython.utils import io
26 26 from IPython.utils.path import filefind
27 27 from IPython.utils.traitlets import (
28 28 Any, Instance, Dict, Unicode, Integer, Bool, DottedObjectName, Type,
29 29 )
30 30 from IPython.utils.importstring import import_item
31 31 from IPython.kernel import write_connection_file
32 32 from IPython.kernel.connect import ConnectionFileMixin
33 33
34 34 # local imports
35 35 from .heartbeat import Heartbeat
36 36 from .ipkernel import IPythonKernel
37 37 from .parentpoller import ParentPollerUnix, ParentPollerWindows
38 38 from .session import (
39 39 Session, session_flags, session_aliases, default_secure,
40 40 )
41 41 from .zmqshell import ZMQInteractiveShell
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Flags and Aliases
45 45 #-----------------------------------------------------------------------------
46 46
47 47 kernel_aliases = dict(base_aliases)
48 48 kernel_aliases.update({
49 49 'ip' : 'IPKernelApp.ip',
50 50 'hb' : 'IPKernelApp.hb_port',
51 51 'shell' : 'IPKernelApp.shell_port',
52 52 'iopub' : 'IPKernelApp.iopub_port',
53 53 'stdin' : 'IPKernelApp.stdin_port',
54 54 'control' : 'IPKernelApp.control_port',
55 55 'f' : 'IPKernelApp.connection_file',
56 'parent': 'IPKernelApp.parent_handle',
57 56 'transport': 'IPKernelApp.transport',
58 57 })
59 if sys.platform.startswith('win'):
60 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
61 58
62 59 kernel_flags = dict(base_flags)
63 60 kernel_flags.update({
64 61 'no-stdout' : (
65 62 {'IPKernelApp' : {'no_stdout' : True}},
66 63 "redirect stdout to the null device"),
67 64 'no-stderr' : (
68 65 {'IPKernelApp' : {'no_stderr' : True}},
69 66 "redirect stderr to the null device"),
70 67 'pylab' : (
71 68 {'IPKernelApp' : {'pylab' : 'auto'}},
72 69 """Pre-load matplotlib and numpy for interactive use with
73 70 the default matplotlib backend."""),
74 71 })
75 72
76 73 # inherit flags&aliases for any IPython shell apps
77 74 kernel_aliases.update(shell_aliases)
78 75 kernel_flags.update(shell_flags)
79 76
80 77 # inherit flags&aliases for Sessions
81 78 kernel_aliases.update(session_aliases)
82 79 kernel_flags.update(session_flags)
83 80
84 81 _ctrl_c_message = """\
85 82 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
86 83
87 84 To exit, you will have to explicitly quit this process, by either sending
88 85 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
89 86
90 87 To read more about this, see https://github.com/ipython/ipython/issues/2049
91 88
92 89 """
93 90
94 91 #-----------------------------------------------------------------------------
95 92 # Application class for starting an IPython Kernel
96 93 #-----------------------------------------------------------------------------
97 94
98 95 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp,
99 96 ConnectionFileMixin):
100 97 name='ipython-kernel'
101 98 aliases = Dict(kernel_aliases)
102 99 flags = Dict(kernel_flags)
103 100 classes = [IPythonKernel, ZMQInteractiveShell, ProfileDir, Session]
104 101 # the kernel class, as an importstring
105 102 kernel_class = Type('IPython.kernel.zmq.ipkernel.IPythonKernel', config=True,
106 103 klass='IPython.kernel.zmq.kernelbase.Kernel',
107 104 help="""The Kernel subclass to be used.
108 105
109 106 This should allow easy re-use of the IPKernelApp entry point
110 107 to configure and launch kernels other than IPython's own.
111 108 """)
112 109 kernel = Any()
113 110 poller = Any() # don't restrict this even though current pollers are all Threads
114 111 heartbeat = Instance(Heartbeat)
115 112 ports = Dict()
116 113
117 114 # connection info:
118 115
119 116 @property
120 117 def abs_connection_file(self):
121 118 if os.path.basename(self.connection_file) == self.connection_file:
122 119 return os.path.join(self.profile_dir.security_dir, self.connection_file)
123 120 else:
124 121 return self.connection_file
125 122
126 123
127 124 # streams, etc.
128 125 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
129 126 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
130 127 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
131 128 config=True, help="The importstring for the OutStream factory")
132 129 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
133 130 config=True, help="The importstring for the DisplayHook factory")
134 131
135 132 # polling
136 parent_handle = Integer(0, config=True,
133 parent_handle = Integer(int(os.environ.get('JPY_PARENT_PID') or 0), config=True,
137 134 help="""kill this process if its parent dies. On Windows, the argument
138 135 specifies the HANDLE of the parent process, otherwise it is simply boolean.
139 136 """)
140 interrupt = Integer(0, config=True,
137 interrupt = Integer(int(os.environ.get('JPY_INTERRUPT_EVENT') or 0), config=True,
141 138 help="""ONLY USED ON WINDOWS
142 139 Interrupt this process when the parent is signaled.
143 140 """)
144 141
145 142 def init_crash_handler(self):
146 143 # Install minimal exception handling
147 144 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
148 145 ostream=sys.__stdout__)
149 146
150 147 def init_poller(self):
151 148 if sys.platform == 'win32':
152 149 if self.interrupt or self.parent_handle:
153 150 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
154 151 elif self.parent_handle:
155 152 self.poller = ParentPollerUnix()
156 153
157 154 def _bind_socket(self, s, port):
158 155 iface = '%s://%s' % (self.transport, self.ip)
159 156 if self.transport == 'tcp':
160 157 if port <= 0:
161 158 port = s.bind_to_random_port(iface)
162 159 else:
163 160 s.bind("tcp://%s:%i" % (self.ip, port))
164 161 elif self.transport == 'ipc':
165 162 if port <= 0:
166 163 port = 1
167 164 path = "%s-%i" % (self.ip, port)
168 165 while os.path.exists(path):
169 166 port = port + 1
170 167 path = "%s-%i" % (self.ip, port)
171 168 else:
172 169 path = "%s-%i" % (self.ip, port)
173 170 s.bind("ipc://%s" % path)
174 171 return port
175 172
176 173 def write_connection_file(self):
177 174 """write connection info to JSON file"""
178 175 cf = self.abs_connection_file
179 176 self.log.debug("Writing connection file: %s", cf)
180 177 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
181 178 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
182 179 iopub_port=self.iopub_port, control_port=self.control_port)
183 180
184 181 def cleanup_connection_file(self):
185 182 cf = self.abs_connection_file
186 183 self.log.debug("Cleaning up connection file: %s", cf)
187 184 try:
188 185 os.remove(cf)
189 186 except (IOError, OSError):
190 187 pass
191 188
192 189 self.cleanup_ipc_files()
193 190
194 191 def init_connection_file(self):
195 192 if not self.connection_file:
196 193 self.connection_file = "kernel-%s.json"%os.getpid()
197 194 try:
198 195 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
199 196 except IOError:
200 197 self.log.debug("Connection file not found: %s", self.connection_file)
201 198 # This means I own it, so I will clean it up:
202 199 atexit.register(self.cleanup_connection_file)
203 200 return
204 201 try:
205 202 self.load_connection_file()
206 203 except Exception:
207 204 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
208 205 self.exit(1)
209 206
210 207 def init_sockets(self):
211 208 # Create a context, a session, and the kernel sockets.
212 209 self.log.info("Starting the kernel at pid: %i", os.getpid())
213 210 context = zmq.Context.instance()
214 211 # Uncomment this to try closing the context.
215 212 # atexit.register(context.term)
216 213
217 214 self.shell_socket = context.socket(zmq.ROUTER)
218 215 self.shell_socket.linger = 1000
219 216 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
220 217 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
221 218
222 219 self.iopub_socket = context.socket(zmq.PUB)
223 220 self.iopub_socket.linger = 1000
224 221 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
225 222 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
226 223
227 224 self.stdin_socket = context.socket(zmq.ROUTER)
228 225 self.stdin_socket.linger = 1000
229 226 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
230 227 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
231 228
232 229 self.control_socket = context.socket(zmq.ROUTER)
233 230 self.control_socket.linger = 1000
234 231 self.control_port = self._bind_socket(self.control_socket, self.control_port)
235 232 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
236 233
237 234 def init_heartbeat(self):
238 235 """start the heart beating"""
239 236 # heartbeat doesn't share context, because it mustn't be blocked
240 237 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
241 238 hb_ctx = zmq.Context()
242 239 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
243 240 self.hb_port = self.heartbeat.port
244 241 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
245 242 self.heartbeat.start()
246 243
247 244 def log_connection_info(self):
248 245 """display connection info, and store ports"""
249 246 basename = os.path.basename(self.connection_file)
250 247 if basename == self.connection_file or \
251 248 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
252 249 # use shortname
253 250 tail = basename
254 251 if self.profile != 'default':
255 252 tail += " --profile %s" % self.profile
256 253 else:
257 254 tail = self.connection_file
258 255 lines = [
259 256 "To connect another client to this kernel, use:",
260 257 " --existing %s" % tail,
261 258 ]
262 259 # log connection info
263 260 # info-level, so often not shown.
264 261 # frontends should use the %connect_info magic
265 262 # to see the connection info
266 263 for line in lines:
267 264 self.log.info(line)
268 265 # also raw print to the terminal if no parent_handle (`ipython kernel`)
269 266 if not self.parent_handle:
270 267 io.rprint(_ctrl_c_message)
271 268 for line in lines:
272 269 io.rprint(line)
273 270
274 271 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
275 272 stdin=self.stdin_port, hb=self.hb_port,
276 273 control=self.control_port)
277 274
278 275 def init_blackhole(self):
279 276 """redirects stdout/stderr to devnull if necessary"""
280 277 if self.no_stdout or self.no_stderr:
281 278 blackhole = open(os.devnull, 'w')
282 279 if self.no_stdout:
283 280 sys.stdout = sys.__stdout__ = blackhole
284 281 if self.no_stderr:
285 282 sys.stderr = sys.__stderr__ = blackhole
286 283
287 284 def init_io(self):
288 285 """Redirect input streams and set a display hook."""
289 286 if self.outstream_class:
290 287 outstream_factory = import_item(str(self.outstream_class))
291 288 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
292 289 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
293 290 if self.displayhook_class:
294 291 displayhook_factory = import_item(str(self.displayhook_class))
295 292 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
296 293
297 294 def init_signal(self):
298 295 signal.signal(signal.SIGINT, signal.SIG_IGN)
299 296
300 297 def init_kernel(self):
301 298 """Create the Kernel object itself"""
302 299 shell_stream = ZMQStream(self.shell_socket)
303 300 control_stream = ZMQStream(self.control_socket)
304 301
305 302 kernel_factory = self.kernel_class.instance
306 303
307 304 kernel = kernel_factory(parent=self, session=self.session,
308 305 shell_streams=[shell_stream, control_stream],
309 306 iopub_socket=self.iopub_socket,
310 307 stdin_socket=self.stdin_socket,
311 308 log=self.log,
312 309 profile_dir=self.profile_dir,
313 310 user_ns=self.user_ns,
314 311 )
315 312 kernel.record_ports(self.ports)
316 313 self.kernel = kernel
317 314
318 315 def init_gui_pylab(self):
319 316 """Enable GUI event loop integration, taking pylab into account."""
320 317
321 318 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
322 319 # to ensure that any exception is printed straight to stderr.
323 320 # Normally _showtraceback associates the reply with an execution,
324 321 # which means frontends will never draw it, as this exception
325 322 # is not associated with any execute request.
326 323
327 324 shell = self.shell
328 325 _showtraceback = shell._showtraceback
329 326 try:
330 327 # replace error-sending traceback with stderr
331 328 def print_tb(etype, evalue, stb):
332 329 print ("GUI event loop or pylab initialization failed",
333 330 file=io.stderr)
334 331 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
335 332 shell._showtraceback = print_tb
336 333 InteractiveShellApp.init_gui_pylab(self)
337 334 finally:
338 335 shell._showtraceback = _showtraceback
339 336
340 337 def init_shell(self):
341 338 self.shell = getattr(self.kernel, 'shell', None)
342 339 if self.shell:
343 340 self.shell.configurables.append(self)
344 341
345 342 @catch_config_error
346 343 def initialize(self, argv=None):
347 344 super(IPKernelApp, self).initialize(argv)
348 345 default_secure(self.config)
349 346 self.init_blackhole()
350 347 self.init_connection_file()
351 348 self.init_poller()
352 349 self.init_sockets()
353 350 self.init_heartbeat()
354 351 # writing/displaying connection info must be *after* init_sockets/heartbeat
355 352 self.log_connection_info()
356 353 self.write_connection_file()
357 354 self.init_io()
358 355 self.init_signal()
359 356 self.init_kernel()
360 357 # shell init steps
361 358 self.init_path()
362 359 self.init_shell()
363 360 if self.shell:
364 361 self.init_gui_pylab()
365 362 self.init_extensions()
366 363 self.init_code()
367 364 # flush stdout/stderr, so that anything written to these streams during
368 365 # initialization do not get associated with the first execution request
369 366 sys.stdout.flush()
370 367 sys.stderr.flush()
371 368
372 369 def start(self):
373 370 if self.poller is not None:
374 371 self.poller.start()
375 372 self.kernel.start()
376 373 try:
377 374 ioloop.IOLoop.instance().start()
378 375 except KeyboardInterrupt:
379 376 pass
380 377
381 378 launch_new_instance = IPKernelApp.launch_instance
382 379
383 380 def main():
384 381 """Run an IPKernel as an application"""
385 382 app = IPKernelApp.instance()
386 383 app.initialize()
387 384 app.start()
388 385
389 386
390 387 if __name__ == '__main__':
391 388 main()
General Comments 0
You need to be logged in to leave comments. Login now