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