##// END OF EJS Templates
Don't pass IPython-specific args to non-IPython kernels...
Min RK -
Show More
@@ -1,341 +1,345 b''
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations. This is a
4 input, there is no real readline support, among other limitations. This is a
5 refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
5 refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
6 """
6 """
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 import atexit
10 import atexit
11 import os
11 import os
12 import signal
12 import signal
13 import sys
13 import sys
14 import uuid
14 import uuid
15
15
16
16
17 from IPython.config.application import boolean_flag
17 from IPython.config.application import boolean_flag
18 from IPython.core.profiledir import ProfileDir
18 from IPython.core.profiledir import ProfileDir
19 from IPython.kernel.blocking import BlockingKernelClient
19 from IPython.kernel.blocking import BlockingKernelClient
20 from IPython.kernel import KernelManager
20 from IPython.kernel import KernelManager
21 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
21 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
22 from IPython.kernel.kernelspec import NoSuchKernel
22 from IPython.kernel.kernelspec import NoSuchKernel
23 from IPython.utils.path import filefind
23 from IPython.utils.path import filefind
24 from IPython.utils.traitlets import (
24 from IPython.utils.traitlets import (
25 Dict, List, Unicode, CUnicode, CBool, Any
25 Dict, List, Unicode, CUnicode, CBool, Any
26 )
26 )
27 from IPython.kernel.zmq.kernelapp import (
27 from IPython.kernel.zmq.kernelapp import (
28 kernel_flags,
28 kernel_flags,
29 kernel_aliases,
29 kernel_aliases,
30 IPKernelApp
30 IPKernelApp
31 )
31 )
32 from IPython.kernel.zmq.pylab.config import InlineBackend
32 from IPython.kernel.zmq.pylab.config import InlineBackend
33 from IPython.kernel.zmq.session import Session, default_secure
33 from IPython.kernel.zmq.session import Session, default_secure
34 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
34 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
35 from IPython.kernel.connect import ConnectionFileMixin
35 from IPython.kernel.connect import ConnectionFileMixin
36
36
37 from IPython.utils.localinterfaces import localhost
37 from IPython.utils.localinterfaces import localhost
38
38
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 # Aliases and Flags
40 # Aliases and Flags
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42
42
43 flags = dict(kernel_flags)
43 flags = dict(kernel_flags)
44
44
45 # the flags that are specific to the frontend
45 # the flags that are specific to the frontend
46 # these must be scrubbed before being passed to the kernel,
46 # these must be scrubbed before being passed to the kernel,
47 # or it will raise an error on unrecognized flags
47 # or it will raise an error on unrecognized flags
48 app_flags = {
48 app_flags = {
49 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
49 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
50 "Connect to an existing kernel. If no argument specified, guess most recent"),
50 "Connect to an existing kernel. If no argument specified, guess most recent"),
51 }
51 }
52 app_flags.update(boolean_flag(
52 app_flags.update(boolean_flag(
53 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
53 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
54 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
54 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
55 to force a direct exit without any confirmation.
55 to force a direct exit without any confirmation.
56 """,
56 """,
57 """Don't prompt the user when exiting. This will terminate the kernel
57 """Don't prompt the user when exiting. This will terminate the kernel
58 if it is owned by the frontend, and leave it alive if it is external.
58 if it is owned by the frontend, and leave it alive if it is external.
59 """
59 """
60 ))
60 ))
61 flags.update(app_flags)
61 flags.update(app_flags)
62
62
63 aliases = dict(kernel_aliases)
63 aliases = dict(kernel_aliases)
64
64
65 # also scrub aliases from the frontend
65 # also scrub aliases from the frontend
66 app_aliases = dict(
66 app_aliases = dict(
67 ip = 'IPythonConsoleApp.ip',
67 ip = 'IPythonConsoleApp.ip',
68 transport = 'IPythonConsoleApp.transport',
68 transport = 'IPythonConsoleApp.transport',
69 hb = 'IPythonConsoleApp.hb_port',
69 hb = 'IPythonConsoleApp.hb_port',
70 shell = 'IPythonConsoleApp.shell_port',
70 shell = 'IPythonConsoleApp.shell_port',
71 iopub = 'IPythonConsoleApp.iopub_port',
71 iopub = 'IPythonConsoleApp.iopub_port',
72 stdin = 'IPythonConsoleApp.stdin_port',
72 stdin = 'IPythonConsoleApp.stdin_port',
73 existing = 'IPythonConsoleApp.existing',
73 existing = 'IPythonConsoleApp.existing',
74 f = 'IPythonConsoleApp.connection_file',
74 f = 'IPythonConsoleApp.connection_file',
75
75
76 kernel = 'IPythonConsoleApp.kernel_name',
76 kernel = 'IPythonConsoleApp.kernel_name',
77
77
78 ssh = 'IPythonConsoleApp.sshserver',
78 ssh = 'IPythonConsoleApp.sshserver',
79 )
79 )
80 aliases.update(app_aliases)
80 aliases.update(app_aliases)
81
81
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83 # Classes
83 # Classes
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85
85
86 classes = [KernelManager, ProfileDir, Session]
86 classes = [KernelManager, ProfileDir, Session]
87
87
88 class IPythonConsoleApp(ConnectionFileMixin):
88 class IPythonConsoleApp(ConnectionFileMixin):
89 name = 'ipython-console-mixin'
89 name = 'ipython-console-mixin'
90
90
91 description = """
91 description = """
92 The IPython Mixin Console.
92 The IPython Mixin Console.
93
93
94 This class contains the common portions of console client (QtConsole,
94 This class contains the common portions of console client (QtConsole,
95 ZMQ-based terminal console, etc). It is not a full console, in that
95 ZMQ-based terminal console, etc). It is not a full console, in that
96 launched terminal subprocesses will not be able to accept input.
96 launched terminal subprocesses will not be able to accept input.
97
97
98 The Console using this mixing supports various extra features beyond
98 The Console using this mixing supports various extra features beyond
99 the single-process Terminal IPython shell, such as connecting to
99 the single-process Terminal IPython shell, such as connecting to
100 existing kernel, via:
100 existing kernel, via:
101
101
102 ipython <appname> --existing
102 ipython <appname> --existing
103
103
104 as well as tunnel via SSH
104 as well as tunnel via SSH
105
105
106 """
106 """
107
107
108 classes = classes
108 classes = classes
109 flags = Dict(flags)
109 flags = Dict(flags)
110 aliases = Dict(aliases)
110 aliases = Dict(aliases)
111 kernel_manager_class = KernelManager
111 kernel_manager_class = KernelManager
112 kernel_client_class = BlockingKernelClient
112 kernel_client_class = BlockingKernelClient
113
113
114 kernel_argv = List(Unicode)
114 kernel_argv = List(Unicode)
115 # frontend flags&aliases to be stripped when building kernel_argv
115 # frontend flags&aliases to be stripped when building kernel_argv
116 frontend_flags = Any(app_flags)
116 frontend_flags = Any(app_flags)
117 frontend_aliases = Any(app_aliases)
117 frontend_aliases = Any(app_aliases)
118
118
119 # create requested profiles by default, if they don't exist:
119 # create requested profiles by default, if they don't exist:
120 auto_create = CBool(True)
120 auto_create = CBool(True)
121 # connection info:
121 # connection info:
122
122
123 sshserver = Unicode('', config=True,
123 sshserver = Unicode('', config=True,
124 help="""The SSH server to use to connect to the kernel.""")
124 help="""The SSH server to use to connect to the kernel.""")
125 sshkey = Unicode('', config=True,
125 sshkey = Unicode('', config=True,
126 help="""Path to the ssh key to use for logging in to the ssh server.""")
126 help="""Path to the ssh key to use for logging in to the ssh server.""")
127
127
128 def _connection_file_default(self):
128 def _connection_file_default(self):
129 return 'kernel-%i.json' % os.getpid()
129 return 'kernel-%i.json' % os.getpid()
130
130
131 existing = CUnicode('', config=True,
131 existing = CUnicode('', config=True,
132 help="""Connect to an already running kernel""")
132 help="""Connect to an already running kernel""")
133
133
134 kernel_name = Unicode('python', config=True,
134 kernel_name = Unicode('python', config=True,
135 help="""The name of the default kernel to start.""")
135 help="""The name of the default kernel to start.""")
136
136
137 confirm_exit = CBool(True, config=True,
137 confirm_exit = CBool(True, config=True,
138 help="""
138 help="""
139 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
139 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
140 to force a direct exit without any confirmation.""",
140 to force a direct exit without any confirmation.""",
141 )
141 )
142
142
143 @property
143 @property
144 def help_classes(self):
144 def help_classes(self):
145 """ConsoleApps can configure kernels on the command-line
145 """ConsoleApps can configure kernels on the command-line
146
146
147 But this shouldn't be written to a file
147 But this shouldn't be written to a file
148 """
148 """
149 return self.classes + [IPKernelApp] + IPKernelApp.classes
149 return self.classes + [IPKernelApp] + IPKernelApp.classes
150
150
151 def build_kernel_argv(self, argv=None):
151 def build_kernel_argv(self, argv=None):
152 """build argv to be passed to kernel subprocess"""
152 """build argv to be passed to kernel subprocess"""
153 if argv is None:
153 if argv is None:
154 argv = sys.argv[1:]
154 argv = sys.argv[1:]
155 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
155 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
156
156
157 def init_connection_file(self):
157 def init_connection_file(self):
158 """find the connection file, and load the info if found.
158 """find the connection file, and load the info if found.
159
159
160 The current working directory and the current profile's security
160 The current working directory and the current profile's security
161 directory will be searched for the file if it is not given by
161 directory will be searched for the file if it is not given by
162 absolute path.
162 absolute path.
163
163
164 When attempting to connect to an existing kernel and the `--existing`
164 When attempting to connect to an existing kernel and the `--existing`
165 argument does not match an existing file, it will be interpreted as a
165 argument does not match an existing file, it will be interpreted as a
166 fileglob, and the matching file in the current profile's security dir
166 fileglob, and the matching file in the current profile's security dir
167 with the latest access time will be used.
167 with the latest access time will be used.
168
168
169 After this method is called, self.connection_file contains the *full path*
169 After this method is called, self.connection_file contains the *full path*
170 to the connection file, never just its name.
170 to the connection file, never just its name.
171 """
171 """
172 if self.existing:
172 if self.existing:
173 try:
173 try:
174 cf = find_connection_file(self.existing)
174 cf = find_connection_file(self.existing)
175 except Exception:
175 except Exception:
176 self.log.critical("Could not find existing kernel connection file %s", self.existing)
176 self.log.critical("Could not find existing kernel connection file %s", self.existing)
177 self.exit(1)
177 self.exit(1)
178 self.log.debug("Connecting to existing kernel: %s" % cf)
178 self.log.debug("Connecting to existing kernel: %s" % cf)
179 self.connection_file = cf
179 self.connection_file = cf
180 else:
180 else:
181 # not existing, check if we are going to write the file
181 # not existing, check if we are going to write the file
182 # and ensure that self.connection_file is a full path, not just the shortname
182 # and ensure that self.connection_file is a full path, not just the shortname
183 try:
183 try:
184 cf = find_connection_file(self.connection_file)
184 cf = find_connection_file(self.connection_file)
185 except Exception:
185 except Exception:
186 # file might not exist
186 # file might not exist
187 if self.connection_file == os.path.basename(self.connection_file):
187 if self.connection_file == os.path.basename(self.connection_file):
188 # just shortname, put it in security dir
188 # just shortname, put it in security dir
189 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
189 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
190 else:
190 else:
191 cf = self.connection_file
191 cf = self.connection_file
192 self.connection_file = cf
192 self.connection_file = cf
193 try:
193 try:
194 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
194 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
195 except IOError:
195 except IOError:
196 self.log.debug("Connection File not found: %s", self.connection_file)
196 self.log.debug("Connection File not found: %s", self.connection_file)
197 return
197 return
198
198
199 # should load_connection_file only be used for existing?
199 # should load_connection_file only be used for existing?
200 # as it is now, this allows reusing ports if an existing
200 # as it is now, this allows reusing ports if an existing
201 # file is requested
201 # file is requested
202 try:
202 try:
203 self.load_connection_file()
203 self.load_connection_file()
204 except Exception:
204 except Exception:
205 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
205 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
206 self.exit(1)
206 self.exit(1)
207
207
208 def init_ssh(self):
208 def init_ssh(self):
209 """set up ssh tunnels, if needed."""
209 """set up ssh tunnels, if needed."""
210 if not self.existing or (not self.sshserver and not self.sshkey):
210 if not self.existing or (not self.sshserver and not self.sshkey):
211 return
211 return
212 self.load_connection_file()
212 self.load_connection_file()
213
213
214 transport = self.transport
214 transport = self.transport
215 ip = self.ip
215 ip = self.ip
216
216
217 if transport != 'tcp':
217 if transport != 'tcp':
218 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
218 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
219 sys.exit(-1)
219 sys.exit(-1)
220
220
221 if self.sshkey and not self.sshserver:
221 if self.sshkey and not self.sshserver:
222 # specifying just the key implies that we are connecting directly
222 # specifying just the key implies that we are connecting directly
223 self.sshserver = ip
223 self.sshserver = ip
224 ip = localhost()
224 ip = localhost()
225
225
226 # build connection dict for tunnels:
226 # build connection dict for tunnels:
227 info = dict(ip=ip,
227 info = dict(ip=ip,
228 shell_port=self.shell_port,
228 shell_port=self.shell_port,
229 iopub_port=self.iopub_port,
229 iopub_port=self.iopub_port,
230 stdin_port=self.stdin_port,
230 stdin_port=self.stdin_port,
231 hb_port=self.hb_port
231 hb_port=self.hb_port
232 )
232 )
233
233
234 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
234 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
235
235
236 # tunnels return a new set of ports, which will be on localhost:
236 # tunnels return a new set of ports, which will be on localhost:
237 self.ip = localhost()
237 self.ip = localhost()
238 try:
238 try:
239 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
239 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
240 except:
240 except:
241 # even catch KeyboardInterrupt
241 # even catch KeyboardInterrupt
242 self.log.error("Could not setup tunnels", exc_info=True)
242 self.log.error("Could not setup tunnels", exc_info=True)
243 self.exit(1)
243 self.exit(1)
244
244
245 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
245 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
246
246
247 cf = self.connection_file
247 cf = self.connection_file
248 base,ext = os.path.splitext(cf)
248 base,ext = os.path.splitext(cf)
249 base = os.path.basename(base)
249 base = os.path.basename(base)
250 self.connection_file = os.path.basename(base)+'-ssh'+ext
250 self.connection_file = os.path.basename(base)+'-ssh'+ext
251 self.log.info("To connect another client via this tunnel, use:")
251 self.log.info("To connect another client via this tunnel, use:")
252 self.log.info("--existing %s" % self.connection_file)
252 self.log.info("--existing %s" % self.connection_file)
253
253
254 def _new_connection_file(self):
254 def _new_connection_file(self):
255 cf = ''
255 cf = ''
256 while not cf:
256 while not cf:
257 # we don't need a 128b id to distinguish kernels, use more readable
257 # we don't need a 128b id to distinguish kernels, use more readable
258 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
258 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
259 # kernels can subclass.
259 # kernels can subclass.
260 ident = str(uuid.uuid4()).split('-')[-1]
260 ident = str(uuid.uuid4()).split('-')[-1]
261 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
261 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
262 # only keep if it's actually new. Protect against unlikely collision
262 # only keep if it's actually new. Protect against unlikely collision
263 # in 48b random search space
263 # in 48b random search space
264 cf = cf if not os.path.exists(cf) else ''
264 cf = cf if not os.path.exists(cf) else ''
265 return cf
265 return cf
266
266
267 def init_kernel_manager(self):
267 def init_kernel_manager(self):
268 # Don't let Qt or ZMQ swallow KeyboardInterupts.
268 # Don't let Qt or ZMQ swallow KeyboardInterupts.
269 if self.existing:
269 if self.existing:
270 self.kernel_manager = None
270 self.kernel_manager = None
271 return
271 return
272 signal.signal(signal.SIGINT, signal.SIG_DFL)
272 signal.signal(signal.SIGINT, signal.SIG_DFL)
273
273
274 # Create a KernelManager and start a kernel.
274 # Create a KernelManager and start a kernel.
275 try:
275 try:
276 self.kernel_manager = self.kernel_manager_class(
276 self.kernel_manager = self.kernel_manager_class(
277 ip=self.ip,
277 ip=self.ip,
278 session=self.session,
278 session=self.session,
279 transport=self.transport,
279 transport=self.transport,
280 shell_port=self.shell_port,
280 shell_port=self.shell_port,
281 iopub_port=self.iopub_port,
281 iopub_port=self.iopub_port,
282 stdin_port=self.stdin_port,
282 stdin_port=self.stdin_port,
283 hb_port=self.hb_port,
283 hb_port=self.hb_port,
284 connection_file=self.connection_file,
284 connection_file=self.connection_file,
285 kernel_name=self.kernel_name,
285 kernel_name=self.kernel_name,
286 parent=self,
286 parent=self,
287 ipython_dir=self.ipython_dir,
287 ipython_dir=self.ipython_dir,
288 )
288 )
289 except NoSuchKernel:
289 except NoSuchKernel:
290 self.log.critical("Could not find kernel %s", self.kernel_name)
290 self.log.critical("Could not find kernel %s", self.kernel_name)
291 self.exit(1)
291 self.exit(1)
292
292
293 self.kernel_manager.client_factory = self.kernel_client_class
293 self.kernel_manager.client_factory = self.kernel_client_class
294 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
294 # FIXME: remove special treatment of IPython kernels
295 kwargs = {}
296 if self.kernel_manager.ipython_kernel:
297 kwargs['extra_arguments'] = self.kernel_argv
298 self.kernel_manager.start_kernel(**kwargs)
295 atexit.register(self.kernel_manager.cleanup_ipc_files)
299 atexit.register(self.kernel_manager.cleanup_ipc_files)
296
300
297 if self.sshserver:
301 if self.sshserver:
298 # ssh, write new connection file
302 # ssh, write new connection file
299 self.kernel_manager.write_connection_file()
303 self.kernel_manager.write_connection_file()
300
304
301 # in case KM defaults / ssh writing changes things:
305 # in case KM defaults / ssh writing changes things:
302 km = self.kernel_manager
306 km = self.kernel_manager
303 self.shell_port=km.shell_port
307 self.shell_port=km.shell_port
304 self.iopub_port=km.iopub_port
308 self.iopub_port=km.iopub_port
305 self.stdin_port=km.stdin_port
309 self.stdin_port=km.stdin_port
306 self.hb_port=km.hb_port
310 self.hb_port=km.hb_port
307 self.connection_file = km.connection_file
311 self.connection_file = km.connection_file
308
312
309 atexit.register(self.kernel_manager.cleanup_connection_file)
313 atexit.register(self.kernel_manager.cleanup_connection_file)
310
314
311 def init_kernel_client(self):
315 def init_kernel_client(self):
312 if self.kernel_manager is not None:
316 if self.kernel_manager is not None:
313 self.kernel_client = self.kernel_manager.client()
317 self.kernel_client = self.kernel_manager.client()
314 else:
318 else:
315 self.kernel_client = self.kernel_client_class(
319 self.kernel_client = self.kernel_client_class(
316 session=self.session,
320 session=self.session,
317 ip=self.ip,
321 ip=self.ip,
318 transport=self.transport,
322 transport=self.transport,
319 shell_port=self.shell_port,
323 shell_port=self.shell_port,
320 iopub_port=self.iopub_port,
324 iopub_port=self.iopub_port,
321 stdin_port=self.stdin_port,
325 stdin_port=self.stdin_port,
322 hb_port=self.hb_port,
326 hb_port=self.hb_port,
323 connection_file=self.connection_file,
327 connection_file=self.connection_file,
324 parent=self,
328 parent=self,
325 )
329 )
326
330
327 self.kernel_client.start_channels()
331 self.kernel_client.start_channels()
328
332
329
333
330
334
331 def initialize(self, argv=None):
335 def initialize(self, argv=None):
332 """
336 """
333 Classes which mix this class in should call:
337 Classes which mix this class in should call:
334 IPythonConsoleApp.initialize(self,argv)
338 IPythonConsoleApp.initialize(self,argv)
335 """
339 """
336 self.init_connection_file()
340 self.init_connection_file()
337 default_secure(self.config)
341 default_secure(self.config)
338 self.init_ssh()
342 self.init_ssh()
339 self.init_kernel_manager()
343 self.init_kernel_manager()
340 self.init_kernel_client()
344 self.init_kernel_client()
341
345
@@ -1,992 +1,993 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
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 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import errno
10 import errno
11 import io
11 import io
12 import json
12 import json
13 import logging
13 import logging
14 import os
14 import os
15 import random
15 import random
16 import re
16 import re
17 import select
17 import select
18 import signal
18 import signal
19 import socket
19 import socket
20 import sys
20 import sys
21 import threading
21 import threading
22 import time
22 import time
23 import webbrowser
23 import webbrowser
24
24
25
25
26 # check for pyzmq 2.1.11
26 # check for pyzmq 2.1.11
27 from IPython.utils.zmqrelated import check_for_zmq
27 from IPython.utils.zmqrelated import check_for_zmq
28 check_for_zmq('2.1.11', 'IPython.html')
28 check_for_zmq('2.1.11', 'IPython.html')
29
29
30 from jinja2 import Environment, FileSystemLoader
30 from jinja2 import Environment, FileSystemLoader
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 ioloop.install()
35 ioloop.install()
36
36
37 # check for tornado 3.1.0
37 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 4.0"
38 msg = "The IPython Notebook requires tornado >= 4.0"
39 try:
39 try:
40 import tornado
40 import tornado
41 except ImportError:
41 except ImportError:
42 raise ImportError(msg)
42 raise ImportError(msg)
43 try:
43 try:
44 version_info = tornado.version_info
44 version_info = tornado.version_info
45 except AttributeError:
45 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
46 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (4,0):
47 if version_info < (4,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49
49
50 from tornado import httpserver
50 from tornado import httpserver
51 from tornado import web
51 from tornado import web
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53
53
54 from IPython.html import (
54 from IPython.html import (
55 DEFAULT_STATIC_FILES_PATH,
55 DEFAULT_STATIC_FILES_PATH,
56 DEFAULT_TEMPLATE_PATH_LIST,
56 DEFAULT_TEMPLATE_PATH_LIST,
57 )
57 )
58 from .base.handlers import Template404
58 from .base.handlers import Template404
59 from .log import log_request
59 from .log import log_request
60 from .services.kernels.kernelmanager import MappingKernelManager
60 from .services.kernels.kernelmanager import MappingKernelManager
61 from .services.contents.manager import ContentsManager
61 from .services.contents.manager import ContentsManager
62 from .services.contents.filemanager import FileContentsManager
62 from .services.contents.filemanager import FileContentsManager
63 from .services.clusters.clustermanager import ClusterManager
63 from .services.clusters.clustermanager import ClusterManager
64 from .services.sessions.sessionmanager import SessionManager
64 from .services.sessions.sessionmanager import SessionManager
65
65
66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
67
67
68 from IPython.config import Config
68 from IPython.config import Config
69 from IPython.config.application import catch_config_error, boolean_flag
69 from IPython.config.application import catch_config_error, boolean_flag
70 from IPython.core.application import (
70 from IPython.core.application import (
71 BaseIPythonApplication, base_flags, base_aliases,
71 BaseIPythonApplication, base_flags, base_aliases,
72 )
72 )
73 from IPython.core.profiledir import ProfileDir
73 from IPython.core.profiledir import ProfileDir
74 from IPython.kernel import KernelManager
74 from IPython.kernel import KernelManager
75 from IPython.kernel.kernelspec import KernelSpecManager
75 from IPython.kernel.kernelspec import KernelSpecManager
76 from IPython.kernel.zmq.session import default_secure, Session
76 from IPython.kernel.zmq.session import default_secure, Session
77 from IPython.nbformat.sign import NotebookNotary
77 from IPython.nbformat.sign import NotebookNotary
78 from IPython.utils.importstring import import_item
78 from IPython.utils.importstring import import_item
79 from IPython.utils import submodule
79 from IPython.utils import submodule
80 from IPython.utils.process import check_pid
80 from IPython.utils.process import check_pid
81 from IPython.utils.traitlets import (
81 from IPython.utils.traitlets import (
82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 DottedObjectName, TraitError,
83 DottedObjectName, TraitError,
84 )
84 )
85 from IPython.utils import py3compat
85 from IPython.utils import py3compat
86 from IPython.utils.path import filefind, get_ipython_dir
86 from IPython.utils.path import filefind, get_ipython_dir
87
87
88 from .utils import url_path_join
88 from .utils import url_path_join
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Module globals
91 # Module globals
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94 _examples = """
94 _examples = """
95 ipython notebook # start the notebook
95 ipython notebook # start the notebook
96 ipython notebook --profile=sympy # use the sympy profile
96 ipython notebook --profile=sympy # use the sympy profile
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 """
98 """
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Helper functions
101 # Helper functions
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 def random_ports(port, n):
104 def random_ports(port, n):
105 """Generate a list of n random ports near the given port.
105 """Generate a list of n random ports near the given port.
106
106
107 The first 5 ports will be sequential, and the remaining n-5 will be
107 The first 5 ports will be sequential, and the remaining n-5 will be
108 randomly selected in the range [port-2*n, port+2*n].
108 randomly selected in the range [port-2*n, port+2*n].
109 """
109 """
110 for i in range(min(5, n)):
110 for i in range(min(5, n)):
111 yield port + i
111 yield port + i
112 for i in range(n-5):
112 for i in range(n-5):
113 yield max(1, port + random.randint(-2*n, 2*n))
113 yield max(1, port + random.randint(-2*n, 2*n))
114
114
115 def load_handlers(name):
115 def load_handlers(name):
116 """Load the (URL pattern, handler) tuples for each component."""
116 """Load the (URL pattern, handler) tuples for each component."""
117 name = 'IPython.html.' + name
117 name = 'IPython.html.' + name
118 mod = __import__(name, fromlist=['default_handlers'])
118 mod = __import__(name, fromlist=['default_handlers'])
119 return mod.default_handlers
119 return mod.default_handlers
120
120
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122 # The Tornado web application
122 # The Tornado web application
123 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
124
124
125 class NotebookWebApplication(web.Application):
125 class NotebookWebApplication(web.Application):
126
126
127 def __init__(self, ipython_app, kernel_manager, contents_manager,
127 def __init__(self, ipython_app, kernel_manager, contents_manager,
128 cluster_manager, session_manager, kernel_spec_manager, log,
128 cluster_manager, session_manager, kernel_spec_manager, log,
129 base_url, default_url, settings_overrides, jinja_env_options):
129 base_url, default_url, settings_overrides, jinja_env_options):
130
130
131 settings = self.init_settings(
131 settings = self.init_settings(
132 ipython_app, kernel_manager, contents_manager, cluster_manager,
132 ipython_app, kernel_manager, contents_manager, cluster_manager,
133 session_manager, kernel_spec_manager, log, base_url, default_url,
133 session_manager, kernel_spec_manager, log, base_url, default_url,
134 settings_overrides, jinja_env_options)
134 settings_overrides, jinja_env_options)
135 handlers = self.init_handlers(settings)
135 handlers = self.init_handlers(settings)
136
136
137 super(NotebookWebApplication, self).__init__(handlers, **settings)
137 super(NotebookWebApplication, self).__init__(handlers, **settings)
138
138
139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
140 cluster_manager, session_manager, kernel_spec_manager,
140 cluster_manager, session_manager, kernel_spec_manager,
141 log, base_url, default_url, settings_overrides,
141 log, base_url, default_url, settings_overrides,
142 jinja_env_options=None):
142 jinja_env_options=None):
143
143
144 _template_path = settings_overrides.get(
144 _template_path = settings_overrides.get(
145 "template_path",
145 "template_path",
146 ipython_app.template_file_path,
146 ipython_app.template_file_path,
147 )
147 )
148 if isinstance(_template_path, str):
148 if isinstance(_template_path, str):
149 _template_path = (_template_path,)
149 _template_path = (_template_path,)
150 template_path = [os.path.expanduser(path) for path in _template_path]
150 template_path = [os.path.expanduser(path) for path in _template_path]
151
151
152 jenv_opt = jinja_env_options if jinja_env_options else {}
152 jenv_opt = jinja_env_options if jinja_env_options else {}
153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
154 settings = dict(
154 settings = dict(
155 # basics
155 # basics
156 log_function=log_request,
156 log_function=log_request,
157 base_url=base_url,
157 base_url=base_url,
158 default_url=default_url,
158 default_url=default_url,
159 template_path=template_path,
159 template_path=template_path,
160 static_path=ipython_app.static_file_path,
160 static_path=ipython_app.static_file_path,
161 static_handler_class = FileFindHandler,
161 static_handler_class = FileFindHandler,
162 static_url_prefix = url_path_join(base_url,'/static/'),
162 static_url_prefix = url_path_join(base_url,'/static/'),
163
163
164 # authentication
164 # authentication
165 cookie_secret=ipython_app.cookie_secret,
165 cookie_secret=ipython_app.cookie_secret,
166 login_url=url_path_join(base_url,'/login'),
166 login_url=url_path_join(base_url,'/login'),
167 password=ipython_app.password,
167 password=ipython_app.password,
168
168
169 # managers
169 # managers
170 kernel_manager=kernel_manager,
170 kernel_manager=kernel_manager,
171 contents_manager=contents_manager,
171 contents_manager=contents_manager,
172 cluster_manager=cluster_manager,
172 cluster_manager=cluster_manager,
173 session_manager=session_manager,
173 session_manager=session_manager,
174 kernel_spec_manager=kernel_spec_manager,
174 kernel_spec_manager=kernel_spec_manager,
175
175
176 # IPython stuff
176 # IPython stuff
177 nbextensions_path = ipython_app.nbextensions_path,
177 nbextensions_path = ipython_app.nbextensions_path,
178 websocket_url=ipython_app.websocket_url,
178 websocket_url=ipython_app.websocket_url,
179 mathjax_url=ipython_app.mathjax_url,
179 mathjax_url=ipython_app.mathjax_url,
180 config=ipython_app.config,
180 config=ipython_app.config,
181 jinja2_env=env,
181 jinja2_env=env,
182 terminals_available=False, # Set later if terminals are available
182 terminals_available=False, # Set later if terminals are available
183 profile_dir = ipython_app.profile_dir.location,
183 profile_dir = ipython_app.profile_dir.location,
184 )
184 )
185
185
186 # allow custom overrides for the tornado web app.
186 # allow custom overrides for the tornado web app.
187 settings.update(settings_overrides)
187 settings.update(settings_overrides)
188 return settings
188 return settings
189
189
190 def init_handlers(self, settings):
190 def init_handlers(self, settings):
191 """Load the (URL pattern, handler) tuples for each component."""
191 """Load the (URL pattern, handler) tuples for each component."""
192
192
193 # Order matters. The first handler to match the URL will handle the request.
193 # Order matters. The first handler to match the URL will handle the request.
194 handlers = []
194 handlers = []
195 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('tree.handlers'))
196 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.login'))
197 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('auth.logout'))
198 handlers.extend(load_handlers('files.handlers'))
198 handlers.extend(load_handlers('files.handlers'))
199 handlers.extend(load_handlers('notebook.handlers'))
199 handlers.extend(load_handlers('notebook.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
201 handlers.extend(load_handlers('kernelspecs.handlers'))
201 handlers.extend(load_handlers('kernelspecs.handlers'))
202 handlers.extend(load_handlers('services.config.handlers'))
202 handlers.extend(load_handlers('services.config.handlers'))
203 handlers.extend(load_handlers('services.kernels.handlers'))
203 handlers.extend(load_handlers('services.kernels.handlers'))
204 handlers.extend(load_handlers('services.contents.handlers'))
204 handlers.extend(load_handlers('services.contents.handlers'))
205 handlers.extend(load_handlers('services.clusters.handlers'))
205 handlers.extend(load_handlers('services.clusters.handlers'))
206 handlers.extend(load_handlers('services.sessions.handlers'))
206 handlers.extend(load_handlers('services.sessions.handlers'))
207 handlers.extend(load_handlers('services.nbconvert.handlers'))
207 handlers.extend(load_handlers('services.nbconvert.handlers'))
208 handlers.extend(load_handlers('services.kernelspecs.handlers'))
208 handlers.extend(load_handlers('services.kernelspecs.handlers'))
209 handlers.append(
209 handlers.append(
210 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
210 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
211 )
211 )
212 # register base handlers last
212 # register base handlers last
213 handlers.extend(load_handlers('base.handlers'))
213 handlers.extend(load_handlers('base.handlers'))
214 # set the URL that will be redirected from `/`
214 # set the URL that will be redirected from `/`
215 handlers.append(
215 handlers.append(
216 (r'/?', web.RedirectHandler, {
216 (r'/?', web.RedirectHandler, {
217 'url' : url_path_join(settings['base_url'], settings['default_url']),
217 'url' : url_path_join(settings['base_url'], settings['default_url']),
218 'permanent': False, # want 302, not 301
218 'permanent': False, # want 302, not 301
219 })
219 })
220 )
220 )
221 # prepend base_url onto the patterns that we match
221 # prepend base_url onto the patterns that we match
222 new_handlers = []
222 new_handlers = []
223 for handler in handlers:
223 for handler in handlers:
224 pattern = url_path_join(settings['base_url'], handler[0])
224 pattern = url_path_join(settings['base_url'], handler[0])
225 new_handler = tuple([pattern] + list(handler[1:]))
225 new_handler = tuple([pattern] + list(handler[1:]))
226 new_handlers.append(new_handler)
226 new_handlers.append(new_handler)
227 # add 404 on the end, which will catch everything that falls through
227 # add 404 on the end, which will catch everything that falls through
228 new_handlers.append((r'(.*)', Template404))
228 new_handlers.append((r'(.*)', Template404))
229 return new_handlers
229 return new_handlers
230
230
231
231
232 class NbserverListApp(BaseIPythonApplication):
232 class NbserverListApp(BaseIPythonApplication):
233
233
234 description="List currently running notebook servers in this profile."
234 description="List currently running notebook servers in this profile."
235
235
236 flags = dict(
236 flags = dict(
237 json=({'NbserverListApp': {'json': True}},
237 json=({'NbserverListApp': {'json': True}},
238 "Produce machine-readable JSON output."),
238 "Produce machine-readable JSON output."),
239 )
239 )
240
240
241 json = Bool(False, config=True,
241 json = Bool(False, config=True,
242 help="If True, each line of output will be a JSON object with the "
242 help="If True, each line of output will be a JSON object with the "
243 "details from the server info file.")
243 "details from the server info file.")
244
244
245 def start(self):
245 def start(self):
246 if not self.json:
246 if not self.json:
247 print("Currently running servers:")
247 print("Currently running servers:")
248 for serverinfo in list_running_servers(self.profile):
248 for serverinfo in list_running_servers(self.profile):
249 if self.json:
249 if self.json:
250 print(json.dumps(serverinfo))
250 print(json.dumps(serverinfo))
251 else:
251 else:
252 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
252 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
253
253
254 #-----------------------------------------------------------------------------
254 #-----------------------------------------------------------------------------
255 # Aliases and Flags
255 # Aliases and Flags
256 #-----------------------------------------------------------------------------
256 #-----------------------------------------------------------------------------
257
257
258 flags = dict(base_flags)
258 flags = dict(base_flags)
259 flags['no-browser']=(
259 flags['no-browser']=(
260 {'NotebookApp' : {'open_browser' : False}},
260 {'NotebookApp' : {'open_browser' : False}},
261 "Don't open the notebook in a browser after startup."
261 "Don't open the notebook in a browser after startup."
262 )
262 )
263 flags['pylab']=(
263 flags['pylab']=(
264 {'NotebookApp' : {'pylab' : 'warn'}},
264 {'NotebookApp' : {'pylab' : 'warn'}},
265 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
265 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
266 )
266 )
267 flags['no-mathjax']=(
267 flags['no-mathjax']=(
268 {'NotebookApp' : {'enable_mathjax' : False}},
268 {'NotebookApp' : {'enable_mathjax' : False}},
269 """Disable MathJax
269 """Disable MathJax
270
270
271 MathJax is the javascript library IPython uses to render math/LaTeX. It is
271 MathJax is the javascript library IPython uses to render math/LaTeX. It is
272 very large, so you may want to disable it if you have a slow internet
272 very large, so you may want to disable it if you have a slow internet
273 connection, or for offline use of the notebook.
273 connection, or for offline use of the notebook.
274
274
275 When disabled, equations etc. will appear as their untransformed TeX source.
275 When disabled, equations etc. will appear as their untransformed TeX source.
276 """
276 """
277 )
277 )
278
278
279 # Add notebook manager flags
279 # Add notebook manager flags
280 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
280 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
281 'DEPRECATED, IGNORED',
281 'DEPRECATED, IGNORED',
282 'DEPRECATED, IGNORED'))
282 'DEPRECATED, IGNORED'))
283
283
284 aliases = dict(base_aliases)
284 aliases = dict(base_aliases)
285
285
286 aliases.update({
286 aliases.update({
287 'ip': 'NotebookApp.ip',
287 'ip': 'NotebookApp.ip',
288 'port': 'NotebookApp.port',
288 'port': 'NotebookApp.port',
289 'port-retries': 'NotebookApp.port_retries',
289 'port-retries': 'NotebookApp.port_retries',
290 'transport': 'KernelManager.transport',
290 'transport': 'KernelManager.transport',
291 'keyfile': 'NotebookApp.keyfile',
291 'keyfile': 'NotebookApp.keyfile',
292 'certfile': 'NotebookApp.certfile',
292 'certfile': 'NotebookApp.certfile',
293 'notebook-dir': 'NotebookApp.notebook_dir',
293 'notebook-dir': 'NotebookApp.notebook_dir',
294 'browser': 'NotebookApp.browser',
294 'browser': 'NotebookApp.browser',
295 'pylab': 'NotebookApp.pylab',
295 'pylab': 'NotebookApp.pylab',
296 })
296 })
297
297
298 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
299 # NotebookApp
299 # NotebookApp
300 #-----------------------------------------------------------------------------
300 #-----------------------------------------------------------------------------
301
301
302 class NotebookApp(BaseIPythonApplication):
302 class NotebookApp(BaseIPythonApplication):
303
303
304 name = 'ipython-notebook'
304 name = 'ipython-notebook'
305
305
306 description = """
306 description = """
307 The IPython HTML Notebook.
307 The IPython HTML Notebook.
308
308
309 This launches a Tornado based HTML Notebook Server that serves up an
309 This launches a Tornado based HTML Notebook Server that serves up an
310 HTML5/Javascript Notebook client.
310 HTML5/Javascript Notebook client.
311 """
311 """
312 examples = _examples
312 examples = _examples
313 aliases = aliases
313 aliases = aliases
314 flags = flags
314 flags = flags
315
315
316 classes = [
316 classes = [
317 KernelManager, ProfileDir, Session, MappingKernelManager,
317 KernelManager, ProfileDir, Session, MappingKernelManager,
318 ContentsManager, FileContentsManager, NotebookNotary,
318 ContentsManager, FileContentsManager, NotebookNotary,
319 ]
319 ]
320 flags = Dict(flags)
320 flags = Dict(flags)
321 aliases = Dict(aliases)
321 aliases = Dict(aliases)
322
322
323 subcommands = dict(
323 subcommands = dict(
324 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
324 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
325 )
325 )
326
326
327 kernel_argv = List(Unicode)
327 ipython_kernel_argv = List(Unicode)
328
328
329 _log_formatter_cls = LogFormatter
329 _log_formatter_cls = LogFormatter
330
330
331 def _log_level_default(self):
331 def _log_level_default(self):
332 return logging.INFO
332 return logging.INFO
333
333
334 def _log_datefmt_default(self):
334 def _log_datefmt_default(self):
335 """Exclude date from default date format"""
335 """Exclude date from default date format"""
336 return "%H:%M:%S"
336 return "%H:%M:%S"
337
337
338 def _log_format_default(self):
338 def _log_format_default(self):
339 """override default log format to include time"""
339 """override default log format to include time"""
340 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
340 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
341
341
342 # create requested profiles by default, if they don't exist:
342 # create requested profiles by default, if they don't exist:
343 auto_create = Bool(True)
343 auto_create = Bool(True)
344
344
345 # file to be opened in the notebook server
345 # file to be opened in the notebook server
346 file_to_run = Unicode('', config=True)
346 file_to_run = Unicode('', config=True)
347 def _file_to_run_changed(self, name, old, new):
347 def _file_to_run_changed(self, name, old, new):
348 path, base = os.path.split(new)
348 path, base = os.path.split(new)
349 if path:
349 if path:
350 self.file_to_run = base
350 self.file_to_run = base
351 self.notebook_dir = path
351 self.notebook_dir = path
352
352
353 # Network related information
353 # Network related information
354
354
355 allow_origin = Unicode('', config=True,
355 allow_origin = Unicode('', config=True,
356 help="""Set the Access-Control-Allow-Origin header
356 help="""Set the Access-Control-Allow-Origin header
357
357
358 Use '*' to allow any origin to access your server.
358 Use '*' to allow any origin to access your server.
359
359
360 Takes precedence over allow_origin_pat.
360 Takes precedence over allow_origin_pat.
361 """
361 """
362 )
362 )
363
363
364 allow_origin_pat = Unicode('', config=True,
364 allow_origin_pat = Unicode('', config=True,
365 help="""Use a regular expression for the Access-Control-Allow-Origin header
365 help="""Use a regular expression for the Access-Control-Allow-Origin header
366
366
367 Requests from an origin matching the expression will get replies with:
367 Requests from an origin matching the expression will get replies with:
368
368
369 Access-Control-Allow-Origin: origin
369 Access-Control-Allow-Origin: origin
370
370
371 where `origin` is the origin of the request.
371 where `origin` is the origin of the request.
372
372
373 Ignored if allow_origin is set.
373 Ignored if allow_origin is set.
374 """
374 """
375 )
375 )
376
376
377 allow_credentials = Bool(False, config=True,
377 allow_credentials = Bool(False, config=True,
378 help="Set the Access-Control-Allow-Credentials: true header"
378 help="Set the Access-Control-Allow-Credentials: true header"
379 )
379 )
380
380
381 default_url = Unicode('/tree', config=True,
381 default_url = Unicode('/tree', config=True,
382 help="The default URL to redirect to from `/`"
382 help="The default URL to redirect to from `/`"
383 )
383 )
384
384
385 ip = Unicode('localhost', config=True,
385 ip = Unicode('localhost', config=True,
386 help="The IP address the notebook server will listen on."
386 help="The IP address the notebook server will listen on."
387 )
387 )
388
388
389 def _ip_changed(self, name, old, new):
389 def _ip_changed(self, name, old, new):
390 if new == u'*': self.ip = u''
390 if new == u'*': self.ip = u''
391
391
392 port = Integer(8888, config=True,
392 port = Integer(8888, config=True,
393 help="The port the notebook server will listen on."
393 help="The port the notebook server will listen on."
394 )
394 )
395 port_retries = Integer(50, config=True,
395 port_retries = Integer(50, config=True,
396 help="The number of additional ports to try if the specified port is not available."
396 help="The number of additional ports to try if the specified port is not available."
397 )
397 )
398
398
399 certfile = Unicode(u'', config=True,
399 certfile = Unicode(u'', config=True,
400 help="""The full path to an SSL/TLS certificate file."""
400 help="""The full path to an SSL/TLS certificate file."""
401 )
401 )
402
402
403 keyfile = Unicode(u'', config=True,
403 keyfile = Unicode(u'', config=True,
404 help="""The full path to a private key file for usage with SSL/TLS."""
404 help="""The full path to a private key file for usage with SSL/TLS."""
405 )
405 )
406
406
407 cookie_secret_file = Unicode(config=True,
407 cookie_secret_file = Unicode(config=True,
408 help="""The file where the cookie secret is stored."""
408 help="""The file where the cookie secret is stored."""
409 )
409 )
410 def _cookie_secret_file_default(self):
410 def _cookie_secret_file_default(self):
411 if self.profile_dir is None:
411 if self.profile_dir is None:
412 return ''
412 return ''
413 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
413 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
414
414
415 cookie_secret = Bytes(b'', config=True,
415 cookie_secret = Bytes(b'', config=True,
416 help="""The random bytes used to secure cookies.
416 help="""The random bytes used to secure cookies.
417 By default this is a new random number every time you start the Notebook.
417 By default this is a new random number every time you start the Notebook.
418 Set it to a value in a config file to enable logins to persist across server sessions.
418 Set it to a value in a config file to enable logins to persist across server sessions.
419
419
420 Note: Cookie secrets should be kept private, do not share config files with
420 Note: Cookie secrets should be kept private, do not share config files with
421 cookie_secret stored in plaintext (you can read the value from a file).
421 cookie_secret stored in plaintext (you can read the value from a file).
422 """
422 """
423 )
423 )
424 def _cookie_secret_default(self):
424 def _cookie_secret_default(self):
425 if os.path.exists(self.cookie_secret_file):
425 if os.path.exists(self.cookie_secret_file):
426 with io.open(self.cookie_secret_file, 'rb') as f:
426 with io.open(self.cookie_secret_file, 'rb') as f:
427 return f.read()
427 return f.read()
428 else:
428 else:
429 secret = base64.encodestring(os.urandom(1024))
429 secret = base64.encodestring(os.urandom(1024))
430 self._write_cookie_secret_file(secret)
430 self._write_cookie_secret_file(secret)
431 return secret
431 return secret
432
432
433 def _write_cookie_secret_file(self, secret):
433 def _write_cookie_secret_file(self, secret):
434 """write my secret to my secret_file"""
434 """write my secret to my secret_file"""
435 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
435 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
436 with io.open(self.cookie_secret_file, 'wb') as f:
436 with io.open(self.cookie_secret_file, 'wb') as f:
437 f.write(secret)
437 f.write(secret)
438 try:
438 try:
439 os.chmod(self.cookie_secret_file, 0o600)
439 os.chmod(self.cookie_secret_file, 0o600)
440 except OSError:
440 except OSError:
441 self.log.warn(
441 self.log.warn(
442 "Could not set permissions on %s",
442 "Could not set permissions on %s",
443 self.cookie_secret_file
443 self.cookie_secret_file
444 )
444 )
445
445
446 password = Unicode(u'', config=True,
446 password = Unicode(u'', config=True,
447 help="""Hashed password to use for web authentication.
447 help="""Hashed password to use for web authentication.
448
448
449 To generate, type in a python/IPython shell:
449 To generate, type in a python/IPython shell:
450
450
451 from IPython.lib import passwd; passwd()
451 from IPython.lib import passwd; passwd()
452
452
453 The string should be of the form type:salt:hashed-password.
453 The string should be of the form type:salt:hashed-password.
454 """
454 """
455 )
455 )
456
456
457 open_browser = Bool(True, config=True,
457 open_browser = Bool(True, config=True,
458 help="""Whether to open in a browser after starting.
458 help="""Whether to open in a browser after starting.
459 The specific browser used is platform dependent and
459 The specific browser used is platform dependent and
460 determined by the python standard library `webbrowser`
460 determined by the python standard library `webbrowser`
461 module, unless it is overridden using the --browser
461 module, unless it is overridden using the --browser
462 (NotebookApp.browser) configuration option.
462 (NotebookApp.browser) configuration option.
463 """)
463 """)
464
464
465 browser = Unicode(u'', config=True,
465 browser = Unicode(u'', config=True,
466 help="""Specify what command to use to invoke a web
466 help="""Specify what command to use to invoke a web
467 browser when opening the notebook. If not specified, the
467 browser when opening the notebook. If not specified, the
468 default browser will be determined by the `webbrowser`
468 default browser will be determined by the `webbrowser`
469 standard library module, which allows setting of the
469 standard library module, which allows setting of the
470 BROWSER environment variable to override it.
470 BROWSER environment variable to override it.
471 """)
471 """)
472
472
473 webapp_settings = Dict(config=True,
473 webapp_settings = Dict(config=True,
474 help="DEPRECATED, use tornado_settings"
474 help="DEPRECATED, use tornado_settings"
475 )
475 )
476 def _webapp_settings_changed(self, name, old, new):
476 def _webapp_settings_changed(self, name, old, new):
477 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
477 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
478 self.tornado_settings = new
478 self.tornado_settings = new
479
479
480 tornado_settings = Dict(config=True,
480 tornado_settings = Dict(config=True,
481 help="Supply overrides for the tornado.web.Application that the "
481 help="Supply overrides for the tornado.web.Application that the "
482 "IPython notebook uses.")
482 "IPython notebook uses.")
483
483
484 jinja_environment_options = Dict(config=True,
484 jinja_environment_options = Dict(config=True,
485 help="Supply extra arguments that will be passed to Jinja environment.")
485 help="Supply extra arguments that will be passed to Jinja environment.")
486
486
487
487
488 enable_mathjax = Bool(True, config=True,
488 enable_mathjax = Bool(True, config=True,
489 help="""Whether to enable MathJax for typesetting math/TeX
489 help="""Whether to enable MathJax for typesetting math/TeX
490
490
491 MathJax is the javascript library IPython uses to render math/LaTeX. It is
491 MathJax is the javascript library IPython uses to render math/LaTeX. It is
492 very large, so you may want to disable it if you have a slow internet
492 very large, so you may want to disable it if you have a slow internet
493 connection, or for offline use of the notebook.
493 connection, or for offline use of the notebook.
494
494
495 When disabled, equations etc. will appear as their untransformed TeX source.
495 When disabled, equations etc. will appear as their untransformed TeX source.
496 """
496 """
497 )
497 )
498 def _enable_mathjax_changed(self, name, old, new):
498 def _enable_mathjax_changed(self, name, old, new):
499 """set mathjax url to empty if mathjax is disabled"""
499 """set mathjax url to empty if mathjax is disabled"""
500 if not new:
500 if not new:
501 self.mathjax_url = u''
501 self.mathjax_url = u''
502
502
503 base_url = Unicode('/', config=True,
503 base_url = Unicode('/', config=True,
504 help='''The base URL for the notebook server.
504 help='''The base URL for the notebook server.
505
505
506 Leading and trailing slashes can be omitted,
506 Leading and trailing slashes can be omitted,
507 and will automatically be added.
507 and will automatically be added.
508 ''')
508 ''')
509 def _base_url_changed(self, name, old, new):
509 def _base_url_changed(self, name, old, new):
510 if not new.startswith('/'):
510 if not new.startswith('/'):
511 self.base_url = '/'+new
511 self.base_url = '/'+new
512 elif not new.endswith('/'):
512 elif not new.endswith('/'):
513 self.base_url = new+'/'
513 self.base_url = new+'/'
514
514
515 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
515 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
516 def _base_project_url_changed(self, name, old, new):
516 def _base_project_url_changed(self, name, old, new):
517 self.log.warn("base_project_url is deprecated, use base_url")
517 self.log.warn("base_project_url is deprecated, use base_url")
518 self.base_url = new
518 self.base_url = new
519
519
520 extra_static_paths = List(Unicode, config=True,
520 extra_static_paths = List(Unicode, config=True,
521 help="""Extra paths to search for serving static files.
521 help="""Extra paths to search for serving static files.
522
522
523 This allows adding javascript/css to be available from the notebook server machine,
523 This allows adding javascript/css to be available from the notebook server machine,
524 or overriding individual files in the IPython"""
524 or overriding individual files in the IPython"""
525 )
525 )
526 def _extra_static_paths_default(self):
526 def _extra_static_paths_default(self):
527 return [os.path.join(self.profile_dir.location, 'static')]
527 return [os.path.join(self.profile_dir.location, 'static')]
528
528
529 @property
529 @property
530 def static_file_path(self):
530 def static_file_path(self):
531 """return extra paths + the default location"""
531 """return extra paths + the default location"""
532 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
532 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
533
533
534 extra_template_paths = List(Unicode, config=True,
534 extra_template_paths = List(Unicode, config=True,
535 help="""Extra paths to search for serving jinja templates.
535 help="""Extra paths to search for serving jinja templates.
536
536
537 Can be used to override templates from IPython.html.templates."""
537 Can be used to override templates from IPython.html.templates."""
538 )
538 )
539 def _extra_template_paths_default(self):
539 def _extra_template_paths_default(self):
540 return []
540 return []
541
541
542 @property
542 @property
543 def template_file_path(self):
543 def template_file_path(self):
544 """return extra paths + the default locations"""
544 """return extra paths + the default locations"""
545 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
545 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
546
546
547 nbextensions_path = List(Unicode, config=True,
547 nbextensions_path = List(Unicode, config=True,
548 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
548 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
549 )
549 )
550 def _nbextensions_path_default(self):
550 def _nbextensions_path_default(self):
551 return [os.path.join(get_ipython_dir(), 'nbextensions')]
551 return [os.path.join(get_ipython_dir(), 'nbextensions')]
552
552
553 websocket_url = Unicode("", config=True,
553 websocket_url = Unicode("", config=True,
554 help="""The base URL for websockets,
554 help="""The base URL for websockets,
555 if it differs from the HTTP server (hint: it almost certainly doesn't).
555 if it differs from the HTTP server (hint: it almost certainly doesn't).
556
556
557 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
557 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
558 """
558 """
559 )
559 )
560 mathjax_url = Unicode("", config=True,
560 mathjax_url = Unicode("", config=True,
561 help="""The url for MathJax.js."""
561 help="""The url for MathJax.js."""
562 )
562 )
563 def _mathjax_url_default(self):
563 def _mathjax_url_default(self):
564 if not self.enable_mathjax:
564 if not self.enable_mathjax:
565 return u''
565 return u''
566 static_url_prefix = self.tornado_settings.get("static_url_prefix",
566 static_url_prefix = self.tornado_settings.get("static_url_prefix",
567 url_path_join(self.base_url, "static")
567 url_path_join(self.base_url, "static")
568 )
568 )
569
569
570 # try local mathjax, either in nbextensions/mathjax or static/mathjax
570 # try local mathjax, either in nbextensions/mathjax or static/mathjax
571 for (url_prefix, search_path) in [
571 for (url_prefix, search_path) in [
572 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
572 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
573 (static_url_prefix, self.static_file_path),
573 (static_url_prefix, self.static_file_path),
574 ]:
574 ]:
575 self.log.debug("searching for local mathjax in %s", search_path)
575 self.log.debug("searching for local mathjax in %s", search_path)
576 try:
576 try:
577 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
577 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
578 except IOError:
578 except IOError:
579 continue
579 continue
580 else:
580 else:
581 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
581 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
582 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
582 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
583 return url
583 return url
584
584
585 # no local mathjax, serve from CDN
585 # no local mathjax, serve from CDN
586 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
586 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
587 self.log.info("Using MathJax from CDN: %s", url)
587 self.log.info("Using MathJax from CDN: %s", url)
588 return url
588 return url
589
589
590 def _mathjax_url_changed(self, name, old, new):
590 def _mathjax_url_changed(self, name, old, new):
591 if new and not self.enable_mathjax:
591 if new and not self.enable_mathjax:
592 # enable_mathjax=False overrides mathjax_url
592 # enable_mathjax=False overrides mathjax_url
593 self.mathjax_url = u''
593 self.mathjax_url = u''
594 else:
594 else:
595 self.log.info("Using MathJax: %s", new)
595 self.log.info("Using MathJax: %s", new)
596
596
597 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
597 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
598 config=True,
598 config=True,
599 help='The notebook manager class to use.'
599 help='The notebook manager class to use.'
600 )
600 )
601 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
601 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
602 config=True,
602 config=True,
603 help='The kernel manager class to use.'
603 help='The kernel manager class to use.'
604 )
604 )
605 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
605 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
606 config=True,
606 config=True,
607 help='The session manager class to use.'
607 help='The session manager class to use.'
608 )
608 )
609 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
609 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
610 config=True,
610 config=True,
611 help='The cluster manager class to use.'
611 help='The cluster manager class to use.'
612 )
612 )
613
613
614 kernel_spec_manager = Instance(KernelSpecManager)
614 kernel_spec_manager = Instance(KernelSpecManager)
615
615
616 def _kernel_spec_manager_default(self):
616 def _kernel_spec_manager_default(self):
617 return KernelSpecManager(ipython_dir=self.ipython_dir)
617 return KernelSpecManager(ipython_dir=self.ipython_dir)
618
618
619 trust_xheaders = Bool(False, config=True,
619 trust_xheaders = Bool(False, config=True,
620 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
620 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
621 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
621 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
622 )
622 )
623
623
624 info_file = Unicode()
624 info_file = Unicode()
625
625
626 def _info_file_default(self):
626 def _info_file_default(self):
627 info_file = "nbserver-%s.json"%os.getpid()
627 info_file = "nbserver-%s.json"%os.getpid()
628 return os.path.join(self.profile_dir.security_dir, info_file)
628 return os.path.join(self.profile_dir.security_dir, info_file)
629
629
630 notebook_dir = Unicode(py3compat.getcwd(), config=True,
630 notebook_dir = Unicode(py3compat.getcwd(), config=True,
631 help="The directory to use for notebooks and kernels."
631 help="The directory to use for notebooks and kernels."
632 )
632 )
633
633
634 pylab = Unicode('disabled', config=True,
634 pylab = Unicode('disabled', config=True,
635 help="""
635 help="""
636 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
636 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
637 """
637 """
638 )
638 )
639 def _pylab_changed(self, name, old, new):
639 def _pylab_changed(self, name, old, new):
640 """when --pylab is specified, display a warning and exit"""
640 """when --pylab is specified, display a warning and exit"""
641 if new != 'warn':
641 if new != 'warn':
642 backend = ' %s' % new
642 backend = ' %s' % new
643 else:
643 else:
644 backend = ''
644 backend = ''
645 self.log.error("Support for specifying --pylab on the command line has been removed.")
645 self.log.error("Support for specifying --pylab on the command line has been removed.")
646 self.log.error(
646 self.log.error(
647 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
647 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
648 )
648 )
649 self.exit(1)
649 self.exit(1)
650
650
651 def _notebook_dir_changed(self, name, old, new):
651 def _notebook_dir_changed(self, name, old, new):
652 """Do a bit of validation of the notebook dir."""
652 """Do a bit of validation of the notebook dir."""
653 if not os.path.isabs(new):
653 if not os.path.isabs(new):
654 # If we receive a non-absolute path, make it absolute.
654 # If we receive a non-absolute path, make it absolute.
655 self.notebook_dir = os.path.abspath(new)
655 self.notebook_dir = os.path.abspath(new)
656 return
656 return
657 if not os.path.isdir(new):
657 if not os.path.isdir(new):
658 raise TraitError("No such notebook dir: %r" % new)
658 raise TraitError("No such notebook dir: %r" % new)
659
659
660 # setting App.notebook_dir implies setting notebook and kernel dirs as well
660 # setting App.notebook_dir implies setting notebook and kernel dirs as well
661 self.config.FileContentsManager.root_dir = new
661 self.config.FileContentsManager.root_dir = new
662 self.config.MappingKernelManager.root_dir = new
662 self.config.MappingKernelManager.root_dir = new
663
663
664
664
665 def parse_command_line(self, argv=None):
665 def parse_command_line(self, argv=None):
666 super(NotebookApp, self).parse_command_line(argv)
666 super(NotebookApp, self).parse_command_line(argv)
667
667
668 if self.extra_args:
668 if self.extra_args:
669 arg0 = self.extra_args[0]
669 arg0 = self.extra_args[0]
670 f = os.path.abspath(arg0)
670 f = os.path.abspath(arg0)
671 self.argv.remove(arg0)
671 self.argv.remove(arg0)
672 if not os.path.exists(f):
672 if not os.path.exists(f):
673 self.log.critical("No such file or directory: %s", f)
673 self.log.critical("No such file or directory: %s", f)
674 self.exit(1)
674 self.exit(1)
675
675
676 # Use config here, to ensure that it takes higher priority than
676 # Use config here, to ensure that it takes higher priority than
677 # anything that comes from the profile.
677 # anything that comes from the profile.
678 c = Config()
678 c = Config()
679 if os.path.isdir(f):
679 if os.path.isdir(f):
680 c.NotebookApp.notebook_dir = f
680 c.NotebookApp.notebook_dir = f
681 elif os.path.isfile(f):
681 elif os.path.isfile(f):
682 c.NotebookApp.file_to_run = f
682 c.NotebookApp.file_to_run = f
683 self.update_config(c)
683 self.update_config(c)
684
684
685 def init_kernel_argv(self):
685 def init_kernel_argv(self):
686 """construct the kernel arguments"""
686 """add the profile-dir to arguments to be passed to IPython kernels"""
687 # FIXME: remove special treatment of IPython kernels
687 # Kernel should get *absolute* path to profile directory
688 # Kernel should get *absolute* path to profile directory
688 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
689 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
689
690
690 def init_configurables(self):
691 def init_configurables(self):
691 # force Session default to be secure
692 # force Session default to be secure
692 default_secure(self.config)
693 default_secure(self.config)
693 kls = import_item(self.kernel_manager_class)
694 kls = import_item(self.kernel_manager_class)
694 self.kernel_manager = kls(
695 self.kernel_manager = kls(
695 parent=self, log=self.log, kernel_argv=self.kernel_argv,
696 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
696 connection_dir = self.profile_dir.security_dir,
697 connection_dir = self.profile_dir.security_dir,
697 )
698 )
698 kls = import_item(self.contents_manager_class)
699 kls = import_item(self.contents_manager_class)
699 self.contents_manager = kls(parent=self, log=self.log)
700 self.contents_manager = kls(parent=self, log=self.log)
700 kls = import_item(self.session_manager_class)
701 kls = import_item(self.session_manager_class)
701 self.session_manager = kls(parent=self, log=self.log,
702 self.session_manager = kls(parent=self, log=self.log,
702 kernel_manager=self.kernel_manager,
703 kernel_manager=self.kernel_manager,
703 contents_manager=self.contents_manager)
704 contents_manager=self.contents_manager)
704 kls = import_item(self.cluster_manager_class)
705 kls = import_item(self.cluster_manager_class)
705 self.cluster_manager = kls(parent=self, log=self.log)
706 self.cluster_manager = kls(parent=self, log=self.log)
706 self.cluster_manager.update_profiles()
707 self.cluster_manager.update_profiles()
707
708
708 def init_logging(self):
709 def init_logging(self):
709 # This prevents double log messages because tornado use a root logger that
710 # This prevents double log messages because tornado use a root logger that
710 # self.log is a child of. The logging module dipatches log messages to a log
711 # self.log is a child of. The logging module dipatches log messages to a log
711 # and all of its ancenstors until propagate is set to False.
712 # and all of its ancenstors until propagate is set to False.
712 self.log.propagate = False
713 self.log.propagate = False
713
714
714 for log in app_log, access_log, gen_log:
715 for log in app_log, access_log, gen_log:
715 # consistent log output name (NotebookApp instead of tornado.access, etc.)
716 # consistent log output name (NotebookApp instead of tornado.access, etc.)
716 log.name = self.log.name
717 log.name = self.log.name
717 # hook up tornado 3's loggers to our app handlers
718 # hook up tornado 3's loggers to our app handlers
718 logger = logging.getLogger('tornado')
719 logger = logging.getLogger('tornado')
719 logger.propagate = True
720 logger.propagate = True
720 logger.parent = self.log
721 logger.parent = self.log
721 logger.setLevel(self.log.level)
722 logger.setLevel(self.log.level)
722
723
723 def init_webapp(self):
724 def init_webapp(self):
724 """initialize tornado webapp and httpserver"""
725 """initialize tornado webapp and httpserver"""
725 self.tornado_settings['allow_origin'] = self.allow_origin
726 self.tornado_settings['allow_origin'] = self.allow_origin
726 if self.allow_origin_pat:
727 if self.allow_origin_pat:
727 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
728 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
728 self.tornado_settings['allow_credentials'] = self.allow_credentials
729 self.tornado_settings['allow_credentials'] = self.allow_credentials
729
730
730 self.web_app = NotebookWebApplication(
731 self.web_app = NotebookWebApplication(
731 self, self.kernel_manager, self.contents_manager,
732 self, self.kernel_manager, self.contents_manager,
732 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
733 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
733 self.log, self.base_url, self.default_url, self.tornado_settings,
734 self.log, self.base_url, self.default_url, self.tornado_settings,
734 self.jinja_environment_options
735 self.jinja_environment_options
735 )
736 )
736 if self.certfile:
737 if self.certfile:
737 ssl_options = dict(certfile=self.certfile)
738 ssl_options = dict(certfile=self.certfile)
738 if self.keyfile:
739 if self.keyfile:
739 ssl_options['keyfile'] = self.keyfile
740 ssl_options['keyfile'] = self.keyfile
740 else:
741 else:
741 ssl_options = None
742 ssl_options = None
742 self.web_app.password = self.password
743 self.web_app.password = self.password
743 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
744 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
744 xheaders=self.trust_xheaders)
745 xheaders=self.trust_xheaders)
745 if not self.ip:
746 if not self.ip:
746 warning = "WARNING: The notebook server is listening on all IP addresses"
747 warning = "WARNING: The notebook server is listening on all IP addresses"
747 if ssl_options is None:
748 if ssl_options is None:
748 self.log.critical(warning + " and not using encryption. This "
749 self.log.critical(warning + " and not using encryption. This "
749 "is not recommended.")
750 "is not recommended.")
750 if not self.password:
751 if not self.password:
751 self.log.critical(warning + " and not using authentication. "
752 self.log.critical(warning + " and not using authentication. "
752 "This is highly insecure and not recommended.")
753 "This is highly insecure and not recommended.")
753 success = None
754 success = None
754 for port in random_ports(self.port, self.port_retries+1):
755 for port in random_ports(self.port, self.port_retries+1):
755 try:
756 try:
756 self.http_server.listen(port, self.ip)
757 self.http_server.listen(port, self.ip)
757 except socket.error as e:
758 except socket.error as e:
758 if e.errno == errno.EADDRINUSE:
759 if e.errno == errno.EADDRINUSE:
759 self.log.info('The port %i is already in use, trying another random port.' % port)
760 self.log.info('The port %i is already in use, trying another random port.' % port)
760 continue
761 continue
761 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
762 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
762 self.log.warn("Permission to listen on port %i denied" % port)
763 self.log.warn("Permission to listen on port %i denied" % port)
763 continue
764 continue
764 else:
765 else:
765 raise
766 raise
766 else:
767 else:
767 self.port = port
768 self.port = port
768 success = True
769 success = True
769 break
770 break
770 if not success:
771 if not success:
771 self.log.critical('ERROR: the notebook server could not be started because '
772 self.log.critical('ERROR: the notebook server could not be started because '
772 'no available port could be found.')
773 'no available port could be found.')
773 self.exit(1)
774 self.exit(1)
774
775
775 @property
776 @property
776 def display_url(self):
777 def display_url(self):
777 ip = self.ip if self.ip else '[all ip addresses on your system]'
778 ip = self.ip if self.ip else '[all ip addresses on your system]'
778 return self._url(ip)
779 return self._url(ip)
779
780
780 @property
781 @property
781 def connection_url(self):
782 def connection_url(self):
782 ip = self.ip if self.ip else 'localhost'
783 ip = self.ip if self.ip else 'localhost'
783 return self._url(ip)
784 return self._url(ip)
784
785
785 def _url(self, ip):
786 def _url(self, ip):
786 proto = 'https' if self.certfile else 'http'
787 proto = 'https' if self.certfile else 'http'
787 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
788 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
788
789
789 def init_terminals(self):
790 def init_terminals(self):
790 try:
791 try:
791 from .terminal import initialize
792 from .terminal import initialize
792 initialize(self.web_app)
793 initialize(self.web_app)
793 self.web_app.settings['terminals_available'] = True
794 self.web_app.settings['terminals_available'] = True
794 except ImportError as e:
795 except ImportError as e:
795 self.log.info("Terminals not available (error was %s)", e)
796 self.log.info("Terminals not available (error was %s)", e)
796
797
797 def init_signal(self):
798 def init_signal(self):
798 if not sys.platform.startswith('win'):
799 if not sys.platform.startswith('win'):
799 signal.signal(signal.SIGINT, self._handle_sigint)
800 signal.signal(signal.SIGINT, self._handle_sigint)
800 signal.signal(signal.SIGTERM, self._signal_stop)
801 signal.signal(signal.SIGTERM, self._signal_stop)
801 if hasattr(signal, 'SIGUSR1'):
802 if hasattr(signal, 'SIGUSR1'):
802 # Windows doesn't support SIGUSR1
803 # Windows doesn't support SIGUSR1
803 signal.signal(signal.SIGUSR1, self._signal_info)
804 signal.signal(signal.SIGUSR1, self._signal_info)
804 if hasattr(signal, 'SIGINFO'):
805 if hasattr(signal, 'SIGINFO'):
805 # only on BSD-based systems
806 # only on BSD-based systems
806 signal.signal(signal.SIGINFO, self._signal_info)
807 signal.signal(signal.SIGINFO, self._signal_info)
807
808
808 def _handle_sigint(self, sig, frame):
809 def _handle_sigint(self, sig, frame):
809 """SIGINT handler spawns confirmation dialog"""
810 """SIGINT handler spawns confirmation dialog"""
810 # register more forceful signal handler for ^C^C case
811 # register more forceful signal handler for ^C^C case
811 signal.signal(signal.SIGINT, self._signal_stop)
812 signal.signal(signal.SIGINT, self._signal_stop)
812 # request confirmation dialog in bg thread, to avoid
813 # request confirmation dialog in bg thread, to avoid
813 # blocking the App
814 # blocking the App
814 thread = threading.Thread(target=self._confirm_exit)
815 thread = threading.Thread(target=self._confirm_exit)
815 thread.daemon = True
816 thread.daemon = True
816 thread.start()
817 thread.start()
817
818
818 def _restore_sigint_handler(self):
819 def _restore_sigint_handler(self):
819 """callback for restoring original SIGINT handler"""
820 """callback for restoring original SIGINT handler"""
820 signal.signal(signal.SIGINT, self._handle_sigint)
821 signal.signal(signal.SIGINT, self._handle_sigint)
821
822
822 def _confirm_exit(self):
823 def _confirm_exit(self):
823 """confirm shutdown on ^C
824 """confirm shutdown on ^C
824
825
825 A second ^C, or answering 'y' within 5s will cause shutdown,
826 A second ^C, or answering 'y' within 5s will cause shutdown,
826 otherwise original SIGINT handler will be restored.
827 otherwise original SIGINT handler will be restored.
827
828
828 This doesn't work on Windows.
829 This doesn't work on Windows.
829 """
830 """
830 info = self.log.info
831 info = self.log.info
831 info('interrupted')
832 info('interrupted')
832 print(self.notebook_info())
833 print(self.notebook_info())
833 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
834 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
834 sys.stdout.flush()
835 sys.stdout.flush()
835 r,w,x = select.select([sys.stdin], [], [], 5)
836 r,w,x = select.select([sys.stdin], [], [], 5)
836 if r:
837 if r:
837 line = sys.stdin.readline()
838 line = sys.stdin.readline()
838 if line.lower().startswith('y') and 'n' not in line.lower():
839 if line.lower().startswith('y') and 'n' not in line.lower():
839 self.log.critical("Shutdown confirmed")
840 self.log.critical("Shutdown confirmed")
840 ioloop.IOLoop.instance().stop()
841 ioloop.IOLoop.instance().stop()
841 return
842 return
842 else:
843 else:
843 print("No answer for 5s:", end=' ')
844 print("No answer for 5s:", end=' ')
844 print("resuming operation...")
845 print("resuming operation...")
845 # no answer, or answer is no:
846 # no answer, or answer is no:
846 # set it back to original SIGINT handler
847 # set it back to original SIGINT handler
847 # use IOLoop.add_callback because signal.signal must be called
848 # use IOLoop.add_callback because signal.signal must be called
848 # from main thread
849 # from main thread
849 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
850 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
850
851
851 def _signal_stop(self, sig, frame):
852 def _signal_stop(self, sig, frame):
852 self.log.critical("received signal %s, stopping", sig)
853 self.log.critical("received signal %s, stopping", sig)
853 ioloop.IOLoop.instance().stop()
854 ioloop.IOLoop.instance().stop()
854
855
855 def _signal_info(self, sig, frame):
856 def _signal_info(self, sig, frame):
856 print(self.notebook_info())
857 print(self.notebook_info())
857
858
858 def init_components(self):
859 def init_components(self):
859 """Check the components submodule, and warn if it's unclean"""
860 """Check the components submodule, and warn if it's unclean"""
860 status = submodule.check_submodule_status()
861 status = submodule.check_submodule_status()
861 if status == 'missing':
862 if status == 'missing':
862 self.log.warn("components submodule missing, running `git submodule update`")
863 self.log.warn("components submodule missing, running `git submodule update`")
863 submodule.update_submodules(submodule.ipython_parent())
864 submodule.update_submodules(submodule.ipython_parent())
864 elif status == 'unclean':
865 elif status == 'unclean':
865 self.log.warn("components submodule unclean, you may see 404s on static/components")
866 self.log.warn("components submodule unclean, you may see 404s on static/components")
866 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
867 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
867
868
868 @catch_config_error
869 @catch_config_error
869 def initialize(self, argv=None):
870 def initialize(self, argv=None):
870 super(NotebookApp, self).initialize(argv)
871 super(NotebookApp, self).initialize(argv)
871 self.init_logging()
872 self.init_logging()
872 self.init_kernel_argv()
873 self.init_kernel_argv()
873 self.init_configurables()
874 self.init_configurables()
874 self.init_components()
875 self.init_components()
875 self.init_webapp()
876 self.init_webapp()
876 self.init_terminals()
877 self.init_terminals()
877 self.init_signal()
878 self.init_signal()
878
879
879 def cleanup_kernels(self):
880 def cleanup_kernels(self):
880 """Shutdown all kernels.
881 """Shutdown all kernels.
881
882
882 The kernels will shutdown themselves when this process no longer exists,
883 The kernels will shutdown themselves when this process no longer exists,
883 but explicit shutdown allows the KernelManagers to cleanup the connection files.
884 but explicit shutdown allows the KernelManagers to cleanup the connection files.
884 """
885 """
885 self.log.info('Shutting down kernels')
886 self.log.info('Shutting down kernels')
886 self.kernel_manager.shutdown_all()
887 self.kernel_manager.shutdown_all()
887
888
888 def notebook_info(self):
889 def notebook_info(self):
889 "Return the current working directory and the server url information"
890 "Return the current working directory and the server url information"
890 info = self.contents_manager.info_string() + "\n"
891 info = self.contents_manager.info_string() + "\n"
891 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
892 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
892 return info + "The IPython Notebook is running at: %s" % self.display_url
893 return info + "The IPython Notebook is running at: %s" % self.display_url
893
894
894 def server_info(self):
895 def server_info(self):
895 """Return a JSONable dict of information about this server."""
896 """Return a JSONable dict of information about this server."""
896 return {'url': self.connection_url,
897 return {'url': self.connection_url,
897 'hostname': self.ip if self.ip else 'localhost',
898 'hostname': self.ip if self.ip else 'localhost',
898 'port': self.port,
899 'port': self.port,
899 'secure': bool(self.certfile),
900 'secure': bool(self.certfile),
900 'base_url': self.base_url,
901 'base_url': self.base_url,
901 'notebook_dir': os.path.abspath(self.notebook_dir),
902 'notebook_dir': os.path.abspath(self.notebook_dir),
902 'pid': os.getpid()
903 'pid': os.getpid()
903 }
904 }
904
905
905 def write_server_info_file(self):
906 def write_server_info_file(self):
906 """Write the result of server_info() to the JSON file info_file."""
907 """Write the result of server_info() to the JSON file info_file."""
907 with open(self.info_file, 'w') as f:
908 with open(self.info_file, 'w') as f:
908 json.dump(self.server_info(), f, indent=2)
909 json.dump(self.server_info(), f, indent=2)
909
910
910 def remove_server_info_file(self):
911 def remove_server_info_file(self):
911 """Remove the nbserver-<pid>.json file created for this server.
912 """Remove the nbserver-<pid>.json file created for this server.
912
913
913 Ignores the error raised when the file has already been removed.
914 Ignores the error raised when the file has already been removed.
914 """
915 """
915 try:
916 try:
916 os.unlink(self.info_file)
917 os.unlink(self.info_file)
917 except OSError as e:
918 except OSError as e:
918 if e.errno != errno.ENOENT:
919 if e.errno != errno.ENOENT:
919 raise
920 raise
920
921
921 def start(self):
922 def start(self):
922 """ Start the IPython Notebook server app, after initialization
923 """ Start the IPython Notebook server app, after initialization
923
924
924 This method takes no arguments so all configuration and initialization
925 This method takes no arguments so all configuration and initialization
925 must be done prior to calling this method."""
926 must be done prior to calling this method."""
926 if self.subapp is not None:
927 if self.subapp is not None:
927 return self.subapp.start()
928 return self.subapp.start()
928
929
929 info = self.log.info
930 info = self.log.info
930 for line in self.notebook_info().split("\n"):
931 for line in self.notebook_info().split("\n"):
931 info(line)
932 info(line)
932 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
933 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
933
934
934 self.write_server_info_file()
935 self.write_server_info_file()
935
936
936 if self.open_browser or self.file_to_run:
937 if self.open_browser or self.file_to_run:
937 try:
938 try:
938 browser = webbrowser.get(self.browser or None)
939 browser = webbrowser.get(self.browser or None)
939 except webbrowser.Error as e:
940 except webbrowser.Error as e:
940 self.log.warn('No web browser found: %s.' % e)
941 self.log.warn('No web browser found: %s.' % e)
941 browser = None
942 browser = None
942
943
943 if self.file_to_run:
944 if self.file_to_run:
944 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
945 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
945 if not os.path.exists(fullpath):
946 if not os.path.exists(fullpath):
946 self.log.critical("%s does not exist" % fullpath)
947 self.log.critical("%s does not exist" % fullpath)
947 self.exit(1)
948 self.exit(1)
948
949
949 uri = url_path_join('notebooks', self.file_to_run)
950 uri = url_path_join('notebooks', self.file_to_run)
950 else:
951 else:
951 uri = 'tree'
952 uri = 'tree'
952 if browser:
953 if browser:
953 b = lambda : browser.open(url_path_join(self.connection_url, uri),
954 b = lambda : browser.open(url_path_join(self.connection_url, uri),
954 new=2)
955 new=2)
955 threading.Thread(target=b).start()
956 threading.Thread(target=b).start()
956 try:
957 try:
957 ioloop.IOLoop.instance().start()
958 ioloop.IOLoop.instance().start()
958 except KeyboardInterrupt:
959 except KeyboardInterrupt:
959 info("Interrupted...")
960 info("Interrupted...")
960 finally:
961 finally:
961 self.cleanup_kernels()
962 self.cleanup_kernels()
962 self.remove_server_info_file()
963 self.remove_server_info_file()
963
964
964
965
965 def list_running_servers(profile='default'):
966 def list_running_servers(profile='default'):
966 """Iterate over the server info files of running notebook servers.
967 """Iterate over the server info files of running notebook servers.
967
968
968 Given a profile name, find nbserver-* files in the security directory of
969 Given a profile name, find nbserver-* files in the security directory of
969 that profile, and yield dicts of their information, each one pertaining to
970 that profile, and yield dicts of their information, each one pertaining to
970 a currently running notebook server instance.
971 a currently running notebook server instance.
971 """
972 """
972 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
973 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
973 for file in os.listdir(pd.security_dir):
974 for file in os.listdir(pd.security_dir):
974 if file.startswith('nbserver-'):
975 if file.startswith('nbserver-'):
975 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
976 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
976 info = json.load(f)
977 info = json.load(f)
977
978
978 # Simple check whether that process is really still running
979 # Simple check whether that process is really still running
979 if check_pid(info['pid']):
980 if check_pid(info['pid']):
980 yield info
981 yield info
981 else:
982 else:
982 # If the process has died, try to delete its info file
983 # If the process has died, try to delete its info file
983 try:
984 try:
984 os.unlink(file)
985 os.unlink(file)
985 except OSError:
986 except OSError:
986 pass # TODO: This should warn or log or something
987 pass # TODO: This should warn or log or something
987 #-----------------------------------------------------------------------------
988 #-----------------------------------------------------------------------------
988 # Main entry point
989 # Main entry point
989 #-----------------------------------------------------------------------------
990 #-----------------------------------------------------------------------------
990
991
991 launch_new_instance = NotebookApp.launch_instance
992 launch_new_instance = NotebookApp.launch_instance
992
993
@@ -1,135 +1,115 b''
1 """A kernel manager relating notebooks and kernels
1 """A kernel manager relating notebooks and kernels"""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
5
19 import os
6 import os
20
7
21 from tornado import web
8 from tornado import web
22
9
23 from IPython.kernel.multikernelmanager import MultiKernelManager
10 from IPython.kernel.multikernelmanager import MultiKernelManager
24 from IPython.utils.traitlets import List, Unicode, TraitError
11 from IPython.utils.traitlets import Unicode, TraitError
25
12
26 from IPython.html.utils import to_os_path
13 from IPython.html.utils import to_os_path
27 from IPython.utils.py3compat import getcwd
14 from IPython.utils.py3compat import getcwd
28
15
29 #-----------------------------------------------------------------------------
30 # Classes
31 #-----------------------------------------------------------------------------
32
33
16
34 class MappingKernelManager(MultiKernelManager):
17 class MappingKernelManager(MultiKernelManager):
35 """A KernelManager that handles notebook mapping and HTTP error handling"""
18 """A KernelManager that handles notebook mapping and HTTP error handling"""
36
19
37 def _kernel_manager_class_default(self):
20 def _kernel_manager_class_default(self):
38 return "IPython.kernel.ioloop.IOLoopKernelManager"
21 return "IPython.kernel.ioloop.IOLoopKernelManager"
39
22
40 kernel_argv = List(Unicode)
41
42 root_dir = Unicode(getcwd(), config=True)
23 root_dir = Unicode(getcwd(), config=True)
43
24
44 def _root_dir_changed(self, name, old, new):
25 def _root_dir_changed(self, name, old, new):
45 """Do a bit of validation of the root dir."""
26 """Do a bit of validation of the root dir."""
46 if not os.path.isabs(new):
27 if not os.path.isabs(new):
47 # If we receive a non-absolute path, make it absolute.
28 # If we receive a non-absolute path, make it absolute.
48 self.root_dir = os.path.abspath(new)
29 self.root_dir = os.path.abspath(new)
49 return
30 return
50 if not os.path.exists(new) or not os.path.isdir(new):
31 if not os.path.exists(new) or not os.path.isdir(new):
51 raise TraitError("kernel root dir %r is not a directory" % new)
32 raise TraitError("kernel root dir %r is not a directory" % new)
52
33
53 #-------------------------------------------------------------------------
34 #-------------------------------------------------------------------------
54 # Methods for managing kernels and sessions
35 # Methods for managing kernels and sessions
55 #-------------------------------------------------------------------------
36 #-------------------------------------------------------------------------
56
37
57 def _handle_kernel_died(self, kernel_id):
38 def _handle_kernel_died(self, kernel_id):
58 """notice that a kernel died"""
39 """notice that a kernel died"""
59 self.log.warn("Kernel %s died, removing from map.", kernel_id)
40 self.log.warn("Kernel %s died, removing from map.", kernel_id)
60 self.remove_kernel(kernel_id)
41 self.remove_kernel(kernel_id)
61
42
62 def cwd_for_path(self, path):
43 def cwd_for_path(self, path):
63 """Turn API path into absolute OS path."""
44 """Turn API path into absolute OS path."""
64 # short circuit for NotebookManagers that pass in absolute paths
45 # short circuit for NotebookManagers that pass in absolute paths
65 if os.path.exists(path):
46 if os.path.exists(path):
66 return path
47 return path
67
48
68 os_path = to_os_path(path, self.root_dir)
49 os_path = to_os_path(path, self.root_dir)
69 # in the case of notebooks and kernels not being on the same filesystem,
50 # in the case of notebooks and kernels not being on the same filesystem,
70 # walk up to root_dir if the paths don't exist
51 # walk up to root_dir if the paths don't exist
71 while not os.path.exists(os_path) and os_path != self.root_dir:
52 while not os.path.exists(os_path) and os_path != self.root_dir:
72 os_path = os.path.dirname(os_path)
53 os_path = os.path.dirname(os_path)
73 return os_path
54 return os_path
74
55
75 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
56 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
76 """Start a kernel for a session and return its kernel_id.
57 """Start a kernel for a session and return its kernel_id.
77
58
78 Parameters
59 Parameters
79 ----------
60 ----------
80 kernel_id : uuid
61 kernel_id : uuid
81 The uuid to associate the new kernel with. If this
62 The uuid to associate the new kernel with. If this
82 is not None, this kernel will be persistent whenever it is
63 is not None, this kernel will be persistent whenever it is
83 requested.
64 requested.
84 path : API path
65 path : API path
85 The API path (unicode, '/' delimited) for the cwd.
66 The API path (unicode, '/' delimited) for the cwd.
86 Will be transformed to an OS path relative to root_dir.
67 Will be transformed to an OS path relative to root_dir.
87 kernel_name : str
68 kernel_name : str
88 The name identifying which kernel spec to launch. This is ignored if
69 The name identifying which kernel spec to launch. This is ignored if
89 an existing kernel is returned, but it may be checked in the future.
70 an existing kernel is returned, but it may be checked in the future.
90 """
71 """
91 if kernel_id is None:
72 if kernel_id is None:
92 kwargs['extra_arguments'] = self.kernel_argv
93 if path is not None:
73 if path is not None:
94 kwargs['cwd'] = self.cwd_for_path(path)
74 kwargs['cwd'] = self.cwd_for_path(path)
95 kernel_id = super(MappingKernelManager, self).start_kernel(
75 kernel_id = super(MappingKernelManager, self).start_kernel(
96 kernel_name=kernel_name, **kwargs)
76 kernel_name=kernel_name, **kwargs)
97 self.log.info("Kernel started: %s" % kernel_id)
77 self.log.info("Kernel started: %s" % kernel_id)
98 self.log.debug("Kernel args: %r" % kwargs)
78 self.log.debug("Kernel args: %r" % kwargs)
99 # register callback for failed auto-restart
79 # register callback for failed auto-restart
100 self.add_restart_callback(kernel_id,
80 self.add_restart_callback(kernel_id,
101 lambda : self._handle_kernel_died(kernel_id),
81 lambda : self._handle_kernel_died(kernel_id),
102 'dead',
82 'dead',
103 )
83 )
104 else:
84 else:
105 self._check_kernel_id(kernel_id)
85 self._check_kernel_id(kernel_id)
106 self.log.info("Using existing kernel: %s" % kernel_id)
86 self.log.info("Using existing kernel: %s" % kernel_id)
107 return kernel_id
87 return kernel_id
108
88
109 def shutdown_kernel(self, kernel_id, now=False):
89 def shutdown_kernel(self, kernel_id, now=False):
110 """Shutdown a kernel by kernel_id"""
90 """Shutdown a kernel by kernel_id"""
111 self._check_kernel_id(kernel_id)
91 self._check_kernel_id(kernel_id)
112 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
92 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
113
93
114 def kernel_model(self, kernel_id):
94 def kernel_model(self, kernel_id):
115 """Return a dictionary of kernel information described in the
95 """Return a dictionary of kernel information described in the
116 JSON standard model."""
96 JSON standard model."""
117 self._check_kernel_id(kernel_id)
97 self._check_kernel_id(kernel_id)
118 model = {"id":kernel_id,
98 model = {"id":kernel_id,
119 "name": self._kernels[kernel_id].kernel_name}
99 "name": self._kernels[kernel_id].kernel_name}
120 return model
100 return model
121
101
122 def list_kernels(self):
102 def list_kernels(self):
123 """Returns a list of kernel_id's of kernels running."""
103 """Returns a list of kernel_id's of kernels running."""
124 kernels = []
104 kernels = []
125 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
105 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
126 for kernel_id in kernel_ids:
106 for kernel_id in kernel_ids:
127 model = self.kernel_model(kernel_id)
107 model = self.kernel_model(kernel_id)
128 kernels.append(model)
108 kernels.append(model)
129 return kernels
109 return kernels
130
110
131 # override _check_kernel_id to raise 404 instead of KeyError
111 # override _check_kernel_id to raise 404 instead of KeyError
132 def _check_kernel_id(self, kernel_id):
112 def _check_kernel_id(self, kernel_id):
133 """Check a that a kernel_id exists and raise 404 if not."""
113 """Check a that a kernel_id exists and raise 404 if not."""
134 if kernel_id not in self:
114 if kernel_id not in self:
135 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
115 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,313 +1,318 b''
1 """A kernel manager for multiple kernels"""
1 """A kernel manager for multiple kernels"""
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 uuid
9 import uuid
10
10
11 import zmq
11 import zmq
12
12
13 from IPython.config.configurable import LoggingConfigurable
13 from IPython.config.configurable import LoggingConfigurable
14 from IPython.utils.importstring import import_item
14 from IPython.utils.importstring import import_item
15 from IPython.utils.traitlets import (
15 from IPython.utils.traitlets import (
16 Instance, Dict, Unicode, Any, DottedObjectName
16 Instance, Dict, List, Unicode, Any, DottedObjectName
17 )
17 )
18 from IPython.utils.py3compat import unicode_type
18 from IPython.utils.py3compat import unicode_type
19
19
20 from .kernelspec import NATIVE_KERNEL_NAME
20 from .kernelspec import NATIVE_KERNEL_NAME
21
21
22 class DuplicateKernelError(Exception):
22 class DuplicateKernelError(Exception):
23 pass
23 pass
24
24
25
25
26 def kernel_method(f):
26 def kernel_method(f):
27 """decorator for proxying MKM.method(kernel_id) to individual KMs by ID"""
27 """decorator for proxying MKM.method(kernel_id) to individual KMs by ID"""
28 def wrapped(self, kernel_id, *args, **kwargs):
28 def wrapped(self, kernel_id, *args, **kwargs):
29 # get the kernel
29 # get the kernel
30 km = self.get_kernel(kernel_id)
30 km = self.get_kernel(kernel_id)
31 method = getattr(km, f.__name__)
31 method = getattr(km, f.__name__)
32 # call the kernel's method
32 # call the kernel's method
33 r = method(*args, **kwargs)
33 r = method(*args, **kwargs)
34 # last thing, call anything defined in the actual class method
34 # last thing, call anything defined in the actual class method
35 # such as logging messages
35 # such as logging messages
36 f(self, kernel_id, *args, **kwargs)
36 f(self, kernel_id, *args, **kwargs)
37 # return the method result
37 # return the method result
38 return r
38 return r
39 return wrapped
39 return wrapped
40
40
41
41
42 class MultiKernelManager(LoggingConfigurable):
42 class MultiKernelManager(LoggingConfigurable):
43 """A class for managing multiple kernels."""
43 """A class for managing multiple kernels."""
44
44
45 ipython_kernel_argv = List(Unicode)
46
45 default_kernel_name = Unicode(NATIVE_KERNEL_NAME, config=True,
47 default_kernel_name = Unicode(NATIVE_KERNEL_NAME, config=True,
46 help="The name of the default kernel to start"
48 help="The name of the default kernel to start"
47 )
49 )
48
50
49 kernel_manager_class = DottedObjectName(
51 kernel_manager_class = DottedObjectName(
50 "IPython.kernel.ioloop.IOLoopKernelManager", config=True,
52 "IPython.kernel.ioloop.IOLoopKernelManager", config=True,
51 help="""The kernel manager class. This is configurable to allow
53 help="""The kernel manager class. This is configurable to allow
52 subclassing of the KernelManager for customized behavior.
54 subclassing of the KernelManager for customized behavior.
53 """
55 """
54 )
56 )
55 def _kernel_manager_class_changed(self, name, old, new):
57 def _kernel_manager_class_changed(self, name, old, new):
56 self.kernel_manager_factory = import_item(new)
58 self.kernel_manager_factory = import_item(new)
57
59
58 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
60 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
59 def _kernel_manager_factory_default(self):
61 def _kernel_manager_factory_default(self):
60 return import_item(self.kernel_manager_class)
62 return import_item(self.kernel_manager_class)
61
63
62 context = Instance('zmq.Context')
64 context = Instance('zmq.Context')
63 def _context_default(self):
65 def _context_default(self):
64 return zmq.Context.instance()
66 return zmq.Context.instance()
65
67
66 connection_dir = Unicode('')
68 connection_dir = Unicode('')
67
69
68 _kernels = Dict()
70 _kernels = Dict()
69
71
70 def list_kernel_ids(self):
72 def list_kernel_ids(self):
71 """Return a list of the kernel ids of the active kernels."""
73 """Return a list of the kernel ids of the active kernels."""
72 # Create a copy so we can iterate over kernels in operations
74 # Create a copy so we can iterate over kernels in operations
73 # that delete keys.
75 # that delete keys.
74 return list(self._kernels.keys())
76 return list(self._kernels.keys())
75
77
76 def __len__(self):
78 def __len__(self):
77 """Return the number of running kernels."""
79 """Return the number of running kernels."""
78 return len(self.list_kernel_ids())
80 return len(self.list_kernel_ids())
79
81
80 def __contains__(self, kernel_id):
82 def __contains__(self, kernel_id):
81 return kernel_id in self._kernels
83 return kernel_id in self._kernels
82
84
83 def start_kernel(self, kernel_name=None, **kwargs):
85 def start_kernel(self, kernel_name=None, **kwargs):
84 """Start a new kernel.
86 """Start a new kernel.
85
87
86 The caller can pick a kernel_id by passing one in as a keyword arg,
88 The caller can pick a kernel_id by passing one in as a keyword arg,
87 otherwise one will be picked using a uuid.
89 otherwise one will be picked using a uuid.
88
90
89 To silence the kernel's stdout/stderr, call this using::
91 To silence the kernel's stdout/stderr, call this using::
90
92
91 km.start_kernel(stdout=PIPE, stderr=PIPE)
93 km.start_kernel(stdout=PIPE, stderr=PIPE)
92
94
93 """
95 """
94 kernel_id = kwargs.pop('kernel_id', unicode_type(uuid.uuid4()))
96 kernel_id = kwargs.pop('kernel_id', unicode_type(uuid.uuid4()))
95 if kernel_id in self:
97 if kernel_id in self:
96 raise DuplicateKernelError('Kernel already exists: %s' % kernel_id)
98 raise DuplicateKernelError('Kernel already exists: %s' % kernel_id)
97
99
98 if kernel_name is None:
100 if kernel_name is None:
99 kernel_name = self.default_kernel_name
101 kernel_name = self.default_kernel_name
100 # kernel_manager_factory is the constructor for the KernelManager
102 # kernel_manager_factory is the constructor for the KernelManager
101 # subclass we are using. It can be configured as any Configurable,
103 # subclass we are using. It can be configured as any Configurable,
102 # including things like its transport and ip.
104 # including things like its transport and ip.
103 km = self.kernel_manager_factory(connection_file=os.path.join(
105 km = self.kernel_manager_factory(connection_file=os.path.join(
104 self.connection_dir, "kernel-%s.json" % kernel_id),
106 self.connection_dir, "kernel-%s.json" % kernel_id),
105 parent=self, autorestart=True, log=self.log, kernel_name=kernel_name,
107 parent=self, autorestart=True, log=self.log, kernel_name=kernel_name,
106 )
108 )
109 # FIXME: remove special treatment of IPython kernels
110 if km.ipython_kernel:
111 kwargs.setdefault('extra_arguments', self.ipython_kernel_argv)
107 km.start_kernel(**kwargs)
112 km.start_kernel(**kwargs)
108 self._kernels[kernel_id] = km
113 self._kernels[kernel_id] = km
109 return kernel_id
114 return kernel_id
110
115
111 @kernel_method
116 @kernel_method
112 def shutdown_kernel(self, kernel_id, now=False, restart=False):
117 def shutdown_kernel(self, kernel_id, now=False, restart=False):
113 """Shutdown a kernel by its kernel uuid.
118 """Shutdown a kernel by its kernel uuid.
114
119
115 Parameters
120 Parameters
116 ==========
121 ==========
117 kernel_id : uuid
122 kernel_id : uuid
118 The id of the kernel to shutdown.
123 The id of the kernel to shutdown.
119 now : bool
124 now : bool
120 Should the kernel be shutdown forcibly using a signal.
125 Should the kernel be shutdown forcibly using a signal.
121 restart : bool
126 restart : bool
122 Will the kernel be restarted?
127 Will the kernel be restarted?
123 """
128 """
124 self.log.info("Kernel shutdown: %s" % kernel_id)
129 self.log.info("Kernel shutdown: %s" % kernel_id)
125 self.remove_kernel(kernel_id)
130 self.remove_kernel(kernel_id)
126
131
127 @kernel_method
132 @kernel_method
128 def request_shutdown(self, kernel_id, restart=False):
133 def request_shutdown(self, kernel_id, restart=False):
129 """Ask a kernel to shut down by its kernel uuid"""
134 """Ask a kernel to shut down by its kernel uuid"""
130
135
131 @kernel_method
136 @kernel_method
132 def finish_shutdown(self, kernel_id, waittime=1, pollinterval=0.1):
137 def finish_shutdown(self, kernel_id, waittime=1, pollinterval=0.1):
133 """Wait for a kernel to finish shutting down, and kill it if it doesn't
138 """Wait for a kernel to finish shutting down, and kill it if it doesn't
134 """
139 """
135 self.log.info("Kernel shutdown: %s" % kernel_id)
140 self.log.info("Kernel shutdown: %s" % kernel_id)
136
141
137 @kernel_method
142 @kernel_method
138 def cleanup(self, kernel_id, connection_file=True):
143 def cleanup(self, kernel_id, connection_file=True):
139 """Clean up a kernel's resources"""
144 """Clean up a kernel's resources"""
140
145
141 def remove_kernel(self, kernel_id):
146 def remove_kernel(self, kernel_id):
142 """remove a kernel from our mapping.
147 """remove a kernel from our mapping.
143
148
144 Mainly so that a kernel can be removed if it is already dead,
149 Mainly so that a kernel can be removed if it is already dead,
145 without having to call shutdown_kernel.
150 without having to call shutdown_kernel.
146
151
147 The kernel object is returned.
152 The kernel object is returned.
148 """
153 """
149 return self._kernels.pop(kernel_id)
154 return self._kernels.pop(kernel_id)
150
155
151 def shutdown_all(self, now=False):
156 def shutdown_all(self, now=False):
152 """Shutdown all kernels."""
157 """Shutdown all kernels."""
153 kids = self.list_kernel_ids()
158 kids = self.list_kernel_ids()
154 for kid in kids:
159 for kid in kids:
155 self.request_shutdown(kid)
160 self.request_shutdown(kid)
156 for kid in kids:
161 for kid in kids:
157 self.finish_shutdown(kid)
162 self.finish_shutdown(kid)
158 self.cleanup(kid)
163 self.cleanup(kid)
159
164
160 @kernel_method
165 @kernel_method
161 def interrupt_kernel(self, kernel_id):
166 def interrupt_kernel(self, kernel_id):
162 """Interrupt (SIGINT) the kernel by its uuid.
167 """Interrupt (SIGINT) the kernel by its uuid.
163
168
164 Parameters
169 Parameters
165 ==========
170 ==========
166 kernel_id : uuid
171 kernel_id : uuid
167 The id of the kernel to interrupt.
172 The id of the kernel to interrupt.
168 """
173 """
169 self.log.info("Kernel interrupted: %s" % kernel_id)
174 self.log.info("Kernel interrupted: %s" % kernel_id)
170
175
171 @kernel_method
176 @kernel_method
172 def signal_kernel(self, kernel_id, signum):
177 def signal_kernel(self, kernel_id, signum):
173 """Sends a signal to the kernel by its uuid.
178 """Sends a signal to the kernel by its uuid.
174
179
175 Note that since only SIGTERM is supported on Windows, this function
180 Note that since only SIGTERM is supported on Windows, this function
176 is only useful on Unix systems.
181 is only useful on Unix systems.
177
182
178 Parameters
183 Parameters
179 ==========
184 ==========
180 kernel_id : uuid
185 kernel_id : uuid
181 The id of the kernel to signal.
186 The id of the kernel to signal.
182 """
187 """
183 self.log.info("Signaled Kernel %s with %s" % (kernel_id, signum))
188 self.log.info("Signaled Kernel %s with %s" % (kernel_id, signum))
184
189
185 @kernel_method
190 @kernel_method
186 def restart_kernel(self, kernel_id, now=False):
191 def restart_kernel(self, kernel_id, now=False):
187 """Restart a kernel by its uuid, keeping the same ports.
192 """Restart a kernel by its uuid, keeping the same ports.
188
193
189 Parameters
194 Parameters
190 ==========
195 ==========
191 kernel_id : uuid
196 kernel_id : uuid
192 The id of the kernel to interrupt.
197 The id of the kernel to interrupt.
193 """
198 """
194 self.log.info("Kernel restarted: %s" % kernel_id)
199 self.log.info("Kernel restarted: %s" % kernel_id)
195
200
196 @kernel_method
201 @kernel_method
197 def is_alive(self, kernel_id):
202 def is_alive(self, kernel_id):
198 """Is the kernel alive.
203 """Is the kernel alive.
199
204
200 This calls KernelManager.is_alive() which calls Popen.poll on the
205 This calls KernelManager.is_alive() which calls Popen.poll on the
201 actual kernel subprocess.
206 actual kernel subprocess.
202
207
203 Parameters
208 Parameters
204 ==========
209 ==========
205 kernel_id : uuid
210 kernel_id : uuid
206 The id of the kernel.
211 The id of the kernel.
207 """
212 """
208
213
209 def _check_kernel_id(self, kernel_id):
214 def _check_kernel_id(self, kernel_id):
210 """check that a kernel id is valid"""
215 """check that a kernel id is valid"""
211 if kernel_id not in self:
216 if kernel_id not in self:
212 raise KeyError("Kernel with id not found: %s" % kernel_id)
217 raise KeyError("Kernel with id not found: %s" % kernel_id)
213
218
214 def get_kernel(self, kernel_id):
219 def get_kernel(self, kernel_id):
215 """Get the single KernelManager object for a kernel by its uuid.
220 """Get the single KernelManager object for a kernel by its uuid.
216
221
217 Parameters
222 Parameters
218 ==========
223 ==========
219 kernel_id : uuid
224 kernel_id : uuid
220 The id of the kernel.
225 The id of the kernel.
221 """
226 """
222 self._check_kernel_id(kernel_id)
227 self._check_kernel_id(kernel_id)
223 return self._kernels[kernel_id]
228 return self._kernels[kernel_id]
224
229
225 @kernel_method
230 @kernel_method
226 def add_restart_callback(self, kernel_id, callback, event='restart'):
231 def add_restart_callback(self, kernel_id, callback, event='restart'):
227 """add a callback for the KernelRestarter"""
232 """add a callback for the KernelRestarter"""
228
233
229 @kernel_method
234 @kernel_method
230 def remove_restart_callback(self, kernel_id, callback, event='restart'):
235 def remove_restart_callback(self, kernel_id, callback, event='restart'):
231 """remove a callback for the KernelRestarter"""
236 """remove a callback for the KernelRestarter"""
232
237
233 @kernel_method
238 @kernel_method
234 def get_connection_info(self, kernel_id):
239 def get_connection_info(self, kernel_id):
235 """Return a dictionary of connection data for a kernel.
240 """Return a dictionary of connection data for a kernel.
236
241
237 Parameters
242 Parameters
238 ==========
243 ==========
239 kernel_id : uuid
244 kernel_id : uuid
240 The id of the kernel.
245 The id of the kernel.
241
246
242 Returns
247 Returns
243 =======
248 =======
244 connection_dict : dict
249 connection_dict : dict
245 A dict of the information needed to connect to a kernel.
250 A dict of the information needed to connect to a kernel.
246 This includes the ip address and the integer port
251 This includes the ip address and the integer port
247 numbers of the different channels (stdin_port, iopub_port,
252 numbers of the different channels (stdin_port, iopub_port,
248 shell_port, hb_port).
253 shell_port, hb_port).
249 """
254 """
250
255
251 @kernel_method
256 @kernel_method
252 def connect_iopub(self, kernel_id, identity=None):
257 def connect_iopub(self, kernel_id, identity=None):
253 """Return a zmq Socket connected to the iopub channel.
258 """Return a zmq Socket connected to the iopub channel.
254
259
255 Parameters
260 Parameters
256 ==========
261 ==========
257 kernel_id : uuid
262 kernel_id : uuid
258 The id of the kernel
263 The id of the kernel
259 identity : bytes (optional)
264 identity : bytes (optional)
260 The zmq identity of the socket
265 The zmq identity of the socket
261
266
262 Returns
267 Returns
263 =======
268 =======
264 stream : zmq Socket or ZMQStream
269 stream : zmq Socket or ZMQStream
265 """
270 """
266
271
267 @kernel_method
272 @kernel_method
268 def connect_shell(self, kernel_id, identity=None):
273 def connect_shell(self, kernel_id, identity=None):
269 """Return a zmq Socket connected to the shell channel.
274 """Return a zmq Socket connected to the shell channel.
270
275
271 Parameters
276 Parameters
272 ==========
277 ==========
273 kernel_id : uuid
278 kernel_id : uuid
274 The id of the kernel
279 The id of the kernel
275 identity : bytes (optional)
280 identity : bytes (optional)
276 The zmq identity of the socket
281 The zmq identity of the socket
277
282
278 Returns
283 Returns
279 =======
284 =======
280 stream : zmq Socket or ZMQStream
285 stream : zmq Socket or ZMQStream
281 """
286 """
282
287
283 @kernel_method
288 @kernel_method
284 def connect_stdin(self, kernel_id, identity=None):
289 def connect_stdin(self, kernel_id, identity=None):
285 """Return a zmq Socket connected to the stdin channel.
290 """Return a zmq Socket connected to the stdin channel.
286
291
287 Parameters
292 Parameters
288 ==========
293 ==========
289 kernel_id : uuid
294 kernel_id : uuid
290 The id of the kernel
295 The id of the kernel
291 identity : bytes (optional)
296 identity : bytes (optional)
292 The zmq identity of the socket
297 The zmq identity of the socket
293
298
294 Returns
299 Returns
295 =======
300 =======
296 stream : zmq Socket or ZMQStream
301 stream : zmq Socket or ZMQStream
297 """
302 """
298
303
299 @kernel_method
304 @kernel_method
300 def connect_hb(self, kernel_id, identity=None):
305 def connect_hb(self, kernel_id, identity=None):
301 """Return a zmq Socket connected to the hb channel.
306 """Return a zmq Socket connected to the hb channel.
302
307
303 Parameters
308 Parameters
304 ==========
309 ==========
305 kernel_id : uuid
310 kernel_id : uuid
306 The id of the kernel
311 The id of the kernel
307 identity : bytes (optional)
312 identity : bytes (optional)
308 The zmq identity of the socket
313 The zmq identity of the socket
309
314
310 Returns
315 Returns
311 =======
316 =======
312 stream : zmq Socket or ZMQStream
317 stream : zmq Socket or ZMQStream
313 """
318 """
@@ -1,371 +1,373 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5 """
5 """
6
6
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 # stdlib imports
14 # stdlib imports
15 import os
15 import os
16 import signal
16 import signal
17 import sys
17 import sys
18
18
19 # If run on Windows, install an exception hook which pops up a
19 # If run on Windows, install an exception hook which pops up a
20 # message box. Pythonw.exe hides the console, so without this
20 # message box. Pythonw.exe hides the console, so without this
21 # the application silently fails to load.
21 # the application silently fails to load.
22 #
22 #
23 # We always install this handler, because the expectation is for
23 # We always install this handler, because the expectation is for
24 # qtconsole to bring up a GUI even if called from the console.
24 # qtconsole to bring up a GUI even if called from the console.
25 # The old handler is called, so the exception is printed as well.
25 # The old handler is called, so the exception is printed as well.
26 # If desired, check for pythonw with an additional condition
26 # If desired, check for pythonw with an additional condition
27 # (sys.executable.lower().find('pythonw.exe') >= 0).
27 # (sys.executable.lower().find('pythonw.exe') >= 0).
28 if os.name == 'nt':
28 if os.name == 'nt':
29 old_excepthook = sys.excepthook
29 old_excepthook = sys.excepthook
30
30
31 # Exclude this from our autogenerated API docs.
31 # Exclude this from our autogenerated API docs.
32 undoc = lambda func: func
32 undoc = lambda func: func
33
33
34 @undoc
34 @undoc
35 def gui_excepthook(exctype, value, tb):
35 def gui_excepthook(exctype, value, tb):
36 try:
36 try:
37 import ctypes, traceback
37 import ctypes, traceback
38 MB_ICONERROR = 0x00000010
38 MB_ICONERROR = 0x00000010
39 title = u'Error starting IPython QtConsole'
39 title = u'Error starting IPython QtConsole'
40 msg = u''.join(traceback.format_exception(exctype, value, tb))
40 msg = u''.join(traceback.format_exception(exctype, value, tb))
41 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
41 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
42 finally:
42 finally:
43 # Also call the old exception hook to let it do
43 # Also call the old exception hook to let it do
44 # its thing too.
44 # its thing too.
45 old_excepthook(exctype, value, tb)
45 old_excepthook(exctype, value, tb)
46
46
47 sys.excepthook = gui_excepthook
47 sys.excepthook = gui_excepthook
48
48
49 # System library imports
49 # System library imports
50 from IPython.external.qt import QtCore, QtGui
50 from IPython.external.qt import QtCore, QtGui
51
51
52 # Local imports
52 # Local imports
53 from IPython.config.application import catch_config_error
53 from IPython.config.application import catch_config_error
54 from IPython.core.application import BaseIPythonApplication
54 from IPython.core.application import BaseIPythonApplication
55 from IPython.qt.console.ipython_widget import IPythonWidget
55 from IPython.qt.console.ipython_widget import IPythonWidget
56 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
56 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
57 from IPython.qt.console import styles
57 from IPython.qt.console import styles
58 from IPython.qt.console.mainwindow import MainWindow
58 from IPython.qt.console.mainwindow import MainWindow
59 from IPython.qt.client import QtKernelClient
59 from IPython.qt.client import QtKernelClient
60 from IPython.qt.manager import QtKernelManager
60 from IPython.qt.manager import QtKernelManager
61 from IPython.utils.traitlets import (
61 from IPython.utils.traitlets import (
62 Dict, Unicode, CBool, Any
62 Dict, Unicode, CBool, Any
63 )
63 )
64
64
65 from IPython.consoleapp import (
65 from IPython.consoleapp import (
66 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
66 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
67 )
67 )
68
68
69 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
70 # Network Constants
70 # Network Constants
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72
72
73 from IPython.utils.localinterfaces import is_local_ip
73 from IPython.utils.localinterfaces import is_local_ip
74
74
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76 # Globals
76 # Globals
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78
78
79 _examples = """
79 _examples = """
80 ipython qtconsole # start the qtconsole
80 ipython qtconsole # start the qtconsole
81 ipython qtconsole --matplotlib=inline # start with matplotlib inline plotting mode
81 ipython qtconsole --matplotlib=inline # start with matplotlib inline plotting mode
82 """
82 """
83
83
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85 # Aliases and Flags
85 # Aliases and Flags
86 #-----------------------------------------------------------------------------
86 #-----------------------------------------------------------------------------
87
87
88 # start with copy of flags
88 # start with copy of flags
89 flags = dict(flags)
89 flags = dict(flags)
90 qt_flags = {
90 qt_flags = {
91 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
91 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
92 "Disable rich text support."),
92 "Disable rich text support."),
93 }
93 }
94
94
95 # and app_flags from the Console Mixin
95 # and app_flags from the Console Mixin
96 qt_flags.update(app_flags)
96 qt_flags.update(app_flags)
97 # add frontend flags to the full set
97 # add frontend flags to the full set
98 flags.update(qt_flags)
98 flags.update(qt_flags)
99
99
100 # start with copy of front&backend aliases list
100 # start with copy of front&backend aliases list
101 aliases = dict(aliases)
101 aliases = dict(aliases)
102 qt_aliases = dict(
102 qt_aliases = dict(
103 style = 'IPythonWidget.syntax_style',
103 style = 'IPythonWidget.syntax_style',
104 stylesheet = 'IPythonQtConsoleApp.stylesheet',
104 stylesheet = 'IPythonQtConsoleApp.stylesheet',
105 colors = 'ZMQInteractiveShell.colors',
105 colors = 'ZMQInteractiveShell.colors',
106
106
107 editor = 'IPythonWidget.editor',
107 editor = 'IPythonWidget.editor',
108 paging = 'ConsoleWidget.paging',
108 paging = 'ConsoleWidget.paging',
109 )
109 )
110 # and app_aliases from the Console Mixin
110 # and app_aliases from the Console Mixin
111 qt_aliases.update(app_aliases)
111 qt_aliases.update(app_aliases)
112 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
112 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
113 # add frontend aliases to the full set
113 # add frontend aliases to the full set
114 aliases.update(qt_aliases)
114 aliases.update(qt_aliases)
115
115
116 # get flags&aliases into sets, and remove a couple that
116 # get flags&aliases into sets, and remove a couple that
117 # shouldn't be scrubbed from backend flags:
117 # shouldn't be scrubbed from backend flags:
118 qt_aliases = set(qt_aliases.keys())
118 qt_aliases = set(qt_aliases.keys())
119 qt_aliases.remove('colors')
119 qt_aliases.remove('colors')
120 qt_flags = set(qt_flags.keys())
120 qt_flags = set(qt_flags.keys())
121
121
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 # Classes
123 # Classes
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125
125
126 #-----------------------------------------------------------------------------
126 #-----------------------------------------------------------------------------
127 # IPythonQtConsole
127 # IPythonQtConsole
128 #-----------------------------------------------------------------------------
128 #-----------------------------------------------------------------------------
129
129
130
130
131 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
131 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
132 name = 'ipython-qtconsole'
132 name = 'ipython-qtconsole'
133
133
134 description = """
134 description = """
135 The IPython QtConsole.
135 The IPython QtConsole.
136
136
137 This launches a Console-style application using Qt. It is not a full
137 This launches a Console-style application using Qt. It is not a full
138 console, in that launched terminal subprocesses will not be able to accept
138 console, in that launched terminal subprocesses will not be able to accept
139 input.
139 input.
140
140
141 The QtConsole supports various extra features beyond the Terminal IPython
141 The QtConsole supports various extra features beyond the Terminal IPython
142 shell, such as inline plotting with matplotlib, via:
142 shell, such as inline plotting with matplotlib, via:
143
143
144 ipython qtconsole --matplotlib=inline
144 ipython qtconsole --matplotlib=inline
145
145
146 as well as saving your session as HTML, and printing the output.
146 as well as saving your session as HTML, and printing the output.
147
147
148 """
148 """
149 examples = _examples
149 examples = _examples
150
150
151 classes = [IPythonWidget] + IPythonConsoleApp.classes
151 classes = [IPythonWidget] + IPythonConsoleApp.classes
152 flags = Dict(flags)
152 flags = Dict(flags)
153 aliases = Dict(aliases)
153 aliases = Dict(aliases)
154 frontend_flags = Any(qt_flags)
154 frontend_flags = Any(qt_flags)
155 frontend_aliases = Any(qt_aliases)
155 frontend_aliases = Any(qt_aliases)
156 kernel_client_class = QtKernelClient
156 kernel_client_class = QtKernelClient
157 kernel_manager_class = QtKernelManager
157 kernel_manager_class = QtKernelManager
158
158
159 stylesheet = Unicode('', config=True,
159 stylesheet = Unicode('', config=True,
160 help="path to a custom CSS stylesheet")
160 help="path to a custom CSS stylesheet")
161
161
162 hide_menubar = CBool(False, config=True,
162 hide_menubar = CBool(False, config=True,
163 help="Start the console window with the menu bar hidden.")
163 help="Start the console window with the menu bar hidden.")
164
164
165 maximize = CBool(False, config=True,
165 maximize = CBool(False, config=True,
166 help="Start the console window maximized.")
166 help="Start the console window maximized.")
167
167
168 plain = CBool(False, config=True,
168 plain = CBool(False, config=True,
169 help="Use a plaintext widget instead of rich text (plain can't print/save).")
169 help="Use a plaintext widget instead of rich text (plain can't print/save).")
170
170
171 def _plain_changed(self, name, old, new):
171 def _plain_changed(self, name, old, new):
172 kind = 'plain' if new else 'rich'
172 kind = 'plain' if new else 'rich'
173 self.config.ConsoleWidget.kind = kind
173 self.config.ConsoleWidget.kind = kind
174 if new:
174 if new:
175 self.widget_factory = IPythonWidget
175 self.widget_factory = IPythonWidget
176 else:
176 else:
177 self.widget_factory = RichIPythonWidget
177 self.widget_factory = RichIPythonWidget
178
178
179 # the factory for creating a widget
179 # the factory for creating a widget
180 widget_factory = Any(RichIPythonWidget)
180 widget_factory = Any(RichIPythonWidget)
181
181
182 def parse_command_line(self, argv=None):
182 def parse_command_line(self, argv=None):
183 super(IPythonQtConsoleApp, self).parse_command_line(argv)
183 super(IPythonQtConsoleApp, self).parse_command_line(argv)
184 self.build_kernel_argv(argv)
184 self.build_kernel_argv(argv)
185
185
186
186
187 def new_frontend_master(self):
187 def new_frontend_master(self):
188 """ Create and return new frontend attached to new kernel, launched on localhost.
188 """ Create and return new frontend attached to new kernel, launched on localhost.
189 """
189 """
190 kernel_manager = self.kernel_manager_class(
190 kernel_manager = self.kernel_manager_class(
191 connection_file=self._new_connection_file(),
191 connection_file=self._new_connection_file(),
192 parent=self,
192 parent=self,
193 autorestart=True,
193 autorestart=True,
194 )
194 )
195 # start the kernel
195 # start the kernel
196 kwargs = dict()
196 kwargs = {}
197 kwargs['extra_arguments'] = self.kernel_argv
197 # FIXME: remove special treatment of IPython kernels
198 if self.kernel_manager.ipython_kernel:
199 kwargs['extra_arguments'] = self.kernel_argv
198 kernel_manager.start_kernel(**kwargs)
200 kernel_manager.start_kernel(**kwargs)
199 kernel_manager.client_factory = self.kernel_client_class
201 kernel_manager.client_factory = self.kernel_client_class
200 kernel_client = kernel_manager.client()
202 kernel_client = kernel_manager.client()
201 kernel_client.start_channels(shell=True, iopub=True)
203 kernel_client.start_channels(shell=True, iopub=True)
202 widget = self.widget_factory(config=self.config,
204 widget = self.widget_factory(config=self.config,
203 local_kernel=True)
205 local_kernel=True)
204 self.init_colors(widget)
206 self.init_colors(widget)
205 widget.kernel_manager = kernel_manager
207 widget.kernel_manager = kernel_manager
206 widget.kernel_client = kernel_client
208 widget.kernel_client = kernel_client
207 widget._existing = False
209 widget._existing = False
208 widget._may_close = True
210 widget._may_close = True
209 widget._confirm_exit = self.confirm_exit
211 widget._confirm_exit = self.confirm_exit
210 return widget
212 return widget
211
213
212 def new_frontend_slave(self, current_widget):
214 def new_frontend_slave(self, current_widget):
213 """Create and return a new frontend attached to an existing kernel.
215 """Create and return a new frontend attached to an existing kernel.
214
216
215 Parameters
217 Parameters
216 ----------
218 ----------
217 current_widget : IPythonWidget
219 current_widget : IPythonWidget
218 The IPythonWidget whose kernel this frontend is to share
220 The IPythonWidget whose kernel this frontend is to share
219 """
221 """
220 kernel_client = self.kernel_client_class(
222 kernel_client = self.kernel_client_class(
221 connection_file=current_widget.kernel_client.connection_file,
223 connection_file=current_widget.kernel_client.connection_file,
222 config = self.config,
224 config = self.config,
223 )
225 )
224 kernel_client.load_connection_file()
226 kernel_client.load_connection_file()
225 kernel_client.start_channels()
227 kernel_client.start_channels()
226 widget = self.widget_factory(config=self.config,
228 widget = self.widget_factory(config=self.config,
227 local_kernel=False)
229 local_kernel=False)
228 self.init_colors(widget)
230 self.init_colors(widget)
229 widget._existing = True
231 widget._existing = True
230 widget._may_close = False
232 widget._may_close = False
231 widget._confirm_exit = False
233 widget._confirm_exit = False
232 widget.kernel_client = kernel_client
234 widget.kernel_client = kernel_client
233 widget.kernel_manager = current_widget.kernel_manager
235 widget.kernel_manager = current_widget.kernel_manager
234 return widget
236 return widget
235
237
236 def init_qt_app(self):
238 def init_qt_app(self):
237 # separate from qt_elements, because it must run first
239 # separate from qt_elements, because it must run first
238 self.app = QtGui.QApplication([])
240 self.app = QtGui.QApplication([])
239
241
240 def init_qt_elements(self):
242 def init_qt_elements(self):
241 # Create the widget.
243 # Create the widget.
242
244
243 base_path = os.path.abspath(os.path.dirname(__file__))
245 base_path = os.path.abspath(os.path.dirname(__file__))
244 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
246 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
245 self.app.icon = QtGui.QIcon(icon_path)
247 self.app.icon = QtGui.QIcon(icon_path)
246 QtGui.QApplication.setWindowIcon(self.app.icon)
248 QtGui.QApplication.setWindowIcon(self.app.icon)
247
249
248 ip = self.ip
250 ip = self.ip
249 local_kernel = (not self.existing) or is_local_ip(ip)
251 local_kernel = (not self.existing) or is_local_ip(ip)
250 self.widget = self.widget_factory(config=self.config,
252 self.widget = self.widget_factory(config=self.config,
251 local_kernel=local_kernel)
253 local_kernel=local_kernel)
252 self.init_colors(self.widget)
254 self.init_colors(self.widget)
253 self.widget._existing = self.existing
255 self.widget._existing = self.existing
254 self.widget._may_close = not self.existing
256 self.widget._may_close = not self.existing
255 self.widget._confirm_exit = self.confirm_exit
257 self.widget._confirm_exit = self.confirm_exit
256
258
257 self.widget.kernel_manager = self.kernel_manager
259 self.widget.kernel_manager = self.kernel_manager
258 self.widget.kernel_client = self.kernel_client
260 self.widget.kernel_client = self.kernel_client
259 self.window = MainWindow(self.app,
261 self.window = MainWindow(self.app,
260 confirm_exit=self.confirm_exit,
262 confirm_exit=self.confirm_exit,
261 new_frontend_factory=self.new_frontend_master,
263 new_frontend_factory=self.new_frontend_master,
262 slave_frontend_factory=self.new_frontend_slave,
264 slave_frontend_factory=self.new_frontend_slave,
263 )
265 )
264 self.window.log = self.log
266 self.window.log = self.log
265 self.window.add_tab_with_frontend(self.widget)
267 self.window.add_tab_with_frontend(self.widget)
266 self.window.init_magic_helper()
268 self.window.init_magic_helper()
267 self.window.init_menu_bar()
269 self.window.init_menu_bar()
268
270
269 # Ignore on OSX, where there is always a menu bar
271 # Ignore on OSX, where there is always a menu bar
270 if sys.platform != 'darwin' and self.hide_menubar:
272 if sys.platform != 'darwin' and self.hide_menubar:
271 self.window.menuBar().setVisible(False)
273 self.window.menuBar().setVisible(False)
272
274
273 self.window.setWindowTitle('IPython')
275 self.window.setWindowTitle('IPython')
274
276
275 def init_colors(self, widget):
277 def init_colors(self, widget):
276 """Configure the coloring of the widget"""
278 """Configure the coloring of the widget"""
277 # Note: This will be dramatically simplified when colors
279 # Note: This will be dramatically simplified when colors
278 # are removed from the backend.
280 # are removed from the backend.
279
281
280 # parse the colors arg down to current known labels
282 # parse the colors arg down to current known labels
281 cfg = self.config
283 cfg = self.config
282 colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
284 colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
283 style = cfg.IPythonWidget.syntax_style if 'IPythonWidget.syntax_style' in cfg else None
285 style = cfg.IPythonWidget.syntax_style if 'IPythonWidget.syntax_style' in cfg else None
284 sheet = cfg.IPythonWidget.style_sheet if 'IPythonWidget.style_sheet' in cfg else None
286 sheet = cfg.IPythonWidget.style_sheet if 'IPythonWidget.style_sheet' in cfg else None
285
287
286 # find the value for colors:
288 # find the value for colors:
287 if colors:
289 if colors:
288 colors=colors.lower()
290 colors=colors.lower()
289 if colors in ('lightbg', 'light'):
291 if colors in ('lightbg', 'light'):
290 colors='lightbg'
292 colors='lightbg'
291 elif colors in ('dark', 'linux'):
293 elif colors in ('dark', 'linux'):
292 colors='linux'
294 colors='linux'
293 else:
295 else:
294 colors='nocolor'
296 colors='nocolor'
295 elif style:
297 elif style:
296 if style=='bw':
298 if style=='bw':
297 colors='nocolor'
299 colors='nocolor'
298 elif styles.dark_style(style):
300 elif styles.dark_style(style):
299 colors='linux'
301 colors='linux'
300 else:
302 else:
301 colors='lightbg'
303 colors='lightbg'
302 else:
304 else:
303 colors=None
305 colors=None
304
306
305 # Configure the style
307 # Configure the style
306 if style:
308 if style:
307 widget.style_sheet = styles.sheet_from_template(style, colors)
309 widget.style_sheet = styles.sheet_from_template(style, colors)
308 widget.syntax_style = style
310 widget.syntax_style = style
309 widget._syntax_style_changed()
311 widget._syntax_style_changed()
310 widget._style_sheet_changed()
312 widget._style_sheet_changed()
311 elif colors:
313 elif colors:
312 # use a default dark/light/bw style
314 # use a default dark/light/bw style
313 widget.set_default_style(colors=colors)
315 widget.set_default_style(colors=colors)
314
316
315 if self.stylesheet:
317 if self.stylesheet:
316 # we got an explicit stylesheet
318 # we got an explicit stylesheet
317 if os.path.isfile(self.stylesheet):
319 if os.path.isfile(self.stylesheet):
318 with open(self.stylesheet) as f:
320 with open(self.stylesheet) as f:
319 sheet = f.read()
321 sheet = f.read()
320 else:
322 else:
321 raise IOError("Stylesheet %r not found." % self.stylesheet)
323 raise IOError("Stylesheet %r not found." % self.stylesheet)
322 if sheet:
324 if sheet:
323 widget.style_sheet = sheet
325 widget.style_sheet = sheet
324 widget._style_sheet_changed()
326 widget._style_sheet_changed()
325
327
326
328
327 def init_signal(self):
329 def init_signal(self):
328 """allow clean shutdown on sigint"""
330 """allow clean shutdown on sigint"""
329 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
331 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
330 # need a timer, so that QApplication doesn't block until a real
332 # need a timer, so that QApplication doesn't block until a real
331 # Qt event fires (can require mouse movement)
333 # Qt event fires (can require mouse movement)
332 # timer trick from http://stackoverflow.com/q/4938723/938949
334 # timer trick from http://stackoverflow.com/q/4938723/938949
333 timer = QtCore.QTimer()
335 timer = QtCore.QTimer()
334 # Let the interpreter run each 200 ms:
336 # Let the interpreter run each 200 ms:
335 timer.timeout.connect(lambda: None)
337 timer.timeout.connect(lambda: None)
336 timer.start(200)
338 timer.start(200)
337 # hold onto ref, so the timer doesn't get cleaned up
339 # hold onto ref, so the timer doesn't get cleaned up
338 self._sigint_timer = timer
340 self._sigint_timer = timer
339
341
340 @catch_config_error
342 @catch_config_error
341 def initialize(self, argv=None):
343 def initialize(self, argv=None):
342 self.init_qt_app()
344 self.init_qt_app()
343 super(IPythonQtConsoleApp, self).initialize(argv)
345 super(IPythonQtConsoleApp, self).initialize(argv)
344 IPythonConsoleApp.initialize(self,argv)
346 IPythonConsoleApp.initialize(self,argv)
345 self.init_qt_elements()
347 self.init_qt_elements()
346 self.init_signal()
348 self.init_signal()
347
349
348 def start(self):
350 def start(self):
349
351
350 # draw the window
352 # draw the window
351 if self.maximize:
353 if self.maximize:
352 self.window.showMaximized()
354 self.window.showMaximized()
353 else:
355 else:
354 self.window.show()
356 self.window.show()
355 self.window.raise_()
357 self.window.raise_()
356
358
357 # Start the application main loop.
359 # Start the application main loop.
358 self.app.exec_()
360 self.app.exec_()
359
361
360 #-----------------------------------------------------------------------------
362 #-----------------------------------------------------------------------------
361 # Main entry point
363 # Main entry point
362 #-----------------------------------------------------------------------------
364 #-----------------------------------------------------------------------------
363
365
364 def main():
366 def main():
365 app = IPythonQtConsoleApp()
367 app = IPythonQtConsoleApp()
366 app.initialize()
368 app.initialize()
367 app.start()
369 app.start()
368
370
369
371
370 if __name__ == '__main__':
372 if __name__ == '__main__':
371 main()
373 main()
General Comments 0
You need to be logged in to leave comments. Login now