##// END OF EJS Templates
Merge pull request #6291 from takluyver/explicit-install-native-kernelspec...
Min RK -
r17758:2ecbf779 merge
parent child Browse files
Show More
@@ -0,0 +1,2 b''
1 - :meth:`~.KernelManager.start_kernel` and :meth:`~.KernelManager.format_kernel_cmd`
2 no longer accept a ``executable`` parameter. Use the kernelspec machinery instead.
@@ -1,196 +1,222 b''
1 1 import io
2 2 import json
3 3 import os
4 4 import shutil
5 5 import sys
6 6
7 7 pjoin = os.path.join
8 8
9 9 from IPython.utils.path import get_ipython_dir
10 10 from IPython.utils.py3compat import PY3
11 11 from IPython.utils.traitlets import HasTraits, List, Unicode, Dict, Any
12 from .launcher import make_ipkernel_cmd
12 13
13 14 if os.name == 'nt':
14 15 programdata = os.environ.get('PROGRAMDATA', None)
15 16 if programdata:
16 17 SYSTEM_KERNEL_DIRS = [pjoin(programdata, 'ipython', 'kernels')]
17 18 else: # PROGRAMDATA is not defined by default on XP.
18 19 SYSTEM_KERNEL_DIRS = []
19 20 else:
20 21 SYSTEM_KERNEL_DIRS = ["/usr/share/ipython/kernels",
21 22 "/usr/local/share/ipython/kernels",
22 23 ]
23 24
24 25 NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
25 26
26 27 def _pythonfirst(s):
27 28 "Sort key function that will put strings starting with 'python' first."
28 29 if s == NATIVE_KERNEL_NAME:
29 30 return ' ' + s # Two spaces to sort this first of all
30 31 elif s.startswith('python'):
31 32 # Space is not valid in kernel names, so this should sort first
32 33 return ' ' + s
33 34 return s
34 35
35 36 class KernelSpec(HasTraits):
36 37 argv = List()
37 38 display_name = Unicode()
38 39 language = Unicode()
39 40 codemirror_mode = Any() # can be unicode or dict
40 41 env = Dict()
41 42 resource_dir = Unicode()
42 43
43 44 def _codemirror_mode_default(self):
44 45 return self.language
45 46
46 47 @classmethod
47 48 def from_resource_dir(cls, resource_dir):
48 49 """Create a KernelSpec object by reading kernel.json
49 50
50 51 Pass the path to the *directory* containing kernel.json.
51 52 """
52 53 kernel_file = pjoin(resource_dir, 'kernel.json')
53 54 with io.open(kernel_file, 'r', encoding='utf-8') as f:
54 55 kernel_dict = json.load(f)
55 56 return cls(resource_dir=resource_dir, **kernel_dict)
56 57
57 58 def to_dict(self):
58 59 return dict(argv=self.argv,
59 60 env=self.env,
60 61 display_name=self.display_name,
61 62 language=self.language,
62 63 codemirror_mode=self.codemirror_mode,
63 64 )
64 65
65 66 def to_json(self):
66 67 return json.dumps(self.to_dict())
67 68
68 69 def _is_kernel_dir(path):
69 70 """Is ``path`` a kernel directory?"""
70 71 return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
71 72
72 73 def _list_kernels_in(dir):
73 74 """Return a mapping of kernel names to resource directories from dir.
74 75
75 76 If dir is None or does not exist, returns an empty dict.
76 77 """
77 78 if dir is None or not os.path.isdir(dir):
78 79 return {}
79 80 return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
80 81 if _is_kernel_dir(pjoin(dir, f))}
81 82
82 83 class NoSuchKernel(KeyError):
83 84 def __init__(self, name):
84 85 self.name = name
85 86
86 87 class KernelSpecManager(HasTraits):
87 88 ipython_dir = Unicode()
88 89 def _ipython_dir_default(self):
89 90 return get_ipython_dir()
90 91
91 92 user_kernel_dir = Unicode()
92 93 def _user_kernel_dir_default(self):
93 94 return pjoin(self.ipython_dir, 'kernels')
94 95
95 96 kernel_dirs = List(
96 97 help="List of kernel directories to search. Later ones take priority over earlier."
97 98 )
98 99 def _kernel_dirs_default(self):
99 100 return SYSTEM_KERNEL_DIRS + [
100 101 self.user_kernel_dir,
101 102 ]
102 103
103 def _make_native_kernel_dir(self):
104 @property
105 def _native_kernel_dict(self):
104 106 """Makes a kernel directory for the native kernel.
105 107
106 108 The native kernel is the kernel using the same Python runtime as this
107 109 process. This will put its informatino in the user kernels directory.
108 110 """
109 path = pjoin(self.user_kernel_dir, NATIVE_KERNEL_NAME)
110 os.makedirs(path, mode=0o755)
111 with open(pjoin(path, 'kernel.json'), 'w') as f:
112 json.dump({'argv':[NATIVE_KERNEL_NAME, '-c',
113 'from IPython.kernel.zmq.kernelapp import main; main()',
114 '-f', '{connection_file}'],
115 'display_name': 'IPython (Python %d)' % (3 if PY3 else 2),
116 'language': 'python',
117 'codemirror_mode': {'name': 'ipython',
118 'version': sys.version_info[0]},
119 },
120 f, indent=1)
121 # TODO: Copy icons into directory
122 return path
123
111 return {'argv':make_ipkernel_cmd(
112 'from IPython.kernel.zmq.kernelapp import main; main()'),
113 'display_name': 'IPython (Python %d)' % (3 if PY3 else 2),
114 'language': 'python',
115 'codemirror_mode': {'name': 'ipython',
116 'version': sys.version_info[0]},
117 }
118
119 @property
120 def _native_kernel_resource_dir(self):
121 # TODO: This may be different when we actually have any resources
122 return os.path.dirname(__file__)
123
124 124 def find_kernel_specs(self):
125 125 """Returns a dict mapping kernel names to resource directories."""
126 126 d = {}
127 127 for kernel_dir in self.kernel_dirs:
128 128 d.update(_list_kernels_in(kernel_dir))
129
130 if NATIVE_KERNEL_NAME not in d:
131 d[NATIVE_KERNEL_NAME] = self._make_native_kernel_dir()
129
130 d[NATIVE_KERNEL_NAME] = self._native_kernel_resource_dir
132 131 return d
133 132 # TODO: Caching?
134
133
135 134 def get_kernel_spec(self, kernel_name):
136 135 """Returns a :class:`KernelSpec` instance for the given kernel_name.
137 136
138 137 Raises :exc:`NoSuchKernel` if the given kernel name is not found.
139 138 """
140 if kernel_name == 'python':
141 kernel_name = NATIVE_KERNEL_NAME
139 if kernel_name in {'python', NATIVE_KERNEL_NAME}:
140 return KernelSpec(self._native_kernel_resource_dir, **self._native_kernel_dict)
141
142 142 d = self.find_kernel_specs()
143 143 try:
144 144 resource_dir = d[kernel_name.lower()]
145 145 except KeyError:
146 146 raise NoSuchKernel(kernel_name)
147 147 return KernelSpec.from_resource_dir(resource_dir)
148
149 def _get_destination_dir(self, kernel_name, system=False):
150 if system:
151 if SYSTEM_KERNEL_DIRS:
152 return os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name)
153 else:
154 raise EnvironmentError("No system kernel directory is available")
155 else:
156 return os.path.join(self.user_kernel_dir, kernel_name)
148 157
149 158 def install_kernel_spec(self, source_dir, kernel_name=None, system=False,
150 159 replace=False):
151 160 """Install a kernel spec by copying its directory.
152 161
153 162 If ``kernel_name`` is not given, the basename of ``source_dir`` will
154 163 be used.
155 164
156 165 If ``system`` is True, it will attempt to install into the systemwide
157 166 kernel registry. If the process does not have appropriate permissions,
158 167 an :exc:`OSError` will be raised.
159 168
160 169 If ``replace`` is True, this will replace an existing kernel of the same
161 170 name. Otherwise, if the destination already exists, an :exc:`OSError`
162 171 will be raised.
163 172 """
164 173 if not kernel_name:
165 174 kernel_name = os.path.basename(source_dir)
166 175 kernel_name = kernel_name.lower()
167
168 if system:
169 if SYSTEM_KERNEL_DIRS:
170 destination = os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name)
171 else:
172 raise EnvironmentError("No system kernel directory is available")
173 else:
174 destination = os.path.join(self.user_kernel_dir, kernel_name)
176
177 destination = self._get_destination_dir(kernel_name, system=system)
175 178
176 179 if replace and os.path.isdir(destination):
177 180 shutil.rmtree(destination)
178 181
179 182 shutil.copytree(source_dir, destination)
180 183
184 def install_native_kernel_spec(self, system=False):
185 """Install the native kernel spec to the filesystem
186
187 This allows a Python 3 frontend to use a Python 2 kernel, or vice versa.
188 The kernelspec will be written pointing to the Python executable on
189 which this is run.
190
191 If ``system`` is True, it will attempt to install into the systemwide
192 kernel registry. If the process does not have appropriate permissions,
193 an :exc:`OSError` will be raised.
194 """
195 path = self._get_destination_dir(NATIVE_KERNEL_NAME, system=system)
196 os.makedirs(path, mode=0o755)
197 with open(pjoin(path, 'kernel.json'), 'w') as f:
198 json.dump(self._native_kernel_dict, f, indent=1)
199 # TODO: Copy icons into directory
200 return path
201
181 202 def find_kernel_specs():
182 203 """Returns a dict mapping kernel names to resource directories."""
183 204 return KernelSpecManager().find_kernel_specs()
184 205
185 206 def get_kernel_spec(kernel_name):
186 207 """Returns a :class:`KernelSpec` instance for the given kernel_name.
187 208
188 209 Raises KeyError if the given kernel name is not found.
189 210 """
190 211 return KernelSpecManager().get_kernel_spec(kernel_name)
191 212
192 213 def install_kernel_spec(source_dir, kernel_name=None, system=False, replace=False):
193 214 return KernelSpecManager().install_kernel_spec(source_dir, kernel_name,
194 215 system, replace)
195 216
196 217 install_kernel_spec.__doc__ = KernelSpecManager.install_kernel_spec.__doc__
218
219 def install_native_kernel_spec(self, system=False):
220 return KernelSpecManager().install_native_kernel_spec(system=system)
221
222 install_native_kernel_spec.__doc__ = KernelSpecManager.install_native_kernel_spec.__doc__
@@ -1,114 +1,145 b''
1 1
2 2 # Copyright (c) IPython Development Team.
3 3 # Distributed under the terms of the Modified BSD License.
4 4
5 5 import errno
6 6 import os.path
7 7
8 8 from IPython.config.application import Application
9 9 from IPython.core.application import (
10 10 BaseIPythonApplication, base_flags, base_aliases
11 11 )
12 12 from IPython.utils.traitlets import Instance, Dict, Unicode, Bool
13 13
14 14 from .kernelspec import KernelSpecManager, _pythonfirst
15 15
16 16 class ListKernelSpecs(BaseIPythonApplication):
17 17 description = """List installed kernel specifications."""
18 18 kernel_spec_manager = Instance(KernelSpecManager)
19 19
20 20 # Not all of the base aliases are meaningful (e.g. profile)
21 21 aliases = {k: base_aliases[k] for k in ['ipython-dir', 'log-level']}
22 22 flags = {'debug': base_flags['debug'],}
23 23
24 24 def _kernel_spec_manager_default(self):
25 25 return KernelSpecManager(ipython_dir=self.ipython_dir)
26 26
27 27 def start(self):
28 28 print("Available kernels:")
29 29 for kernelname in sorted(self.kernel_spec_manager.find_kernel_specs(),
30 30 key=_pythonfirst):
31 31 print(" %s" % kernelname)
32 32
33 33
34 34 class InstallKernelSpec(BaseIPythonApplication):
35 35 description = """Install a kernel specification directory."""
36 36 kernel_spec_manager = Instance(KernelSpecManager)
37 37
38 38 def _kernel_spec_manager_default(self):
39 39 return KernelSpecManager(ipython_dir=self.ipython_dir)
40 40
41 41 sourcedir = Unicode()
42 42 kernel_name = Unicode("", config=True,
43 43 help="Install the kernel spec with this name"
44 44 )
45 45 def _kernel_name_default(self):
46 46 return os.path.basename(self.sourcedir)
47 47
48 48 system = Bool(False, config=True,
49 49 help="""
50 50 Try to install the kernel spec to the systemwide directory instead of
51 51 the per-user directory.
52 52 """
53 53 )
54 54 replace = Bool(False, config=True,
55 55 help="Replace any existing kernel spec with this name."
56 56 )
57 57
58 58 aliases = {'name': 'InstallKernelSpec.kernel_name'}
59 59 for k in ['ipython-dir', 'log-level']:
60 60 aliases[k] = base_aliases[k]
61 61
62 62 flags = {'system': ({'InstallKernelSpec': {'system': True}},
63 63 "Install to the systemwide kernel registry"),
64 64 'replace': ({'InstallKernelSpec': {'replace': True}},
65 65 "Replace any existing kernel spec with this name."),
66 66 'debug': base_flags['debug'],
67 67 }
68 68
69 69 def parse_command_line(self, argv):
70 70 super(InstallKernelSpec, self).parse_command_line(argv)
71 71 # accept positional arg as profile name
72 72 if self.extra_args:
73 73 self.sourcedir = self.extra_args[0]
74 74 else:
75 75 print("No source directory specified.")
76 76 self.exit(1)
77 77
78 78 def start(self):
79 79 try:
80 80 self.kernel_spec_manager.install_kernel_spec(self.sourcedir,
81 81 kernel_name=self.kernel_name,
82 82 system=self.system,
83 83 replace=self.replace,
84 84 )
85 85 except OSError as e:
86 86 if e.errno == errno.EACCES:
87 87 print("Permission denied")
88 88 self.exit(1)
89 89 elif e.errno == errno.EEXIST:
90 90 print("A kernel spec is already present at %s" % e.filename)
91 91 self.exit(1)
92 92 raise
93 93
94 class InstallNativeKernelSpec(BaseIPythonApplication):
95 description = """Install the native kernel spec directory for this Python."""
96 kernel_spec_manager = Instance(KernelSpecManager)
97
98 def _kernel_spec_manager_default(self):
99 return KernelSpecManager(ipython_dir=self.ipython_dir)
100
101 system = Bool(False, config=True,
102 help="""
103 Try to install the kernel spec to the systemwide directory instead of
104 the per-user directory.
105 """
106 )
107
108 # Not all of the base aliases are meaningful (e.g. profile)
109 aliases = {k: base_aliases[k] for k in ['ipython-dir', 'log-level']}
110 flags = {'system': ({'InstallOwnKernelSpec': {'system': True}},
111 "Install to the systemwide kernel registry"),
112 'debug': base_flags['debug'],
113 }
114
115 def start(self):
116 try:
117 self.kernel_spec_manager.install_native_kernel_spec(system=self.system)
118 except OSError as e:
119 if e.errno == errno.EACCES:
120 print("Permission denied")
121 self.exit(1)
122 raise
123
94 124 class KernelSpecApp(Application):
95 125 name = "ipython kernelspec"
96 126 description = """Manage IPython kernel specifications."""
97 127
98 subcommands = Dict(dict(
99 list = (ListKernelSpecs, ListKernelSpecs.description.splitlines()[0]),
100 install = (InstallKernelSpec, InstallKernelSpec.description.splitlines()[0])
101 ))
102
128 subcommands = Dict({
129 'list': (ListKernelSpecs, ListKernelSpecs.description.splitlines()[0]),
130 'install': (InstallKernelSpec, InstallKernelSpec.description.splitlines()[0]),
131 'install-self': (InstallNativeKernelSpec, InstallNativeKernelSpec.description.splitlines()[0]),
132 })
133
103 134 aliases = {}
104 135 flags = {}
105 136
106 137 def start(self):
107 138 if self.subapp is None:
108 139 print("No subcommand specified. Must specify one of: %s"% list(self.subcommands))
109 140 print()
110 141 self.print_description()
111 142 self.print_subcommands()
112 143 self.exit(1)
113 144 else:
114 145 return self.subapp.start()
@@ -1,261 +1,258 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(code, 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 code : str,
87 87 A string of Python code that imports and executes a kernel entry point.
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
101 # Build the kernel launch command.
102 100 if executable is None:
103 101 executable = sys.executable
104 102 arguments = [ executable, '-c', code, '-f', '{connection_file}' ]
105 103 arguments.extend(extra_arguments)
106 104
107 # Spawn a kernel.
108 105 if sys.platform == 'win32':
109 106
110 107 # If the kernel is running on pythonw and stdout/stderr are not been
111 108 # re-directed, it will crash when more than 4KB of data is written to
112 109 # stdout or stderr. This is a bug that has been with Python for a very
113 110 # long time; see http://bugs.python.org/issue706263.
114 111 # A cleaner solution to this problem would be to pass os.devnull to
115 112 # Popen directly. Unfortunately, that does not work.
116 113 if executable.endswith('pythonw.exe'):
117 114 arguments.append('--no-stdout')
118 115 arguments.append('--no-stderr')
119 116
120 117 return arguments
121 118
122 119
123 120 def launch_kernel(cmd, stdin=None, stdout=None, stderr=None, env=None,
124 121 independent=False,
125 122 cwd=None, ipython_kernel=True,
126 123 **kw
127 124 ):
128 125 """ Launches a localhost kernel, binding to the specified ports.
129 126
130 127 Parameters
131 128 ----------
132 129 cmd : Popen list,
133 130 A string of Python code that imports and executes a kernel entry point.
134 131
135 132 stdin, stdout, stderr : optional (default None)
136 133 Standards streams, as defined in subprocess.Popen.
137 134
138 135 independent : bool, optional (default False)
139 136 If set, the kernel process is guaranteed to survive if this process
140 137 dies. If not set, an effort is made to ensure that the kernel is killed
141 138 when this process dies. Note that in this case it is still good practice
142 139 to kill kernels manually before exiting.
143 140
144 141 cwd : path, optional
145 142 The working dir of the kernel process (default: cwd of this process).
146 143
147 144 ipython_kernel : bool, optional
148 145 Whether the kernel is an official IPython one,
149 146 and should get a bit of special treatment.
150 147
151 148 Returns
152 149 -------
153 150
154 151 Popen instance for the kernel subprocess
155 152 """
156 153
157 154 # Popen will fail (sometimes with a deadlock) if stdin, stdout, and stderr
158 155 # are invalid. Unfortunately, there is in general no way to detect whether
159 156 # they are valid. The following two blocks redirect them to (temporary)
160 157 # pipes in certain important cases.
161 158
162 159 # If this process has been backgrounded, our stdin is invalid. Since there
163 160 # is no compelling reason for the kernel to inherit our stdin anyway, we'll
164 161 # place this one safe and always redirect.
165 162 redirect_in = True
166 163 _stdin = PIPE if stdin is None else stdin
167 164
168 165 # If this process in running on pythonw, we know that stdin, stdout, and
169 166 # stderr are all invalid.
170 167 redirect_out = sys.executable.endswith('pythonw.exe')
171 168 if redirect_out:
172 169 _stdout = PIPE if stdout is None else stdout
173 170 _stderr = PIPE if stderr is None else stderr
174 171 else:
175 172 _stdout, _stderr = stdout, stderr
176 173
177 174 env = env if (env is not None) else os.environ.copy()
178 175
179 176 encoding = getdefaultencoding(prefer_stream=False)
180 177
181 178 # Spawn a kernel.
182 179 if sys.platform == 'win32':
183 180 # Popen on Python 2 on Windows cannot handle unicode args or cwd
184 181 cmd = [ cast_bytes_py2(c, encoding) for c in cmd ]
185 182 if cwd:
186 183 cwd = cast_bytes_py2(cwd, sys.getfilesystemencoding() or 'ascii')
187 184
188 185 from IPython.kernel.zmq.parentpoller import ParentPollerWindows
189 186 # Create a Win32 event for interrupting the kernel.
190 187 interrupt_event = ParentPollerWindows.create_interrupt_event()
191 188 # Store this in an environment variable for third party kernels, but at
192 189 # present, our own kernel expects this as a command line argument.
193 190 env["IPY_INTERRUPT_EVENT"] = str(interrupt_event)
194 191 if ipython_kernel:
195 192 cmd += [ '--interrupt=%i' % interrupt_event ]
196 193
197 194 # If the kernel is running on pythonw and stdout/stderr are not been
198 195 # re-directed, it will crash when more than 4KB of data is written to
199 196 # stdout or stderr. This is a bug that has been with Python for a very
200 197 # long time; see http://bugs.python.org/issue706263.
201 198 # A cleaner solution to this problem would be to pass os.devnull to
202 199 # Popen directly. Unfortunately, that does not work.
203 200 if cmd[0].endswith('pythonw.exe'):
204 201 if stdout is None:
205 202 cmd.append('--no-stdout')
206 203 if stderr is None:
207 204 cmd.append('--no-stderr')
208 205
209 206 # Launch the kernel process.
210 207 if independent:
211 208 proc = Popen(cmd,
212 209 creationflags=512, # CREATE_NEW_PROCESS_GROUP
213 210 stdin=_stdin, stdout=_stdout, stderr=_stderr, env=env)
214 211 else:
215 212 if ipython_kernel:
216 213 try:
217 214 from _winapi import DuplicateHandle, GetCurrentProcess, \
218 215 DUPLICATE_SAME_ACCESS
219 216 except:
220 217 from _subprocess import DuplicateHandle, GetCurrentProcess, \
221 218 DUPLICATE_SAME_ACCESS
222 219 pid = GetCurrentProcess()
223 220 handle = DuplicateHandle(pid, pid, pid, 0,
224 221 True, # Inheritable by new processes.
225 222 DUPLICATE_SAME_ACCESS)
226 223 cmd +=[ '--parent=%i' % handle ]
227 224
228 225
229 226 proc = Popen(cmd,
230 227 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
231 228
232 229 # Attach the interrupt event to the Popen objet so it can be used later.
233 230 proc.win32_interrupt_event = interrupt_event
234 231
235 232 else:
236 233 if independent:
237 234 proc = Popen(cmd, preexec_fn=lambda: os.setsid(),
238 235 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
239 236 else:
240 237 if ipython_kernel:
241 238 cmd += ['--parent=1']
242 239 proc = Popen(cmd,
243 240 stdin=_stdin, stdout=_stdout, stderr=_stderr, cwd=cwd, env=env)
244 241
245 242 # Clean up pipes created to work around Popen bug.
246 243 if redirect_in:
247 244 if stdin is None:
248 245 proc.stdin.close()
249 246 if redirect_out:
250 247 if stdout is None:
251 248 proc.stdout.close()
252 249 if stderr is None:
253 250 proc.stderr.close()
254 251
255 252 return proc
256 253
257 254 __all__ = [
258 255 'swallow_argv',
259 256 'make_ipkernel_cmd',
260 257 'launch_kernel',
261 258 ]
@@ -1,416 +1,412 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 import os
9 9 import re
10 10 import signal
11 11 import sys
12 12 import time
13 13 import warnings
14 14
15 15 import zmq
16 16
17 17 from IPython.utils.importstring import import_item
18 18 from IPython.utils.localinterfaces import is_local_ip, local_ips
19 19 from IPython.utils.path import get_ipython_dir
20 20 from IPython.utils.traitlets import (
21 21 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
22 22 )
23 23 from IPython.kernel import (
24 24 make_ipkernel_cmd,
25 25 launch_kernel,
26 26 kernelspec,
27 27 )
28 28 from .connect import ConnectionFileMixin
29 29 from .zmq.session import Session
30 30 from .managerabc import (
31 31 KernelManagerABC
32 32 )
33 33
34 34
35 35 class KernelManager(ConnectionFileMixin):
36 36 """Manages a single kernel in a subprocess on this host.
37 37
38 38 This version starts kernels with Popen.
39 39 """
40 40
41 41 # The PyZMQ Context to use for communication with the kernel.
42 42 context = Instance(zmq.Context)
43 43 def _context_default(self):
44 44 return zmq.Context.instance()
45 45
46 46 # the class to create with our `client` method
47 47 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
48 48 client_factory = Type()
49 49 def _client_class_changed(self, name, old, new):
50 50 self.client_factory = import_item(str(new))
51 51
52 52 # The kernel process with which the KernelManager is communicating.
53 53 # generally a Popen instance
54 54 kernel = Any()
55 55
56 56 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
57 57
58 58 def _kernel_spec_manager_default(self):
59 59 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
60 60
61 61 kernel_name = Unicode(kernelspec.NATIVE_KERNEL_NAME)
62 62
63 63 kernel_spec = Instance(kernelspec.KernelSpec)
64 64
65 65 def _kernel_spec_default(self):
66 66 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
67 67
68 68 def _kernel_name_changed(self, name, old, new):
69 69 if new == 'python':
70 70 self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
71 71 # This triggered another run of this function, so we can exit now
72 72 return
73 73 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
74 74 self.ipython_kernel = new in {'python', 'python2', 'python3'}
75 75
76 76 kernel_cmd = List(Unicode, config=True,
77 77 help="""DEPRECATED: Use kernel_name instead.
78 78
79 79 The Popen Command to launch the kernel.
80 80 Override this if you have a custom kernel.
81 81 If kernel_cmd is specified in a configuration file,
82 82 IPython does not pass any arguments to the kernel,
83 83 because it cannot make any assumptions about the
84 84 arguments that the kernel understands. In particular,
85 85 this means that the kernel does not receive the
86 86 option --debug if it given on the IPython command line.
87 87 """
88 88 )
89 89
90 90 def _kernel_cmd_changed(self, name, old, new):
91 91 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
92 92 "start different kernels.")
93 93 self.ipython_kernel = False
94 94
95 95 ipython_kernel = Bool(True)
96 96
97 97 ipython_dir = Unicode()
98 98 def _ipython_dir_default(self):
99 99 return get_ipython_dir()
100 100
101 101 # Protected traits
102 102 _launch_args = Any()
103 103 _control_socket = Any()
104 104
105 105 _restarter = Any()
106 106
107 107 autorestart = Bool(False, config=True,
108 108 help="""Should we autorestart the kernel if it dies."""
109 109 )
110 110
111 111 def __del__(self):
112 112 self._close_control_socket()
113 113 self.cleanup_connection_file()
114 114
115 115 #--------------------------------------------------------------------------
116 116 # Kernel restarter
117 117 #--------------------------------------------------------------------------
118 118
119 119 def start_restarter(self):
120 120 pass
121 121
122 122 def stop_restarter(self):
123 123 pass
124 124
125 125 def add_restart_callback(self, callback, event='restart'):
126 126 """register a callback to be called when a kernel is restarted"""
127 127 if self._restarter is None:
128 128 return
129 129 self._restarter.add_callback(callback, event)
130 130
131 131 def remove_restart_callback(self, callback, event='restart'):
132 132 """unregister a callback to be called when a kernel is restarted"""
133 133 if self._restarter is None:
134 134 return
135 135 self._restarter.remove_callback(callback, event)
136 136
137 137 #--------------------------------------------------------------------------
138 138 # create a Client connected to our Kernel
139 139 #--------------------------------------------------------------------------
140 140
141 141 def client(self, **kwargs):
142 142 """Create a client configured to connect to our kernel"""
143 143 if self.client_factory is None:
144 144 self.client_factory = import_item(self.client_class)
145 145
146 146 kw = {}
147 147 kw.update(self.get_connection_info())
148 148 kw.update(dict(
149 149 connection_file=self.connection_file,
150 150 session=self.session,
151 151 parent=self,
152 152 ))
153 153
154 154 # add kwargs last, for manual overrides
155 155 kw.update(kwargs)
156 156 return self.client_factory(**kw)
157 157
158 158 #--------------------------------------------------------------------------
159 159 # Kernel management
160 160 #--------------------------------------------------------------------------
161 161
162 def format_kernel_cmd(self, **kw):
162 def format_kernel_cmd(self, extra_arguments=None):
163 163 """replace templated args (e.g. {connection_file})"""
164 extra_arguments = extra_arguments or []
164 165 if self.kernel_cmd:
165 cmd = self.kernel_cmd
166 elif self.kernel_name == kernelspec.NATIVE_KERNEL_NAME:
167 # The native kernel gets special handling
168 cmd = make_ipkernel_cmd(
169 'from IPython.kernel.zmq.kernelapp import main; main()',
170 **kw
171 )
166 cmd = self.kernel_cmd + extra_arguments
172 167 else:
173 cmd = self.kernel_spec.argv
168 cmd = self.kernel_spec.argv + extra_arguments
174 169
175 170 ns = dict(connection_file=self.connection_file)
176 171 ns.update(self._launch_args)
177 172
178 173 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
179 174 def from_ns(match):
180 175 """Get the key out of ns if it's there, otherwise no change."""
181 176 return ns.get(match.group(1), match.group())
182 177
183 178 return [ pat.sub(from_ns, arg) for arg in cmd ]
184 179
185 180 def _launch_kernel(self, kernel_cmd, **kw):
186 181 """actually launch the kernel
187 182
188 183 override in a subclass to launch kernel subprocesses differently
189 184 """
190 185 return launch_kernel(kernel_cmd, **kw)
191 186
192 187 # Control socket used for polite kernel shutdown
193 188
194 189 def _connect_control_socket(self):
195 190 if self._control_socket is None:
196 191 self._control_socket = self.connect_control()
197 192 self._control_socket.linger = 100
198 193
199 194 def _close_control_socket(self):
200 195 if self._control_socket is None:
201 196 return
202 197 self._control_socket.close()
203 198 self._control_socket = None
204 199
205 200 def start_kernel(self, **kw):
206 201 """Starts a kernel on this host in a separate process.
207 202
208 203 If random ports (port=0) are being used, this method must be called
209 204 before the channels are created.
210 205
211 206 Parameters
212 207 ----------
213 208 **kw : optional
214 209 keyword arguments that are passed down to build the kernel_cmd
215 210 and launching the kernel (e.g. Popen kwargs).
216 211 """
217 212 if self.transport == 'tcp' and not is_local_ip(self.ip):
218 213 raise RuntimeError("Can only launch a kernel on a local interface. "
219 214 "Make sure that the '*_address' attributes are "
220 215 "configured properly. "
221 216 "Currently valid addresses are: %s" % local_ips()
222 217 )
223 218
224 219 # write connection file / get default ports
225 220 self.write_connection_file()
226 221
227 222 # save kwargs for use in restart
228 223 self._launch_args = kw.copy()
229 224 # build the Popen cmd
230 kernel_cmd = self.format_kernel_cmd(**kw)
225 extra_arguments = kw.pop('extra_arguments', [])
226 kernel_cmd = self.format_kernel_cmd(extra_arguments=extra_arguments)
231 227 if self.kernel_cmd:
232 228 # If kernel_cmd has been set manually, don't refer to a kernel spec
233 229 env = os.environ
234 230 else:
235 231 # Environment variables from kernel spec are added to os.environ
236 232 env = os.environ.copy()
237 233 env.update(self.kernel_spec.env or {})
238 234 # launch the kernel subprocess
239 235 self.kernel = self._launch_kernel(kernel_cmd, env=env,
240 236 ipython_kernel=self.ipython_kernel,
241 237 **kw)
242 238 self.start_restarter()
243 239 self._connect_control_socket()
244 240
245 241 def request_shutdown(self, restart=False):
246 242 """Send a shutdown request via control channel
247 243
248 244 On Windows, this just kills kernels instead, because the shutdown
249 245 messages don't work.
250 246 """
251 247 content = dict(restart=restart)
252 248 msg = self.session.msg("shutdown_request", content=content)
253 249 self.session.send(self._control_socket, msg)
254 250
255 251 def finish_shutdown(self, waittime=1, pollinterval=0.1):
256 252 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
257 253
258 254 This does not send shutdown requests - use :meth:`request_shutdown`
259 255 first.
260 256 """
261 257 for i in range(int(waittime/pollinterval)):
262 258 if self.is_alive():
263 259 time.sleep(pollinterval)
264 260 else:
265 261 break
266 262 else:
267 263 # OK, we've waited long enough.
268 264 if self.has_kernel:
269 265 self._kill_kernel()
270 266
271 267 def cleanup(self, connection_file=True):
272 268 """Clean up resources when the kernel is shut down"""
273 269 if connection_file:
274 270 self.cleanup_connection_file()
275 271
276 272 self.cleanup_ipc_files()
277 273 self._close_control_socket()
278 274
279 275 def shutdown_kernel(self, now=False, restart=False):
280 276 """Attempts to the stop the kernel process cleanly.
281 277
282 278 This attempts to shutdown the kernels cleanly by:
283 279
284 280 1. Sending it a shutdown message over the shell channel.
285 281 2. If that fails, the kernel is shutdown forcibly by sending it
286 282 a signal.
287 283
288 284 Parameters
289 285 ----------
290 286 now : bool
291 287 Should the kernel be forcible killed *now*. This skips the
292 288 first, nice shutdown attempt.
293 289 restart: bool
294 290 Will this kernel be restarted after it is shutdown. When this
295 291 is True, connection files will not be cleaned up.
296 292 """
297 293 # Stop monitoring for restarting while we shutdown.
298 294 self.stop_restarter()
299 295
300 296 if now:
301 297 self._kill_kernel()
302 298 else:
303 299 self.request_shutdown(restart=restart)
304 300 # Don't send any additional kernel kill messages immediately, to give
305 301 # the kernel a chance to properly execute shutdown actions. Wait for at
306 302 # most 1s, checking every 0.1s.
307 303 self.finish_shutdown()
308 304
309 305 self.cleanup(connection_file=not restart)
310 306
311 307 def restart_kernel(self, now=False, **kw):
312 308 """Restarts a kernel with the arguments that were used to launch it.
313 309
314 310 If the old kernel was launched with random ports, the same ports will be
315 311 used for the new kernel. The same connection file is used again.
316 312
317 313 Parameters
318 314 ----------
319 315 now : bool, optional
320 316 If True, the kernel is forcefully restarted *immediately*, without
321 317 having a chance to do any cleanup action. Otherwise the kernel is
322 318 given 1s to clean up before a forceful restart is issued.
323 319
324 320 In all cases the kernel is restarted, the only difference is whether
325 321 it is given a chance to perform a clean shutdown or not.
326 322
327 323 **kw : optional
328 324 Any options specified here will overwrite those used to launch the
329 325 kernel.
330 326 """
331 327 if self._launch_args is None:
332 328 raise RuntimeError("Cannot restart the kernel. "
333 329 "No previous call to 'start_kernel'.")
334 330 else:
335 331 # Stop currently running kernel.
336 332 self.shutdown_kernel(now=now, restart=True)
337 333
338 334 # Start new kernel.
339 335 self._launch_args.update(kw)
340 336 self.start_kernel(**self._launch_args)
341 337
342 338 @property
343 339 def has_kernel(self):
344 340 """Has a kernel been started that we are managing."""
345 341 return self.kernel is not None
346 342
347 343 def _kill_kernel(self):
348 344 """Kill the running kernel.
349 345
350 346 This is a private method, callers should use shutdown_kernel(now=True).
351 347 """
352 348 if self.has_kernel:
353 349
354 350 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
355 351 # TerminateProcess() on Win32).
356 352 try:
357 353 self.kernel.kill()
358 354 except OSError as e:
359 355 # In Windows, we will get an Access Denied error if the process
360 356 # has already terminated. Ignore it.
361 357 if sys.platform == 'win32':
362 358 if e.winerror != 5:
363 359 raise
364 360 # On Unix, we may get an ESRCH error if the process has already
365 361 # terminated. Ignore it.
366 362 else:
367 363 from errno import ESRCH
368 364 if e.errno != ESRCH:
369 365 raise
370 366
371 367 # Block until the kernel terminates.
372 368 self.kernel.wait()
373 369 self.kernel = None
374 370 else:
375 371 raise RuntimeError("Cannot kill kernel. No kernel is running!")
376 372
377 373 def interrupt_kernel(self):
378 374 """Interrupts the kernel by sending it a signal.
379 375
380 376 Unlike ``signal_kernel``, this operation is well supported on all
381 377 platforms.
382 378 """
383 379 if self.has_kernel:
384 380 if sys.platform == 'win32':
385 381 from .zmq.parentpoller import ParentPollerWindows as Poller
386 382 Poller.send_interrupt(self.kernel.win32_interrupt_event)
387 383 else:
388 384 self.kernel.send_signal(signal.SIGINT)
389 385 else:
390 386 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
391 387
392 388 def signal_kernel(self, signum):
393 389 """Sends a signal to the kernel.
394 390
395 391 Note that since only SIGTERM is supported on Windows, this function is
396 392 only useful on Unix systems.
397 393 """
398 394 if self.has_kernel:
399 395 self.kernel.send_signal(signum)
400 396 else:
401 397 raise RuntimeError("Cannot signal kernel. No kernel is running!")
402 398
403 399 def is_alive(self):
404 400 """Is the kernel process still running?"""
405 401 if self.has_kernel:
406 402 if self.kernel.poll() is None:
407 403 return True
408 404 else:
409 405 return False
410 406 else:
411 407 # we don't have a kernel
412 408 return False
413 409
414 410
415 411 KernelManagerABC.register(KernelManager)
416 412
General Comments 0
You need to be logged in to leave comments. Login now